Merge "Add critical blocking calls related to compose" into main
diff --git a/Android.bp b/Android.bp
index f3f4a52..69c599d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -574,6 +574,7 @@
         ":perfetto_src_android_stats_android_stats",
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_clock_snapshots",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_ipc_client",
@@ -820,6 +821,7 @@
         ":perfetto_src_android_stats_android_stats",
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_clock_snapshots",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_ipc_client",
@@ -995,6 +997,7 @@
         ":perfetto_src_android_stats_android_stats",
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_clock_snapshots",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_ipc_client",
@@ -1354,6 +1357,7 @@
         "protos/perfetto/config/android/windowmanager_config.proto",
         "protos/perfetto/config/chrome/chrome_config.proto",
         "protos/perfetto/config/chrome/scenario_config.proto",
+        "protos/perfetto/config/chrome/system_metrics.proto",
         "protos/perfetto/config/chrome/v8_config.proto",
         "protos/perfetto/config/data_source_config.proto",
         "protos/perfetto/config/etw/etw_config.proto",
@@ -1379,6 +1383,72 @@
     ],
 }
 
+// GN: [//protos/perfetto/config:source_set]
+java_library {
+    name: "perfetto_config_java_protos",
+    srcs: [
+        "protos/perfetto/common/android_energy_consumer_descriptor.proto",
+        "protos/perfetto/common/android_log_constants.proto",
+        "protos/perfetto/common/builtin_clock.proto",
+        "protos/perfetto/common/commit_data_request.proto",
+        "protos/perfetto/common/data_source_descriptor.proto",
+        "protos/perfetto/common/descriptor.proto",
+        "protos/perfetto/common/ftrace_descriptor.proto",
+        "protos/perfetto/common/gpu_counter_descriptor.proto",
+        "protos/perfetto/common/interceptor_descriptor.proto",
+        "protos/perfetto/common/observable_events.proto",
+        "protos/perfetto/common/perf_events.proto",
+        "protos/perfetto/common/protolog_common.proto",
+        "protos/perfetto/common/sys_stats_counters.proto",
+        "protos/perfetto/common/trace_stats.proto",
+        "protos/perfetto/common/tracing_service_capabilities.proto",
+        "protos/perfetto/common/tracing_service_state.proto",
+        "protos/perfetto/common/track_event_descriptor.proto",
+        "protos/perfetto/config/android/android_game_intervention_list_config.proto",
+        "protos/perfetto/config/android/android_input_event_config.proto",
+        "protos/perfetto/config/android/android_log_config.proto",
+        "protos/perfetto/config/android/android_polled_state_config.proto",
+        "protos/perfetto/config/android/android_sdk_sysprop_guard_config.proto",
+        "protos/perfetto/config/android/android_system_property_config.proto",
+        "protos/perfetto/config/android/network_trace_config.proto",
+        "protos/perfetto/config/android/packages_list_config.proto",
+        "protos/perfetto/config/android/pixel_modem_config.proto",
+        "protos/perfetto/config/android/protolog_config.proto",
+        "protos/perfetto/config/android/surfaceflinger_layers_config.proto",
+        "protos/perfetto/config/android/surfaceflinger_transactions_config.proto",
+        "protos/perfetto/config/android/windowmanager_config.proto",
+        "protos/perfetto/config/chrome/chrome_config.proto",
+        "protos/perfetto/config/chrome/scenario_config.proto",
+        "protos/perfetto/config/chrome/system_metrics.proto",
+        "protos/perfetto/config/chrome/v8_config.proto",
+        "protos/perfetto/config/data_source_config.proto",
+        "protos/perfetto/config/etw/etw_config.proto",
+        "protos/perfetto/config/ftrace/ftrace_config.proto",
+        "protos/perfetto/config/gpu/gpu_counter_config.proto",
+        "protos/perfetto/config/gpu/vulkan_memory_config.proto",
+        "protos/perfetto/config/inode_file/inode_file_config.proto",
+        "protos/perfetto/config/interceptor_config.proto",
+        "protos/perfetto/config/interceptors/console_config.proto",
+        "protos/perfetto/config/power/android_power_config.proto",
+        "protos/perfetto/config/process_stats/process_stats_config.proto",
+        "protos/perfetto/config/profiling/heapprofd_config.proto",
+        "protos/perfetto/config/profiling/java_hprof_config.proto",
+        "protos/perfetto/config/profiling/perf_event_config.proto",
+        "protos/perfetto/config/statsd/atom_ids.proto",
+        "protos/perfetto/config/statsd/statsd_tracing_config.proto",
+        "protos/perfetto/config/stress_test_config.proto",
+        "protos/perfetto/config/sys_stats/sys_stats_config.proto",
+        "protos/perfetto/config/system_info/system_info.proto",
+        "protos/perfetto/config/test_config.proto",
+        "protos/perfetto/config/trace_config.proto",
+        "protos/perfetto/config/track_event/track_event_config.proto",
+    ],
+    proto: {
+        type: "lite",
+        canonical_path_from_root: false,
+    },
+}
+
 // GN: //test/cts:perfetto_cts_deps
 cc_library_static {
     name: "perfetto_cts_deps",
@@ -1477,6 +1547,7 @@
         ":perfetto_src_android_stats_android_stats",
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_clock_snapshots",
         ":perfetto_src_base_test_support",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
@@ -1788,6 +1859,7 @@
         ":perfetto_src_android_stats_android_stats",
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_clock_snapshots",
         ":perfetto_src_base_test_support",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
@@ -2355,6 +2427,7 @@
         ":perfetto_src_android_stats_android_stats",
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_clock_snapshots",
         ":perfetto_src_base_test_support",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
@@ -2399,6 +2472,9 @@
         ":perfetto_src_trace_processor_export_json",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_bugreport",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
+        ":perfetto_src_trace_processor_importers_archive_archive",
+        ":perfetto_src_trace_processor_importers_art_method_art_method",
+        ":perfetto_src_trace_processor_importers_art_method_art_method_event",
         ":perfetto_src_trace_processor_importers_common_common",
         ":perfetto_src_trace_processor_importers_common_parser_types",
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
@@ -2410,14 +2486,18 @@
         ":perfetto_src_trace_processor_importers_fuchsia_fuchsia_record",
         ":perfetto_src_trace_processor_importers_fuchsia_full",
         ":perfetto_src_trace_processor_importers_fuchsia_minimal",
-        ":perfetto_src_trace_processor_importers_gzip_full",
+        ":perfetto_src_trace_processor_importers_gecko_gecko_event",
         ":perfetto_src_trace_processor_importers_i2c_full",
-        ":perfetto_src_trace_processor_importers_json_full",
+        ":perfetto_src_trace_processor_importers_instruments_instruments",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
         ":perfetto_src_trace_processor_importers_ninja_ninja",
         ":perfetto_src_trace_processor_importers_perf_perf",
         ":perfetto_src_trace_processor_importers_perf_record",
+        ":perfetto_src_trace_processor_importers_perf_text_perf_text",
+        ":perfetto_src_trace_processor_importers_perf_text_perf_text_event",
+        ":perfetto_src_trace_processor_importers_perf_text_perf_text_sample_line_parser",
         ":perfetto_src_trace_processor_importers_perf_tracker",
         ":perfetto_src_trace_processor_importers_proto_full",
         ":perfetto_src_trace_processor_importers_proto_minimal",
@@ -2428,17 +2508,22 @@
         ":perfetto_src_trace_processor_importers_systrace_full",
         ":perfetto_src_trace_processor_importers_systrace_systrace_line",
         ":perfetto_src_trace_processor_importers_systrace_systrace_parser",
-        ":perfetto_src_trace_processor_importers_zip_full",
         ":perfetto_src_trace_processor_lib",
         ":perfetto_src_trace_processor_metatrace",
         ":perfetto_src_trace_processor_metrics_metrics",
         ":perfetto_src_trace_processor_perfetto_sql_engine_engine",
+        ":perfetto_src_trace_processor_perfetto_sql_grammar_grammar",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_functions_functions",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_functions_interface",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_operators_operators",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_table_functions_interface",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_table_functions_table_functions",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_types_types",
+        ":perfetto_src_trace_processor_perfetto_sql_parser_parser",
+        ":perfetto_src_trace_processor_perfetto_sql_preprocessor_grammar",
+        ":perfetto_src_trace_processor_perfetto_sql_preprocessor_preprocessor",
+        ":perfetto_src_trace_processor_perfetto_sql_tokenizer_tokenize_internal",
+        ":perfetto_src_trace_processor_perfetto_sql_tokenizer_tokenizer",
         ":perfetto_src_trace_processor_sorter_sorter",
         ":perfetto_src_trace_processor_sqlite_bindings_bindings",
         ":perfetto_src_trace_processor_sqlite_sqlite",
@@ -2464,6 +2549,7 @@
         ":perfetto_src_trace_processor_util_trace_blob_view_reader",
         ":perfetto_src_trace_processor_util_trace_type",
         ":perfetto_src_trace_processor_util_util",
+        ":perfetto_src_trace_processor_util_winscope_proto_mapping",
         ":perfetto_src_trace_processor_util_zip_reader",
         ":perfetto_src_traced_probes_android_game_intervention_list_android_game_intervention_list",
         ":perfetto_src_traced_probes_android_log_android_log",
@@ -2507,11 +2593,14 @@
         ":perfetto_src_tracing_test_client_api_integrationtests",
         ":perfetto_src_tracing_test_test_support",
         ":perfetto_src_tracing_test_tracing_integration_test",
+        ":perfetto_test_integrationtest_initializer",
+        ":perfetto_test_integrationtest_main",
         ":perfetto_test_test_helper",
     ],
     shared_libs: [
         "heapprofd_client_api",
         "libbase",
+        "libexpat",
         "libicu",
         "liblog",
         "libprocinfo",
@@ -2677,6 +2766,79 @@
     test_config: "PerfettoIntegrationTests.xml",
 }
 
+// GN: [//protos/perfetto/metrics:source_set]
+python_library_host {
+    name: "perfetto_metrics_python_protos",
+    srcs: [
+        "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_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
+        "protos/perfetto/metrics/android/android_boot.proto",
+        "protos/perfetto/metrics/android/android_boot_unagg.proto",
+        "protos/perfetto/metrics/android/android_broadcasts_metric.proto",
+        "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
+        "protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto",
+        "protos/perfetto/metrics/android/android_oom_adjuster_metric.proto",
+        "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
+        "protos/perfetto/metrics/android/anr_metric.proto",
+        "protos/perfetto/metrics/android/app_process_starts_metric.proto",
+        "protos/perfetto/metrics/android/auto_metric.proto",
+        "protos/perfetto/metrics/android/batt_metric.proto",
+        "protos/perfetto/metrics/android/binder_metric.proto",
+        "protos/perfetto/metrics/android/camera_metric.proto",
+        "protos/perfetto/metrics/android/camera_unagg_metric.proto",
+        "protos/perfetto/metrics/android/codec_metrics.proto",
+        "protos/perfetto/metrics/android/cpu_metric.proto",
+        "protos/perfetto/metrics/android/display_metrics.proto",
+        "protos/perfetto/metrics/android/dma_heap_metric.proto",
+        "protos/perfetto/metrics/android/dvfs_metric.proto",
+        "protos/perfetto/metrics/android/fastrpc_metric.proto",
+        "protos/perfetto/metrics/android/g2d_metric.proto",
+        "protos/perfetto/metrics/android/gpu_metric.proto",
+        "protos/perfetto/metrics/android/hwcomposer.proto",
+        "protos/perfetto/metrics/android/hwui_metric.proto",
+        "protos/perfetto/metrics/android/io_metric.proto",
+        "protos/perfetto/metrics/android/io_unagg_metric.proto",
+        "protos/perfetto/metrics/android/ion_metric.proto",
+        "protos/perfetto/metrics/android/irq_runtime_metric.proto",
+        "protos/perfetto/metrics/android/jank_cuj_metric.proto",
+        "protos/perfetto/metrics/android/java_heap_class_stats.proto",
+        "protos/perfetto/metrics/android/java_heap_histogram.proto",
+        "protos/perfetto/metrics/android/java_heap_stats.proto",
+        "protos/perfetto/metrics/android/lmk_metric.proto",
+        "protos/perfetto/metrics/android/lmk_reason_metric.proto",
+        "protos/perfetto/metrics/android/mem_metric.proto",
+        "protos/perfetto/metrics/android/mem_unagg_metric.proto",
+        "protos/perfetto/metrics/android/monitor_contention_agg_metric.proto",
+        "protos/perfetto/metrics/android/monitor_contention_metric.proto",
+        "protos/perfetto/metrics/android/multiuser_metric.proto",
+        "protos/perfetto/metrics/android/network_metric.proto",
+        "protos/perfetto/metrics/android/package_list.proto",
+        "protos/perfetto/metrics/android/powrails_metric.proto",
+        "protos/perfetto/metrics/android/process_metadata.proto",
+        "protos/perfetto/metrics/android/profiler_smaps.proto",
+        "protos/perfetto/metrics/android/rt_runtime_metric.proto",
+        "protos/perfetto/metrics/android/simpleperf.proto",
+        "protos/perfetto/metrics/android/startup_metric.proto",
+        "protos/perfetto/metrics/android/surfaceflinger.proto",
+        "protos/perfetto/metrics/android/sysui_notif_shade_list_builder_metric.proto",
+        "protos/perfetto/metrics/android/sysui_slice_performance_statistical_data.proto",
+        "protos/perfetto/metrics/android/sysui_update_notif_on_ui_mode_changed_metric.proto",
+        "protos/perfetto/metrics/android/task_names.proto",
+        "protos/perfetto/metrics/android/thread_time_in_state_metric.proto",
+        "protos/perfetto/metrics/android/trace_quality.proto",
+        "protos/perfetto/metrics/android/unsymbolized_frames.proto",
+        "protos/perfetto/metrics/android/wattson_in_time_period.proto",
+        "protos/perfetto/metrics/android/wattson_tasks_attribution.proto",
+        "protos/perfetto/metrics/metrics.proto",
+    ],
+    proto: {
+        canonical_path_from_root: false,
+    },
+}
+
 // GN: //protos/perfetto/common:cpp
 filegroup {
     name: "perfetto_protos_perfetto_common_cpp",
@@ -3201,6 +3363,7 @@
     srcs: [
         "protos/perfetto/config/chrome/chrome_config.proto",
         "protos/perfetto/config/chrome/scenario_config.proto",
+        "protos/perfetto/config/chrome/system_metrics.proto",
         "protos/perfetto/config/chrome/v8_config.proto",
         "protos/perfetto/config/data_source_config.proto",
         "protos/perfetto/config/etw/etw_config.proto",
@@ -3238,6 +3401,7 @@
     out: [
         "external/perfetto/protos/perfetto/config/chrome/chrome_config.gen.cc",
         "external/perfetto/protos/perfetto/config/chrome/scenario_config.gen.cc",
+        "external/perfetto/protos/perfetto/config/chrome/system_metrics.gen.cc",
         "external/perfetto/protos/perfetto/config/chrome/v8_config.gen.cc",
         "external/perfetto/protos/perfetto/config/data_source_config.gen.cc",
         "external/perfetto/protos/perfetto/config/etw/etw_config.gen.cc",
@@ -3275,6 +3439,7 @@
     out: [
         "external/perfetto/protos/perfetto/config/chrome/chrome_config.gen.h",
         "external/perfetto/protos/perfetto/config/chrome/scenario_config.gen.h",
+        "external/perfetto/protos/perfetto/config/chrome/system_metrics.gen.h",
         "external/perfetto/protos/perfetto/config/chrome/v8_config.gen.h",
         "external/perfetto/protos/perfetto/config/data_source_config.gen.h",
         "external/perfetto/protos/perfetto/config/etw/etw_config.gen.h",
@@ -3325,6 +3490,7 @@
         "protos/perfetto/config/android/windowmanager_config.proto",
         "protos/perfetto/config/chrome/chrome_config.proto",
         "protos/perfetto/config/chrome/scenario_config.proto",
+        "protos/perfetto/config/chrome/system_metrics.proto",
         "protos/perfetto/config/chrome/v8_config.proto",
         "protos/perfetto/config/data_source_config.proto",
         "protos/perfetto/config/etw/etw_config.proto",
@@ -3898,6 +4064,7 @@
     srcs: [
         "protos/perfetto/config/chrome/chrome_config.proto",
         "protos/perfetto/config/chrome/scenario_config.proto",
+        "protos/perfetto/config/chrome/system_metrics.proto",
         "protos/perfetto/config/chrome/v8_config.proto",
         "protos/perfetto/config/data_source_config.proto",
         "protos/perfetto/config/etw/etw_config.proto",
@@ -3934,6 +4101,7 @@
     out: [
         "external/perfetto/protos/perfetto/config/chrome/chrome_config.pb.cc",
         "external/perfetto/protos/perfetto/config/chrome/scenario_config.pb.cc",
+        "external/perfetto/protos/perfetto/config/chrome/system_metrics.pb.cc",
         "external/perfetto/protos/perfetto/config/chrome/v8_config.pb.cc",
         "external/perfetto/protos/perfetto/config/data_source_config.pb.cc",
         "external/perfetto/protos/perfetto/config/etw/etw_config.pb.cc",
@@ -3970,6 +4138,7 @@
     out: [
         "external/perfetto/protos/perfetto/config/chrome/chrome_config.pb.h",
         "external/perfetto/protos/perfetto/config/chrome/scenario_config.pb.h",
+        "external/perfetto/protos/perfetto/config/chrome/system_metrics.pb.h",
         "external/perfetto/protos/perfetto/config/chrome/v8_config.pb.h",
         "external/perfetto/protos/perfetto/config/data_source_config.pb.h",
         "external/perfetto/protos/perfetto/config/etw/etw_config.pb.h",
@@ -4945,6 +5114,7 @@
     srcs: [
         "protos/perfetto/config/chrome/chrome_config.proto",
         "protos/perfetto/config/chrome/scenario_config.proto",
+        "protos/perfetto/config/chrome/system_metrics.proto",
         "protos/perfetto/config/chrome/v8_config.proto",
         "protos/perfetto/config/data_source_config.proto",
         "protos/perfetto/config/etw/etw_config.proto",
@@ -4982,6 +5152,7 @@
     out: [
         "external/perfetto/protos/perfetto/config/chrome/chrome_config.pbzero.cc",
         "external/perfetto/protos/perfetto/config/chrome/scenario_config.pbzero.cc",
+        "external/perfetto/protos/perfetto/config/chrome/system_metrics.pbzero.cc",
         "external/perfetto/protos/perfetto/config/chrome/v8_config.pbzero.cc",
         "external/perfetto/protos/perfetto/config/data_source_config.pbzero.cc",
         "external/perfetto/protos/perfetto/config/etw/etw_config.pbzero.cc",
@@ -5019,6 +5190,7 @@
     out: [
         "external/perfetto/protos/perfetto/config/chrome/chrome_config.pbzero.h",
         "external/perfetto/protos/perfetto/config/chrome/scenario_config.pbzero.h",
+        "external/perfetto/protos/perfetto/config/chrome/system_metrics.pbzero.h",
         "external/perfetto/protos/perfetto/config/chrome/v8_config.pbzero.h",
         "external/perfetto/protos/perfetto/config/data_source_config.pbzero.h",
         "external/perfetto/protos/perfetto/config/etw/etw_config.pbzero.h",
@@ -5254,7 +5426,6 @@
         "protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto",
         "protos/perfetto/metrics/android/android_oom_adjuster_metric.proto",
         "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
-        "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
         "protos/perfetto/metrics/android/anr_metric.proto",
         "protos/perfetto/metrics/android/app_process_starts_metric.proto",
         "protos/perfetto/metrics/android/auto_metric.proto",
@@ -5288,7 +5459,6 @@
         "protos/perfetto/metrics/android/monitor_contention_metric.proto",
         "protos/perfetto/metrics/android/multiuser_metric.proto",
         "protos/perfetto/metrics/android/network_metric.proto",
-        "protos/perfetto/metrics/android/other_traces.proto",
         "protos/perfetto/metrics/android/package_list.proto",
         "protos/perfetto/metrics/android/powrails_metric.proto",
         "protos/perfetto/metrics/android/process_metadata.proto",
@@ -5350,7 +5520,6 @@
         "protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto",
         "protos/perfetto/metrics/android/android_oom_adjuster_metric.proto",
         "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
-        "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
         "protos/perfetto/metrics/android/anr_metric.proto",
         "protos/perfetto/metrics/android/app_process_starts_metric.proto",
         "protos/perfetto/metrics/android/auto_metric.proto",
@@ -5384,7 +5553,6 @@
         "protos/perfetto/metrics/android/monitor_contention_metric.proto",
         "protos/perfetto/metrics/android/multiuser_metric.proto",
         "protos/perfetto/metrics/android/network_metric.proto",
-        "protos/perfetto/metrics/android/other_traces.proto",
         "protos/perfetto/metrics/android/package_list.proto",
         "protos/perfetto/metrics/android/powrails_metric.proto",
         "protos/perfetto/metrics/android/process_metadata.proto",
@@ -5430,7 +5598,6 @@
         "protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto",
         "protos/perfetto/metrics/android/android_oom_adjuster_metric.proto",
         "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
-        "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
         "protos/perfetto/metrics/android/anr_metric.proto",
         "protos/perfetto/metrics/android/app_process_starts_metric.proto",
         "protos/perfetto/metrics/android/auto_metric.proto",
@@ -5464,7 +5631,6 @@
         "protos/perfetto/metrics/android/monitor_contention_metric.proto",
         "protos/perfetto/metrics/android/multiuser_metric.proto",
         "protos/perfetto/metrics/android/network_metric.proto",
-        "protos/perfetto/metrics/android/other_traces.proto",
         "protos/perfetto/metrics/android/package_list.proto",
         "protos/perfetto/metrics/android/powrails_metric.proto",
         "protos/perfetto/metrics/android/process_metadata.proto",
@@ -6549,6 +6715,7 @@
         "protos/perfetto/config/android/windowmanager_config.proto",
         "protos/perfetto/config/chrome/chrome_config.proto",
         "protos/perfetto/config/chrome/scenario_config.proto",
+        "protos/perfetto/config/chrome/system_metrics.proto",
         "protos/perfetto/config/chrome/v8_config.proto",
         "protos/perfetto/config/data_source_config.proto",
         "protos/perfetto/config/etw/etw_config.proto",
@@ -6609,9 +6776,11 @@
         "protos/perfetto/trace/ftrace/clk.proto",
         "protos/perfetto/trace/ftrace/cma.proto",
         "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpm_trace.proto",
         "protos/perfetto/trace/ftrace/cpuhp.proto",
         "protos/perfetto/trace/ftrace/cros_ec.proto",
         "protos/perfetto/trace/ftrace/dcvsh.proto",
+        "protos/perfetto/trace/ftrace/devfreq.proto",
         "protos/perfetto/trace/ftrace/dma_fence.proto",
         "protos/perfetto/trace/ftrace/dmabuf_heap.proto",
         "protos/perfetto/trace/ftrace/dpu.proto",
@@ -6648,6 +6817,7 @@
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/panel.proto",
         "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/pixel_mm.proto",
         "protos/perfetto/trace/ftrace/power.proto",
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
@@ -7036,9 +7206,11 @@
         "protos/perfetto/trace/ftrace/clk.proto",
         "protos/perfetto/trace/ftrace/cma.proto",
         "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpm_trace.proto",
         "protos/perfetto/trace/ftrace/cpuhp.proto",
         "protos/perfetto/trace/ftrace/cros_ec.proto",
         "protos/perfetto/trace/ftrace/dcvsh.proto",
+        "protos/perfetto/trace/ftrace/devfreq.proto",
         "protos/perfetto/trace/ftrace/dma_fence.proto",
         "protos/perfetto/trace/ftrace/dmabuf_heap.proto",
         "protos/perfetto/trace/ftrace/dpu.proto",
@@ -7075,6 +7247,7 @@
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/panel.proto",
         "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/pixel_mm.proto",
         "protos/perfetto/trace/ftrace/power.proto",
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
@@ -7125,9 +7298,11 @@
         "external/perfetto/protos/perfetto/trace/ftrace/clk.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cma.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/compaction.gen.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/cpm_trace.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cpuhp.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cros_ec.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dcvsh.gen.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/devfreq.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dma_fence.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dmabuf_heap.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dpu.gen.cc",
@@ -7164,6 +7339,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/oom.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/panel.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/perf_trace_counters.gen.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/pixel_mm.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/power.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/printk.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.gen.cc",
@@ -7214,9 +7390,11 @@
         "external/perfetto/protos/perfetto/trace/ftrace/clk.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cma.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/compaction.gen.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/cpm_trace.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cpuhp.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cros_ec.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dcvsh.gen.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/devfreq.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dma_fence.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dmabuf_heap.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dpu.gen.h",
@@ -7253,6 +7431,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/oom.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/panel.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/perf_trace_counters.gen.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/pixel_mm.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/power.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/printk.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.gen.h",
@@ -7299,9 +7478,11 @@
         "protos/perfetto/trace/ftrace/clk.proto",
         "protos/perfetto/trace/ftrace/cma.proto",
         "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpm_trace.proto",
         "protos/perfetto/trace/ftrace/cpuhp.proto",
         "protos/perfetto/trace/ftrace/cros_ec.proto",
         "protos/perfetto/trace/ftrace/dcvsh.proto",
+        "protos/perfetto/trace/ftrace/devfreq.proto",
         "protos/perfetto/trace/ftrace/dma_fence.proto",
         "protos/perfetto/trace/ftrace/dmabuf_heap.proto",
         "protos/perfetto/trace/ftrace/dpu.proto",
@@ -7338,6 +7519,7 @@
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/panel.proto",
         "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/pixel_mm.proto",
         "protos/perfetto/trace/ftrace/power.proto",
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
@@ -7387,9 +7569,11 @@
         "external/perfetto/protos/perfetto/trace/ftrace/clk.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cma.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/compaction.pb.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/cpm_trace.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cpuhp.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cros_ec.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dcvsh.pb.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/devfreq.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dma_fence.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dmabuf_heap.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dpu.pb.cc",
@@ -7426,6 +7610,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/oom.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/panel.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/perf_trace_counters.pb.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/pixel_mm.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/power.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/printk.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pb.cc",
@@ -7475,9 +7660,11 @@
         "external/perfetto/protos/perfetto/trace/ftrace/clk.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cma.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/compaction.pb.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/cpm_trace.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cpuhp.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cros_ec.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dcvsh.pb.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/devfreq.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dma_fence.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dmabuf_heap.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dpu.pb.h",
@@ -7514,6 +7701,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/oom.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/panel.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/perf_trace_counters.pb.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/pixel_mm.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/power.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/printk.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pb.h",
@@ -7560,9 +7748,11 @@
         "protos/perfetto/trace/ftrace/clk.proto",
         "protos/perfetto/trace/ftrace/cma.proto",
         "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpm_trace.proto",
         "protos/perfetto/trace/ftrace/cpuhp.proto",
         "protos/perfetto/trace/ftrace/cros_ec.proto",
         "protos/perfetto/trace/ftrace/dcvsh.proto",
+        "protos/perfetto/trace/ftrace/devfreq.proto",
         "protos/perfetto/trace/ftrace/dma_fence.proto",
         "protos/perfetto/trace/ftrace/dmabuf_heap.proto",
         "protos/perfetto/trace/ftrace/dpu.proto",
@@ -7599,6 +7789,7 @@
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/panel.proto",
         "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/pixel_mm.proto",
         "protos/perfetto/trace/ftrace/power.proto",
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
@@ -7649,9 +7840,11 @@
         "external/perfetto/protos/perfetto/trace/ftrace/clk.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cma.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/compaction.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/cpm_trace.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cpuhp.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cros_ec.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dcvsh.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/devfreq.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dma_fence.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dmabuf_heap.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dpu.pbzero.cc",
@@ -7688,6 +7881,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/oom.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/panel.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/perf_trace_counters.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/pixel_mm.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/power.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/printk.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pbzero.cc",
@@ -7738,9 +7932,11 @@
         "external/perfetto/protos/perfetto/trace/ftrace/clk.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cma.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/compaction.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/cpm_trace.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cpuhp.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cros_ec.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dcvsh.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/devfreq.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dma_fence.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dmabuf_heap.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dpu.pbzero.h",
@@ -7777,6 +7973,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/oom.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/panel.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/perf_trace_counters.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/pixel_mm.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/power.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/printk.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pbzero.h",
@@ -10692,6 +10889,14 @@
     ],
 }
 
+// GN: //src/base:clock_snapshots
+filegroup {
+    name: "perfetto_src_base_clock_snapshots",
+    srcs: [
+        "src/base/clock_snapshots.cc",
+    ],
+}
+
 // GN: //src/base/http:http
 filegroup {
     name: "perfetto_src_base_http_http",
@@ -11032,7 +11237,6 @@
         "src/perfetto_cmd/packet_writer.cc",
         "src/perfetto_cmd/perfetto_cmd.cc",
         "src/perfetto_cmd/perfetto_cmd_android.cc",
-        "src/perfetto_cmd/rate_limiter.cc",
     ],
 }
 
@@ -11103,7 +11307,6 @@
         "src/perfetto_cmd/config_unittest.cc",
         "src/perfetto_cmd/packet_writer_unittest.cc",
         "src/perfetto_cmd/pbtxt_to_pb_unittest.cc",
-        "src/perfetto_cmd/rate_limiter_unittest.cc",
     ],
 }
 
@@ -11340,6 +11543,7 @@
     name: "perfetto_src_profiling_perf_producer_unittests",
     srcs: [
         "src/profiling/perf/event_config_unittest.cc",
+        "src/profiling/perf/frame_pointer_unwinder_unittest.cc",
         "src/profiling/perf/perf_producer_unittest.cc",
         "src/profiling/perf/unwind_queue_unittest.cc",
     ],
@@ -11365,6 +11569,7 @@
 filegroup {
     name: "perfetto_src_profiling_perf_unwinding",
     srcs: [
+        "src/profiling/perf/frame_pointer_unwinder.cc",
         "src/profiling/perf/unwinding.cc",
     ],
 }
@@ -11518,27 +11723,6 @@
     ],
 }
 
-// GN: //src/protozero:test_messages_descriptor
-genrule {
-    name: "perfetto_src_protozero_test_messages_descriptor",
-    srcs: [
-        "src/protozero/test/example_proto/extensions.proto",
-        "src/protozero/test/example_proto/library.proto",
-        "src/protozero/test/example_proto/library_internals/galaxies.proto",
-        "src/protozero/test/example_proto/other_package/test_messages.proto",
-        "src/protozero/test/example_proto/subpackage/test_messages.proto",
-        "src/protozero/test/example_proto/test_messages.proto",
-        "src/protozero/test/example_proto/upper_import.proto",
-    ],
-    tools: [
-        "aprotoc",
-    ],
-    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --descriptor_set_out=$(out) $(in)",
-    out: [
-        "perfetto_src_protozero_test_messages_descriptor.bin",
-    ],
-}
-
 // GN: //src/protozero:testing_messages_cpp
 filegroup {
     name: "perfetto_src_protozero_testing_messages_cpp",
@@ -11599,6 +11783,27 @@
     ],
 }
 
+// GN: //src/protozero:testing_messages_descriptor
+genrule {
+    name: "perfetto_src_protozero_testing_messages_descriptor",
+    srcs: [
+        "src/protozero/test/example_proto/extensions.proto",
+        "src/protozero/test/example_proto/library.proto",
+        "src/protozero/test/example_proto/library_internals/galaxies.proto",
+        "src/protozero/test/example_proto/other_package/test_messages.proto",
+        "src/protozero/test/example_proto/subpackage/test_messages.proto",
+        "src/protozero/test/example_proto/test_messages.proto",
+        "src/protozero/test/example_proto/upper_import.proto",
+    ],
+    tools: [
+        "aprotoc",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --descriptor_set_out=$(out) $(in)",
+    out: [
+        "perfetto_src_protozero_testing_messages_descriptor.bin",
+    ],
+}
+
 // GN: //src/protozero:testing_messages_lite
 filegroup {
     name: "perfetto_src_protozero_testing_messages_lite",
@@ -12180,7 +12385,7 @@
 genrule {
     name: "perfetto_src_trace_processor_gen_cc_test_messages_descriptor",
     srcs: [
-        ":perfetto_src_protozero_test_messages_descriptor",
+        ":perfetto_src_protozero_testing_messages_descriptor",
     ],
     cmd: "$(location tools/gen_cc_proto_descriptor.py) --gen_dir=$(genDir) --cpp_out=$(out) $(in)",
     out: [
@@ -12219,6 +12424,31 @@
     ],
 }
 
+// GN: //src/trace_processor/importers/archive:archive
+filegroup {
+    name: "perfetto_src_trace_processor_importers_archive_archive",
+    srcs: [
+        "src/trace_processor/importers/archive/archive_entry.cc",
+        "src/trace_processor/importers/archive/gzip_trace_parser.cc",
+        "src/trace_processor/importers/archive/tar_trace_reader.cc",
+        "src/trace_processor/importers/archive/zip_trace_reader.cc",
+    ],
+}
+
+// GN: //src/trace_processor/importers/art_method:art_method
+filegroup {
+    name: "perfetto_src_trace_processor_importers_art_method_art_method",
+    srcs: [
+        "src/trace_processor/importers/art_method/art_method_parser_impl.cc",
+        "src/trace_processor/importers/art_method/art_method_tokenizer.cc",
+    ],
+}
+
+// GN: //src/trace_processor/importers/art_method:art_method_event
+filegroup {
+    name: "perfetto_src_trace_processor_importers_art_method_art_method_event",
+}
+
 // GN: //src/trace_processor/importers/common:common
 filegroup {
     name: "perfetto_src_trace_processor_importers_common_common",
@@ -12234,13 +12464,13 @@
         "src/trace_processor/importers/common/flow_tracker.cc",
         "src/trace_processor/importers/common/global_args_tracker.cc",
         "src/trace_processor/importers/common/jit_cache.cc",
+        "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc",
         "src/trace_processor/importers/common/machine_tracker.cc",
         "src/trace_processor/importers/common/mapping_tracker.cc",
         "src/trace_processor/importers/common/metadata_tracker.cc",
         "src/trace_processor/importers/common/process_track_translation_table.cc",
         "src/trace_processor/importers/common/process_tracker.cc",
         "src/trace_processor/importers/common/sched_event_tracker.cc",
-        "src/trace_processor/importers/common/scoped_active_trace_file.cc",
         "src/trace_processor/importers/common/slice_tracker.cc",
         "src/trace_processor/importers/common/slice_translation_table.cc",
         "src/trace_processor/importers/common/stack_profile_tracker.cc",
@@ -12322,6 +12552,7 @@
         "src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc",
         "src/trace_processor/importers/ftrace/iostat_tracker.cc",
         "src/trace_processor/importers/ftrace/mali_gpu_event_tracker.cc",
+        "src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.cc",
         "src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.cc",
         "src/trace_processor/importers/ftrace/rss_stat_tracker.cc",
         "src/trace_processor/importers/ftrace/thermal_tracker.cc",
@@ -12380,12 +12611,9 @@
     ],
 }
 
-// GN: //src/trace_processor/importers/gzip:full
+// GN: //src/trace_processor/importers/gecko:gecko_event
 filegroup {
-    name: "perfetto_src_trace_processor_importers_gzip_full",
-    srcs: [
-        "src/trace_processor/importers/gzip/gzip_trace_parser.cc",
-    ],
+    name: "perfetto_src_trace_processor_importers_gecko_gecko_event",
 }
 
 // GN: //src/trace_processor/importers/i2c:full
@@ -12396,15 +12624,21 @@
     ],
 }
 
-// GN: //src/trace_processor/importers/json:full
+// GN: //src/trace_processor/importers/instruments:instruments
 filegroup {
-    name: "perfetto_src_trace_processor_importers_json_full",
+    name: "perfetto_src_trace_processor_importers_instruments_instruments",
     srcs: [
-        "src/trace_processor/importers/json/json_trace_parser_impl.cc",
-        "src/trace_processor/importers/json/json_trace_tokenizer.cc",
+        "src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc",
+        "src/trace_processor/importers/instruments/row_data_tracker.cc",
+        "src/trace_processor/importers/instruments/row_parser.cc",
     ],
 }
 
+// GN: //src/trace_processor/importers/instruments:row
+filegroup {
+    name: "perfetto_src_trace_processor_importers_instruments_row",
+}
+
 // GN: //src/trace_processor/importers/json:minimal
 filegroup {
     name: "perfetto_src_trace_processor_importers_json_minimal",
@@ -12448,11 +12682,21 @@
     name: "perfetto_src_trace_processor_importers_perf_perf",
     srcs: [
         "src/trace_processor/importers/perf/attrs_section_reader.cc",
+        "src/trace_processor/importers/perf/aux_data_tokenizer.cc",
+        "src/trace_processor/importers/perf/aux_record.cc",
+        "src/trace_processor/importers/perf/aux_stream_manager.cc",
+        "src/trace_processor/importers/perf/auxtrace_info_record.cc",
+        "src/trace_processor/importers/perf/auxtrace_record.cc",
+        "src/trace_processor/importers/perf/etm_tokenizer.cc",
         "src/trace_processor/importers/perf/features.cc",
+        "src/trace_processor/importers/perf/itrace_start_record.cc",
         "src/trace_processor/importers/perf/mmap_record.cc",
         "src/trace_processor/importers/perf/perf_data_tokenizer.cc",
         "src/trace_processor/importers/perf/record_parser.cc",
         "src/trace_processor/importers/perf/sample.cc",
+        "src/trace_processor/importers/perf/sample_id.cc",
+        "src/trace_processor/importers/perf/spe_record_parser.cc",
+        "src/trace_processor/importers/perf/spe_tokenizer.cc",
     ],
 }
 
@@ -12466,6 +12710,28 @@
     ],
 }
 
+// GN: //src/trace_processor/importers/perf_text:perf_text
+filegroup {
+    name: "perfetto_src_trace_processor_importers_perf_text_perf_text",
+    srcs: [
+        "src/trace_processor/importers/perf_text/perf_text_trace_parser_impl.cc",
+        "src/trace_processor/importers/perf_text/perf_text_trace_tokenizer.cc",
+    ],
+}
+
+// GN: //src/trace_processor/importers/perf_text:perf_text_event
+filegroup {
+    name: "perfetto_src_trace_processor_importers_perf_text_perf_text_event",
+}
+
+// GN: //src/trace_processor/importers/perf_text:perf_text_sample_line_parser
+filegroup {
+    name: "perfetto_src_trace_processor_importers_perf_text_perf_text_sample_line_parser",
+    srcs: [
+        "src/trace_processor/importers/perf_text/perf_text_sample_line_parser.cc",
+    ],
+}
+
 // GN: //src/trace_processor/importers/perf:tracker
 filegroup {
     name: "perfetto_src_trace_processor_importers_perf_tracker",
@@ -12478,6 +12744,7 @@
 filegroup {
     name: "perfetto_src_trace_processor_importers_perf_unittests",
     srcs: [
+        "src/trace_processor/importers/perf/aux_stream_manager_unittest.cc",
         "src/trace_processor/importers/perf/perf_session_unittest.cc",
         "src/trace_processor/importers/perf/reader_unittest.cc",
     ],
@@ -12675,7 +12942,6 @@
     srcs: [
         "src/trace_processor/importers/proto/winscope/android_input_event_parser.cc",
         "src/trace_processor/importers/proto/winscope/protolog_message_decoder.cc",
-        "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.cc",
         "src/trace_processor/importers/proto/winscope/protolog_parser.cc",
         "src/trace_processor/importers/proto/winscope/shell_transitions_parser.cc",
         "src/trace_processor/importers/proto/winscope/shell_transitions_tracker.cc",
@@ -12748,14 +13014,6 @@
     ],
 }
 
-// GN: //src/trace_processor/importers/zip:full
-filegroup {
-    name: "perfetto_src_trace_processor_importers_zip_full",
-    srcs: [
-        "src/trace_processor/importers/zip/zip_trace_reader.cc",
-    ],
-}
-
 // GN: //src/trace_processor:lib
 filegroup {
     name: "perfetto_src_trace_processor_lib",
@@ -12873,7 +13131,6 @@
         "src/trace_processor/metrics/sql/android/android_multiuser_populator.sql",
         "src/trace_processor/metrics/sql/android/android_netperf.sql",
         "src/trace_processor/metrics/sql/android/android_oom_adjuster.sql",
-        "src/trace_processor/metrics/sql/android/android_other_traces.sql",
         "src/trace_processor/metrics/sql/android/android_package_list.sql",
         "src/trace_processor/metrics/sql/android/android_powrails.sql",
         "src/trace_processor/metrics/sql/android/android_proxy_power.sql",
@@ -12884,7 +13141,6 @@
         "src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql",
         "src/trace_processor/metrics/sql/android/android_task_names.sql",
         "src/trace_processor/metrics/sql/android/android_trace_quality.sql",
-        "src/trace_processor/metrics/sql/android/android_trusty_workqueues.sql",
         "src/trace_processor/metrics/sql/android/codec_metrics.sql",
         "src/trace_processor/metrics/sql/android/composer_execution.sql",
         "src/trace_processor/metrics/sql/android/composition_layers.sql",
@@ -12900,7 +13156,6 @@
         "src/trace_processor/metrics/sql/android/jank/cujs_boundaries.sql",
         "src/trace_processor/metrics/sql/android/jank/frames.sql",
         "src/trace_processor/metrics/sql/android/jank/internal/counters.sql",
-        "src/trace_processor/metrics/sql/android/jank/internal/derived_events.sql",
         "src/trace_processor/metrics/sql/android/jank/internal/query_base.sql",
         "src/trace_processor/metrics/sql/android/jank/internal/query_frame_slice.sql",
         "src/trace_processor/metrics/sql/android/jank/params.sql",
@@ -12950,13 +13205,16 @@
         "src/trace_processor/metrics/sql/android/startup/mcycles_per_launch.sql",
         "src/trace_processor/metrics/sql/android/startup/slice_functions.sql",
         "src/trace_processor/metrics/sql/android/startup/slow_start_reasons.sql",
+        "src/trace_processor/metrics/sql/android/startup/slow_start_thresholds.sql",
         "src/trace_processor/metrics/sql/android/startup/system_state.sql",
         "src/trace_processor/metrics/sql/android/startup/thread_state_breakdown.sql",
         "src/trace_processor/metrics/sql/android/sysui_notif_shade_list_builder_metric.sql",
         "src/trace_processor/metrics/sql/android/sysui_notif_shade_list_builder_slices.sql",
         "src/trace_processor/metrics/sql/android/sysui_update_notif_on_ui_mode_changed_metric.sql",
         "src/trace_processor/metrics/sql/android/unsymbolized_frames.sql",
-        "src/trace_processor/metrics/sql/android/wattson_app_startup.sql",
+        "src/trace_processor/metrics/sql/android/wattson_app_startup_rails.sql",
+        "src/trace_processor/metrics/sql/android/wattson_markers_rails.sql",
+        "src/trace_processor/metrics/sql/android/wattson_markers_threads.sql",
         "src/trace_processor/metrics/sql/android/wattson_rail_relations.sql",
         "src/trace_processor/metrics/sql/android/wattson_tasks_attribution.sql",
         "src/trace_processor/metrics/sql/android/wattson_trace_rails.sql",
@@ -13041,10 +13299,7 @@
     name: "perfetto_src_trace_processor_perfetto_sql_engine_engine",
     srcs: [
         "src/trace_processor/perfetto_sql/engine/created_function.cc",
-        "src/trace_processor/perfetto_sql/engine/function_util.cc",
         "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc",
-        "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc",
-        "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.cc",
         "src/trace_processor/perfetto_sql/engine/runtime_table_function.cc",
         "src/trace_processor/perfetto_sql/engine/table_pointer_module.cc",
     ],
@@ -13055,8 +13310,14 @@
     name: "perfetto_src_trace_processor_perfetto_sql_engine_unittests",
     srcs: [
         "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine_unittest.cc",
-        "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser_unittest.cc",
-        "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor_unittest.cc",
+    ],
+}
+
+// GN: //src/trace_processor/perfetto_sql/grammar:grammar
+filegroup {
+    name: "perfetto_src_trace_processor_perfetto_sql_grammar_grammar",
+    srcs: [
+        "src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.c",
     ],
 }
 
@@ -13065,6 +13326,7 @@
     name: "perfetto_src_trace_processor_perfetto_sql_intrinsics_functions_functions",
     srcs: [
         "src/trace_processor/perfetto_sql/intrinsics/functions/base64.cc",
+        "src/trace_processor/perfetto_sql/intrinsics/functions/counter_intervals.cc",
         "src/trace_processor/perfetto_sql/intrinsics/functions/create_function.cc",
         "src/trace_processor/perfetto_sql/intrinsics/functions/create_view_function.cc",
         "src/trace_processor/perfetto_sql/intrinsics/functions/dominator_tree.cc",
@@ -13132,7 +13394,6 @@
     name: "perfetto_src_trace_processor_perfetto_sql_intrinsics_operators_operators",
     srcs: [
         "src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc",
-        "src/trace_processor/perfetto_sql/intrinsics/operators/interval_intersect_operator.cc",
         "src/trace_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.cc",
         "src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc",
         "src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.cc",
@@ -13171,6 +13432,7 @@
         "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",
+        "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.cc",
     ],
 }
 
@@ -13203,6 +13465,7 @@
         "src/trace_processor/tables/jit_tables.py",
         "src/trace_processor/tables/memory_tables.py",
         "src/trace_processor/tables/metadata_tables.py",
+        "src/trace_processor/tables/perf_tables.py",
         "src/trace_processor/tables/profiler_tables.py",
         "src/trace_processor/tables/sched_tables.py",
         "src/trace_processor/tables/slice_tables.py",
@@ -13233,6 +13496,52 @@
     name: "perfetto_src_trace_processor_perfetto_sql_intrinsics_types_types",
 }
 
+// GN: //src/trace_processor/perfetto_sql/parser:parser
+filegroup {
+    name: "perfetto_src_trace_processor_perfetto_sql_parser_parser",
+    srcs: [
+        "src/trace_processor/perfetto_sql/parser/function_util.cc",
+        "src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.cc",
+    ],
+}
+
+// GN: //src/trace_processor/perfetto_sql/parser:test_utils
+filegroup {
+    name: "perfetto_src_trace_processor_perfetto_sql_parser_test_utils",
+}
+
+// GN: //src/trace_processor/perfetto_sql/parser:unittests
+filegroup {
+    name: "perfetto_src_trace_processor_perfetto_sql_parser_unittests",
+    srcs: [
+        "src/trace_processor/perfetto_sql/parser/perfetto_sql_parser_unittest.cc",
+    ],
+}
+
+// GN: //src/trace_processor/perfetto_sql/preprocessor:grammar
+filegroup {
+    name: "perfetto_src_trace_processor_perfetto_sql_preprocessor_grammar",
+    srcs: [
+        "src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.c",
+    ],
+}
+
+// GN: //src/trace_processor/perfetto_sql/preprocessor:preprocessor
+filegroup {
+    name: "perfetto_src_trace_processor_perfetto_sql_preprocessor_preprocessor",
+    srcs: [
+        "src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.cc",
+    ],
+}
+
+// GN: //src/trace_processor/perfetto_sql/preprocessor:unittests
+filegroup {
+    name: "perfetto_src_trace_processor_perfetto_sql_preprocessor_unittests",
+    srcs: [
+        "src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor_unittest.cc",
+    ],
+}
+
 // GN: //src/trace_processor/perfetto_sql/stdlib:stdlib
 genrule {
     name: "perfetto_src_trace_processor_perfetto_sql_stdlib_stdlib",
@@ -13241,12 +13550,14 @@
         "src/trace_processor/perfetto_sql/stdlib/android/app_process_starts.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/auto/multiuser.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/battery.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/battery/charging_states.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/battery_stats.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/binder.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/binder_breakdown.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/broadcasts.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/cpu/cluster_type.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/desktop_mode.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/device.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/dvfs.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/frames/jank_type.sql",
@@ -13257,10 +13568,13 @@
         "src/trace_processor/perfetto_sql/stdlib/android/garbage_collection.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/gpu/frequency.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/gpu/memory.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/gpu/work_period.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/input.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/io.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/job_scheduler.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/job_scheduler_states.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/memory/dmabuf.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/class_summary_tree.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/class_tree.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/dominator_class_tree.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/dominator_tree.sql",
@@ -13269,6 +13583,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/helpers.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/raw_dominator_tree.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/memory/heap_profile/callstacks.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/memory/heap_profile/summary_tree.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/memory/process.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/monitor_contention.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/network_packets.sql",
@@ -13278,6 +13593,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/services.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/slices.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/startup/startup_breakdowns.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/startup/startup_events.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/startup/startups.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/startup/startups_maxsdk28.sql",
@@ -13293,19 +13609,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/winscope/windowmanager.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/common/args.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/counters.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/metadata.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/percentiles.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/slices.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/timestamps.sql",
         "src/trace_processor/perfetto_sql/stdlib/counters/intervals.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/args.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/counters.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/metadata.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/percentiles.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/slices.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/timestamps.sql",
         "src/trace_processor/perfetto_sql/stdlib/export/to_firefox_profile.sql",
         "src/trace_processor/perfetto_sql/stdlib/graphs/critical_path.sql",
         "src/trace_processor/perfetto_sql/stdlib/graphs/dominator_tree.sql",
@@ -13318,24 +13622,26 @@
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/frequency.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle_stats.sql",
+        "src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle_time_in_state.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/general.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/process.sql",
+        "src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/slice.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/system.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/thread.sql",
+        "src/trace_processor/perfetto_sql/stdlib/linux/devfreq.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/memory/general.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/memory/high_watermark.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/memory/process.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql",
+        "src/trace_processor/perfetto_sql/stdlib/linux/perf/spe.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/threads.sql",
-        "src/trace_processor/perfetto_sql/stdlib/metasql/column_list.sql",
-        "src/trace_processor/perfetto_sql/stdlib/metasql/table_list.sql",
         "src/trace_processor/perfetto_sql/stdlib/pkvm/hypervisor.sql",
-        "src/trace_processor/perfetto_sql/stdlib/prelude/casts.sql",
-        "src/trace_processor/perfetto_sql/stdlib/prelude/slices.sql",
-        "src/trace_processor/perfetto_sql/stdlib/prelude/tables.sql",
-        "src/trace_processor/perfetto_sql/stdlib/prelude/tables_views.sql",
-        "src/trace_processor/perfetto_sql/stdlib/prelude/trace_bounds.sql",
-        "src/trace_processor/perfetto_sql/stdlib/prelude/views.sql",
+        "src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/casts.sql",
+        "src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/slices.sql",
+        "src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/tables_views.sql",
+        "src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/views.sql",
+        "src/trace_processor/perfetto_sql/stdlib/prelude/before_eof/tables.sql",
+        "src/trace_processor/perfetto_sql/stdlib/prelude/before_eof/trace_bounds.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/runnable.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/states.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span.sql",
@@ -13348,8 +13654,10 @@
         "src/trace_processor/perfetto_sql/stdlib/slices/flow.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/hierarchy.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/slices.sql",
+        "src/trace_processor/perfetto_sql/stdlib/slices/time_in_state.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql",
         "src/trace_processor/perfetto_sql/stdlib/stack_trace/jit.sql",
+        "src/trace_processor/perfetto_sql/stdlib/stacks/cpu_profiling.sql",
         "src/trace_processor/perfetto_sql/stdlib/time/conversion.sql",
         "src/trace_processor/perfetto_sql/stdlib/v8/jit.sql",
         "src/trace_processor/perfetto_sql/stdlib/viz/flamegraph.sql",
@@ -13363,12 +13671,16 @@
         "src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.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_idle.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/cpu_split.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/curves/device.sql",
-        "src/trace_processor/perfetto_sql/stdlib/wattson/curves/grouped.sql",
-        "src/trace_processor/perfetto_sql/stdlib/wattson/curves/ungrouped.sql",
+        "src/trace_processor/perfetto_sql/stdlib/wattson/curves/estimates.sql",
+        "src/trace_processor/perfetto_sql/stdlib/wattson/curves/idle_attribution.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/curves/utils.sql",
+        "src/trace_processor/perfetto_sql/stdlib/wattson/curves/w_cpu_dependence.sql",
+        "src/trace_processor/perfetto_sql/stdlib/wattson/curves/w_dsu_dependence.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/device_infos.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/system_state.sql",
     ],
@@ -13381,6 +13693,30 @@
     ],
 }
 
+// GN: //src/trace_processor/perfetto_sql/tokenizer:tokenize_internal
+filegroup {
+    name: "perfetto_src_trace_processor_perfetto_sql_tokenizer_tokenize_internal",
+    srcs: [
+        "src/trace_processor/perfetto_sql/tokenizer/tokenize_internal.c",
+    ],
+}
+
+// GN: //src/trace_processor/perfetto_sql/tokenizer:tokenizer
+filegroup {
+    name: "perfetto_src_trace_processor_perfetto_sql_tokenizer_tokenizer",
+    srcs: [
+        "src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.cc",
+    ],
+}
+
+// GN: //src/trace_processor/perfetto_sql/tokenizer:unittests
+filegroup {
+    name: "perfetto_src_trace_processor_perfetto_sql_tokenizer_unittests",
+    srcs: [
+        "src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer_unittest.cc",
+    ],
+}
+
 // GN: //src/trace_processor/rpc:httpd
 filegroup {
     name: "perfetto_src_trace_processor_rpc_httpd",
@@ -13445,7 +13781,6 @@
         "src/trace_processor/sqlite/sql_source.cc",
         "src/trace_processor/sqlite/sql_stats_table.cc",
         "src/trace_processor/sqlite/sqlite_engine.cc",
-        "src/trace_processor/sqlite/sqlite_tokenizer.cc",
         "src/trace_processor/sqlite/sqlite_utils.cc",
         "src/trace_processor/sqlite/stats_table.cc",
     ],
@@ -13457,7 +13792,6 @@
     srcs: [
         "src/trace_processor/sqlite/db_sqlite_table_unittest.cc",
         "src/trace_processor/sqlite/sql_source_unittest.cc",
-        "src/trace_processor/sqlite/sqlite_tokenizer_unittest.cc",
         "src/trace_processor/sqlite/sqlite_utils_unittest.cc",
     ],
 }
@@ -13531,6 +13865,7 @@
         "src/trace_processor/tables/jit_tables.py",
         "src/trace_processor/tables/memory_tables.py",
         "src/trace_processor/tables/metadata_tables.py",
+        "src/trace_processor/tables/perf_tables.py",
         "src/trace_processor/tables/profiler_tables.py",
         "src/trace_processor/tables/sched_tables.py",
         "src/trace_processor/tables/slice_tables.py",
@@ -13550,6 +13885,7 @@
         "src/trace_processor/tables/jit_tables_py.h",
         "src/trace_processor/tables/memory_tables_py.h",
         "src/trace_processor/tables/metadata_tables_py.h",
+        "src/trace_processor/tables/perf_tables_py.h",
         "src/trace_processor/tables/profiler_tables_py.h",
         "src/trace_processor/tables/sched_tables_py.h",
         "src/trace_processor/tables/slice_tables_py.h",
@@ -13573,6 +13909,7 @@
         "src/trace_processor/tables/jit_tables.py",
         "src/trace_processor/tables/memory_tables.py",
         "src/trace_processor/tables/metadata_tables.py",
+        "src/trace_processor/tables/perf_tables.py",
         "src/trace_processor/tables/profiler_tables.py",
         "src/trace_processor/tables/sched_tables.py",
         "src/trace_processor/tables/slice_tables.py",
@@ -13779,6 +14116,11 @@
     name: "perfetto_src_trace_processor_util_util",
 }
 
+// GN: //src/trace_processor/util:winscope_proto_mapping
+filegroup {
+    name: "perfetto_src_trace_processor_util_winscope_proto_mapping",
+}
+
 // GN: //src/trace_processor/util:zip_reader
 filegroup {
     name: "perfetto_src_trace_processor_util_zip_reader",
@@ -14456,7 +14798,6 @@
 filegroup {
     name: "perfetto_src_tracing_core_core",
     srcs: [
-        "src/tracing/core/clock_snapshots.cc",
         "src/tracing/core/id_allocator.cc",
         "src/tracing/core/in_process_shared_memory.cc",
         "src/tracing/core/null_trace_writer.cc",
@@ -14580,8 +14921,10 @@
 filegroup {
     name: "perfetto_src_tracing_service_service",
     srcs: [
+        "src/tracing/service/clock.cc",
         "src/tracing/service/metatrace_writer.cc",
         "src/tracing/service/packet_stream_validator.cc",
+        "src/tracing/service/random.cc",
         "src/tracing/service/trace_buffer.cc",
         "src/tracing/service/tracing_service_impl.cc",
     ],
@@ -14628,7 +14971,6 @@
     name: "perfetto_src_tracing_test_client_api_integrationtests",
     srcs: [
         "src/tracing/test/api_integrationtest.cc",
-        "src/tracing/test/api_integrationtest_main.cc",
         "src/tracing/test/tracing_module.cc",
         "src/tracing/test/tracing_module2.cc",
         "src/tracing/test/tracing_module3.cc",
@@ -14643,6 +14985,8 @@
         "src/tracing/test/fake_packet.cc",
         "src/tracing/test/mock_consumer.cc",
         "src/tracing/test/mock_producer.cc",
+        "src/tracing/test/proxy_producer_endpoint.cc",
+        "src/tracing/test/test_shared_memory.cc",
         "src/tracing/test/traced_value_test_support.cc",
     ],
 }
@@ -14666,6 +15010,19 @@
     ],
 }
 
+// GN: //test:integrationtest_initializer
+filegroup {
+    name: "perfetto_test_integrationtest_initializer",
+}
+
+// GN: //test:integrationtest_main
+filegroup {
+    name: "perfetto_test_integrationtest_main",
+    srcs: [
+        "test/integrationtest_main.cc",
+    ],
+}
+
 // GN: //test/sanitizers:unittests
 filegroup {
     name: "perfetto_test_sanitizers_unittests",
@@ -14720,6 +15077,7 @@
         "protos/perfetto/config/android/windowmanager_config.proto",
         "protos/perfetto/config/chrome/chrome_config.proto",
         "protos/perfetto/config/chrome/scenario_config.proto",
+        "protos/perfetto/config/chrome/system_metrics.proto",
         "protos/perfetto/config/chrome/v8_config.proto",
         "protos/perfetto/config/data_source_config.proto",
         "protos/perfetto/config/etw/etw_config.proto",
@@ -14780,9 +15138,11 @@
         "protos/perfetto/trace/ftrace/clk.proto",
         "protos/perfetto/trace/ftrace/cma.proto",
         "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpm_trace.proto",
         "protos/perfetto/trace/ftrace/cpuhp.proto",
         "protos/perfetto/trace/ftrace/cros_ec.proto",
         "protos/perfetto/trace/ftrace/dcvsh.proto",
+        "protos/perfetto/trace/ftrace/devfreq.proto",
         "protos/perfetto/trace/ftrace/dma_fence.proto",
         "protos/perfetto/trace/ftrace/dmabuf_heap.proto",
         "protos/perfetto/trace/ftrace/dpu.proto",
@@ -14819,6 +15179,7 @@
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/panel.proto",
         "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/pixel_mm.proto",
         "protos/perfetto/trace/ftrace/power.proto",
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
@@ -15194,6 +15555,7 @@
         ":perfetto_src_android_stats_android_stats",
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_clock_snapshots",
         ":perfetto_src_base_http_http",
         ":perfetto_src_base_http_unittests",
         ":perfetto_src_base_test_support",
@@ -15279,6 +15641,9 @@
         ":perfetto_src_trace_processor_importers_android_bugreport_android_bugreport",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
         ":perfetto_src_trace_processor_importers_android_bugreport_unittests",
+        ":perfetto_src_trace_processor_importers_archive_archive",
+        ":perfetto_src_trace_processor_importers_art_method_art_method",
+        ":perfetto_src_trace_processor_importers_art_method_art_method_event",
         ":perfetto_src_trace_processor_importers_common_common",
         ":perfetto_src_trace_processor_importers_common_parser_types",
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
@@ -15293,15 +15658,19 @@
         ":perfetto_src_trace_processor_importers_fuchsia_full",
         ":perfetto_src_trace_processor_importers_fuchsia_minimal",
         ":perfetto_src_trace_processor_importers_fuchsia_unittests",
-        ":perfetto_src_trace_processor_importers_gzip_full",
+        ":perfetto_src_trace_processor_importers_gecko_gecko_event",
         ":perfetto_src_trace_processor_importers_i2c_full",
-        ":perfetto_src_trace_processor_importers_json_full",
+        ":perfetto_src_trace_processor_importers_instruments_instruments",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
         ":perfetto_src_trace_processor_importers_memory_tracker_unittests",
         ":perfetto_src_trace_processor_importers_ninja_ninja",
         ":perfetto_src_trace_processor_importers_perf_perf",
         ":perfetto_src_trace_processor_importers_perf_record",
+        ":perfetto_src_trace_processor_importers_perf_text_perf_text",
+        ":perfetto_src_trace_processor_importers_perf_text_perf_text_event",
+        ":perfetto_src_trace_processor_importers_perf_text_perf_text_sample_line_parser",
         ":perfetto_src_trace_processor_importers_perf_tracker",
         ":perfetto_src_trace_processor_importers_perf_unittests",
         ":perfetto_src_trace_processor_importers_proto_full",
@@ -15316,13 +15685,13 @@
         ":perfetto_src_trace_processor_importers_systrace_systrace_line",
         ":perfetto_src_trace_processor_importers_systrace_systrace_parser",
         ":perfetto_src_trace_processor_importers_systrace_unittests",
-        ":perfetto_src_trace_processor_importers_zip_full",
         ":perfetto_src_trace_processor_lib",
         ":perfetto_src_trace_processor_metatrace",
         ":perfetto_src_trace_processor_metrics_metrics",
         ":perfetto_src_trace_processor_metrics_unittests",
         ":perfetto_src_trace_processor_perfetto_sql_engine_engine",
         ":perfetto_src_trace_processor_perfetto_sql_engine_unittests",
+        ":perfetto_src_trace_processor_perfetto_sql_grammar_grammar",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_functions_functions",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_functions_interface",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_functions_unittests",
@@ -15332,6 +15701,15 @@
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_table_functions_table_functions",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_table_functions_unittests",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_types_types",
+        ":perfetto_src_trace_processor_perfetto_sql_parser_parser",
+        ":perfetto_src_trace_processor_perfetto_sql_parser_test_utils",
+        ":perfetto_src_trace_processor_perfetto_sql_parser_unittests",
+        ":perfetto_src_trace_processor_perfetto_sql_preprocessor_grammar",
+        ":perfetto_src_trace_processor_perfetto_sql_preprocessor_preprocessor",
+        ":perfetto_src_trace_processor_perfetto_sql_preprocessor_unittests",
+        ":perfetto_src_trace_processor_perfetto_sql_tokenizer_tokenize_internal",
+        ":perfetto_src_trace_processor_perfetto_sql_tokenizer_tokenizer",
+        ":perfetto_src_trace_processor_perfetto_sql_tokenizer_unittests",
         ":perfetto_src_trace_processor_rpc_rpc",
         ":perfetto_src_trace_processor_rpc_unittests",
         ":perfetto_src_trace_processor_sorter_sorter",
@@ -15366,6 +15744,7 @@
         ":perfetto_src_trace_processor_util_trace_type",
         ":perfetto_src_trace_processor_util_unittests",
         ":perfetto_src_trace_processor_util_util",
+        ":perfetto_src_trace_processor_util_winscope_proto_mapping",
         ":perfetto_src_trace_processor_util_zip_reader",
         ":perfetto_src_trace_redaction_trace_redaction",
         ":perfetto_src_trace_redaction_unittests",
@@ -15434,6 +15813,7 @@
     ],
     shared_libs: [
         "libbase",
+        "libexpat",
         "libicu",
         "liblog",
         "libprocinfo",
@@ -15726,6 +16106,7 @@
         ":perfetto_src_android_stats_android_stats",
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_clock_snapshots",
         ":perfetto_src_base_test_support",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
@@ -15997,6 +16378,7 @@
         "protos/perfetto/config/android/windowmanager_config.proto",
         "protos/perfetto/config/chrome/chrome_config.proto",
         "protos/perfetto/config/chrome/scenario_config.proto",
+        "protos/perfetto/config/chrome/system_metrics.proto",
         "protos/perfetto/config/chrome/v8_config.proto",
         "protos/perfetto/config/data_source_config.proto",
         "protos/perfetto/config/etw/etw_config.proto",
@@ -16097,9 +16479,11 @@
         "protos/perfetto/trace/ftrace/clk.proto",
         "protos/perfetto/trace/ftrace/cma.proto",
         "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpm_trace.proto",
         "protos/perfetto/trace/ftrace/cpuhp.proto",
         "protos/perfetto/trace/ftrace/cros_ec.proto",
         "protos/perfetto/trace/ftrace/dcvsh.proto",
+        "protos/perfetto/trace/ftrace/devfreq.proto",
         "protos/perfetto/trace/ftrace/dma_fence.proto",
         "protos/perfetto/trace/ftrace/dmabuf_heap.proto",
         "protos/perfetto/trace/ftrace/dpu.proto",
@@ -16136,6 +16520,7 @@
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/panel.proto",
         "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/pixel_mm.proto",
         "protos/perfetto/trace/ftrace/power.proto",
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
@@ -16268,8 +16653,11 @@
         ":perfetto_include_perfetto_ext_traced_sys_stats_counters",
         ":perfetto_include_perfetto_protozero_protozero",
         ":perfetto_include_perfetto_public_abi_base",
+        ":perfetto_include_perfetto_public_abi_public",
         ":perfetto_include_perfetto_public_base",
+        ":perfetto_include_perfetto_public_protos_protos",
         ":perfetto_include_perfetto_public_protozero",
+        ":perfetto_include_perfetto_public_public",
         ":perfetto_include_perfetto_trace_processor_basic_types",
         ":perfetto_include_perfetto_trace_processor_storage",
         ":perfetto_include_perfetto_trace_processor_trace_processor",
@@ -16313,6 +16701,7 @@
         ":perfetto_protos_third_party_pprof_zero_gen",
         ":perfetto_protos_third_party_simpleperf_zero_gen",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_clock_snapshots",
         ":perfetto_src_base_http_http",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
@@ -16330,6 +16719,9 @@
         ":perfetto_src_trace_processor_export_json",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_bugreport",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
+        ":perfetto_src_trace_processor_importers_archive_archive",
+        ":perfetto_src_trace_processor_importers_art_method_art_method",
+        ":perfetto_src_trace_processor_importers_art_method_art_method_event",
         ":perfetto_src_trace_processor_importers_common_common",
         ":perfetto_src_trace_processor_importers_common_parser_types",
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
@@ -16341,14 +16733,18 @@
         ":perfetto_src_trace_processor_importers_fuchsia_fuchsia_record",
         ":perfetto_src_trace_processor_importers_fuchsia_full",
         ":perfetto_src_trace_processor_importers_fuchsia_minimal",
-        ":perfetto_src_trace_processor_importers_gzip_full",
+        ":perfetto_src_trace_processor_importers_gecko_gecko_event",
         ":perfetto_src_trace_processor_importers_i2c_full",
-        ":perfetto_src_trace_processor_importers_json_full",
+        ":perfetto_src_trace_processor_importers_instruments_instruments",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
         ":perfetto_src_trace_processor_importers_ninja_ninja",
         ":perfetto_src_trace_processor_importers_perf_perf",
         ":perfetto_src_trace_processor_importers_perf_record",
+        ":perfetto_src_trace_processor_importers_perf_text_perf_text",
+        ":perfetto_src_trace_processor_importers_perf_text_perf_text_event",
+        ":perfetto_src_trace_processor_importers_perf_text_perf_text_sample_line_parser",
         ":perfetto_src_trace_processor_importers_perf_tracker",
         ":perfetto_src_trace_processor_importers_proto_full",
         ":perfetto_src_trace_processor_importers_proto_minimal",
@@ -16359,17 +16755,22 @@
         ":perfetto_src_trace_processor_importers_systrace_full",
         ":perfetto_src_trace_processor_importers_systrace_systrace_line",
         ":perfetto_src_trace_processor_importers_systrace_systrace_parser",
-        ":perfetto_src_trace_processor_importers_zip_full",
         ":perfetto_src_trace_processor_lib",
         ":perfetto_src_trace_processor_metatrace",
         ":perfetto_src_trace_processor_metrics_metrics",
         ":perfetto_src_trace_processor_perfetto_sql_engine_engine",
+        ":perfetto_src_trace_processor_perfetto_sql_grammar_grammar",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_functions_functions",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_functions_interface",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_operators_operators",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_table_functions_interface",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_table_functions_table_functions",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_types_types",
+        ":perfetto_src_trace_processor_perfetto_sql_parser_parser",
+        ":perfetto_src_trace_processor_perfetto_sql_preprocessor_grammar",
+        ":perfetto_src_trace_processor_perfetto_sql_preprocessor_preprocessor",
+        ":perfetto_src_trace_processor_perfetto_sql_tokenizer_tokenize_internal",
+        ":perfetto_src_trace_processor_perfetto_sql_tokenizer_tokenizer",
         ":perfetto_src_trace_processor_rpc_httpd",
         ":perfetto_src_trace_processor_rpc_rpc",
         ":perfetto_src_trace_processor_rpc_stdiod",
@@ -16398,6 +16799,7 @@
         ":perfetto_src_trace_processor_util_trace_blob_view_reader",
         ":perfetto_src_trace_processor_util_trace_type",
         ":perfetto_src_trace_processor_util_util",
+        ":perfetto_src_trace_processor_util_winscope_proto_mapping",
         ":perfetto_src_trace_processor_util_zip_reader",
         "src/trace_processor/trace_processor_shell.cc",
     ],
@@ -16476,6 +16878,7 @@
     target: {
         android: {
             shared_libs: [
+                "libexpat",
                 "libicu",
                 "liblog",
                 "libprotobuf-cpp-full",
@@ -16489,6 +16892,7 @@
         },
         host: {
             static_libs: [
+                "libexpat",
                 "libprotobuf-cpp-full",
                 "libsqlite_static_noicu",
                 "libz",
@@ -16567,15 +16971,20 @@
         ":perfetto_src_trace_processor_db_compare",
         ":perfetto_src_trace_processor_db_minimal",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
+        ":perfetto_src_trace_processor_importers_art_method_art_method_event",
         ":perfetto_src_trace_processor_importers_common_common",
         ":perfetto_src_trace_processor_importers_common_parser_types",
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
         ":perfetto_src_trace_processor_importers_etw_minimal",
         ":perfetto_src_trace_processor_importers_ftrace_minimal",
         ":perfetto_src_trace_processor_importers_fuchsia_fuchsia_record",
+        ":perfetto_src_trace_processor_importers_gecko_gecko_event",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
         ":perfetto_src_trace_processor_importers_perf_record",
+        ":perfetto_src_trace_processor_importers_perf_text_perf_text_event",
+        ":perfetto_src_trace_processor_importers_perf_text_perf_text_sample_line_parser",
         ":perfetto_src_trace_processor_importers_perf_tracker",
         ":perfetto_src_trace_processor_importers_proto_minimal",
         ":perfetto_src_trace_processor_importers_proto_packet_sequence_state_generation_hdr",
@@ -16673,8 +17082,11 @@
         ":perfetto_include_perfetto_profiling_pprof_builder",
         ":perfetto_include_perfetto_protozero_protozero",
         ":perfetto_include_perfetto_public_abi_base",
+        ":perfetto_include_perfetto_public_abi_public",
         ":perfetto_include_perfetto_public_base",
+        ":perfetto_include_perfetto_public_protos_protos",
         ":perfetto_include_perfetto_public_protozero",
+        ":perfetto_include_perfetto_public_public",
         ":perfetto_include_perfetto_trace_processor_basic_types",
         ":perfetto_include_perfetto_trace_processor_storage",
         ":perfetto_include_perfetto_trace_processor_trace_processor",
@@ -16718,6 +17130,7 @@
         ":perfetto_protos_third_party_pprof_zero_gen",
         ":perfetto_protos_third_party_simpleperf_zero_gen",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_clock_snapshots",
         ":perfetto_src_base_version",
         ":perfetto_src_kernel_utils_syscall_table",
         ":perfetto_src_profiling_deobfuscator",
@@ -16733,6 +17146,9 @@
         ":perfetto_src_trace_processor_export_json",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_bugreport",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
+        ":perfetto_src_trace_processor_importers_archive_archive",
+        ":perfetto_src_trace_processor_importers_art_method_art_method",
+        ":perfetto_src_trace_processor_importers_art_method_art_method_event",
         ":perfetto_src_trace_processor_importers_common_common",
         ":perfetto_src_trace_processor_importers_common_parser_types",
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
@@ -16744,14 +17160,18 @@
         ":perfetto_src_trace_processor_importers_fuchsia_fuchsia_record",
         ":perfetto_src_trace_processor_importers_fuchsia_full",
         ":perfetto_src_trace_processor_importers_fuchsia_minimal",
-        ":perfetto_src_trace_processor_importers_gzip_full",
+        ":perfetto_src_trace_processor_importers_gecko_gecko_event",
         ":perfetto_src_trace_processor_importers_i2c_full",
-        ":perfetto_src_trace_processor_importers_json_full",
+        ":perfetto_src_trace_processor_importers_instruments_instruments",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
         ":perfetto_src_trace_processor_importers_ninja_ninja",
         ":perfetto_src_trace_processor_importers_perf_perf",
         ":perfetto_src_trace_processor_importers_perf_record",
+        ":perfetto_src_trace_processor_importers_perf_text_perf_text",
+        ":perfetto_src_trace_processor_importers_perf_text_perf_text_event",
+        ":perfetto_src_trace_processor_importers_perf_text_perf_text_sample_line_parser",
         ":perfetto_src_trace_processor_importers_perf_tracker",
         ":perfetto_src_trace_processor_importers_proto_full",
         ":perfetto_src_trace_processor_importers_proto_minimal",
@@ -16762,17 +17182,22 @@
         ":perfetto_src_trace_processor_importers_systrace_full",
         ":perfetto_src_trace_processor_importers_systrace_systrace_line",
         ":perfetto_src_trace_processor_importers_systrace_systrace_parser",
-        ":perfetto_src_trace_processor_importers_zip_full",
         ":perfetto_src_trace_processor_lib",
         ":perfetto_src_trace_processor_metatrace",
         ":perfetto_src_trace_processor_metrics_metrics",
         ":perfetto_src_trace_processor_perfetto_sql_engine_engine",
+        ":perfetto_src_trace_processor_perfetto_sql_grammar_grammar",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_functions_functions",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_functions_interface",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_operators_operators",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_table_functions_interface",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_table_functions_table_functions",
         ":perfetto_src_trace_processor_perfetto_sql_intrinsics_types_types",
+        ":perfetto_src_trace_processor_perfetto_sql_parser_parser",
+        ":perfetto_src_trace_processor_perfetto_sql_preprocessor_grammar",
+        ":perfetto_src_trace_processor_perfetto_sql_preprocessor_preprocessor",
+        ":perfetto_src_trace_processor_perfetto_sql_tokenizer_tokenize_internal",
+        ":perfetto_src_trace_processor_perfetto_sql_tokenizer_tokenizer",
         ":perfetto_src_trace_processor_sorter_sorter",
         ":perfetto_src_trace_processor_sqlite_bindings_bindings",
         ":perfetto_src_trace_processor_sqlite_sqlite",
@@ -16798,6 +17223,7 @@
         ":perfetto_src_trace_processor_util_trace_blob_view_reader",
         ":perfetto_src_trace_processor_util_trace_type",
         ":perfetto_src_trace_processor_util_util",
+        ":perfetto_src_trace_processor_util_winscope_proto_mapping",
         ":perfetto_src_trace_processor_util_zip_reader",
         ":perfetto_src_traceconv_lib",
         ":perfetto_src_traceconv_main",
@@ -16805,6 +17231,7 @@
         ":perfetto_src_traceconv_utils",
     ],
     static_libs: [
+        "libexpat",
         "libsqlite_static_noicu",
         "libz",
         "perfetto_src_trace_processor_demangle",
@@ -16985,6 +17412,7 @@
         ":perfetto_src_android_stats_android_stats",
         ":perfetto_src_android_stats_perfetto_atoms",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_clock_snapshots",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_ipc_client",
@@ -17186,6 +17614,7 @@
         ":perfetto_protos_perfetto_trace_track_event_zero_gen",
         ":perfetto_protos_perfetto_trace_translation_zero_gen",
         ":perfetto_src_base_base",
+        ":perfetto_src_base_clock_snapshots",
         ":perfetto_src_base_unix_socket",
         ":perfetto_src_base_version",
         ":perfetto_src_ipc_client",
@@ -17620,17 +18049,20 @@
 }
 
 java_library {
-    name: "perfetto_config_java_protos",
+    name: "perfetto_winscope-lite",
+    proto: {
+        type: "lite",
+        include_dirs: ["external/protobuf/src"],
+        canonical_path_from_root: false,
+    },
     srcs: [
-        ":perfetto_config_filegroup_proto",
+        ":libprotobuf-internal-descriptor-proto",
+        ":perfetto_winscope_filegroup_proto",
     ],
     static_libs: [
         "libprotobuf-java-lite",
     ],
-    proto: {
-        type: "lite",
-        canonical_path_from_root: false,
-    },
+    sdk_version: "current",
 }
 
 java_library {
@@ -17668,3 +18100,10 @@
         "trigger_perfetto",
     ],
 }
+
+filegroup {
+    name: "heap_profile",
+    srcs: [
+        "tools/heap_profile",
+    ],
+}
diff --git a/Android.bp.extras b/Android.bp.extras
index 51a9345..3f6101b 100644
--- a/Android.bp.extras
+++ b/Android.bp.extras
@@ -199,17 +199,20 @@
 }
 
 java_library {
-    name: "perfetto_config_java_protos",
+    name: "perfetto_winscope-lite",
+    proto: {
+        type: "lite",
+        include_dirs: ["external/protobuf/src"],
+        canonical_path_from_root: false,
+    },
     srcs: [
-        ":perfetto_config_filegroup_proto",
+        ":libprotobuf-internal-descriptor-proto",
+        ":perfetto_winscope_filegroup_proto",
     ],
     static_libs: [
         "libprotobuf-java-lite",
     ],
-    proto: {
-        type: "lite",
-        canonical_path_from_root: false,
-    },
+    sdk_version: "current",
 }
 
 java_library {
@@ -247,3 +250,10 @@
         "trigger_perfetto",
     ],
 }
+
+filegroup {
+    name: "heap_profile",
+    srcs: [
+        "tools/heap_profile",
+    ],
+}
diff --git a/BUILD b/BUILD
index 18695c7..66c9ff0 100644
--- a/BUILD
+++ b/BUILD
@@ -152,6 +152,113 @@
     linkstatic = True,
 )
 
+# GN target: //src/shared_lib:libperfetto_c
+perfetto_cc_library(
+    name = "libperfetto_c",
+    srcs = [
+        ":src_android_stats_android_stats",
+        ":src_android_stats_perfetto_atoms",
+        ":src_protozero_filtering_bytecode_common",
+        ":src_protozero_filtering_bytecode_parser",
+        ":src_protozero_filtering_message_filter",
+        ":src_protozero_filtering_string_filter",
+        ":src_shared_lib_intern_map",
+        ":src_shared_lib_shared_lib",
+        ":src_tracing_client_api_without_backends",
+        ":src_tracing_common",
+        ":src_tracing_core_core",
+        ":src_tracing_in_process_backend",
+        ":src_tracing_ipc_common",
+        ":src_tracing_ipc_consumer_consumer",
+        ":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",
+    ],
+    hdrs = [
+        ":include_perfetto_base_base",
+        ":include_perfetto_ext_base_base",
+        ":include_perfetto_ext_ipc_ipc",
+        ":include_perfetto_ext_tracing_core_core",
+        ":include_perfetto_ext_tracing_ipc_ipc",
+        ":include_perfetto_protozero_protozero",
+        ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_abi_public",
+        ":include_perfetto_public_base",
+        ":include_perfetto_public_protos_protos",
+        ":include_perfetto_public_protozero",
+        ":include_perfetto_public_public",
+        ":include_perfetto_tracing_core_core",
+        ":include_perfetto_tracing_core_forward_decls",
+        ":include_perfetto_tracing_tracing",
+    ],
+    defines = [
+        "PERFETTO_SHLIB_SDK_IMPLEMENTATION",
+    ],
+    visibility = PERFETTO_CONFIG.public_visibility,
+    deps = [
+        ":perfetto_ipc",
+        ":protos_perfetto_common_cpp",
+        ":protos_perfetto_common_zero",
+        ":protos_perfetto_config_android_cpp",
+        ":protos_perfetto_config_android_zero",
+        ":protos_perfetto_config_cpp",
+        ":protos_perfetto_config_ftrace_cpp",
+        ":protos_perfetto_config_ftrace_zero",
+        ":protos_perfetto_config_gpu_cpp",
+        ":protos_perfetto_config_gpu_zero",
+        ":protos_perfetto_config_inode_file_cpp",
+        ":protos_perfetto_config_inode_file_zero",
+        ":protos_perfetto_config_interceptors_cpp",
+        ":protos_perfetto_config_interceptors_zero",
+        ":protos_perfetto_config_power_cpp",
+        ":protos_perfetto_config_power_zero",
+        ":protos_perfetto_config_process_stats_cpp",
+        ":protos_perfetto_config_process_stats_zero",
+        ":protos_perfetto_config_profiling_cpp",
+        ":protos_perfetto_config_profiling_zero",
+        ":protos_perfetto_config_statsd_cpp",
+        ":protos_perfetto_config_statsd_zero",
+        ":protos_perfetto_config_sys_stats_cpp",
+        ":protos_perfetto_config_sys_stats_zero",
+        ":protos_perfetto_config_system_info_cpp",
+        ":protos_perfetto_config_system_info_zero",
+        ":protos_perfetto_config_track_event_cpp",
+        ":protos_perfetto_config_track_event_zero",
+        ":protos_perfetto_config_zero",
+        ":protos_perfetto_ipc_cpp",
+        ":protos_perfetto_ipc_ipc",
+        ":protos_perfetto_trace_android_winscope_common_zero",
+        ":protos_perfetto_trace_android_winscope_regular_zero",
+        ":protos_perfetto_trace_android_zero",
+        ":protos_perfetto_trace_chrome_zero",
+        ":protos_perfetto_trace_etw_zero",
+        ":protos_perfetto_trace_filesystem_zero",
+        ":protos_perfetto_trace_ftrace_zero",
+        ":protos_perfetto_trace_gpu_zero",
+        ":protos_perfetto_trace_interned_data_zero",
+        ":protos_perfetto_trace_minimal_zero",
+        ":protos_perfetto_trace_non_minimal_zero",
+        ":protos_perfetto_trace_perfetto_zero",
+        ":protos_perfetto_trace_power_zero",
+        ":protos_perfetto_trace_profiling_zero",
+        ":protos_perfetto_trace_ps_zero",
+        ":protos_perfetto_trace_statsd_zero",
+        ":protos_perfetto_trace_sys_stats_zero",
+        ":protos_perfetto_trace_system_info_zero",
+        ":protos_perfetto_trace_track_event_cpp",
+        ":protos_perfetto_trace_track_event_zero",
+        ":protos_perfetto_trace_translation_zero",
+        ":protozero",
+        ":src_base_base",
+        ":src_base_clock_snapshots",
+        ":src_base_version",
+    ],
+    linkstatic = True,
+)
+
 # GN target: //src/tools/proto_filter:proto_filter
 perfetto_cc_binary(
     name = "proto_filter",
@@ -223,6 +330,9 @@
         ":src_trace_processor_export_json",
         ":src_trace_processor_importers_android_bugreport_android_bugreport",
         ":src_trace_processor_importers_android_bugreport_android_log_event",
+        ":src_trace_processor_importers_archive_archive",
+        ":src_trace_processor_importers_art_method_art_method",
+        ":src_trace_processor_importers_art_method_art_method_event",
         ":src_trace_processor_importers_common_common",
         ":src_trace_processor_importers_common_parser_types",
         ":src_trace_processor_importers_common_trace_parser_hdr",
@@ -234,14 +344,20 @@
         ":src_trace_processor_importers_fuchsia_fuchsia_record",
         ":src_trace_processor_importers_fuchsia_full",
         ":src_trace_processor_importers_fuchsia_minimal",
-        ":src_trace_processor_importers_gzip_full",
+        ":src_trace_processor_importers_gecko_gecko",
+        ":src_trace_processor_importers_gecko_gecko_event",
         ":src_trace_processor_importers_i2c_full",
-        ":src_trace_processor_importers_json_full",
+        ":src_trace_processor_importers_instruments_instruments",
+        ":src_trace_processor_importers_instruments_row",
+        ":src_trace_processor_importers_json_json",
         ":src_trace_processor_importers_json_minimal",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
         ":src_trace_processor_importers_ninja_ninja",
         ":src_trace_processor_importers_perf_perf",
         ":src_trace_processor_importers_perf_record",
+        ":src_trace_processor_importers_perf_text_perf_text",
+        ":src_trace_processor_importers_perf_text_perf_text_event",
+        ":src_trace_processor_importers_perf_text_perf_text_sample_line_parser",
         ":src_trace_processor_importers_perf_tracker",
         ":src_trace_processor_importers_proto_full",
         ":src_trace_processor_importers_proto_minimal",
@@ -252,11 +368,11 @@
         ":src_trace_processor_importers_systrace_full",
         ":src_trace_processor_importers_systrace_systrace_line",
         ":src_trace_processor_importers_systrace_systrace_parser",
-        ":src_trace_processor_importers_zip_full",
         ":src_trace_processor_lib",
         ":src_trace_processor_metatrace",
         ":src_trace_processor_metrics_metrics",
         ":src_trace_processor_perfetto_sql_engine_engine",
+        ":src_trace_processor_perfetto_sql_grammar_grammar",
         ":src_trace_processor_perfetto_sql_intrinsics_functions_functions",
         ":src_trace_processor_perfetto_sql_intrinsics_functions_interface",
         ":src_trace_processor_perfetto_sql_intrinsics_functions_tables",
@@ -265,6 +381,11 @@
         ":src_trace_processor_perfetto_sql_intrinsics_table_functions_table_functions",
         ":src_trace_processor_perfetto_sql_intrinsics_table_functions_tables",
         ":src_trace_processor_perfetto_sql_intrinsics_types_types",
+        ":src_trace_processor_perfetto_sql_parser_parser",
+        ":src_trace_processor_perfetto_sql_preprocessor_grammar",
+        ":src_trace_processor_perfetto_sql_preprocessor_preprocessor",
+        ":src_trace_processor_perfetto_sql_tokenizer_tokenize_internal",
+        ":src_trace_processor_perfetto_sql_tokenizer_tokenizer",
         ":src_trace_processor_rpc_rpc",
         ":src_trace_processor_sorter_sorter",
         ":src_trace_processor_sqlite_bindings_bindings",
@@ -292,6 +413,7 @@
         ":src_trace_processor_util_trace_blob_view_reader",
         ":src_trace_processor_util_trace_type",
         ":src_trace_processor_util_util",
+        ":src_trace_processor_util_winscope_proto_mapping",
         ":src_trace_processor_util_zip_reader",
     ],
     hdrs = [
@@ -305,8 +427,11 @@
         ":include_perfetto_ext_traced_sys_stats_counters",
         ":include_perfetto_protozero_protozero",
         ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_abi_public",
         ":include_perfetto_public_base",
+        ":include_perfetto_public_protos_protos",
         ":include_perfetto_public_protozero",
+        ":include_perfetto_public_public",
         ":include_perfetto_trace_processor_basic_types",
         ":include_perfetto_trace_processor_storage",
         ":include_perfetto_trace_processor_trace_processor",
@@ -353,6 +478,7 @@
                ":protos_third_party_simpleperf_zero",
                ":protozero",
                ":src_base_base",
+               ":src_base_clock_snapshots",
                ":src_base_version",
                ":src_trace_processor_containers_containers",
                ":src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
@@ -367,7 +493,8 @@
                ":src_trace_processor_metrics_gen_cc_metrics_descriptor",
                ":src_trace_processor_metrics_sql_gen_amalgamated_sql_metrics",
                ":src_trace_processor_perfetto_sql_stdlib_stdlib",
-           ] + PERFETTO_CONFIG.deps.jsoncpp +
+           ] + PERFETTO_CONFIG.deps.expat +
+           PERFETTO_CONFIG.deps.jsoncpp +
            PERFETTO_CONFIG.deps.sqlite +
            PERFETTO_CONFIG.deps.sqlite_ext_percentile +
            PERFETTO_CONFIG.deps.zlib +
@@ -565,6 +692,7 @@
         ":protos_third_party_statsd_config_zero",
         ":protozero",
         ":src_base_base",
+        ":src_base_clock_snapshots",
         ":src_base_version",
     ] + PERFETTO_CONFIG.deps.zlib,
     linkstatic = True,
@@ -584,6 +712,7 @@
         "include/perfetto/base/status.h",
         "include/perfetto/base/task_runner.h",
         "include/perfetto/base/template_util.h",
+        "include/perfetto/base/thread_annotations.h",
         "include/perfetto/base/thread_utils.h",
         "include/perfetto/base/time.h",
     ],
@@ -605,6 +734,7 @@
         "include/perfetto/ext/base/android_utils.h",
         "include/perfetto/ext/base/base64.h",
         "include/perfetto/ext/base/circular_queue.h",
+        "include/perfetto/ext/base/clock_snapshots.h",
         "include/perfetto/ext/base/container_annotations.h",
         "include/perfetto/ext/base/crash_keys.h",
         "include/perfetto/ext/base/ctrl_c_handler.h",
@@ -919,7 +1049,6 @@
     name = "include_perfetto_tracing_core_core",
     srcs = [
         "include/perfetto/tracing/core/chrome_config.h",
-        "include/perfetto/tracing/core/clock_snapshots.h",
         "include/perfetto/tracing/core/data_source_config.h",
         "include/perfetto/tracing/core/data_source_descriptor.h",
         "include/perfetto/tracing/core/flush_flags.h",
@@ -1095,6 +1224,24 @@
     linkstatic = True,
 )
 
+# GN target: //src/base:clock_snapshots
+perfetto_cc_library(
+    name = "src_base_clock_snapshots",
+    srcs = [
+        "src/base/clock_snapshots.cc",
+    ],
+    hdrs = [
+        ":include_perfetto_base_base",
+        ":include_perfetto_ext_base_base",
+        ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_base",
+    ],
+    deps = [
+        ":protos_perfetto_common_zero",
+    ],
+    linkstatic = True,
+)
+
 # GN target: //src/base:unix_socket
 perfetto_cc_library(
     name = "src_base_unix_socket",
@@ -1229,8 +1376,6 @@
         "src/perfetto_cmd/packet_writer.h",
         "src/perfetto_cmd/perfetto_cmd.cc",
         "src/perfetto_cmd/perfetto_cmd.h",
-        "src/perfetto_cmd/rate_limiter.cc",
-        "src/perfetto_cmd/rate_limiter.h",
     ],
 )
 
@@ -1501,6 +1646,40 @@
     ],
 )
 
+# GN target: //src/trace_processor/importers/archive:archive
+perfetto_filegroup(
+    name = "src_trace_processor_importers_archive_archive",
+    srcs = [
+        "src/trace_processor/importers/archive/archive_entry.cc",
+        "src/trace_processor/importers/archive/archive_entry.h",
+        "src/trace_processor/importers/archive/gzip_trace_parser.cc",
+        "src/trace_processor/importers/archive/gzip_trace_parser.h",
+        "src/trace_processor/importers/archive/tar_trace_reader.cc",
+        "src/trace_processor/importers/archive/tar_trace_reader.h",
+        "src/trace_processor/importers/archive/zip_trace_reader.cc",
+        "src/trace_processor/importers/archive/zip_trace_reader.h",
+    ],
+)
+
+# GN target: //src/trace_processor/importers/art_method:art_method
+perfetto_filegroup(
+    name = "src_trace_processor_importers_art_method_art_method",
+    srcs = [
+        "src/trace_processor/importers/art_method/art_method_parser_impl.cc",
+        "src/trace_processor/importers/art_method/art_method_parser_impl.h",
+        "src/trace_processor/importers/art_method/art_method_tokenizer.cc",
+        "src/trace_processor/importers/art_method/art_method_tokenizer.h",
+    ],
+)
+
+# GN target: //src/trace_processor/importers/art_method:art_method_event
+perfetto_filegroup(
+    name = "src_trace_processor_importers_art_method_art_method_event",
+    srcs = [
+        "src/trace_processor/importers/art_method/art_method_event.h",
+    ],
+)
+
 # GN target: //src/trace_processor/importers/common:common
 perfetto_filegroup(
     name = "src_trace_processor_importers_common_common",
@@ -1530,6 +1709,8 @@
         "src/trace_processor/importers/common/global_args_tracker.h",
         "src/trace_processor/importers/common/jit_cache.cc",
         "src/trace_processor/importers/common/jit_cache.h",
+        "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc",
+        "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h",
         "src/trace_processor/importers/common/machine_tracker.cc",
         "src/trace_processor/importers/common/machine_tracker.h",
         "src/trace_processor/importers/common/mapping_tracker.cc",
@@ -1543,8 +1724,6 @@
         "src/trace_processor/importers/common/sched_event_state.h",
         "src/trace_processor/importers/common/sched_event_tracker.cc",
         "src/trace_processor/importers/common/sched_event_tracker.h",
-        "src/trace_processor/importers/common/scoped_active_trace_file.cc",
-        "src/trace_processor/importers/common/scoped_active_trace_file.h",
         "src/trace_processor/importers/common/slice_tracker.cc",
         "src/trace_processor/importers/common/slice_tracker.h",
         "src/trace_processor/importers/common/slice_translation_table.cc",
@@ -1560,6 +1739,7 @@
         "src/trace_processor/importers/common/trace_parser.cc",
         "src/trace_processor/importers/common/track_tracker.cc",
         "src/trace_processor/importers/common/track_tracker.h",
+        "src/trace_processor/importers/common/tracks.h",
         "src/trace_processor/importers/common/virtual_memory_mapping.cc",
         "src/trace_processor/importers/common/virtual_memory_mapping.h",
     ],
@@ -1634,6 +1814,8 @@
         "src/trace_processor/importers/ftrace/iostat_tracker.h",
         "src/trace_processor/importers/ftrace/mali_gpu_event_tracker.cc",
         "src/trace_processor/importers/ftrace/mali_gpu_event_tracker.h",
+        "src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.cc",
+        "src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.h",
         "src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.cc",
         "src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.h",
         "src/trace_processor/importers/ftrace/rss_stat_tracker.cc",
@@ -1687,12 +1869,22 @@
     ],
 )
 
-# GN target: //src/trace_processor/importers/gzip:full
+# GN target: //src/trace_processor/importers/gecko:gecko
 perfetto_filegroup(
-    name = "src_trace_processor_importers_gzip_full",
+    name = "src_trace_processor_importers_gecko_gecko",
     srcs = [
-        "src/trace_processor/importers/gzip/gzip_trace_parser.cc",
-        "src/trace_processor/importers/gzip/gzip_trace_parser.h",
+        "src/trace_processor/importers/gecko/gecko_trace_parser_impl.cc",
+        "src/trace_processor/importers/gecko/gecko_trace_parser_impl.h",
+        "src/trace_processor/importers/gecko/gecko_trace_tokenizer.cc",
+        "src/trace_processor/importers/gecko/gecko_trace_tokenizer.h",
+    ],
+)
+
+# GN target: //src/trace_processor/importers/gecko:gecko_event
+perfetto_filegroup(
+    name = "src_trace_processor_importers_gecko_gecko_event",
+    srcs = [
+        "src/trace_processor/importers/gecko/gecko_event.h",
     ],
 )
 
@@ -1705,9 +1897,30 @@
     ],
 )
 
-# GN target: //src/trace_processor/importers/json:full
+# GN target: //src/trace_processor/importers/instruments:instruments
 perfetto_filegroup(
-    name = "src_trace_processor_importers_json_full",
+    name = "src_trace_processor_importers_instruments_instruments",
+    srcs = [
+        "src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc",
+        "src/trace_processor/importers/instruments/instruments_xml_tokenizer.h",
+        "src/trace_processor/importers/instruments/row_data_tracker.cc",
+        "src/trace_processor/importers/instruments/row_data_tracker.h",
+        "src/trace_processor/importers/instruments/row_parser.cc",
+        "src/trace_processor/importers/instruments/row_parser.h",
+    ],
+)
+
+# GN target: //src/trace_processor/importers/instruments:row
+perfetto_filegroup(
+    name = "src_trace_processor_importers_instruments_row",
+    srcs = [
+        "src/trace_processor/importers/instruments/row.h",
+    ],
+)
+
+# GN target: //src/trace_processor/importers/json:json
+perfetto_filegroup(
+    name = "src_trace_processor_importers_json_json",
     srcs = [
         "src/trace_processor/importers/json/json_trace_parser_impl.cc",
         "src/trace_processor/importers/json/json_trace_parser_impl.h",
@@ -1752,8 +1965,22 @@
     srcs = [
         "src/trace_processor/importers/perf/attrs_section_reader.cc",
         "src/trace_processor/importers/perf/attrs_section_reader.h",
+        "src/trace_processor/importers/perf/aux_data_tokenizer.cc",
+        "src/trace_processor/importers/perf/aux_data_tokenizer.h",
+        "src/trace_processor/importers/perf/aux_record.cc",
+        "src/trace_processor/importers/perf/aux_record.h",
+        "src/trace_processor/importers/perf/aux_stream_manager.cc",
+        "src/trace_processor/importers/perf/aux_stream_manager.h",
+        "src/trace_processor/importers/perf/auxtrace_info_record.cc",
+        "src/trace_processor/importers/perf/auxtrace_info_record.h",
+        "src/trace_processor/importers/perf/auxtrace_record.cc",
+        "src/trace_processor/importers/perf/auxtrace_record.h",
+        "src/trace_processor/importers/perf/etm_tokenizer.cc",
+        "src/trace_processor/importers/perf/etm_tokenizer.h",
         "src/trace_processor/importers/perf/features.cc",
         "src/trace_processor/importers/perf/features.h",
+        "src/trace_processor/importers/perf/itrace_start_record.cc",
+        "src/trace_processor/importers/perf/itrace_start_record.h",
         "src/trace_processor/importers/perf/mmap_record.cc",
         "src/trace_processor/importers/perf/mmap_record.h",
         "src/trace_processor/importers/perf/perf_data_tokenizer.cc",
@@ -1763,6 +1990,15 @@
         "src/trace_processor/importers/perf/record_parser.h",
         "src/trace_processor/importers/perf/sample.cc",
         "src/trace_processor/importers/perf/sample.h",
+        "src/trace_processor/importers/perf/sample_id.cc",
+        "src/trace_processor/importers/perf/sample_id.h",
+        "src/trace_processor/importers/perf/spe.h",
+        "src/trace_processor/importers/perf/spe_record_parser.cc",
+        "src/trace_processor/importers/perf/spe_record_parser.h",
+        "src/trace_processor/importers/perf/spe_tokenizer.cc",
+        "src/trace_processor/importers/perf/spe_tokenizer.h",
+        "src/trace_processor/importers/perf/time_conv_record.h",
+        "src/trace_processor/importers/perf/util.h",
     ],
 )
 
@@ -1791,6 +2027,34 @@
     ],
 )
 
+# GN target: //src/trace_processor/importers/perf_text:perf_text
+perfetto_filegroup(
+    name = "src_trace_processor_importers_perf_text_perf_text",
+    srcs = [
+        "src/trace_processor/importers/perf_text/perf_text_trace_parser_impl.cc",
+        "src/trace_processor/importers/perf_text/perf_text_trace_parser_impl.h",
+        "src/trace_processor/importers/perf_text/perf_text_trace_tokenizer.cc",
+        "src/trace_processor/importers/perf_text/perf_text_trace_tokenizer.h",
+    ],
+)
+
+# GN target: //src/trace_processor/importers/perf_text:perf_text_event
+perfetto_filegroup(
+    name = "src_trace_processor_importers_perf_text_perf_text_event",
+    srcs = [
+        "src/trace_processor/importers/perf_text/perf_text_event.h",
+    ],
+)
+
+# GN target: //src/trace_processor/importers/perf_text:perf_text_sample_line_parser
+perfetto_filegroup(
+    name = "src_trace_processor_importers_perf_text_perf_text_sample_line_parser",
+    srcs = [
+        "src/trace_processor/importers/perf_text/perf_text_sample_line_parser.cc",
+        "src/trace_processor/importers/perf_text/perf_text_sample_line_parser.h",
+    ],
+)
+
 # GN target: //src/trace_processor/importers/proto/winscope:full
 perfetto_filegroup(
     name = "src_trace_processor_importers_proto_winscope_full",
@@ -1799,8 +2063,6 @@
         "src/trace_processor/importers/proto/winscope/android_input_event_parser.h",
         "src/trace_processor/importers/proto/winscope/protolog_message_decoder.cc",
         "src/trace_processor/importers/proto/winscope/protolog_message_decoder.h",
-        "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.cc",
-        "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h",
         "src/trace_processor/importers/proto/winscope/protolog_parser.cc",
         "src/trace_processor/importers/proto/winscope/protolog_parser.h",
         "src/trace_processor/importers/proto/winscope/shell_transitions_parser.cc",
@@ -2069,15 +2331,6 @@
     ],
 )
 
-# GN target: //src/trace_processor/importers/zip:full
-perfetto_filegroup(
-    name = "src_trace_processor_importers_zip_full",
-    srcs = [
-        "src/trace_processor/importers/zip/zip_trace_reader.cc",
-        "src/trace_processor/importers/zip/zip_trace_reader.h",
-    ],
-)
-
 # GN target: //src/trace_processor/metrics/sql/android:android
 perfetto_filegroup(
     name = "src_trace_processor_metrics_sql_android_android",
@@ -2122,7 +2375,6 @@
         "src/trace_processor/metrics/sql/android/android_multiuser_populator.sql",
         "src/trace_processor/metrics/sql/android/android_netperf.sql",
         "src/trace_processor/metrics/sql/android/android_oom_adjuster.sql",
-        "src/trace_processor/metrics/sql/android/android_other_traces.sql",
         "src/trace_processor/metrics/sql/android/android_package_list.sql",
         "src/trace_processor/metrics/sql/android/android_powrails.sql",
         "src/trace_processor/metrics/sql/android/android_proxy_power.sql",
@@ -2133,7 +2385,6 @@
         "src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql",
         "src/trace_processor/metrics/sql/android/android_task_names.sql",
         "src/trace_processor/metrics/sql/android/android_trace_quality.sql",
-        "src/trace_processor/metrics/sql/android/android_trusty_workqueues.sql",
         "src/trace_processor/metrics/sql/android/codec_metrics.sql",
         "src/trace_processor/metrics/sql/android/composer_execution.sql",
         "src/trace_processor/metrics/sql/android/composition_layers.sql",
@@ -2149,7 +2400,6 @@
         "src/trace_processor/metrics/sql/android/jank/cujs_boundaries.sql",
         "src/trace_processor/metrics/sql/android/jank/frames.sql",
         "src/trace_processor/metrics/sql/android/jank/internal/counters.sql",
-        "src/trace_processor/metrics/sql/android/jank/internal/derived_events.sql",
         "src/trace_processor/metrics/sql/android/jank/internal/query_base.sql",
         "src/trace_processor/metrics/sql/android/jank/internal/query_frame_slice.sql",
         "src/trace_processor/metrics/sql/android/jank/params.sql",
@@ -2199,13 +2449,16 @@
         "src/trace_processor/metrics/sql/android/startup/mcycles_per_launch.sql",
         "src/trace_processor/metrics/sql/android/startup/slice_functions.sql",
         "src/trace_processor/metrics/sql/android/startup/slow_start_reasons.sql",
+        "src/trace_processor/metrics/sql/android/startup/slow_start_thresholds.sql",
         "src/trace_processor/metrics/sql/android/startup/system_state.sql",
         "src/trace_processor/metrics/sql/android/startup/thread_state_breakdown.sql",
         "src/trace_processor/metrics/sql/android/sysui_notif_shade_list_builder_metric.sql",
         "src/trace_processor/metrics/sql/android/sysui_notif_shade_list_builder_slices.sql",
         "src/trace_processor/metrics/sql/android/sysui_update_notif_on_ui_mode_changed_metric.sql",
         "src/trace_processor/metrics/sql/android/unsymbolized_frames.sql",
-        "src/trace_processor/metrics/sql/android/wattson_app_startup.sql",
+        "src/trace_processor/metrics/sql/android/wattson_app_startup_rails.sql",
+        "src/trace_processor/metrics/sql/android/wattson_markers_rails.sql",
+        "src/trace_processor/metrics/sql/android/wattson_markers_threads.sql",
         "src/trace_processor/metrics/sql/android/wattson_rail_relations.sql",
         "src/trace_processor/metrics/sql/android/wattson_tasks_attribution.sql",
         "src/trace_processor/metrics/sql/android/wattson_trace_rails.sql",
@@ -2370,14 +2623,8 @@
     srcs = [
         "src/trace_processor/perfetto_sql/engine/created_function.cc",
         "src/trace_processor/perfetto_sql/engine/created_function.h",
-        "src/trace_processor/perfetto_sql/engine/function_util.cc",
-        "src/trace_processor/perfetto_sql/engine/function_util.h",
         "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc",
         "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h",
-        "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc",
-        "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h",
-        "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.cc",
-        "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h",
         "src/trace_processor/perfetto_sql/engine/runtime_table_function.cc",
         "src/trace_processor/perfetto_sql/engine/runtime_table_function.h",
         "src/trace_processor/perfetto_sql/engine/table_pointer_module.cc",
@@ -2385,6 +2632,17 @@
     ],
 )
 
+# GN target: //src/trace_processor/perfetto_sql/grammar:grammar
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_grammar_grammar",
+    srcs = [
+        "src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.c",
+        "src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.h",
+        "src/trace_processor/perfetto_sql/grammar/perfettosql_keywordhash.h",
+        "src/trace_processor/perfetto_sql/grammar/perfettosql_keywordhash_helper.h",
+    ],
+)
+
 # GN target: //src/trace_processor/perfetto_sql/intrinsics/functions:functions
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_intrinsics_functions_functions",
@@ -2392,6 +2650,8 @@
         "src/trace_processor/perfetto_sql/intrinsics/functions/base64.cc",
         "src/trace_processor/perfetto_sql/intrinsics/functions/base64.h",
         "src/trace_processor/perfetto_sql/intrinsics/functions/clock_functions.h",
+        "src/trace_processor/perfetto_sql/intrinsics/functions/counter_intervals.cc",
+        "src/trace_processor/perfetto_sql/intrinsics/functions/counter_intervals.h",
         "src/trace_processor/perfetto_sql/intrinsics/functions/create_function.cc",
         "src/trace_processor/perfetto_sql/intrinsics/functions/create_function.h",
         "src/trace_processor/perfetto_sql/intrinsics/functions/create_view_function.cc",
@@ -2453,8 +2713,6 @@
     srcs = [
         "src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc",
         "src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h",
-        "src/trace_processor/perfetto_sql/intrinsics/operators/interval_intersect_operator.cc",
-        "src/trace_processor/perfetto_sql/intrinsics/operators/interval_intersect_operator.h",
         "src/trace_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.cc",
         "src/trace_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.h",
         "src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc",
@@ -2501,6 +2759,8 @@
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/flamegraph_construction_algorithms.h",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.h",
+        "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.cc",
+        "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h",
     ],
 )
 
@@ -2523,6 +2783,7 @@
     name = "src_trace_processor_perfetto_sql_intrinsics_types_types",
     srcs = [
         "src/trace_processor/perfetto_sql/intrinsics/types/array.h",
+        "src/trace_processor/perfetto_sql/intrinsics/types/counter.h",
         "src/trace_processor/perfetto_sql/intrinsics/types/node.h",
         "src/trace_processor/perfetto_sql/intrinsics/types/partitioned_intervals.h",
         "src/trace_processor/perfetto_sql/intrinsics/types/row_dataframe.h",
@@ -2531,6 +2792,36 @@
     ],
 )
 
+# GN target: //src/trace_processor/perfetto_sql/parser:parser
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_parser_parser",
+    srcs = [
+        "src/trace_processor/perfetto_sql/parser/function_util.cc",
+        "src/trace_processor/perfetto_sql/parser/function_util.h",
+        "src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.cc",
+        "src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.h",
+    ],
+)
+
+# GN target: //src/trace_processor/perfetto_sql/preprocessor:grammar
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_preprocessor_grammar",
+    srcs = [
+        "src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.c",
+        "src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.h",
+        "src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar_interface.h",
+    ],
+)
+
+# GN target: //src/trace_processor/perfetto_sql/preprocessor:preprocessor
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_preprocessor_preprocessor",
+    srcs = [
+        "src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.cc",
+        "src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.h",
+    ],
+)
+
 # GN target: //src/trace_processor/perfetto_sql/stdlib/android/auto:auto
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_android_auto_auto",
@@ -2539,6 +2830,14 @@
     ],
 )
 
+# GN target: //src/trace_processor/perfetto_sql/stdlib/android/battery:battery
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_stdlib_android_battery_battery",
+    srcs = [
+        "src/trace_processor/perfetto_sql/stdlib/android/battery/charging_states.sql",
+    ],
+)
+
 # GN target: //src/trace_processor/perfetto_sql/stdlib/android/cpu:cpu
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_android_cpu_cpu",
@@ -2564,6 +2863,7 @@
     srcs = [
         "src/trace_processor/perfetto_sql/stdlib/android/gpu/frequency.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/gpu/memory.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/gpu/work_period.sql",
     ],
 )
 
@@ -2571,6 +2871,7 @@
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_android_memory_heap_graph_heap_graph",
     srcs = [
+        "src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/class_summary_tree.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/class_tree.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/dominator_class_tree.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/dominator_tree.sql",
@@ -2586,6 +2887,7 @@
     name = "src_trace_processor_perfetto_sql_stdlib_android_memory_heap_profile_heap_profile",
     srcs = [
         "src/trace_processor/perfetto_sql/stdlib/android/memory/heap_profile/callstacks.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/memory/heap_profile/summary_tree.sql",
     ],
 )
 
@@ -2602,6 +2904,7 @@
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_android_startup_startup",
     srcs = [
+        "src/trace_processor/perfetto_sql/stdlib/android/startup/startup_breakdowns.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/startup/startup_events.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/startup/startups.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/startup/startups_maxsdk28.sql",
@@ -2633,6 +2936,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/binder_breakdown.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/broadcasts.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/desktop_mode.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/device.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/dvfs.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/freezer.sql",
@@ -2640,6 +2944,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/input.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/io.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/job_scheduler.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/job_scheduler_states.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/monitor_contention.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/network_packets.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/oom_adjuster.sql",
@@ -2669,19 +2974,6 @@
     srcs = glob(["src/trace_processor/perfetto_sql/stdlib/chrome/**/*.sql"]),
 )
 
-# GN target: //src/trace_processor/perfetto_sql/stdlib/common:common
-perfetto_filegroup(
-    name = "src_trace_processor_perfetto_sql_stdlib_common_common",
-    srcs = [
-        "src/trace_processor/perfetto_sql/stdlib/common/args.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/counters.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/metadata.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/percentiles.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/slices.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/timestamps.sql",
-    ],
-)
-
 # GN target: //src/trace_processor/perfetto_sql/stdlib/counters:counters
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_counters_counters",
@@ -2690,19 +2982,6 @@
     ],
 )
 
-# GN target: //src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common:common
-perfetto_filegroup(
-    name = "src_trace_processor_perfetto_sql_stdlib_deprecated_v42_common_common",
-    srcs = [
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/args.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/counters.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/metadata.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/percentiles.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/slices.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/timestamps.sql",
-    ],
-)
-
 # GN target: //src/trace_processor/perfetto_sql/stdlib/export:export
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_export_export",
@@ -2739,6 +3018,7 @@
     srcs = [
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/general.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/process.sql",
+        "src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/slice.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/system.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/thread.sql",
     ],
@@ -2751,6 +3031,7 @@
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/frequency.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle_stats.sql",
+        "src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle_time_in_state.sql",
     ],
 )
 
@@ -2769,6 +3050,7 @@
     name = "src_trace_processor_perfetto_sql_stdlib_linux_perf_perf",
     srcs = [
         "src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql",
+        "src/trace_processor/perfetto_sql/stdlib/linux/perf/spe.sql",
     ],
 )
 
@@ -2776,19 +3058,11 @@
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_linux_linux",
     srcs = [
+        "src/trace_processor/perfetto_sql/stdlib/linux/devfreq.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/threads.sql",
     ],
 )
 
-# GN target: //src/trace_processor/perfetto_sql/stdlib/metasql:metasql
-perfetto_filegroup(
-    name = "src_trace_processor_perfetto_sql_stdlib_metasql_metasql",
-    srcs = [
-        "src/trace_processor/perfetto_sql/stdlib/metasql/column_list.sql",
-        "src/trace_processor/perfetto_sql/stdlib/metasql/table_list.sql",
-    ],
-)
-
 # GN target: //src/trace_processor/perfetto_sql/stdlib/pkvm:pkvm
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_pkvm_pkvm",
@@ -2797,17 +3071,29 @@
     ],
 )
 
+# GN target: //src/trace_processor/perfetto_sql/stdlib/prelude/after_eof:after_eof
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_stdlib_prelude_after_eof_after_eof",
+    srcs = [
+        "src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/casts.sql",
+        "src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/slices.sql",
+        "src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/tables_views.sql",
+        "src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/views.sql",
+    ],
+)
+
+# GN target: //src/trace_processor/perfetto_sql/stdlib/prelude/before_eof:before_eof
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_stdlib_prelude_before_eof_before_eof",
+    srcs = [
+        "src/trace_processor/perfetto_sql/stdlib/prelude/before_eof/tables.sql",
+        "src/trace_processor/perfetto_sql/stdlib/prelude/before_eof/trace_bounds.sql",
+    ],
+)
+
 # GN target: //src/trace_processor/perfetto_sql/stdlib/prelude:prelude
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_prelude_prelude",
-    srcs = [
-        "src/trace_processor/perfetto_sql/stdlib/prelude/casts.sql",
-        "src/trace_processor/perfetto_sql/stdlib/prelude/slices.sql",
-        "src/trace_processor/perfetto_sql/stdlib/prelude/tables.sql",
-        "src/trace_processor/perfetto_sql/stdlib/prelude/tables_views.sql",
-        "src/trace_processor/perfetto_sql/stdlib/prelude/trace_bounds.sql",
-        "src/trace_processor/perfetto_sql/stdlib/prelude/views.sql",
-    ],
 )
 
 # GN target: //src/trace_processor/perfetto_sql/stdlib/sched:sched
@@ -2833,6 +3119,7 @@
         "src/trace_processor/perfetto_sql/stdlib/slices/flow.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/hierarchy.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/slices.sql",
+        "src/trace_processor/perfetto_sql/stdlib/slices/time_in_state.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql",
     ],
 )
@@ -2845,6 +3132,14 @@
     ],
 )
 
+# GN target: //src/trace_processor/perfetto_sql/stdlib/stacks:stacks
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_stdlib_stacks_stacks",
+    srcs = [
+        "src/trace_processor/perfetto_sql/stdlib/stacks/cpu_profiling.sql",
+    ],
+)
+
 # GN target: //src/trace_processor/perfetto_sql/stdlib/time:time
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_time_time",
@@ -2890,12 +3185,16 @@
     name = "src_trace_processor_perfetto_sql_stdlib_wattson_wattson",
     srcs = [
         "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_idle.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/cpu_split.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/curves/device.sql",
-        "src/trace_processor/perfetto_sql/stdlib/wattson/curves/grouped.sql",
-        "src/trace_processor/perfetto_sql/stdlib/wattson/curves/ungrouped.sql",
+        "src/trace_processor/perfetto_sql/stdlib/wattson/curves/estimates.sql",
+        "src/trace_processor/perfetto_sql/stdlib/wattson/curves/idle_attribution.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/curves/utils.sql",
+        "src/trace_processor/perfetto_sql/stdlib/wattson/curves/w_cpu_dependence.sql",
+        "src/trace_processor/perfetto_sql/stdlib/wattson/curves/w_dsu_dependence.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/device_infos.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/system_state.sql",
     ],
@@ -2907,6 +3206,7 @@
     deps = [
         ":src_trace_processor_perfetto_sql_stdlib_android_android",
         ":src_trace_processor_perfetto_sql_stdlib_android_auto_auto",
+        ":src_trace_processor_perfetto_sql_stdlib_android_battery_battery",
         ":src_trace_processor_perfetto_sql_stdlib_android_cpu_cpu",
         ":src_trace_processor_perfetto_sql_stdlib_android_frames_frames",
         ":src_trace_processor_perfetto_sql_stdlib_android_gpu_gpu",
@@ -2917,9 +3217,7 @@
         ":src_trace_processor_perfetto_sql_stdlib_android_winscope_winscope",
         ":src_trace_processor_perfetto_sql_stdlib_callstacks_callstacks",
         ":src_trace_processor_perfetto_sql_stdlib_chrome_chrome_sql",
-        ":src_trace_processor_perfetto_sql_stdlib_common_common",
         ":src_trace_processor_perfetto_sql_stdlib_counters_counters",
-        ":src_trace_processor_perfetto_sql_stdlib_deprecated_v42_common_common",
         ":src_trace_processor_perfetto_sql_stdlib_export_export",
         ":src_trace_processor_perfetto_sql_stdlib_graphs_graphs",
         ":src_trace_processor_perfetto_sql_stdlib_intervals_intervals",
@@ -2928,12 +3226,14 @@
         ":src_trace_processor_perfetto_sql_stdlib_linux_linux",
         ":src_trace_processor_perfetto_sql_stdlib_linux_memory_memory",
         ":src_trace_processor_perfetto_sql_stdlib_linux_perf_perf",
-        ":src_trace_processor_perfetto_sql_stdlib_metasql_metasql",
         ":src_trace_processor_perfetto_sql_stdlib_pkvm_pkvm",
+        ":src_trace_processor_perfetto_sql_stdlib_prelude_after_eof_after_eof",
+        ":src_trace_processor_perfetto_sql_stdlib_prelude_before_eof_before_eof",
         ":src_trace_processor_perfetto_sql_stdlib_prelude_prelude",
         ":src_trace_processor_perfetto_sql_stdlib_sched_sched",
         ":src_trace_processor_perfetto_sql_stdlib_slices_slices",
         ":src_trace_processor_perfetto_sql_stdlib_stack_trace_stack_trace",
+        ":src_trace_processor_perfetto_sql_stdlib_stacks_stacks",
         ":src_trace_processor_perfetto_sql_stdlib_time_time",
         ":src_trace_processor_perfetto_sql_stdlib_v8_v8",
         ":src_trace_processor_perfetto_sql_stdlib_viz_summary_summary",
@@ -2946,6 +3246,24 @@
     namespace = "stdlib",
 )
 
+# GN target: //src/trace_processor/perfetto_sql/tokenizer:tokenize_internal
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_tokenizer_tokenize_internal",
+    srcs = [
+        "src/trace_processor/perfetto_sql/tokenizer/tokenize_internal.c",
+        "src/trace_processor/perfetto_sql/tokenizer/tokenize_internal_helper.h",
+    ],
+)
+
+# GN target: //src/trace_processor/perfetto_sql/tokenizer:tokenizer
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_tokenizer_tokenizer",
+    srcs = [
+        "src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.cc",
+        "src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.h",
+    ],
+)
+
 # GN target: //src/trace_processor/rpc:httpd
 perfetto_filegroup(
     name = "src_trace_processor_rpc_httpd",
@@ -3016,8 +3334,6 @@
         "src/trace_processor/sqlite/sql_stats_table.h",
         "src/trace_processor/sqlite/sqlite_engine.cc",
         "src/trace_processor/sqlite/sqlite_engine.h",
-        "src/trace_processor/sqlite/sqlite_tokenizer.cc",
-        "src/trace_processor/sqlite/sqlite_tokenizer.h",
         "src/trace_processor/sqlite/sqlite_utils.cc",
         "src/trace_processor/sqlite/sqlite_utils.h",
         "src/trace_processor/sqlite/stats_table.cc",
@@ -3056,6 +3372,7 @@
         "src/trace_processor/tables/jit_tables.py",
         "src/trace_processor/tables/memory_tables.py",
         "src/trace_processor/tables/metadata_tables.py",
+        "src/trace_processor/tables/perf_tables.py",
         "src/trace_processor/tables/profiler_tables.py",
         "src/trace_processor/tables/sched_tables.py",
         "src/trace_processor/tables/slice_tables.py",
@@ -3071,6 +3388,7 @@
         "src/trace_processor/tables/jit_tables_py.h",
         "src/trace_processor/tables/memory_tables_py.h",
         "src/trace_processor/tables/metadata_tables_py.h",
+        "src/trace_processor/tables/perf_tables_py.h",
         "src/trace_processor/tables/profiler_tables_py.h",
         "src/trace_processor/tables/sched_tables_py.h",
         "src/trace_processor/tables/slice_tables_py.h",
@@ -3261,6 +3579,14 @@
     ],
 )
 
+# GN target: //src/trace_processor/util:winscope_proto_mapping
+perfetto_filegroup(
+    name = "src_trace_processor_util_winscope_proto_mapping",
+    srcs = [
+        "src/trace_processor/util/winscope_proto_mapping.h",
+    ],
+)
+
 # GN target: //src/trace_processor/util:zip_reader
 perfetto_filegroup(
     name = "src_trace_processor_util_zip_reader",
@@ -3652,7 +3978,6 @@
 perfetto_filegroup(
     name = "src_tracing_core_core",
     srcs = [
-        "src/tracing/core/clock_snapshots.cc",
         "src/tracing/core/id_allocator.cc",
         "src/tracing/core/id_allocator.h",
         "src/tracing/core/in_process_shared_memory.cc",
@@ -3728,11 +4053,16 @@
 perfetto_filegroup(
     name = "src_tracing_service_service",
     srcs = [
+        "src/tracing/service/clock.cc",
+        "src/tracing/service/clock.h",
+        "src/tracing/service/dependencies.h",
         "src/tracing/service/histogram.h",
         "src/tracing/service/metatrace_writer.cc",
         "src/tracing/service/metatrace_writer.h",
         "src/tracing/service/packet_stream_validator.cc",
         "src/tracing/service/packet_stream_validator.h",
+        "src/tracing/service/random.cc",
+        "src/tracing/service/random.h",
         "src/tracing/service/trace_buffer.cc",
         "src/tracing/service/trace_buffer.h",
         "src/tracing/service/tracing_service_impl.cc",
@@ -4480,6 +4810,7 @@
     srcs = [
         "protos/perfetto/config/chrome/chrome_config.proto",
         "protos/perfetto/config/chrome/scenario_config.proto",
+        "protos/perfetto/config/chrome/system_metrics.proto",
         "protos/perfetto/config/chrome/v8_config.proto",
         "protos/perfetto/config/data_source_config.proto",
         "protos/perfetto/config/etw/etw_config.proto",
@@ -4757,7 +5088,6 @@
         "protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto",
         "protos/perfetto/metrics/android/android_oom_adjuster_metric.proto",
         "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
-        "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
         "protos/perfetto/metrics/android/anr_metric.proto",
         "protos/perfetto/metrics/android/app_process_starts_metric.proto",
         "protos/perfetto/metrics/android/auto_metric.proto",
@@ -4791,7 +5121,6 @@
         "protos/perfetto/metrics/android/monitor_contention_metric.proto",
         "protos/perfetto/metrics/android/multiuser_metric.proto",
         "protos/perfetto/metrics/android/network_metric.proto",
-        "protos/perfetto/metrics/android/other_traces.proto",
         "protos/perfetto/metrics/android/package_list.proto",
         "protos/perfetto/metrics/android/powrails_metric.proto",
         "protos/perfetto/metrics/android/process_metadata.proto",
@@ -4919,7 +5248,7 @@
     deps = [
         ":protos_perfetto_metrics_android_protos",
         ":protos_perfetto_metrics_protos",
-    ],
+    ] + PERFETTO_CONFIG.deps.protobuf_descriptor_proto,
 )
 
 # GN target: //protos/perfetto/trace/android:android_track_event_descriptor
@@ -5001,8 +5330,7 @@
 perfetto_proto_descriptor(
     name = "protos_perfetto_trace_android_winscope_descriptor",
     deps = [
-        ":protos_perfetto_trace_android_winscope_extensions_protos",
-        ":protos_perfetto_trace_android_winscope_regular_protos",
+        ":protos_perfetto_trace_android_winscope_protos",
     ],
     outs = [
         "protos_perfetto_trace_android_winscope_descriptor.bin",
@@ -5074,6 +5402,23 @@
     ],
 )
 
+# GN target: //protos/perfetto/trace/android:winscope_source_set
+perfetto_proto_library(
+    name = "protos_perfetto_trace_android_winscope_protos",
+    srcs = [
+        "protos/perfetto/trace/android/winscope.proto",
+    ],
+    visibility = [
+        PERFETTO_CONFIG.proto_library_visibility,
+    ],
+    deps = [
+        ":protos_perfetto_common_protos",
+        ":protos_perfetto_trace_android_winscope_common_protos",
+        ":protos_perfetto_trace_android_winscope_extensions_protos",
+        ":protos_perfetto_trace_android_winscope_regular_protos",
+    ] + PERFETTO_CONFIG.deps.protobuf_descriptor_proto,
+)
+
 # GN target: //protos/perfetto/trace/android:winscope_regular_source_set
 perfetto_proto_library(
     name = "protos_perfetto_trace_android_winscope_regular_protos",
@@ -5141,7 +5486,7 @@
 perfetto_proto_descriptor(
     name = "protos_perfetto_trace_descriptor",
     deps = [
-        ":protos_perfetto_trace_non_minimal_protos",
+        ":protos_perfetto_trace_protos",
     ],
     outs = [
         "protos_perfetto_trace_descriptor.bin",
@@ -5200,9 +5545,11 @@
         "protos/perfetto/trace/ftrace/clk.proto",
         "protos/perfetto/trace/ftrace/cma.proto",
         "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpm_trace.proto",
         "protos/perfetto/trace/ftrace/cpuhp.proto",
         "protos/perfetto/trace/ftrace/cros_ec.proto",
         "protos/perfetto/trace/ftrace/dcvsh.proto",
+        "protos/perfetto/trace/ftrace/devfreq.proto",
         "protos/perfetto/trace/ftrace/dma_fence.proto",
         "protos/perfetto/trace/ftrace/dmabuf_heap.proto",
         "protos/perfetto/trace/ftrace/dpu.proto",
@@ -5239,6 +5586,7 @@
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/panel.proto",
         "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/pixel_mm.proto",
         "protos/perfetto/trace/ftrace/power.proto",
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
@@ -5612,6 +5960,50 @@
     ],
 )
 
+# GN target: //protos/perfetto/trace:source_set
+perfetto_proto_library(
+    name = "protos_perfetto_trace_protos",
+    visibility = [
+        PERFETTO_CONFIG.proto_library_visibility,
+    ],
+    deps = [
+        ":protos_perfetto_common_protos",
+        ":protos_perfetto_config_android_protos",
+        ":protos_perfetto_config_ftrace_protos",
+        ":protos_perfetto_config_gpu_protos",
+        ":protos_perfetto_config_inode_file_protos",
+        ":protos_perfetto_config_interceptors_protos",
+        ":protos_perfetto_config_power_protos",
+        ":protos_perfetto_config_process_stats_protos",
+        ":protos_perfetto_config_profiling_protos",
+        ":protos_perfetto_config_protos",
+        ":protos_perfetto_config_statsd_protos",
+        ":protos_perfetto_config_sys_stats_protos",
+        ":protos_perfetto_config_system_info_protos",
+        ":protos_perfetto_config_track_event_protos",
+        ":protos_perfetto_trace_android_protos",
+        ":protos_perfetto_trace_android_winscope_common_protos",
+        ":protos_perfetto_trace_android_winscope_regular_protos",
+        ":protos_perfetto_trace_chrome_protos",
+        ":protos_perfetto_trace_etw_protos",
+        ":protos_perfetto_trace_filesystem_protos",
+        ":protos_perfetto_trace_ftrace_protos",
+        ":protos_perfetto_trace_gpu_protos",
+        ":protos_perfetto_trace_interned_data_protos",
+        ":protos_perfetto_trace_minimal_protos",
+        ":protos_perfetto_trace_non_minimal_protos",
+        ":protos_perfetto_trace_perfetto_protos",
+        ":protos_perfetto_trace_power_protos",
+        ":protos_perfetto_trace_profiling_protos",
+        ":protos_perfetto_trace_ps_protos",
+        ":protos_perfetto_trace_statsd_protos",
+        ":protos_perfetto_trace_sys_stats_protos",
+        ":protos_perfetto_trace_system_info_protos",
+        ":protos_perfetto_trace_track_event_protos",
+        ":protos_perfetto_trace_translation_protos",
+    ],
+)
+
 # GN target: //protos/perfetto/trace/ps:source_set
 perfetto_proto_library(
     name = "protos_perfetto_trace_ps_protos",
@@ -5994,6 +6386,7 @@
         ":protos_perfetto_trace_translation_zero",
         ":protozero",
         ":src_base_base",
+        ":src_base_clock_snapshots",
         ":src_base_version",
     ],
     linkstatic = True,
@@ -6094,114 +6487,6 @@
     ],
 )
 
-# GN target: //src/shared_lib:libperfetto_c
-perfetto_cc_library(
-    name = "libperfetto_c",
-    srcs = [
-        ":src_android_stats_android_stats",
-        ":src_android_stats_perfetto_atoms",
-        ":src_protozero_filtering_bytecode_common",
-        ":src_protozero_filtering_bytecode_parser",
-        ":src_protozero_filtering_message_filter",
-        ":src_protozero_filtering_string_filter",
-        ":src_shared_lib_intern_map",
-        ":src_shared_lib_shared_lib",
-        ":src_tracing_client_api_without_backends",
-        ":src_tracing_common",
-        ":src_tracing_core_core",
-        ":src_tracing_in_process_backend",
-        ":src_tracing_ipc_common",
-        ":src_tracing_ipc_consumer_consumer",
-        ":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",
-    ],
-    hdrs = [
-        ":include_perfetto_base_base",
-        ":include_perfetto_ext_base_base",
-        ":include_perfetto_ext_ipc_ipc",
-        ":include_perfetto_ext_tracing_core_core",
-        ":include_perfetto_ext_tracing_ipc_ipc",
-        ":include_perfetto_protozero_protozero",
-        ":include_perfetto_public_abi_base",
-        ":include_perfetto_public_abi_public",
-        ":include_perfetto_public_base",
-        ":include_perfetto_public_protos_protos",
-        ":include_perfetto_public_protozero",
-        ":include_perfetto_public_public",
-        ":include_perfetto_tracing_core_core",
-        ":include_perfetto_tracing_core_forward_decls",
-        ":include_perfetto_tracing_tracing",
-    ],
-    defines = [
-        "PERFETTO_SHLIB_SDK_IMPLEMENTATION",
-    ],
-    visibility = [
-        "//visibility:public",
-    ],
-    deps = [
-        ":perfetto_ipc",
-        ":protos_perfetto_common_cpp",
-        ":protos_perfetto_common_zero",
-        ":protos_perfetto_config_android_cpp",
-        ":protos_perfetto_config_android_zero",
-        ":protos_perfetto_config_cpp",
-        ":protos_perfetto_config_ftrace_cpp",
-        ":protos_perfetto_config_ftrace_zero",
-        ":protos_perfetto_config_gpu_cpp",
-        ":protos_perfetto_config_gpu_zero",
-        ":protos_perfetto_config_inode_file_cpp",
-        ":protos_perfetto_config_inode_file_zero",
-        ":protos_perfetto_config_interceptors_cpp",
-        ":protos_perfetto_config_interceptors_zero",
-        ":protos_perfetto_config_power_cpp",
-        ":protos_perfetto_config_power_zero",
-        ":protos_perfetto_config_process_stats_cpp",
-        ":protos_perfetto_config_process_stats_zero",
-        ":protos_perfetto_config_profiling_cpp",
-        ":protos_perfetto_config_profiling_zero",
-        ":protos_perfetto_config_statsd_cpp",
-        ":protos_perfetto_config_statsd_zero",
-        ":protos_perfetto_config_sys_stats_cpp",
-        ":protos_perfetto_config_sys_stats_zero",
-        ":protos_perfetto_config_system_info_cpp",
-        ":protos_perfetto_config_system_info_zero",
-        ":protos_perfetto_config_track_event_cpp",
-        ":protos_perfetto_config_track_event_zero",
-        ":protos_perfetto_config_zero",
-        ":protos_perfetto_ipc_cpp",
-        ":protos_perfetto_ipc_ipc",
-        ":protos_perfetto_trace_android_winscope_common_zero",
-        ":protos_perfetto_trace_android_winscope_regular_zero",
-        ":protos_perfetto_trace_android_zero",
-        ":protos_perfetto_trace_chrome_zero",
-        ":protos_perfetto_trace_etw_zero",
-        ":protos_perfetto_trace_filesystem_zero",
-        ":protos_perfetto_trace_ftrace_zero",
-        ":protos_perfetto_trace_gpu_zero",
-        ":protos_perfetto_trace_interned_data_zero",
-        ":protos_perfetto_trace_minimal_zero",
-        ":protos_perfetto_trace_non_minimal_zero",
-        ":protos_perfetto_trace_perfetto_zero",
-        ":protos_perfetto_trace_power_zero",
-        ":protos_perfetto_trace_profiling_zero",
-        ":protos_perfetto_trace_ps_zero",
-        ":protos_perfetto_trace_statsd_zero",
-        ":protos_perfetto_trace_sys_stats_zero",
-        ":protos_perfetto_trace_system_info_zero",
-        ":protos_perfetto_trace_track_event_cpp",
-        ":protos_perfetto_trace_track_event_zero",
-        ":protos_perfetto_trace_translation_zero",
-        ":protozero",
-        ":src_base_base",
-        ":src_base_version",
-    ],
-    linkstatic = True,
-)
-
 # GN target: //src/trace_processor:trace_processor
 perfetto_cc_library(
     name = "trace_processor",
@@ -6214,6 +6499,9 @@
         ":src_trace_processor_export_json",
         ":src_trace_processor_importers_android_bugreport_android_bugreport",
         ":src_trace_processor_importers_android_bugreport_android_log_event",
+        ":src_trace_processor_importers_archive_archive",
+        ":src_trace_processor_importers_art_method_art_method",
+        ":src_trace_processor_importers_art_method_art_method_event",
         ":src_trace_processor_importers_common_common",
         ":src_trace_processor_importers_common_parser_types",
         ":src_trace_processor_importers_common_trace_parser_hdr",
@@ -6225,14 +6513,20 @@
         ":src_trace_processor_importers_fuchsia_fuchsia_record",
         ":src_trace_processor_importers_fuchsia_full",
         ":src_trace_processor_importers_fuchsia_minimal",
-        ":src_trace_processor_importers_gzip_full",
+        ":src_trace_processor_importers_gecko_gecko",
+        ":src_trace_processor_importers_gecko_gecko_event",
         ":src_trace_processor_importers_i2c_full",
-        ":src_trace_processor_importers_json_full",
+        ":src_trace_processor_importers_instruments_instruments",
+        ":src_trace_processor_importers_instruments_row",
+        ":src_trace_processor_importers_json_json",
         ":src_trace_processor_importers_json_minimal",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
         ":src_trace_processor_importers_ninja_ninja",
         ":src_trace_processor_importers_perf_perf",
         ":src_trace_processor_importers_perf_record",
+        ":src_trace_processor_importers_perf_text_perf_text",
+        ":src_trace_processor_importers_perf_text_perf_text_event",
+        ":src_trace_processor_importers_perf_text_perf_text_sample_line_parser",
         ":src_trace_processor_importers_perf_tracker",
         ":src_trace_processor_importers_proto_full",
         ":src_trace_processor_importers_proto_minimal",
@@ -6243,11 +6537,11 @@
         ":src_trace_processor_importers_systrace_full",
         ":src_trace_processor_importers_systrace_systrace_line",
         ":src_trace_processor_importers_systrace_systrace_parser",
-        ":src_trace_processor_importers_zip_full",
         ":src_trace_processor_lib",
         ":src_trace_processor_metatrace",
         ":src_trace_processor_metrics_metrics",
         ":src_trace_processor_perfetto_sql_engine_engine",
+        ":src_trace_processor_perfetto_sql_grammar_grammar",
         ":src_trace_processor_perfetto_sql_intrinsics_functions_functions",
         ":src_trace_processor_perfetto_sql_intrinsics_functions_interface",
         ":src_trace_processor_perfetto_sql_intrinsics_functions_tables",
@@ -6256,6 +6550,11 @@
         ":src_trace_processor_perfetto_sql_intrinsics_table_functions_table_functions",
         ":src_trace_processor_perfetto_sql_intrinsics_table_functions_tables",
         ":src_trace_processor_perfetto_sql_intrinsics_types_types",
+        ":src_trace_processor_perfetto_sql_parser_parser",
+        ":src_trace_processor_perfetto_sql_preprocessor_grammar",
+        ":src_trace_processor_perfetto_sql_preprocessor_preprocessor",
+        ":src_trace_processor_perfetto_sql_tokenizer_tokenize_internal",
+        ":src_trace_processor_perfetto_sql_tokenizer_tokenizer",
         ":src_trace_processor_sorter_sorter",
         ":src_trace_processor_sqlite_bindings_bindings",
         ":src_trace_processor_sqlite_sqlite",
@@ -6282,6 +6581,7 @@
         ":src_trace_processor_util_trace_blob_view_reader",
         ":src_trace_processor_util_trace_type",
         ":src_trace_processor_util_util",
+        ":src_trace_processor_util_winscope_proto_mapping",
         ":src_trace_processor_util_zip_reader",
     ],
     hdrs = [
@@ -6293,8 +6593,11 @@
         ":include_perfetto_ext_traced_sys_stats_counters",
         ":include_perfetto_protozero_protozero",
         ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_abi_public",
         ":include_perfetto_public_base",
+        ":include_perfetto_public_protos_protos",
         ":include_perfetto_public_protozero",
+        ":include_perfetto_public_public",
         ":include_perfetto_trace_processor_basic_types",
         ":include_perfetto_trace_processor_storage",
         ":include_perfetto_trace_processor_trace_processor",
@@ -6344,6 +6647,7 @@
                ":protos_third_party_simpleperf_zero",
                ":protozero",
                ":src_base_base",
+               ":src_base_clock_snapshots",
                ":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",
@@ -6357,7 +6661,8 @@
                ":src_trace_processor_metrics_gen_cc_metrics_descriptor",
                ":src_trace_processor_metrics_sql_gen_amalgamated_sql_metrics",
                ":src_trace_processor_perfetto_sql_stdlib_stdlib",
-           ] + PERFETTO_CONFIG.deps.jsoncpp +
+           ] + PERFETTO_CONFIG.deps.expat +
+           PERFETTO_CONFIG.deps.jsoncpp +
            PERFETTO_CONFIG.deps.sqlite +
            PERFETTO_CONFIG.deps.sqlite_ext_percentile +
            PERFETTO_CONFIG.deps.zlib +
@@ -6379,8 +6684,11 @@
         ":include_perfetto_ext_traced_sys_stats_counters",
         ":include_perfetto_protozero_protozero",
         ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_abi_public",
         ":include_perfetto_public_base",
+        ":include_perfetto_public_protos_protos",
         ":include_perfetto_public_protozero",
+        ":include_perfetto_public_public",
         ":include_perfetto_trace_processor_basic_types",
         ":include_perfetto_trace_processor_storage",
         ":include_perfetto_trace_processor_trace_processor",
@@ -6396,6 +6704,9 @@
         ":src_trace_processor_export_json",
         ":src_trace_processor_importers_android_bugreport_android_bugreport",
         ":src_trace_processor_importers_android_bugreport_android_log_event",
+        ":src_trace_processor_importers_archive_archive",
+        ":src_trace_processor_importers_art_method_art_method",
+        ":src_trace_processor_importers_art_method_art_method_event",
         ":src_trace_processor_importers_common_common",
         ":src_trace_processor_importers_common_parser_types",
         ":src_trace_processor_importers_common_trace_parser_hdr",
@@ -6407,14 +6718,20 @@
         ":src_trace_processor_importers_fuchsia_fuchsia_record",
         ":src_trace_processor_importers_fuchsia_full",
         ":src_trace_processor_importers_fuchsia_minimal",
-        ":src_trace_processor_importers_gzip_full",
+        ":src_trace_processor_importers_gecko_gecko",
+        ":src_trace_processor_importers_gecko_gecko_event",
         ":src_trace_processor_importers_i2c_full",
-        ":src_trace_processor_importers_json_full",
+        ":src_trace_processor_importers_instruments_instruments",
+        ":src_trace_processor_importers_instruments_row",
+        ":src_trace_processor_importers_json_json",
         ":src_trace_processor_importers_json_minimal",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
         ":src_trace_processor_importers_ninja_ninja",
         ":src_trace_processor_importers_perf_perf",
         ":src_trace_processor_importers_perf_record",
+        ":src_trace_processor_importers_perf_text_perf_text",
+        ":src_trace_processor_importers_perf_text_perf_text_event",
+        ":src_trace_processor_importers_perf_text_perf_text_sample_line_parser",
         ":src_trace_processor_importers_perf_tracker",
         ":src_trace_processor_importers_proto_full",
         ":src_trace_processor_importers_proto_minimal",
@@ -6425,11 +6742,11 @@
         ":src_trace_processor_importers_systrace_full",
         ":src_trace_processor_importers_systrace_systrace_line",
         ":src_trace_processor_importers_systrace_systrace_parser",
-        ":src_trace_processor_importers_zip_full",
         ":src_trace_processor_lib",
         ":src_trace_processor_metatrace",
         ":src_trace_processor_metrics_metrics",
         ":src_trace_processor_perfetto_sql_engine_engine",
+        ":src_trace_processor_perfetto_sql_grammar_grammar",
         ":src_trace_processor_perfetto_sql_intrinsics_functions_functions",
         ":src_trace_processor_perfetto_sql_intrinsics_functions_interface",
         ":src_trace_processor_perfetto_sql_intrinsics_functions_tables",
@@ -6438,6 +6755,11 @@
         ":src_trace_processor_perfetto_sql_intrinsics_table_functions_table_functions",
         ":src_trace_processor_perfetto_sql_intrinsics_table_functions_tables",
         ":src_trace_processor_perfetto_sql_intrinsics_types_types",
+        ":src_trace_processor_perfetto_sql_parser_parser",
+        ":src_trace_processor_perfetto_sql_preprocessor_grammar",
+        ":src_trace_processor_perfetto_sql_preprocessor_preprocessor",
+        ":src_trace_processor_perfetto_sql_tokenizer_tokenize_internal",
+        ":src_trace_processor_perfetto_sql_tokenizer_tokenizer",
         ":src_trace_processor_rpc_httpd",
         ":src_trace_processor_rpc_rpc",
         ":src_trace_processor_rpc_stdiod",
@@ -6467,6 +6789,7 @@
         ":src_trace_processor_util_trace_blob_view_reader",
         ":src_trace_processor_util_trace_type",
         ":src_trace_processor_util_util",
+        ":src_trace_processor_util_winscope_proto_mapping",
         ":src_trace_processor_util_zip_reader",
         "src/trace_processor/trace_processor_shell.cc",
     ],
@@ -6515,6 +6838,7 @@
                ":protos_third_party_simpleperf_zero",
                ":protozero",
                ":src_base_base",
+               ":src_base_clock_snapshots",
                ":src_base_http_http",
                ":src_base_version",
                ":src_trace_processor_containers_containers",
@@ -6530,7 +6854,8 @@
                ":src_trace_processor_metrics_gen_cc_metrics_descriptor",
                ":src_trace_processor_metrics_sql_gen_amalgamated_sql_metrics",
                ":src_trace_processor_perfetto_sql_stdlib_stdlib",
-           ] + PERFETTO_CONFIG.deps.jsoncpp +
+           ] + PERFETTO_CONFIG.deps.expat +
+           PERFETTO_CONFIG.deps.jsoncpp +
            PERFETTO_CONFIG.deps.linenoise +
            PERFETTO_CONFIG.deps.protobuf_full +
            PERFETTO_CONFIG.deps.sqlite +
@@ -6621,8 +6946,11 @@
         ":include_perfetto_profiling_pprof_builder",
         ":include_perfetto_protozero_protozero",
         ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_abi_public",
         ":include_perfetto_public_base",
+        ":include_perfetto_public_protos_protos",
         ":include_perfetto_public_protozero",
+        ":include_perfetto_public_public",
         ":include_perfetto_trace_processor_basic_types",
         ":include_perfetto_trace_processor_storage",
         ":include_perfetto_trace_processor_trace_processor",
@@ -6638,6 +6966,9 @@
         ":src_trace_processor_export_json",
         ":src_trace_processor_importers_android_bugreport_android_bugreport",
         ":src_trace_processor_importers_android_bugreport_android_log_event",
+        ":src_trace_processor_importers_archive_archive",
+        ":src_trace_processor_importers_art_method_art_method",
+        ":src_trace_processor_importers_art_method_art_method_event",
         ":src_trace_processor_importers_common_common",
         ":src_trace_processor_importers_common_parser_types",
         ":src_trace_processor_importers_common_trace_parser_hdr",
@@ -6649,14 +6980,20 @@
         ":src_trace_processor_importers_fuchsia_fuchsia_record",
         ":src_trace_processor_importers_fuchsia_full",
         ":src_trace_processor_importers_fuchsia_minimal",
-        ":src_trace_processor_importers_gzip_full",
+        ":src_trace_processor_importers_gecko_gecko",
+        ":src_trace_processor_importers_gecko_gecko_event",
         ":src_trace_processor_importers_i2c_full",
-        ":src_trace_processor_importers_json_full",
+        ":src_trace_processor_importers_instruments_instruments",
+        ":src_trace_processor_importers_instruments_row",
+        ":src_trace_processor_importers_json_json",
         ":src_trace_processor_importers_json_minimal",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
         ":src_trace_processor_importers_ninja_ninja",
         ":src_trace_processor_importers_perf_perf",
         ":src_trace_processor_importers_perf_record",
+        ":src_trace_processor_importers_perf_text_perf_text",
+        ":src_trace_processor_importers_perf_text_perf_text_event",
+        ":src_trace_processor_importers_perf_text_perf_text_sample_line_parser",
         ":src_trace_processor_importers_perf_tracker",
         ":src_trace_processor_importers_proto_full",
         ":src_trace_processor_importers_proto_minimal",
@@ -6667,11 +7004,11 @@
         ":src_trace_processor_importers_systrace_full",
         ":src_trace_processor_importers_systrace_systrace_line",
         ":src_trace_processor_importers_systrace_systrace_parser",
-        ":src_trace_processor_importers_zip_full",
         ":src_trace_processor_lib",
         ":src_trace_processor_metatrace",
         ":src_trace_processor_metrics_metrics",
         ":src_trace_processor_perfetto_sql_engine_engine",
+        ":src_trace_processor_perfetto_sql_grammar_grammar",
         ":src_trace_processor_perfetto_sql_intrinsics_functions_functions",
         ":src_trace_processor_perfetto_sql_intrinsics_functions_interface",
         ":src_trace_processor_perfetto_sql_intrinsics_functions_tables",
@@ -6680,6 +7017,11 @@
         ":src_trace_processor_perfetto_sql_intrinsics_table_functions_table_functions",
         ":src_trace_processor_perfetto_sql_intrinsics_table_functions_tables",
         ":src_trace_processor_perfetto_sql_intrinsics_types_types",
+        ":src_trace_processor_perfetto_sql_parser_parser",
+        ":src_trace_processor_perfetto_sql_preprocessor_grammar",
+        ":src_trace_processor_perfetto_sql_preprocessor_preprocessor",
+        ":src_trace_processor_perfetto_sql_tokenizer_tokenize_internal",
+        ":src_trace_processor_perfetto_sql_tokenizer_tokenizer",
         ":src_trace_processor_sorter_sorter",
         ":src_trace_processor_sqlite_bindings_bindings",
         ":src_trace_processor_sqlite_sqlite",
@@ -6706,6 +7048,7 @@
         ":src_trace_processor_util_trace_blob_view_reader",
         ":src_trace_processor_util_trace_type",
         ":src_trace_processor_util_util",
+        ":src_trace_processor_util_winscope_proto_mapping",
         ":src_trace_processor_util_zip_reader",
         ":src_traceconv_lib",
         ":src_traceconv_main",
@@ -6757,6 +7100,7 @@
                ":protos_third_party_simpleperf_zero",
                ":protozero",
                ":src_base_base",
+               ":src_base_clock_snapshots",
                ":src_base_version",
                ":src_trace_processor_containers_containers",
                ":src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
@@ -6773,7 +7117,8 @@
                ":src_trace_processor_perfetto_sql_stdlib_stdlib",
                ":src_traceconv_gen_cc_trace_descriptor",
                ":src_traceconv_gen_cc_winscope_descriptor",
-           ] + PERFETTO_CONFIG.deps.jsoncpp +
+           ] + PERFETTO_CONFIG.deps.expat +
+           PERFETTO_CONFIG.deps.jsoncpp +
            PERFETTO_CONFIG.deps.sqlite +
            PERFETTO_CONFIG.deps.sqlite_ext_percentile +
            PERFETTO_CONFIG.deps.zlib +
diff --git a/BUILD.gn b/BUILD.gn
index ee45b3c..7dd9d31 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -116,7 +116,11 @@
 if (enable_perfetto_integration_tests) {
   import("gn/perfetto_integrationtests.gni")
   test("perfetto_integrationtests") {
-    deps = perfetto_integrationtests_targets
+    deps = [
+      "gn:default_deps",
+      "test:integrationtest_main",
+    ]
+    deps += perfetto_integrationtests_targets
   }
   all_targets += [
     ":perfetto_integrationtests",
diff --git a/CHANGELOG b/CHANGELOG
index f61d576..2a33ef5 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,8 +1,33 @@
 Unreleased:
   Tracing service and probes:
-    *
+    * Add `--clone-by-name` to the perfetto command line. This allows cloning a
+      tracing session by its unique_session_name.
   SQL Standard library:
-    *
+    * Removed the `linux_device_track` table. Linux RPM (runtime power
+      management) tracks formerly in this table now can be found in the `track`
+      table with `classification` `linux_rpm`.
+    * Removed the `energy_counter_track` table. Android energy estimate
+      breakdown tracks formerly in this table now can be found in the `track`
+      table with `classification` `android_energy_estimation_breakdown`.
+    * Removed the `energy_per_uid_counter_track` table. Android energy estimate
+      breakdown tracks formerly in this table now can be found in the `track`
+      table with `classification` `android_energy_estimation_breakdown_per_uid`.
+    * Moved the `gpu_work_period_track` table into the `android.gpu.work_period`
+      standard library module and renamed it to `android_gpu_work_period_track`.
+    * Removed the `irq_counter_track` table. Irq counter tracks formerly in this
+      table now can be found in the `track` table with `classification`
+      `irq_counter`.
+    * Removed the `softirq_counter_track` table. Softirq counter tracks formerly
+      in this table now can be found in the `track` table with `classification`
+      `softirq_counter`.
+    * Removed the `uid_counter_track` table. It was redundant as no data was
+      inserted directly into it.
+    * Removed the `uid_track` table. It was redundant as no data was
+      inserted directly into it.
+    * Removed `common` package. It has been deprecated since `v42` and most
+      functionality has been moved into other packages. Notably, the time
+      conversion functions can be found in `time.conversion` module, and
+      `thread_slice` is available with `slices.with_context`.
   Trace Processor:
     *
   UI:
@@ -11,6 +36,79 @@
     *
 
 
+v48.1 - 2024-10-14:
+  SDK:
+    * Fix build with MSVC.
+
+
+v48.0 - 2024-10-11:
+  Tracing service and probes:
+    * Improved accuracy of ftrace event cropping when there are multiple
+      concurrent tracing sessions. See `previous_bundle_end_timestamp` in
+      ftrace_event_bundle.proto.
+    * Increased watchdog timeout to 180s from 30s to make watchdog crashes
+      much less likely when system is under heavy load.
+  SQL Standard library:
+    * Improved CPU cycles calculation in `linux.cpu.utilization` modules:
+     `process`, `system` and `thread` by fixing a bug responsible for too high
+      CPU cycles values.
+    * Introduces functions responsible for calculating CPU cycles with
+      breakdown by CPU, thread, process and slice for a given interval.
+    * Added `linux.perf.samples` module for easy querying of perf samples
+      in traces.
+    * Added `stacks.cpu_profiling` module for easy querying of all CPU
+      profiling data in traces.
+  Trace Processor:
+    * Added (partial) support for the Gecko (Firefox) JSON profiler format.
+      Parsing is optimized for CPU profiling collected with `perf` and converted
+      to the Gecko format. Only parsing of samples is supported; parsing of
+      markers and any other features (e.g. colours) is *not* supported.
+    * Added (partial) suppoort for the perf script text format from both perf
+      and simpleperf. Only parsing files with the default formating or with pids
+      included (i.e. `-F +pid`) is supported. Any other formatting options are
+      *not* supported.
+    * Added support for parsing non-streaming ART method tracing format.
+    * Added support for parsing GZIP files with multiple gzip streams.
+    * Added support for parsing V8 CPU profling samples from proto traces.
+u    * Renamed Trace Processor's C++ method `RegisterSqlModule()` to
+     `RegisterSqlPackage()`, which better represents the module/package
+      relationship. Package is the top level grouping of modules, which are
+      objects being included with `INCLUDE PERFETTO MODULE`.
+      `RegisterSqlModule()` is still available and runs `RegisterSqlPackage()`.
+      `RegisterSqlModule()` will be deprecated in v50.0.
+  UI:
+    * Scheduling wakeup information now reflects whether the wakeup came
+      from an interrupt context. The per-cpu scheduling tracks now show only
+      non-interrupt wakeups, while the per-thread thread state tracks either
+      link to an exact waker slice or state that the wakeup is from an
+      interrupt. Older traces that are recorded without interrupt context
+      information treat all wakeups as non-interrupt.
+    * Nest global/user async tracks according to their parent/child relationship
+      in the trace.
+    * Introduced new workspace API which allows nested tracks & multiple
+      workspace support.
+    * Introduced middle ellipsis in track titles when title text is longer than
+      the available space, while preserving the start and end of title text &
+      add popup of full title text on hover.
+    * Fixed spurious judder issue in popups with tall content.
+    * Fixed bug where marker durations were not rendered on selected markers.
+    * Removed ChromeScrollJank V1 track (V2 should be used from now on).
+    * Major internal changes (affecting pugin developers and core contributors):
+      * Removed legacy selection types, use track event selection types going
+        forward for all single event selections.
+      * Details panels are now attached to the track rather than registered
+        separately.
+      * Introduced `registerSqlSelectionResolver`, which allow plugins to add
+        handlers allowing other parts of the codebase to make selection on
+        tracks from other plugins based on a sql table name and id.
+      * Changed lifetime of track instances; they are now created on trace load
+        by plugins and survive the lifetime of the trace, rather than getting
+        created and destroyed as they appear/disappear on the timeline.
+      * Fix circular dependencies and turn future instances into errors.
+  SDK:
+    *
+
+
 v47.0 - 2024-08-07:
   SQL Standard library:
     * Removed `cpu.cpus` and `cpu.size` modules. The functions inside
diff --git a/LICENSE b/LICENSE
index cd2eba5..bbcb67c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -175,6 +175,10 @@
 
    END OF TERMS AND CONDITIONS
 
+------------------
+
+Files: * except those files noted below
+
    Copyright (c) 2017, The Android Open Source Project
 
    Licensed under the Apache License, Version 2.0 (the "License");
@@ -187,6 +191,10 @@
    limitations under the License.
 
 
+------------------
+
+Files: src/trace_processor/perfetto_sql/stdlib/chromium/*, protos/third_party/chromium/*, test/trace_processor/diff_tests/stdlib/chrome/*
+
    Copyright 2015 The Chromium Authors
 
    Redistribution and use in source and binary forms, with or without
@@ -214,3 +222,14 @@
    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+------------------
+
+Files: src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.{c, h}
+
+The author disclaims copyright to this source code. In place of a legal notice, here is a blessing:
+
+May you do good and not evil.
+May you find forgiveness for yourself and forgive others.
+May you share freely, never taking more than you give.
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index c615ffa..e3583ee 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -470,7 +470,11 @@
     return input_api.FilterSourceFile(
         x,
         files_to_check=[r'.*\.gni?$'],
-        files_to_skip=['^.gn$', '^gn/.*', '^buildtools/.*'])
+        files_to_skip=[
+            '^.gn$',
+            '^gn/.*',
+            '^buildtools/.*',
+        ])
 
   error_lines = []
   for f in input_api.AffectedSourceFiles(file_filter):
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 0e6a83d..070a460 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -33,5 +33,10 @@
     {
       "name": "CtsPerfettoTestCases"
     }
+  ],
+  "postsubmit": [
+    {
+      "name": "libtracing_perfetto_tests"
+    }
   ]
 }
diff --git a/bazel/deps.bzl b/bazel/deps.bzl
index 69dbcbb..4334036 100644
--- a/bazel/deps.bzl
+++ b/bazel/deps.bzl
@@ -67,6 +67,14 @@
     )
 
     _add_repo_if_not_existing(
+        new_git_repository,
+        name = "perfetto_dep_expat",
+        remote = "https://github.com/libexpat/libexpat",
+        commit = "fa75b96546c069d17b8f80d91e0f4ef0cde3790d",  # R_2_6_2
+        build_file = "//bazel:expat.BUILD",
+    )
+
+    _add_repo_if_not_existing(
         http_archive,
         name = "perfetto_dep_zlib",
         url = "https://storage.googleapis.com/perfetto/zlib-6d3f6aa0f87c9791ca7724c279ef61384f331dfd.tar.gz",
@@ -87,9 +95,11 @@
     _add_repo_if_not_existing(
         http_archive,
         name = "bazel_skylib",
-        sha256 = "bbccf674aa441c266df9894182d80de104cabd19be98be002f6d478aaa31574d",
-        strip_prefix = "bazel-skylib-2169ae1c374aab4a09aa90e65efe1a3aad4e279b",
-        url = "https://github.com/bazelbuild/bazel-skylib/archive/2169ae1c374aab4a09aa90e65efe1a3aad4e279b.tar.gz",
+        sha256 = "bc283cdfcd526a52c3201279cda4bc298652efa898b10b4db0837dc51652756f",
+        urls = [
+            "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.7.1/bazel-skylib-1.7.1.tar.gz",
+            "https://github.com/bazelbuild/bazel-skylib/releases/download/1.7.1/bazel-skylib-1.7.1.tar.gz",
+        ]
     )
 
 def _add_repo_if_not_existing(repo_rule, name, **kwargs):
diff --git a/bazel/expat.BUILD b/bazel/expat.BUILD
new file mode 100644
index 0000000..10f88c7
--- /dev/null
+++ b/bazel/expat.BUILD
@@ -0,0 +1,64 @@
+# 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.
+
+load("@perfetto_cfg//:perfetto_cfg.bzl", "PERFETTO_CONFIG")
+
+cc_library(
+    name = "expat",
+    hdrs = glob(["expat/lib/*.h"]),
+    deps = [
+        ":expat_impl",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+cc_library(
+    name = "expat_impl",
+    srcs = [
+      "expat/lib/xmlparse.c",
+      "expat/lib/xmlrole.c",
+      "expat/lib/xmltok.c",
+    ],
+    hdrs = [
+      "expat/lib/ascii.h",
+      "expat/lib/asciitab.h",
+      "expat/lib/expat.h",
+      "expat/lib/expat_external.h",
+      "expat/lib/iasciitab.h",
+      "expat/lib/internal.h",
+      "expat/lib/latin1tab.h",
+      "expat/lib/nametab.h",
+      "expat/lib/siphash.h",
+      "expat/lib/utf8tab.h",
+      "expat/lib/winconfig.h",
+      "expat/lib/xmlrole.h",
+      "expat/lib/xmltok.h",
+      "expat/lib/xmltok_impl.c",
+      "expat/lib/xmltok_impl.h",
+      "expat/lib/xmltok_ns.c",
+    ],
+    deps = [
+        "@perfetto//buildtools/expat/include:expat_config",
+    ],
+    copts = [
+        "-DHAVE_EXPAT_CONFIG_H",
+    ] + PERFETTO_CONFIG.deps_copts.expat,
+    defines = [
+        "XML_STATIC"
+    ],
+    includes = [
+        "expat",
+        "expat/lib",
+    ],
+)
diff --git a/bazel/rules.bzl b/bazel/rules.bzl
index 7be5b02..7291750 100644
--- a/bazel/rules.bzl
+++ b/bazel/rules.bzl
@@ -29,7 +29,7 @@
         "linkopts": select({
             "@perfetto//bazel:os_linux": ["-ldl", "-lrt", "-lpthread"],
             "@perfetto//bazel:os_osx": [],
-            "@perfetto//bazel:os_windows": [],
+            "@perfetto//bazel:os_windows": ["ws2_32.lib"],
             "//conditions:default": ["-ldl"],
         }),
     }
diff --git a/bazel/standalone/perfetto_cfg.bzl b/bazel/standalone/perfetto_cfg.bzl
index cbabb29..254bcb8 100644
--- a/bazel/standalone/perfetto_cfg.bzl
+++ b/bazel/standalone/perfetto_cfg.bzl
@@ -45,6 +45,7 @@
         base_platform = ["//:perfetto_base_default_platform"],
 
         zlib = ["@perfetto_dep_zlib//:zlib"],
+        expat = ["@perfetto_dep_expat//:expat"],
         jsoncpp = ["@perfetto_dep_jsoncpp//:jsoncpp"],
         linenoise = ["@perfetto_dep_linenoise//:linenoise"],
         sqlite = ["@perfetto_dep_sqlite//:sqlite"],
@@ -83,6 +84,7 @@
     # initialized with the Perfetto build files (i.e. via perfetto_deps()).
     deps_copts = struct(
         zlib = [],
+        expat = [],
         jsoncpp = [],
         linenoise = [],
         sqlite = [],
diff --git a/buildtools/.gitignore b/buildtools/.gitignore
index a3e6376..42137c3 100644
--- a/buildtools/.gitignore
+++ b/buildtools/.gitignore
@@ -11,6 +11,7 @@
 /catapult_trace_viewer/
 /clang_format/
 /clang/
+/expat/src/
 /d8/
 /debian_sid_arm-sysroot/
 /debian_sid_arm64-sysroot/
diff --git a/buildtools/BUILD.gn b/buildtools/BUILD.gn
index 2755958..54196e2 100644
--- a/buildtools/BUILD.gn
+++ b/buildtools/BUILD.gn
@@ -1402,6 +1402,43 @@
   deps = [ "//gn:default_deps" ]
 }
 
+config("no_format_warning") {
+  cflags = [ "-Wno-format" ]
+}
+
+if (enable_perfetto_trace_processor_mac_instruments) {
+  config("expat_public_config") {
+    defines = [ "XML_STATIC" ]
+    cflags = [
+      # Using -isystem instead of include_dirs (-I), so we don't need to
+      # suppress warnings coming from third-party headers. Doing so would mask
+      # warnings in our own code.
+      perfetto_isystem_cflag,
+      rebase_path("expat/src/expat/lib", root_build_dir),
+      perfetto_isystem_cflag,
+      rebase_path("expat/include", root_build_dir),
+    ]
+  }
+
+  source_set("expat") {
+    sources = [
+      "expat/src/expat/lib/expat.h",
+      "expat/src/expat/lib/xmlparse.c",
+      "expat/src/expat/lib/xmlrole.c",
+      "expat/src/expat/lib/xmltok.c",
+    ]
+
+    public_configs = [ ":expat_public_config" ]
+    configs -= [ "//gn/standalone:extra_warnings" ]
+    configs += [ ":no_format_warning" ]
+
+    defines = [
+      "_LIB",
+      "HAVE_EXPAT_CONFIG_H",
+    ]
+  }
+}
+
 config("linenoise_config") {
   visibility = _buildtools_visibility
   cflags = [
@@ -1572,4 +1609,18 @@
       rebase_path("grpc/src/include", root_build_dir),
     ]
   }
+  config("cpp_httplib_config") {
+    visibility = _buildtools_visibility
+    cflags = [
+      perfetto_isystem_cflag,
+      rebase_path("cpp-httplib", root_build_dir),
+    ]
+  }
+
+  source_set("cpp_httplib") {
+    visibility = _buildtools_visibility
+    sources = [ "cpp-httplib/httplib.h" ]
+    public_configs = [ ":cpp_httplib_config" ]
+    deps = [ "//gn:default_deps" ]
+  }
 }
diff --git a/buildtools/expat/include/BUILD b/buildtools/expat/include/BUILD
new file mode 100644
index 0000000..1c6cb8f
--- /dev/null
+++ b/buildtools/expat/include/BUILD
@@ -0,0 +1,24 @@
+# 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.
+
+cc_library(
+    name = "expat_config",
+    hdrs = [
+      "expat_config.h",
+    ],
+    includes = [
+        ".",
+    ],
+    visibility = ["//visibility:public"],
+)
diff --git a/buildtools/expat/include/expat_config.h b/buildtools/expat/include/expat_config.h
new file mode 100644
index 0000000..8ce2388
--- /dev/null
+++ b/buildtools/expat/include/expat_config.h
@@ -0,0 +1,146 @@
+/* expat_config.h.  Generated from expat_config.h.in by configure.  */
+/* expat_config.h.in.  Generated from configure.ac by autoheader.  */
+
+#ifndef EXPAT_CONFIG_H
+#define EXPAT_CONFIG_H 1
+
+/* Define if building universal (internal helper macro) */
+/* #undef AC_APPLE_UNIVERSAL_BUILD */
+
+/* 1234 = LILENDIAN, 4321 = BIGENDIAN */
+#define BYTEORDER 1234
+
+/* Define to 1 if you have the `arc4random' function. */
+/* #undef HAVE_ARC4RANDOM */
+
+/* Define to 1 if you have the `arc4random_buf' function. */
+/* #define HAVE_ARC4RANDOM_BUF 1 */
+
+/* define if the compiler supports basic C++11 syntax */
+#define HAVE_CXX11 1
+
+/* Define to 1 if you have the <dlfcn.h> header file. */
+#define HAVE_DLFCN_H 1
+
+/* Define to 1 if you have the <fcntl.h> header file. */
+#define HAVE_FCNTL_H 1
+
+/* Define to 1 if you have the `getpagesize' function. */
+#define HAVE_GETPAGESIZE 1
+
+/* Define to 1 if you have the `getrandom' function. */
+/* #define HAVE_GETRANDOM 1 */
+
+/* Define to 1 if you have the <inttypes.h> header file. */
+#define HAVE_INTTYPES_H 1
+
+/* Define to 1 if you have the `bsd' library (-lbsd). */
+/* #undef HAVE_LIBBSD */
+
+/* Define to 1 if you have a working `mmap' system call. */
+#define HAVE_MMAP 1
+
+/* Define to 1 if you have the <stdint.h> header file. */
+#define HAVE_STDINT_H 1
+
+/* Define to 1 if you have the <stdio.h> header file. */
+#define HAVE_STDIO_H 1
+
+/* Define to 1 if you have the <stdlib.h> header file. */
+#define HAVE_STDLIB_H 1
+
+/* Define to 1 if you have the <strings.h> header file. */
+#define HAVE_STRINGS_H 1
+
+/* Define to 1 if you have the <string.h> header file. */
+#define HAVE_STRING_H 1
+
+/* Define to 1 if you have `syscall' and `SYS_getrandom'. */
+/* #define HAVE_SYSCALL_GETRANDOM 1 */
+
+/* Define to 1 if you have the <sys/param.h> header file. */
+#define HAVE_SYS_PARAM_H 1
+
+/* Define to 1 if you have the <sys/stat.h> header file. */
+#define HAVE_SYS_STAT_H 1
+
+/* Define to 1 if you have the <sys/types.h> header file. */
+#define HAVE_SYS_TYPES_H 1
+
+/* Define to 1 if you have the <unistd.h> header file. */
+#define HAVE_UNISTD_H 1
+
+/* Define to the sub-directory where libtool stores uninstalled libraries. */
+#define LT_OBJDIR ".libs/"
+
+/* Name of package */
+#define PACKAGE "expat"
+
+/* Define to the address where bug reports for this package should be sent. */
+#define PACKAGE_BUGREPORT "https://github.com/libexpat/libexpat/issues"
+
+/* Define to the full name of this package. */
+#define PACKAGE_NAME "expat"
+
+/* Define to the full name and version of this package. */
+#define PACKAGE_STRING "expat 2.6.2"
+
+/* Define to the one symbol short name of this package. */
+#define PACKAGE_TARNAME "expat"
+
+/* Define to the home page for this package. */
+#define PACKAGE_URL ""
+
+/* Define to the version of this package. */
+#define PACKAGE_VERSION "2.6.2"
+
+/* Define to 1 if all of the C90 standard headers exist (not just the ones
+   required in a freestanding environment). This macro is provided for
+   backward compatibility; new code need not use it. */
+#define STDC_HEADERS 1
+
+/* Version number of package */
+#define VERSION "2.6.2"
+
+/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most
+   significant byte first (like Motorola and SPARC, unlike Intel). */
+#if defined AC_APPLE_UNIVERSAL_BUILD
+#if defined __BIG_ENDIAN__
+#define WORDS_BIGENDIAN 1
+#endif
+#else
+#ifndef WORDS_BIGENDIAN
+/* #  undef WORDS_BIGENDIAN */
+#endif
+#endif
+
+/* Define to allow retrieving the byte offsets for attribute names and values.
+ */
+/* #undef XML_ATTR_INFO */
+
+/* Define to specify how much context to retain around the current parse
+   point, 0 to disable. */
+#define XML_CONTEXT_BYTES 1024
+
+/* Define to include code reading entropy from `/dev/urandom'. */
+#define XML_DEV_URANDOM 1
+
+/* Define to make parameter entity parsing functionality available. */
+#define XML_DTD 1
+
+/* Define as 1/0 to enable/disable support for general entities. */
+#define XML_GE 1
+
+/* Define to make XML Namespaces functionality available. */
+#define XML_NS 1
+
+/* Define to empty if `const' does not conform to ANSI C. */
+/* #undef const */
+
+/* Define to `long int' if <sys/types.h> does not define. */
+/* #undef off_t */
+
+/* Define to `unsigned int' if <sys/types.h> does not define. */
+/* #undef size_t */
+
+#endif  // ndef EXPAT_CONFIG_H
\ No newline at end of file
diff --git a/buildtools/libcxx_config/__config_site b/buildtools/libcxx_config/__config_site
index dd396f0..c5a9f13 100644
--- a/buildtools/libcxx_config/__config_site
+++ b/buildtools/libcxx_config/__config_site
@@ -93,4 +93,10 @@
 #define _LIBCPP_CHAR_TRAITS_REMOVE_BASE_SPECIALIZATION
 
 #define _LIBCPP_HARDENING_MODE_DEFAULT _LIBCPP_HARDENING_MODE_NONE
+
+// Enable libcxx part of compile-time thread safety analysis.
+#if defined(__clang__)
+#define _LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS 1
+#endif
+
 #endif // _LIBCPP_CONFIG_SITE
diff --git a/docs/analysis/common-queries.md b/docs/analysis/common-queries.md
index 6675bc0..e69de29 100644
--- a/docs/analysis/common-queries.md
+++ b/docs/analysis/common-queries.md
@@ -1,102 +0,0 @@
-# PerfettoSQL Common Queries
-
-This page acts as a reference guide for queries which often appear when
-performing ad-hoc analysis.
-
-## Computing CPU time for slices
-If collecting traces which including scheduling information (i.e. from ftrace)
-as well as userspace slices (i.e. from atrace), the actual time spent running
-on a CPU for each userspace slice can be computed: this is commonly known as
-the "CPU time" for a slice.
-
-Firstly, setup the views to simplify subsequent queries:
-```
-DROP VIEW IF EXISTS slice_with_utid;
-CREATE VIEW slice_with_utid AS
-SELECT
-  ts,
-  dur,
-  slice.name as slice_name,
-  slice.id as slice_id, utid,
-  thread.name as thread_name
-FROM slice
-JOIN thread_track ON thread_track.id = slice.track_id
-JOIN thread USING (utid);
-
-DROP TABLE IF EXISTS slice_thread_state_breakdown;
-CREATE VIRTUAL TABLE slice_thread_state_breakdown
-USING SPAN_LEFT_JOIN(
-  slice_with_utid PARTITIONED utid,
-  thread_state PARTITIONED utid
-);
-```
-
-Then, to compute the CPU time for all slices in the trace:
-```
-SELECT slice_id, slice_name, SUM(dur) AS cpu_time
-FROM slice_thread_state_breakdown
-WHERE state = 'Running'
-GROUP BY slice_id;
-```
-
-You can also compute CPU time for a specific slice:
-```
-SELECT slice_name, SUM(dur) AS cpu_time
-FROM slice_thread_state_breakdown
-WHERE slice_id = <your slice id> AND state = 'Running';
-```
-
-These queries can be varied easily to compute other similar metrics.
-For example to get the time spent "runnable" and in "uninterruptible sleep":
-```
-SELECT
-  slice_id,
-  slice_name,
-  SUM(IIF(state = 'R', dur, 0)) AS runnable_time,
-  SUM(IIF(state = 'D', dur, 0)) AS uninterruptible_time
-FROM slice_thread_state_breakdown
-GROUP BY slice_id;
-```
-
-## Computing scheduling time by woken threads
-A given thread might cause other threads to wake up i.e. because work was
-scheduled on them. For a given thread, the amount of time threads it
-woke up ran for can be a good proxy to understand how much work is being
-spawned.
-
-To compute this, the following query can be used:
-```
-SELECT
-  SUM((
-    SELECT dur FROM sched
-    WHERE
-      sched.ts > wakee_runnable.ts AND
-      wakee_runnable.utid = wakee_runnable.utid
-    ORDER BY ts
-    LIMIT 1
-  )) AS scheduled_dur
-FROM thread AS waker
-JOIN thread_state AS wakee_runnable ON waker.utid = wakee_runnable.waker_utid
-WHERE waker.name = <your waker thread name here>
-```
-
-To do this for all the threads in the trace simultaenously:
-```
-SELECT
-  waker_process.name AS process_name,
-  waker.name AS thread_name,
-  SUM((
-    SELECT dur FROM sched
-    WHERE
-      sched.ts > wakee_runnable.ts AND
-      sched.utid = wakee_runnable.utid
-    ORDER BY ts
-    LIMIT 1
-  )) AS scheduled_dur
-FROM thread AS waker
-JOIN process AS waker_process USING (upid)
-JOIN thread_state AS wakee_runnable ON waker.utid = wakee_runnable.waker_utid
-WHERE waker.utid != 0
-GROUP BY 1, 2
-ORDER BY 3 desc
-```
diff --git a/docs/analysis/debug-tracks.md b/docs/analysis/debug-tracks.md
index e3c973c..c90b034 100644
--- a/docs/analysis/debug-tracks.md
+++ b/docs/analysis/debug-tracks.md
@@ -1,33 +1,87 @@
 # Debug Tracks
 
 Debug Tracks are a way to display tabular results from running a PerfettoSQL
-query as a new so-called "debug" track. Specifically, if the resultant table can
-be visualised in a slice format (for example, the
-[`slice`](sql-tables.autogen#slice) table), a debug track can be created from
-it.
+query as a so-called "debug" track. Specifically, if the resultant table can
+be visualised in a slice format (ex: the
+[`slice`](sql-tables.autogen#slice) table) or counter format
+(ex: the [`counter`](sql-tables.autogen#counter) table),
+a debug track can be created from it.
 
-For a result table to be able to be visualised in a slice format, it should
+For a result table to be visualised, it should
 include:
 
-1.  A name (the name of the slice) column.
-1.  A non-null timestamp (the timestamp, in nanoseconds, at the start of the
-    slice) column.
-1.  (Optionally) a duration (the duration, in nanoseconds, of the slice) column.
+1. A name (the name of the slice) column.
+1. A non-null timestamp (the timestamp, in nanoseconds, at the start of the
+  slice) column.
+1. (For `slice` tracks) a duration (the duration, in nanoseconds, of the slice)
+   column.
+1. (Optionally) the name of a column to pivot
 
-To create a new debug track:
+    Note: Pivoting means allows you to create a single debug track per distinct
+    value in the selected "pivot" column.
 
-1.  Run a SQL query, and ensure its results are `slice`-like (as described
-    above).
-    ![Query for debug track](/docs/images/debug-tracks/debug-tracks-query.png)
-1.  Navigate to the Timeline view, and click on "Show debug track" to set up a
-    new debug track. Note that the names of the columns in the result table do
-    not necessarily have to be `name`, `ts`, or `dur`. Columns which
-    *semantically* match but have a different name can be selected from the
-    drop-down selectors.
-    ![Create a new debug track](/docs/images/debug-tracks/debug-tracks-create.png)
-1.  The debug track is visible as a pinned track near the top of the Timeline
-    view with slices from the table from which the track was created (note that
-    slices with no/zero duration will be displayed as instant events). The debug
-    track may be manually unpinned and then it should appear on the top of other
-    unpinned tracks.
-    ![Resultant debug track](/docs/images/debug-tracks/debug-tracks-result.png)
+## Creating Debug `slice` Tracks
+
+To create `slice` tracks:
+
+1. Run a SQL query, and ensure its results are `slice`-like (as described
+  above).
+  ![Query for debug slice track](/docs/images/debug-tracks/slice-track-query.png)
+1. Navigate to the "Show Timeline" view, and click on "Show debug track" to set
+   up a new debug track. Select "slice" from the Track type dropdown.
+
+   Note that the names of the columns in the result table do
+   not necessarily have to be `name`, `ts`, or `dur`. Columns which
+   _semantically_ match but have a different name can be selected from the
+   drop-down selectors.
+
+   ![Create a new debug slice track](/docs/images/debug-tracks/slice-track-create.png)
+
+1. The debug slice track is visible as a pinned track near the top of the
+   Timeline view with slices from the table from which the track was created
+   (note that slices with no/zero duration will be displayed as instant events).
+   Debug tracks may be manually unpinned and will appear on the top of other
+   unpinned tracks.
+   ![Resultant debug track](/docs/images/debug-tracks/slice-track-result.png)
+
+1. (Optional) Pivoted `slice` tracks are created by selecting a value from the
+   "pivot" column.
+
+   Note: You can enter queries into the search box directly by typing `:` to
+   enter SQL mode.
+
+   ![Creating pivoted debug slice tracks](/docs/images/debug-tracks/pivot-slice-tracks-create.png)
+
+   This will result in a debug slice track created for each distinct pivot
+   value.
+
+   ![Resultant pivoted debug slice tracks](/docs/images/debug-tracks/pivot-slice-tracks-results.png)
+
+## Creating Debug `counter` Tracks
+
+You can create new debug `counter` tracks by following similar steps to the ones
+mentioned above:
+
+1. Run a SQL query, and ensure its results are `counter`-like (as described
+   above).
+
+   ![Query for debug counter track](/docs/images/debug-tracks/counter-tracks-query.png)
+1. Navigate to the Timeline view, and click on "Show debug track" to set up a
+   new debug track. Select "counter" from the Track type dropdown and the
+   semantically matching column names of interest.
+
+   ![Create a new debug counter track](/docs/images/debug-tracks/counter-tracks-create.png)
+
+1. The counter track will appear as a pinned track near the top of the Timeline view.
+
+   ![Resultant pivoted debug counter track](/docs/images/debug-tracks/counter-tracks-results.png)
+
+1. (Optional) Pivoted `counter` tracks are created by selecting a value from the
+   "pivot" column.
+
+   ![Create a new debug counter track](/docs/images/debug-tracks/pivot-counter-tracks-create.png)
+
+   This will result in a debug counter track created for each distinct pivot
+   value.
+
+   ![Resultant pivoted debug counter track](/docs/images/debug-tracks/pivot-counter-tracks-results.png)
diff --git a/docs/analysis/trace-processor.md b/docs/analysis/trace-processor.md
index a459566..2de3419 100644
--- a/docs/analysis/trace-processor.md
+++ b/docs/analysis/trace-processor.md
@@ -509,36 +509,6 @@
 The metrics subsystem is a significant part of trace processor and thus is
 documented on its own [page](/docs/analysis/metrics.md).
 
-## Creating derived events
-
-TIP: To see how to add to add a new annotation to trace processor, see the
-     checklist [here](/docs/contributing/common-tasks.md#new-annotation).
-
-This feature allows creation of new events (slices and counters) from the data
-in the trace. These events can then be displayed in the UI tracks as if they
-were part of the trace itself.
-
-This is useful as often the data in the trace is very low-level. While low
-level information is important for experts to perform deep debugging, often
-users are just looking for a high level overview without needing to consider
-events from multiple locations.
-
-For example, an app startup in Android spans multiple components including
-`ActivityManager`, `system_server`, and the newly created app process derived
-from `zygote`. Most users do not need this level of detail; they are only
-interested in a single slice spanning the entire startup.
-
-Creating derived events is tied very closely to
-[metrics subsystem](/docs/analysis/metrics.md); often SQL-based metrics need to
-create higher-level abstractions from raw events as intermediate artifacts.
-
-From previous example, the
-[startup metric](/src/trace_processor/metrics/sql/android/android_startup.sql)
-creates the exact `launching` slice we want to display in the UI.
-
-The other benefit of aligning the two is that changes in metrics are
-automatically kept in sync with what the user sees in the UI.
-
 ## Python API
 The trace processor's C++ library is also exposed through Python. This
 is documented on a [separate page](/docs/analysis/trace-processor-python.md).
diff --git a/docs/contributing/common-tasks.md b/docs/contributing/common-tasks.md
index 11d4f14..43c5421 100644
--- a/docs/contributing/common-tasks.md
+++ b/docs/contributing/common-tasks.md
@@ -138,47 +138,6 @@
   3. Run the newly added test with `tools/diff_test_trace_processor.py <path to trace processor shell binary>`.
 4. Upload and land your change as normal.
 
-## Adding new derived events
-
-As derived events depend on metrics, the initial steps are same as that of developing a metric (see above).
-
-NOTE: the metric can be just an empty proto message during prototyping or if no summarization is necessary. However, generally if an event is important enough to display in the UI, it should also be tracked in benchmarks as a metric.
-
-To extend a metric with annotations:
-
-1. Create a new table or view with the name `<metric name>_event`.
-  * For example, for the [`android_startup`]() metric, we create a view named `android_startup_event`.
-  * Note that the trailing `_event` suffix in the table name is important.
-  * The schema required for this table is given below.
-2. List your metric in the `initialiseHelperViews` method of `trace_controller.ts`.
-3. Upload and land your change as normal.
-
-The schema of the `<metric name>_event` table/view is as follows:
-
-| Name         | Type     | Presence                              | Meaning                                                                                                                                                                                                                                     |
-| :----------- | -------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `track_type` | `string` | Mandatory                             | 'slice' for slices, 'counter' for counters                                                                                                                                                                                                  |
-| `track_name` | `string` | Mandatory                             | Name of the track to display in the UI. Also the track identifier i.e. all events with same `track_name` appear on the same track.                                                                                                          |
-| `ts`         | `int64`  | Mandatory                             | The timestamp of the event (slice or counter)                                                                                                                                                                                               |
-| `dur`        | `int64`  | Mandatory for slice, NULL for counter | The duration of the slice                                                                                                                                                                                                                   |
-| `slice_name` | `string` | Mandatory for slice, NULL for counter | The name of the slice                                                                                                                                                                                                                       |
-| `value`      | `double` | Mandatory for counter, NULL for slice | The value of the counter                                                                                                                                                                                                                    |
-| `group_name` | `string` | Optional                              | Name of the track group under which the track appears. All tracks with the same `group_name` are placed under the same group by that name. Tracks that lack this field or have NULL value in this field are displayed without any grouping. |
-
-#### Known issues:
-
-* Nested slices within the same track are not supported. We plan to support this
-  once we have a concrete usecase.
-* Tracks are always created in the global scope. We plan to extend this to
-  threads and processes in the near future with additional contexts added as
-  necessary.
-* Instant events are currently not supported in the UI but this will be
-  implemented in the near future. In trace processor, instants are always `0`
-  duration slices with special rendering on the UI side.
-* There is no way to tie newly added events back to the source events in the
-  trace which were used to generate them. This is not currently a priority but
-  something we may add in the future.
-
 
 ## Update `TRACE_PROCESSOR_CURRENT_API_VERSION`
 
diff --git a/docs/contributing/embedding.md b/docs/contributing/embedding.md
index 0d080c9..58b796f 100644
--- a/docs/contributing/embedding.md
+++ b/docs/contributing/embedding.md
@@ -43,10 +43,3 @@
 Metrics can also be registered at run time using the `RegisterMetric` and `ExtendMetricsProto` functions. These can subsequently be executed with `ComputeMetric`.
 
 WARNING: embedders should ensure that the path of any registered metric is consistent with the name used to execute the metric and output view in the SQL.
-
-### Creating derived events
-
-As creating derived events is tied to the metrics subsystem, the `ComputeMetrics` function in the trace processor API should be called with the appropriate metrics. This will create the `<metric_name>_event` table/view which can then be queried using the `ExectueQuery` function.
-
-NOTE: At some point, there are plans to add an API which does not create the metrics proto but just executes the queries in the metric.
-
diff --git a/docs/contributing/ui-plugins.md b/docs/contributing/ui-plugins.md
index 6baadc6..cc65f43 100644
--- a/docs/contributing/ui-plugins.md
+++ b/docs/contributing/ui-plugins.md
@@ -1,15 +1,15 @@
 # UI plugins
-The Perfetto UI can be extended with plugins. These plugins are shipped
-part of Perfetto.
+The Perfetto UI can be extended with plugins. These plugins are shipped part of
+Perfetto.
 
 ## Create a plugin
-The guide below explains how to create a plugin for the Perfetto UI.
+The guide below explains how to create a plugin for the Perfetto UI. You can
+browse the public plugin API [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public).
 
 ### Prepare for UI development
-First we need to prepare the UI development environment.
-You will need to use a MacOS or Linux machine.
-Follow the steps below or see the
-[Getting Started](./getting-started) guide for more detail.
+First we need to prepare the UI development environment. You will need to use a
+MacOS or Linux machine. Follow the steps below or see the [Getting
+Started](./getting-started) guide for more detail.
 
 ```sh
 git clone https://android.googlesource.com/platform/external/perfetto/
@@ -21,21 +21,19 @@
 ```sh
 cp -r ui/src/plugins/com.example.Skeleton ui/src/plugins/<your-plugin-name>
 ```
-Now edit `ui/src/plugins/<your-plugin-name>/index.ts`.
-Search for all instances of `SKELETON: <instruction>` in the file and
-follow the instructions.
+Now edit `ui/src/plugins/<your-plugin-name>/index.ts`. Search for all instances
+of `SKELETON: <instruction>` in the file and follow the instructions.
 
 Notes on naming:
 - Don't name the directory `XyzPlugin` just `Xyz`.
 - The `pluginId` and directory name must match.
-- Plugins should be prefixed with the reversed components of a domain
-  name you control. For example if `example.com` is your domain your
-  plugin should be named `com.example.Foo`.
-- Core plugins maintained by the Perfetto team should use
-  `dev.perfetto.Foo`.
+- Plugins should be prefixed with the reversed components of a domain name you
+  control. For example if `example.com` is your domain your plugin should be
+  named `com.example.Foo`.
+- Core plugins maintained by the Perfetto team should use `dev.perfetto.Foo`.
 - Commands should have ids with the pattern `example.com#DoSomething`
-- Command's ids should be prefixed with the id of the plugin which
-  provides them.
+- Command's ids should be prefixed with the id of the plugin which provides
+  them.
 - Command names should have the form "Verb something something", and should be
   in normal sentence case. I.e. don't capitalize the first letter of each word.
   - Good: "Pin janky frame timeline tracks"
@@ -48,246 +46,407 @@
 Now navigate to [localhost:10000](http://localhost:10000/)
 
 ### Enable your plugin
-- Navigate to the plugins page: [localhost:10000/#!/plugins](http://localhost:10000/#!/plugins).
+- Navigate to the plugins page:
+  [localhost:10000/#!/plugins](http://localhost:10000/#!/plugins).
 - Ctrl-F for your plugin name and enable it.
+- Enabling/disabling plugins requires a restart of the UI, so refresh the page
+  to start your plugin.
 
-Later you can request for your plugin to be enabled by default.
-Follow the [default plugins](#default-plugins) section for this.
+Later you can request for your plugin to be enabled by default. Follow the
+[default plugins](#default-plugins) section for this.
 
 ### Upload your plugin for review
 - Update `ui/src/plugins/<your-plugin-name>/OWNERS` to include your email.
-- Follow the [Contributing](./getting-started#contributing)
-  instructions to upload your CL to the codereview tool.
+- Follow the [Contributing](./getting-started#contributing) instructions to
+  upload your CL to the codereview tool.
 - Once uploaded add `stevegolton@google.com` as a reviewer for your CL.
 
-## Plugin extension points
-Plugins can extend a handful of specific places in the UI. The sections
-below show these extension points and give examples of how they can be
-used.
+## Plugin Lifecycle
+To demonstrate the plugin's lifecycle, this is a minimal plugin that implements
+the key lifecycle hooks:
 
-### Commands
-Commands are user issuable shortcuts for actions in the UI.
-They can be accessed via the omnibox.
+```ts
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
 
-Follow the [create a plugin](#create-a-plugin) to get an initial
-skeleton for your plugin.
-
-To add your first command, add a call to `ctx.registerCommand()` in either
-your `onActivate()` or `onTraceLoad()` hooks. The recommendation is to register
-commands in `onActivate()` by default unless they require something from
-`PluginContextTrace` which is not available on `PluginContext`.
-
-The tradeoff is that commands registered in `onTraceLoad()` are only available
-while a trace is loaded, whereas commands registered in `onActivate()` are
-available all the time the plugin is active.
-
-```typescript
-class MyPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.registerCommand(
-       {
-         id: 'dev.perfetto.ExampleSimpleCommand#LogHelloPlugin',
-         name: 'Log "Hello, plugin!"',
-         callback: () => console.log('Hello, plugin!'),
-       },
-    );
+  static onActivate(app: App): void {
+    // Called once on app startup
+    console.log('MyPlugin::onActivate()', app.pluginId);
+    // Note: It's rare that plugins would need this hook as most plugins are
+    // interested in trace details. Thus, this function can usually be omitted.
   }
 
-  onTraceLoad(ctx: PluginContextTrace): void {
-    ctx.registerCommand(
-       {
-         id: 'dev.perfetto.ExampleSimpleTraceCommand#LogHelloTrace',
-         name: 'Log "Hello, trace!"',
-         callback: () => console.log('Hello, trace!'),
-       },
-    );
+  constructor(trace: Trace) {
+    // Called each time a trace is loaded
+    console.log('MyPlugin::constructor()', trace.traceInfo.traceTitle);
+  }
+
+  async onTraceLoad(trace: Trace): Promise<void> {
+    // Called each time a trace is loaded
+    console.log('MyPlugin::onTraceLoad()', trace.traceInfo.traceTitle);
+    // Note this function returns a promise, so any any async calls should be
+    // completed before this promise resolves as the app using this promise for
+    // timing and plugin synchronization.
   }
 }
 ```
 
-Here `id` is a unique string which identifies this command.
-The `id` should be prefixed with the plugin id followed by a `#`. All command
-`id`s must be unique system-wide.
-`name` is a human readable name for the command, which is shown in the command
-palette.
-Finally `callback()` is the callback which actually performs the
-action.
+You can run this plugin with devtools to see the log messages in the console,
+which should give you a feel for the plugin lifecycle. Try opening a few traces
+one after another.
 
-Commands are removed automatically when their context disappears. Commands
-registered with the `PluginContext` are removed when the plugin is deactivated,
-and commands registered with the `PluginContextTrace` are removed when the trace
-is unloaded.
+`onActivate()` runs shortly after Perfetto starts up, before a trace is loaded.
+This is where the you'll configure your plugin's capabilities that aren't trace
+dependent. At this point the plugin's class is not instantiated, so you'll
+notice `onActivate()` hook is a static class member. `onActivate()` is only ever
+called once, regardless of the number of traces loaded.
+
+`onActivate()` is passed an `App` object which the plugin can use to configure
+core capabilities such as commands, sidebar items and pages. Capabilities
+registered on the App interface are persisted throughout the lifetime of the app
+(practically forever until the tab is closed), in contrast to what happens for
+the same methods on the `Trace` object (see below).
+
+The plugin class in instantiated when a trace is loaded (a new plugin instance
+is created for each trace). `onTraceLoad()` is called immediately after the
+class is instantiated, which is where you'll configure your plugin's trace
+dependent capabilities.
+
+`onTraceLoad()` is passed a `Trace` object which the plugin can use to configure
+entities that are scoped to a specific trace, such as tracks and tabs. `Trace`
+is a superset of `App`, so anything you can do with `App` you can also do with
+`Trace`, however, capabilities registered on `Trace` will typically be discarded
+when a new trace is loaded.
+
+A plugin will typically register capabilities with the core and return quickly.
+But these capabilities usually contain objects and callbacks which are called
+into later by the core during the runtime of the app. Most capabilities require
+a `Trace` or an `App` to do anything useful so these are usually bound into the
+capabilities at registration time using JavaScript classes or closures.
+
+```ts
+// Toy example: Code will not compile.
+async onTraceLoad(trace: Trace) {
+  // `trace` is captured in the closure and used later by the app
+  trace.regsterXYZ(() => trace.xyz);
+}
+```
+
+That way, the callback is bound to a specific trace object which and the trace
+object can outlive the runtime of the `onTraceLoad()` function, which is a very
+common pattern in Perfetto plugins.
+
+> Note: Some capabilities can be registered on either the `App` or the `Trace`
+> object (i.e. in `onActivate()` or in `onTraceLoad()`), if in doubt about which
+> one to use, use `onTraceLoad()` as this is more than likely the one you want.
+> Most plugins add tracks and tabs that depend on the trace. You'd usually have
+> to be doing something out of the ordinary if you need to use `onActivate()`.
+
+### Performance
+`onActivate()` and `onTraceLoad()` should generally complete as quickly as
+possible, however sometimes `onTraceLoad()` may need to perform async operations
+on trace processor such as performing queries and/or creating views and tables.
+Thus, `onTraceLoad()` should return a promise (or you can simply make it an
+async function). When this promise resolves it tells the core that the plugin is
+fully initialized.
+
+> Note: It's important that any async operations done in onTraceLoad() are
+> awaited so that all async operations are completed by the time the promise is
+> resolved. This is so that plugins can be properly timed and synchronized.
+
+
+```ts
+// GOOD
+async onTraceLoad(trace: Trace) {
+  await trace.engine.query(...);
+}
+
+// BAD
+async onTraceLoad(trace: Trace) {
+  // Note the missing await!
+  trace.engine.query(...);
+}
+```
+
+## Extension Points
+Plugins can extend functionality of Perfetto by registering capabilities via
+extension points on the `App` or `Trace` objects.
+
+The following sections delve into more detail on each extension point and
+provide examples of how they can be used.
+
+### Commands
+Commands are user issuable shortcuts for actions in the UI. They are invoked via
+the command palette which can be opened by pressing Ctrl+Shift+P (or Cmd+Shift+P
+on Mac), or by typing a '>' into the omnibox.
+
+To add a command, add a call to `registerCommand()` on either your
+`onActivate()` or `onTraceLoad()` hooks. The recommendation is to register
+commands in `onTraceLoad()` by default unless you very specifically want the
+command to be available before a trace has loaded.
+
+Example of a command that doesn't require a trace.
+```ts
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  static onActivate(app: App) {
+    app.commands.registerCommand({
+      id: `${app.pluginId}#SayHello`,
+      name: 'Say hello',
+      callback: () => console.log('Hello, world!'),
+    });
+  }
+}
+```
+
+Example of a command that requires a trace object - in this case the trace
+title.
+```ts
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace) {
+    trace.commands.registerCommand({
+      id: `${trace.pluginId}#LogTraceTitle`,
+      name: 'Log trace title',
+      callback: () => console.log(trace.info.traceTitle),
+    });
+  }
+}
+```
+
+> Notice that the trace object is captured in the closure, so it can be used
+> after the onTraceLoad() function has returned. This is a very common pattern
+> in Perfetto plugins.
+
+Command arguments explained:
+- `id` is a unique string which identifies this command. The `id` should be
+prefixed with the plugin id followed by a `#`. All command `id`s must be unique
+system-wide.
+- `name` is a human readable name for the command, which is shown in the command
+palette.
+- `callback()` is the callback which actually performs the action.
+
+#### Async commands
+It's common that commands will perform async operations in their callbacks. It's
+recommended to use async/await for this rather than `.then().catch()`. The
+easiest way to do this is to make the callback an async function.
+
+```ts
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace) {
+    trace.commands.registerCommand({
+      id: `${trace.pluginId}#QueryTraceProcessor`,
+      name: 'Query trace processor',
+      callback: async () => {
+        const results = await trace.engine.query(...);
+        // use results...
+      },
+    });
+  }
+}
+```
+
+If the callback is async (i.e. it returns a promise), nothing special happens.
+The command is still fire-n-forget as far as the core is concerned.
 
 Examples:
-- [dev.perfetto.ExampleSimpleCommand](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts).
+- [com.example.ExampleSimpleCommand](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/com.example.ExampleSimpleCommand/index.ts).
 - [perfetto.CoreCommands](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/core_plugins/commands/index.ts).
-- [dev.perfetto.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleState/index.ts).
+- [com.example.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/com.example.ExampleState/index.ts).
 
-#### Hotkeys
-
-A default hotkey may be provided when registering a command.
+### Hotkeys
+A hotkey may be associated with a command at registration time.
 
 ```typescript
-ctx.registerCommand({
-  id: 'dev.perfetto.ExampleSimpleCommand#LogHelloWorld',
-  name: 'Log "Hello, World!"',
-  callback: () => console.log('Hello, World!'),
+ctx.commands.registerCommand({
+  ...
   defaultHotkey: 'Shift+H',
 });
 ```
 
-Even though the hotkey is a string, it's format checked at compile time using 
-typescript's [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html).
+Despite the fact that the hotkey is a string, its format is checked at compile
+time using typescript's [template literal
+types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html).
 
-See [hotkey.ts](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/base/hotkeys.ts)
+See
+[hotkey.ts](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/base/hotkeys.ts)
 for more details on how the hotkey syntax works, and for the available keys and
 modifiers.
 
+Note this is referred to as the 'default' hotkey because we may introduce a
+feature in the future where users can modify their hotkeys, though this doesn't
+exist at the moment.
+
 ### Tracks
-#### Defining Tracks
-Tracks describe how to render a track and how to respond to mouse interaction.
-However, the interface is a WIP and should be considered unstable.
-This documentation will be added to over the next few months after the design is
-finalised.
+In order to add a new track to the timeline, you'll need to create two entities:
+- A track 'renderer' which controls what the track looks like and how it fetches
+  data from trace processor.
+- A track 'node' controls where the track appears in the workspace.
 
-#### Reusing Existing Tracks
-Creating tracks from scratch is difficult and the API is currently a WIP, so it
-is strongly recommended to use one of our existing base classes which do a lot
-of the heavy lifting for you. These base classes also provide a more stable
-layer between your track and the (currently unstable) track API.
+Track renderers are powerful but complex, so it's, so it's strongly advised not
+to create your own. Instead, by far the easiest way to get started with tracks
+is to use the `createQuerySliceTrack` and `createQueryCounterTrack` helpers.
 
-For example, if your track needs to show slices from a given a SQL expression (a
-very common pattern), extend the `NamedSliceTrack` abstract base class and
-implement `getSqlSource()`, which should return a query with the following
-columns:
-
-- `id: INTEGER`: A unique ID for the slice.
-- `ts: INTEGER`: The timestamp of the start of the slice.
-- `dur: INTEGER`: The duration of the slice.
-- `depth: INTEGER`: Integer value defining how deep the slice should be drawn in
-    the track, 0 being rendered at the top of the track, and increasing numbers
-    being drawn towards the bottom of the track.
-- `name: TEXT`: Text to be rendered on the slice and in the popup.
-
-For example, the following track describes a slice track that displays all
-slices that begin with the letter 'a'.
+Example:
 ```ts
-class MyTrack extends NamedSliceTrack {
-  getSqlSource(): string {
-    return `
-    SELECT
-      id,
-      ts,
-      dur,
-      depth,
-      name
-    from slice
-    where name like 'a%'
-    `;
-  }
-}
-```
+import {createQuerySliceTrack} from '../../public/lib/tracks/query_slice_track';
 
-#### Registering Tracks
-Plugins may register tracks with Perfetto using
-`PluginContextTrace.registerTrack()`, usually in their `onTraceLoad` function.
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace) {
+    const title = 'My Track';
+    const uri = `${trace.pluginId}#MyTrack`;
+    const query = 'select * from slice where track_id = 123';
 
-```ts
-class MyPlugin implements Plugin {
-  onTraceLoad(ctx: PluginContextTrace): void {
-    ctx.registerTrack({
-      uri: 'dev.MyPlugin#ExampleTrack',
-      displayName: 'My Example Track',
-      trackFactory: ({trackKey}) => {
-        return new MyTrack({engine: ctx.engine, trackKey});
+    // Create a new track renderer based on a query
+    const track = await createQuerySliceTrack({
+      trace,
+      uri,
+      data: {
+        sqlSource: query,
       },
     });
+
+    // Register the track renderer with the core
+    trace.tracks.registerTrack({uri, title, track});
+
+    // Create a track node that references the track renderer using its uri
+    const track = new TrackNode({uri, title});
+
+    // Add the track node to the current workspace
+    trace.workspace.addChildInOrder(track);
   }
 }
 ```
 
-#### Default Tracks
-The "default" tracks are a list of tracks that are added to the timeline when a
-fresh trace is loaded (i.e. **not** when loading a trace from a permalink).
-This list is copied into the timeline after the trace has finished loading, at
-which point control is handed over to the user, allowing them add, remove and
-reorder tracks as they please.
-Thus it only makes sense to add default tracks in your plugin's `onTraceLoad`
-function, as adding a default track later will have no effect.
+See [the source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/lib/tracks/query_slice_track.ts)
+for detailed usage.
+
+You can also add a counter track using `createQueryCounterTrack` which works in
+a similar way.
 
 ```ts
-class MyPlugin implements Plugin {
-  onTraceLoad(ctx: PluginContextTrace): void {
-    ctx.registerTrack({
-      // ... as above ...
-    });
+import {createQueryCounterTrack} from '../../public/lib/tracks/query_counter_track';
 
-    ctx.addDefaultTrack({
-      uri: 'dev.MyPlugin#ExampleTrack',
-      displayName: 'My Example Track',
-      sortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-    });
-  }
-}
-```
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace) {
+    const title = 'My Counter Track';
+    const uri = `${trace.pluginId}#MyCounterTrack`;
+    const query = 'select * from counter where track_id = 123';
 
-Registering and adding a default track is such a common pattern that there is a
-shortcut for doing both in one go: `PluginContextTrace.registerStaticTrack()`,
-which saves having to repeat the URI and display name.
-
-```ts
-class MyPlugin implements Plugin {
-  onTraceLoad(ctx: PluginContextTrace): void {
-    ctx.registerStaticTrack({
-      uri: 'dev.MyPlugin#ExampleTrack',
-      displayName: 'My Example Track',
-      trackFactory: ({trackKey}) => {
-        return new MyTrack({engine: ctx.engine, trackKey});
-      },
-      sortKey: PrimaryTrackSortKey.COUNTER_TRACK,
-    });
-  }
-}
-```
-
-#### Adding Tracks Directly
-Sometimes plugins might want to add a track to the timeline immediately, usually
-as a result of a command or on some other user action such as a button click.
-We can do this using `PluginContext.timeline.addTrack()`.
-
-```ts
-class MyPlugin implements Plugin {
-  onTraceLoad(ctx: PluginContextTrace): void {
-    ctx.registerTrack({
-      // ... as above ...
-    });
-
-    // Register a command that directly adds a new track to the timeline
-    ctx.registerCommand({
-      id: 'dev.MyPlugin#AddMyTrack',
-      name: 'Add my track',
-      callback: () => {
-        ctx.timeline.addTrack(
-          'dev.MyPlugin#ExampleTrack',
-          'My Example Track'
-        );
+    // Create a new track renderer based on a query
+    const track = await createQueryCounterTrack({
+      trace,
+      uri,
+      data: {
+        sqlSource: query,
       },
     });
+
+    // Register the track renderer with the core
+    trace.tracks.registerTrack({uri, title, track});
+
+    // Create a track node that references the track renderer using its uri
+    const track = new TrackNode({uri, title});
+
+    // Add the track node to the current workspace
+    trace.workspace.addChildInOrder(track);
   }
 }
 ```
 
+See [the source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/lib/tracks/query_counter_track.ts)
+for detailed usage.
+
+#### Grouping Tracks
+Any track can have children. Just add child nodes any `TrackNode` object using
+its `addChildXYZ()` methods. Nested tracks are rendered as a collapsible tree.
+
+```ts
+const group = new TrackNode({title: 'Group'});
+trace.workspace.addChildInOrder(group);
+group.addChildLast(new TrackNode({title: 'Child Track A'}));
+group.addChildLast(new TrackNode({title: 'Child Track B'}));
+group.addChildLast(new TrackNode({title: 'Child Track C'}));
+```
+
+Tracks nodes with children can be collapsed and expanded manually by the user at
+runtime, or programmatically using their `expand()` and `collapse()` methods. By
+default tracks are collapsed, so to have tracks automatically expanded on
+startup you'll need to call `expand()` after adding the track node.
+
+```ts
+group.expand();
+```
+
+![Nested tracks](../images/ui-plugins/nested_tracks.png)
+
+Summary tracks are behave slightly differently to ordinary tracks. Summary
+tracks:
+- Are rendered with a light blue background when collapsed, dark blue when
+  expanded.
+- Stick to the top of the viewport when scrolling.
+- Area selections made on the track apply to child tracks instead of the summary
+  track itself.
+
+To create a summary track, set the `isSummary: true` option in its initializer
+list at creation time or set its `isSummary` property to true after creation.
+
+```ts
+const group = new TrackNode({title: 'Group', isSummary: true});
+// ~~~ or ~~~
+group.isSummary = true;
+```
+
+![Summary track](../images/ui-plugins/summary_track.png)
+
+Examples
+- [com.example.ExampleNestedTracks](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/com.example.ExampleNestedTracks/index.ts).
+
+#### Track Ordering
+Tracks can be manually reordered using the `addChildXYZ()` functions available on
+the track node api, including `addChildFirst()`, `addChildLast()`,
+`addChildBefore()`, and `addChildAfter()`.
+
+See [the workspace source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/workspace.ts) for detailed usage.
+
+However, when several plugins add tracks to the same node or the workspace, no
+single plugin has complete control over the sorting of child nodes within this
+node. Thus, the sortOrder property is be used to decentralize the sorting logic
+between plugins.
+
+In order to do this we simply give the track a `sortOrder` and call
+`addChildInOrder()` on the parent node and the track will be placed before the
+first track with a higher `sortOrder` in the list. (i.e. lower `sortOrder`s appear
+higher in the stack).
+
+```ts
+// PluginA
+workspace.addChildInOrder(new TrackNode({title: 'Foo', sortOrder: 10}));
+
+// Plugin B
+workspace.addChildInOrder(new TrackNode({title: 'Bar', sortOrder: -10}));
+```
+
+Now it doesn't matter which order plugin are initialized, track `Bar` will
+appear above track `Foo` (unless reordered later).
+
+If no `sortOrder` is defined, the track assumes a `sortOrder` of 0.
+
+> It is recommended to always use `addChildInOrder()` in plugins when adding
+> tracks to the `workspace`, especially if you want your plugin to be enabled by
+> default, as this will ensure it respects the sortOrder of other plugins.
+
+
 ### Tabs
 Tabs are a useful way to display contextual information about the trace, the
 current selection, or to show the results of an operation.
 
-To register a tab from a plugin, use the `PluginContextTrace.registerTab`
-method.
+To register a tab from a plugin, use the `Trace.registerTab` method.
 
 ```ts
-import m from 'mithril';
-import {Tab, Plugin, PluginContext, PluginContextTrace} from '../../public';
-
 class MyTab implements Tab {
   render(): m.Children {
     return m('div', 'Hello from my tab');
@@ -298,11 +457,11 @@
   }
 }
 
-class MyPlugin implements Plugin {
-  onActivate(_: PluginContext): void {}
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerTab({
-      uri: 'dev.MyPlugin#MyTab',
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace) {
+    trace.registerTab({
+      uri: `${trace.pluginId}#MyTab`,
       content: new MyTab(),
     });
   }
@@ -323,8 +482,8 @@
 Alternatively, tabs may be shown or hidden programmatically using the tabs API.
 
 ```ts
-ctx.tabs.showTab('dev.MyPlugin#MyTab');
-ctx.tabs.hideTab('dev.MyPlugin#MyTab');
+trace.tabs.showTab(`${trace.pluginId}#MyTab`);
+trace.tabs.hideTab(`${trace.pluginId}#MyTab`);
 ```
 
 Tabs have the following properties:
@@ -348,9 +507,9 @@
 registering the tab.
 
 ```ts
-ctx.registerTab({
+trace.registerTab({
   isEphemeral: true,
-  uri: 'dev.MyPlugin#MyTab',
+  uri: `${trace.pluginId}#MyTab`,
   content: new MyEphemeralTab(),
 });
 ```
@@ -363,13 +522,6 @@
 ```ts
 import m from 'mithril';
 import {uuidv4} from '../../base/uuid';
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-  Tab,
-} from '../../public';
 
 class MyNameTab implements Tab {
   constructor(private name: string) {}
@@ -381,21 +533,21 @@
   }
 }
 
-class MyPlugin implements Plugin {
-  onActivate(_: PluginContext): void {}
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerCommand({
-      id: 'dev.MyPlugin#AddNewEphemeralTab',
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace): Promise<void> {
+    trace.registerCommand({
+      id: `${trace.pluginId}#AddNewEphemeralTab`,
       name: 'Add new ephemeral tab',
-      callback: () => handleCommand(ctx),
+      callback: () => handleCommand(trace),
     });
   }
 }
 
-function handleCommand(ctx: PluginContextTrace): void {
+function handleCommand(trace: Trace): void {
   const name = prompt('What is your name');
   if (name) {
-    const uri = 'dev.MyPlugin#MyName' + uuidv4();
+    const uri = `${trace.pluginId}#MyName${uuidv4()}`;
     // This makes the tab available to perfetto
     ctx.registerTab({
       isEphemeral: true,
@@ -407,11 +559,6 @@
     ctx.tabs.showTab(uri);
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.MyPlugin',
-  plugin: MyPlugin,
-};
 ```
 
 ### Details Panels & The Current Selection Tab
@@ -425,10 +572,10 @@
 For example:
 
 ```ts
-class MyPlugin implements Plugin {
-  onActivate(_: PluginContext): void {}
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerDetailsPanel({
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace) {
+    trace.registerDetailsPanel({
       render(selection: Selection) {
         if (canHandleSelection(selection)) {
           return m('div', 'Details for selection');
@@ -454,6 +601,142 @@
 is undefined. This is a limitation of the current approach and will be updated
 to a more democratic contribution model in the future.
 
+### Sidebar Menu Items
+Plugins can add new entries to the sidebar menu which appears on the left hand
+side of the UI. These entries can include:
+- Commands
+- Links
+- Arbitrary Callbacks
+
+#### Commands
+If a command is referenced, the command name and hotkey are displayed on the
+sidebar item.
+```ts
+trace.commands.registerCommand({
+  id: 'sayHi',
+  name: 'Say hi',
+  callback: () => window.alert('hi'),
+  defaultHotkey: 'Shift+H',
+});
+
+trace.sidebar.addMenuItem({
+  commandId: 'sayHi',
+  section: 'support',
+  icon: 'waving_hand',
+});
+```
+
+#### Links
+If an href is present, the sidebar will be used as a link. This can be an
+internal link to a page, or an external link.
+```ts
+trace.sidebar.addMenuItem({
+  section: 'navigation',
+  text: 'Plugins',
+  href: '#!/plugins',
+});
+```
+
+#### Callbacks
+Sidebar items can be instructed to execute arbitrary callbacks when the button
+is clicked.
+```ts
+trace.sidebar.addMenuItem({
+  section: 'current_trace',
+  text: 'Copy secrets to clipboard',
+  action: () => copyToClipboard('...'),
+});
+```
+
+If the action returns a promise, the sidebar item will show a little spinner
+animation until the promise returns.
+
+```ts
+trace.sidebar.addMenuItem({
+  section: 'current_trace',
+  text: 'Prepare the data...',
+  action: () => new Promise((r) => setTimeout(r, 1000)),
+});
+```
+Optional params for all types of sidebar items:
+- `icon` - A material design icon to be displayed next to the sidebar menu item.
+  See full list [here](https://fonts.google.com/icons).
+- `tooltip` - Displayed on hover
+- `section` - Where to place the menu item.
+  - `navigation`
+  - `current_trace`
+  - `convert_trace`
+  - `example_traces`
+  - `support`
+- `sortOrder` - The higher the sortOrder the higher the bar.
+
+See the [sidebar source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/sidebar.ts)
+for more detailed usage.
+
+### Pages
+Pages are entities that can be routed via the URL args, and whose content take
+up the entire available space to the right of the sidebar and underneath the
+topbar. Examples of pages are the timeline, record page, and query page, just to
+name a few common examples.
+
+E.g.
+```
+http://ui.perfetto.dev/#!/viewer <-- 'viewer' is is the current page.
+```
+
+Pages are added from a plugin by calling the `pages.registerPage` function.
+
+Pages can be trace-less or trace-ful. Trace-less pages are pages that are to be
+displayed when no trace is loaded - i.e. the record page. Trace-ful pages are
+displayed only when a trace is loaded, as they typically require a trace to work
+with.
+
+You'll typically register trace-less pages in your plugin's `onActivate()`
+function and trace-full pages in either `onActivate()` or `onTraceLoad()`. If
+users navigate to a trace-ful page before a trace is loaded the homepage will be
+shown instead.
+
+> Note: You don't need to bind the `Trace` object for pages unlike other
+> extension points, Perfetto will inject a trace object for you.
+
+Pages should be mithril components that accept `PageWithTraceAttrs` for
+trace-ful pages or `PageAttrs` for trace-less pages.
+
+Example of a trace-less page:
+```ts
+import m from 'mithril';
+import {PageAttrs} from '../../public/page';
+
+class MyPage implements m.ClassComponent<PageAttrs> {
+  view(vnode: m.CVnode<PageAttrs>) {
+    return `The trace title is: ${vnode.attrs.trace.traceInfo.traceTitle}`;
+  }
+}
+
+// ~~~ snip ~~~
+
+app.pages.registerPage({route: '/mypage', page: MyPage, traceless: true});
+```
+
+```ts
+import m from 'mithril';
+import {PageWithTraceAttrs} from '../../public/page';
+
+class MyPage implements m.ClassComponent<PageWithTraceAttrs> {
+  view(_vnode_: m.CVnode<PageWithTraceAttrs>) {
+    return 'Hello from my page';
+  }
+}
+
+// ~~~ snip ~~~
+
+app.pages.registerPage({route: '/mypage', page: MyPage});
+```
+
+Examples:
+- [dev.perfetto.ExplorePage](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExplorePage/index.ts).
+
+
 ### Metric Visualisations
 TBD
 
@@ -463,15 +746,13 @@
 ### State
 NOTE: It is important to consider version skew when using persistent state.
 
-Plugins can persist information into permalinks. This allows plugins
-to gracefully handle permalinking and is an opt-in - not automatic -
-mechanism.
+Plugins can persist information into permalinks. This allows plugins to
+gracefully handle permalinking and is an opt-in - not automatic - mechanism.
 
 Persistent plugin state works using a `Store<T>` where `T` is some JSON
-serializable object.
-`Store` is implemented [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/base/store.ts).
-`Store` allows for reading and writing `T`.
-Reading:
+serializable object. `Store` is implemented
+[here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/base/store.ts).
+`Store` allows for reading and writing `T`. Reading:
 ```typescript
 interface Foo {
   bar: string;
@@ -509,12 +790,13 @@
 }
 ```
 
-To access permalink state, call `mountStore()` on your `PluginContextTrace`
+To access permalink state, call `mountStore()` on your `Trace`
 object, passing in a migration function.
 ```typescript
-class MyPlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const store = ctx.mountStore(migrate);
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace): Promise<void> {
+    const store = trace.mountStore(migrate);
   }
 }
 
@@ -581,20 +863,21 @@
 - [dev.perfetto.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleState/index.ts).
 
 ## Guide to the plugin API
-The plugin interfaces are defined in [ui/src/public/index.ts](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/index.ts).
+The plugin interfaces are defined in
+[ui/src/public/](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public).
 
 ## Default plugins
-Some plugins are enabled by default.
-These plugins are held to a higher quality than non-default plugins since changes to those plugins effect all users of the UI.
-The list of default plugins is specified at [ui/src/core/default_plugins.ts](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/common/default_plugins.ts).
+Some plugins are enabled by default. These plugins are held to a higher quality
+than non-default plugins since changes to those plugins effect all users of the
+UI. The list of default plugins is specified at
+[ui/src/core/default_plugins.ts](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/core/default_plugins.ts).
 
 ## Misc notes
 - Plugins must be licensed under
-  [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)
-  the same as all other code in the repository.
-- Plugins are the responsibility of the OWNERS of that plugin to
-  maintain, not the responsibility of the Perfetto team. All
-  efforts will be made to keep the plugin API stable and existing
-  plugins working however plugins that remain unmaintained for long
-  periods of time will be disabled and ultimately deleted.
+  [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) the same as all other
+  code in the repository.
+- Plugins are the responsibility of the OWNERS of that plugin to maintain, not
+  the responsibility of the Perfetto team. All efforts will be made to keep the
+  plugin API stable and existing plugins working however plugins that remain
+  unmaintained for long periods of time will be disabled and ultimately deleted.
 
diff --git a/docs/deployment/deploying-bigtrace-on-a-single-machine.md b/docs/deployment/deploying-bigtrace-on-a-single-machine.md
new file mode 100644
index 0000000..72aa938
--- /dev/null
+++ b/docs/deployment/deploying-bigtrace-on-a-single-machine.md
@@ -0,0 +1,60 @@
+# Deploying Bigtrace one a single machine
+
+NOTE: This doc is designed for administrators of Bigtrace services NOT Bigtrace users. This is also designed for non-Googlers - Googlers should look at `go/bigtrace` instead.
+
+There are multiple ways to deploy Bigtrace on a single machine:
+
+1. Running the Orchestrator and Worker executables manually
+2. docker-compose
+3. minikube
+
+NOTE: Options 1 and 2 are intended for development purposes and are not recommended for production. For production purposes instead follow the instructions on [Deploying Bigtrace on Kubernetes.](deploying-bigtrace-on-kubernetes)
+
+## Prerequisites
+To build Bigtrace you must first follow the [Quickstart setup and building](/docs/contributing/getting-started.md#quickstart) steps but using `tools/install-build-deps --grpc` in order to install the required dependencies for Bigtrace and gRPC.
+
+## Running the Orchestrator and Worker executables manually
+To manually run Bigtrace locally with the executables you must first build the executables before running them as follows:
+
+### Building the Orchestrator and Worker executables
+```bash
+tools/ninja -C out/[BUILD] orchestrator_main
+tools/ninja -C out/[BUILD] worker_main
+```
+
+### Running the Orchestrator and Worker executables
+Run the Orchestrator and Worker executables using command-line arguments:
+
+```bash
+./out/[BUILD]/orchestrator_main [args]
+./out/[BUILD]/worker_main [args]
+```
+
+### Example
+Creates a service with an Orchestrator and three Workers which can be interacted with using the Python API locally.
+```bash
+tools/ninja -C out/linux_clang_release orchestrator_main
+tools/ninja -C out/linux_clang_release worker_main
+
+./out/linux_clang_release/orchestrator_main -w "127.0.0.1" -p "5052" -n "3"
+./out/linux_clang_release/worker_main --socket="127.0.0.1:5052"
+./out/linux_clang_release/worker_main --socket="127.0.0.1:5053"
+./out/linux_clang_release/worker_main --socket="127.0.0.1:5054"
+```
+
+## docker-compose
+To allow testing of gRPC without the overhead of Kubernetes, docker-compose can be used which builds the Dockerfiles specified in infra/bigtrace/docker and creates containerised instances of the Orchestrator and the specified set of Worker replicas.
+
+```bash
+cd infra/bigtrace/docker
+docker compose up
+# OR if using the docker compose standalone binary
+docker-compose up
+```
+This will build and start the Workers (default of 3) and Orchestrator as specified in the `compose.yaml`.
+
+## minikube
+A minikube cluster can be used to emulate the Kubernetes cluster setup on a local machine. This can be created with the script `tools/setup_minikube_cluster.sh`.
+
+This starts a minikube cluster, builds the Orchestrator and Worker images and deploys them on the cluster. This can then be interacted with using the `minikube ip`:5051 as the Orchestrator service address through a client such as the Python API.
+
diff --git a/docs/deployment/deploying-bigtrace-on-kubernetes.md b/docs/deployment/deploying-bigtrace-on-kubernetes.md
new file mode 100644
index 0000000..93797f4
--- /dev/null
+++ b/docs/deployment/deploying-bigtrace-on-kubernetes.md
@@ -0,0 +1,223 @@
+# Deploying Bigtrace on Kubernetes
+
+NOTE: This doc is designed for administrators of Bigtrace services NOT Bigtrace users. This is also designed for non-Googlers - Googlers should look at `go/bigtrace` instead.
+
+## Overview of Bigtrace
+
+Bigtrace is a tool which facilitates the processing of traces in the O(million) by distributing instances of TraceProcessor across a Kubernetes cluster.
+
+The design of Bigtrace consists of four main parts:
+
+![](/docs/images/bigtrace/bigtrace-diagram.png)
+
+### Client
+There are three clients to interact with Bigtrace: a Python API, clickhouse-client and Apache Superset.
+- The Python API exists in the Perfetto python library and can be used similar to the TraceProcessor and BatchTraceProcessor APIs.
+- Clickhouse is a data warehousing solution which gives a SQL based interface for the user to write queries which are sent through gRPC to the Orchestrator. This can be accessed natively using the clickhouse-client which provides a CLI which allows the user to write queries to the DB.
+- Superset is a GUI for Clickhouse which offers an SQLLab to run queries offering support for modern features such as multiple tabs, autocomplete and syntax highlighting as well as providing data visualization tools to create charts easily from query results.
+
+### Orchestrator
+The Orchestrator is the central component of the service and is responsible for sharding traces to the various Worker pods and streaming the results to the Client.
+
+### Worker
+Each Worker runs an instance of TraceProcessor and performs the inputted query on a given trace. Each Worker runs on its own pod in the cluster.
+
+### Object Store (GCS)
+The object store contains the set of traces the service can query from and is accessed by the Worker.
+Currently, there is support for GCS as the main object store and the loading of traces stored locally on each machine for testing.
+
+Additional integrations can be added by creating a new repository policy in src/bigtrace/worker/repository_policies.
+
+## Deploying Bigtrace on GKE
+
+### GKE
+The recommended way to deploy Bigtrace is on Google Kubernetes Engine and this guide will explain the process.
+
+**Prerequisites:**
+- A GCP Project
+- GCS
+- GKE
+- gcloud (https://cloud.google.com/sdk/gcloud)
+- A clone of the Perfetto directory
+
+#### Service account permissions
+In addition to the default API access of the Compute Engine service account, the following permissions are required:
+- Storage Object User - to allow for the Worker to retrieve GCS authentication tokens
+
+These can be added on GCP through IAM & Admin > IAM > Permissions.
+
+---
+
+### Setting up the cluster
+
+#### Creating the cluster
+1. Navigate to Kubernetes Engine within GCP
+2. Create a Standard cluster (Create > Standard > Configure)
+![](/docs/images/bigtrace/create_cluster_2.png)
+3. In Cluster basics, select a location type - Use zonal for best load balancing performance
+![](/docs/images/bigtrace/create_cluster_3.png)
+4. In Node pools > default-pool > Nodes, select a VM type - Preferably standard - e.g. e2-standard-8 or above
+![](/docs/images/bigtrace/create_cluster_4.png)
+5. In the Networking tab, enable subsetting for L4 internal load balancers (this is required for services using internal load balancing within the VPC)
+![](/docs/images/bigtrace/create_cluster_5.png)
+6. Create the cluster
+
+#### Accessing the cluster
+To use kubectl to apply the yaml files for deployments and services you must first connect and authenticate with the cluster.
+
+You can follow these instructions on device or in cloud shell using the following command:
+
+```bash
+gcloud container clusters get-credentials [CLUSTER_NAME] --zone [ZONE]--project [PROJECT_NAME]
+```
+
+
+---
+
+### Deploying the Orchestrator
+The deployment of Orchestrator requires two main steps: Building and pushing the images to Artifact Registry & deploying to the cluster.
+
+#### Building and uploading the Orchestrator image
+To build the image and push to Artifact Registry, first navigate to the perfetto directory and then run the following commands:
+
+```bash
+docker build -t bigtrace_orchestrator src/bigtrace/orchestrator
+
+docker tag bigtrace_orchestrator [ZONE]-docker.pkg.dev/[PROJECT_NAME]/[REPO_NAME]/bigtrace_orchestrator
+
+docker push [ZONE]-docker.pkg.dev/[PROJECT_NAME]/[REPO_NAME]/bigtrace_orchestrator
+```
+
+#### Applying the yaml files
+To use the images from the registry which were built in the previous step, the orchestrator-deployment.yaml file must be modified to replace the line.
+
+```yaml
+image: [ZONE]-docker.pkg.dev/[PROJECT_NAME]/[REPO_NAME]/bigtrace_orchestrator
+```
+
+The CPU resources should also be set depending on the vCPUs per pod as chosen before.
+
+```yaml
+resources:
+    requests:
+      cpu: [VCPUS_PER_MACHINE]
+    limits:
+      cpu: [VCPUS_PER_MACHINE]
+```
+
+Then to deploy the Orchestrator you apply both the orchestrator-deployment.yaml and the orchestrator-ilb.yaml, for the deployment and internal load balancing service respectively.
+
+```bash
+kubectl apply -f orchestrator-deployment.yaml
+kubectl apply -f orchestrator-ilb.yaml
+```
+
+This deploys the Orchestrator as a single replica in a pod and exposes it as a service for access within the VPC by the client.
+
+### Deploying the Worker
+Similar to the Orchestrator first build and push the images to Artifact Registry.
+
+```bash
+docker build -t bigtrace_worker src/bigtrace/worker
+
+docker tag bigtrace_worker [ZONE]-docker.pkg.dev/[PROJECT_NAME]/[REPO_NAME]/bigtrace_worker
+
+docker push [ZONE]-docker.pkg.dev/[PROJECT_NAME]/[REPO_NAME]/bigtrace_worker
+```
+
+Then modify the yaml files to reflect the image as well as fit the required configuration for the use case.
+
+```yaml
+image: [ZONE]-docker.pkg.dev/[PROJECT_NAME]/[REPO_NAME]/bigtrace_worker
+...
+
+replicas: [DESIRED_REPLICA_COUNT]
+
+...
+
+resources:
+  requests:
+    cpu: [VCPUS_PER_MACHINE]
+```
+
+Then deploy the deployment and service as follows:
+
+```bash
+kubectl apply -f worker-deployment.yaml
+kubectl apply -f worker-service.yaml
+```
+
+### Deploying Clickhouse
+
+#### Build and upload the Clickhouse deployment image
+This image builds on top of the base Clickhouse image and provides the necessary Python libraries for gRPC to communicate with the Orchestrator.
+
+```bash
+docker build -t clickhouse src/bigtrace_clickhouse
+
+docker tag clickhouse [ZONE]-docker.pkg.dev/[PROJECT_NAME]/[REPO_NAME]/clickhouse
+
+docker push [ZONE]-docker.pkg.dev/[PROJECT_NAME]/[REPO_NAME]/clickhouse
+```
+
+To deploy this on a pod in a cluster, the provided yaml files must be applied using kubectl e.g.
+
+```
+kubectl apply -f src/bigtrace_clickhouse/config.yaml
+
+kubectl apply -f src/bigtrace_clickhouse/pvc.yaml
+
+kubectl apply -f src/bigtrace_clickhouse/pv.yaml
+
+kubectl apply -f src/bigtrace_clickhouse/clickhouse-deployment.yaml
+
+kubectl apply -f src/bigtrace_clickhouse/clickhouse-ilb.yaml
+```
+With the clickhouse-deployment.yaml you must replace the image variable with the URI to the image built in the previous step - which contains the Clickhouse image with the necessary Python files for gRPC installed on top.
+
+The env variable BIGTRACE_ORCHESTRATOR_ADDRESS must also be changed to the address of the Orchestrator service given by GKE:
+
+```
+ containers:
+      - name: clickhouse
+        image: # [ZONE]-docker.pkg.dev/[PROJECT_NAME]/[REPO_NAME]/clickhouse
+        env:
+        - name: BIGTRACE_ORCHESTRATOR_ADDRESS
+          value: # Address of Orchestrator service
+```
+### File summary
+
+#### Deployment
+
+Contains the image of the Clickhouse server and configures the necessary volumes and resources.
+
+#### Internal Load Balancer Service (ILB)
+
+This Internal Load Balancer is used to allow for the Clickhouse server pod to be reached from within the VPC in GKE. This means that VMs outside the cluster are able to access the Clickhouse server through Clickhouse Client, without exposing the service to the public.
+
+#### Persistent Volume and Persistent Volume Claim
+
+These files create the volumes needed for the Clickhouse server to persist the databases in the event of pod failure.
+
+#### Config
+
+This is where Clickhouse config files can be specified to customize the server to the user's requirements. (https://clickhouse.com/docs/en/operations/server-configuration-parameters/settings)
+
+### Accessing Clickhouse through clickhouse-client (CLI)
+You can deploy Clickhouse in a variety of ways by following:
+https://clickhouse.com/docs/en/install
+
+When running the client through CLI it is important to specify:
+./clickhouse client --host [ADDRESS]  --port [PORT] --receive-timeout=1000000 --send-timeout=100000 --idle_connection_timeout=1000000
+
+### Deploying Superset
+
+There are two methods of deploying Superset - one for development and one for production.
+
+You can deploy an instance of Superset within a VM for development by following:
+https://superset.apache.org/docs/quickstart
+
+You can deploy a production ready instance on Kubernetes across pods by following:
+https://superset.apache.org/docs/installation/kubernetes
+
+Superset can then be connected to Clickhouse via clickhouse-connect by following the instructions at this link, but replacing the first step with the connection details of the deployment: https://clickhouse.com/docs/en/integrations/superset
diff --git a/docs/images/bigtrace/bigtrace-diagram.png b/docs/images/bigtrace/bigtrace-diagram.png
new file mode 100644
index 0000000..d7aac8e
--- /dev/null
+++ b/docs/images/bigtrace/bigtrace-diagram.png
Binary files differ
diff --git a/docs/images/bigtrace/create_cluster_2.png b/docs/images/bigtrace/create_cluster_2.png
new file mode 100644
index 0000000..25f4958
--- /dev/null
+++ b/docs/images/bigtrace/create_cluster_2.png
Binary files differ
diff --git a/docs/images/bigtrace/create_cluster_3.png b/docs/images/bigtrace/create_cluster_3.png
new file mode 100644
index 0000000..71b1194
--- /dev/null
+++ b/docs/images/bigtrace/create_cluster_3.png
Binary files differ
diff --git a/docs/images/bigtrace/create_cluster_4.png b/docs/images/bigtrace/create_cluster_4.png
new file mode 100644
index 0000000..28ad8a8
--- /dev/null
+++ b/docs/images/bigtrace/create_cluster_4.png
Binary files differ
diff --git a/docs/images/bigtrace/create_cluster_5.png b/docs/images/bigtrace/create_cluster_5.png
new file mode 100644
index 0000000..3b19275
--- /dev/null
+++ b/docs/images/bigtrace/create_cluster_5.png
Binary files differ
diff --git a/docs/images/debug-tracks/counter-tracks-create.png b/docs/images/debug-tracks/counter-tracks-create.png
new file mode 100644
index 0000000..f10a3b3
--- /dev/null
+++ b/docs/images/debug-tracks/counter-tracks-create.png
Binary files differ
diff --git a/docs/images/debug-tracks/counter-tracks-query.png b/docs/images/debug-tracks/counter-tracks-query.png
new file mode 100644
index 0000000..f639037
--- /dev/null
+++ b/docs/images/debug-tracks/counter-tracks-query.png
Binary files differ
diff --git a/docs/images/debug-tracks/counter-tracks-results.png b/docs/images/debug-tracks/counter-tracks-results.png
new file mode 100644
index 0000000..970d16e
--- /dev/null
+++ b/docs/images/debug-tracks/counter-tracks-results.png
Binary files differ
diff --git a/docs/images/debug-tracks/debug-tracks-create.png b/docs/images/debug-tracks/debug-tracks-create.png
deleted file mode 100644
index c98b49b..0000000
--- a/docs/images/debug-tracks/debug-tracks-create.png
+++ /dev/null
Binary files differ
diff --git a/docs/images/debug-tracks/debug-tracks-query.png b/docs/images/debug-tracks/debug-tracks-query.png
deleted file mode 100644
index 99d2029..0000000
--- a/docs/images/debug-tracks/debug-tracks-query.png
+++ /dev/null
Binary files differ
diff --git a/docs/images/debug-tracks/debug-tracks-result.png b/docs/images/debug-tracks/debug-tracks-result.png
deleted file mode 100644
index 51d6fd2..0000000
--- a/docs/images/debug-tracks/debug-tracks-result.png
+++ /dev/null
Binary files differ
diff --git a/docs/images/debug-tracks/pivot-counter-tracks-create.png b/docs/images/debug-tracks/pivot-counter-tracks-create.png
new file mode 100644
index 0000000..83d11c3
--- /dev/null
+++ b/docs/images/debug-tracks/pivot-counter-tracks-create.png
Binary files differ
diff --git a/docs/images/debug-tracks/pivot-counter-tracks-results.png b/docs/images/debug-tracks/pivot-counter-tracks-results.png
new file mode 100644
index 0000000..a41c508
--- /dev/null
+++ b/docs/images/debug-tracks/pivot-counter-tracks-results.png
Binary files differ
diff --git a/docs/images/debug-tracks/pivot-slice-tracks-create.png b/docs/images/debug-tracks/pivot-slice-tracks-create.png
new file mode 100644
index 0000000..912e82e
--- /dev/null
+++ b/docs/images/debug-tracks/pivot-slice-tracks-create.png
Binary files differ
diff --git a/docs/images/debug-tracks/pivot-slice-tracks-results.png b/docs/images/debug-tracks/pivot-slice-tracks-results.png
new file mode 100644
index 0000000..d2371e6
--- /dev/null
+++ b/docs/images/debug-tracks/pivot-slice-tracks-results.png
Binary files differ
diff --git a/docs/images/debug-tracks/slice-track-create.png b/docs/images/debug-tracks/slice-track-create.png
new file mode 100644
index 0000000..62e7abd
--- /dev/null
+++ b/docs/images/debug-tracks/slice-track-create.png
Binary files differ
diff --git a/docs/images/debug-tracks/slice-track-query.png b/docs/images/debug-tracks/slice-track-query.png
new file mode 100644
index 0000000..655f979
--- /dev/null
+++ b/docs/images/debug-tracks/slice-track-query.png
Binary files differ
diff --git a/docs/images/debug-tracks/slice-track-result.png b/docs/images/debug-tracks/slice-track-result.png
new file mode 100644
index 0000000..fabb8d9
--- /dev/null
+++ b/docs/images/debug-tracks/slice-track-result.png
Binary files differ
diff --git a/docs/images/synthetic-track-event-custom-tree.png b/docs/images/synthetic-track-event-custom-tree.png
new file mode 100644
index 0000000..bad4190
--- /dev/null
+++ b/docs/images/synthetic-track-event-custom-tree.png
Binary files differ
diff --git a/docs/images/ui-plugins/nested_tracks.png b/docs/images/ui-plugins/nested_tracks.png
new file mode 100644
index 0000000..a9e87bc
--- /dev/null
+++ b/docs/images/ui-plugins/nested_tracks.png
Binary files differ
diff --git a/docs/images/ui-plugins/summary_track.png b/docs/images/ui-plugins/summary_track.png
new file mode 100644
index 0000000..96999dc
--- /dev/null
+++ b/docs/images/ui-plugins/summary_track.png
Binary files differ
diff --git a/docs/instrumentation/tracing-sdk.md b/docs/instrumentation/tracing-sdk.md
index 36c6b39..a77d363 100644
--- a/docs/instrumentation/tracing-sdk.md
+++ b/docs/instrumentation/tracing-sdk.md
@@ -30,7 +30,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 v47.0
+git clone https://android.googlesource.com/platform/external/perfetto -b v48.1
 ```
 
 The SDK consists of two files, `sdk/perfetto.h` and `sdk/perfetto.cc`. These are
diff --git a/docs/reference/synthetic-track-event.md b/docs/reference/synthetic-track-event.md
index ff894e9..03853b0 100644
--- a/docs/reference/synthetic-track-event.md
+++ b/docs/reference/synthetic-track-event.md
@@ -1,4 +1,5 @@
 # Writing TrackEvent Protos Synthetically
+
 This page acts as a reference guide to synthetically generate TrackEvent,
 Perfetto's native protobuf based tracing format. This allows using Perfetto's
 analysis and visualzation without using collecting traces using the Perfetto
@@ -6,8 +7,8 @@
 
 TrackEvent protos can be manually written using the
 [official protobuf library](https://protobuf.dev/reference/) or any other
-protobuf-compatible library. To be language-agnostic, the rest of this page
-will show examples using the
+protobuf-compatible library. To be language-agnostic, the rest of this page will
+show examples using the
 [text format](https://protobuf.dev/reference/protobuf/textformat-spec/)
 representation of protobufs.
 
@@ -18,6 +19,7 @@
 messages.
 
 ## Thread-scoped (sync) slices
+
 NOTE: in the legacy JSON tracing format, this section correspond to B/E/I/X
 events with the associated M (metadata) events.
 
@@ -29,6 +31,7 @@
 ![Thread track event in UI](/docs/images/synthetic-track-event-thread.png)
 
 This is corresponds to the following protos:
+
 ```
 # Emit this packet once *before* you emit the first event for this process.
 packet {
@@ -100,27 +103,29 @@
 ```
 
 ## Process-scoped (async) slices
+
 NOTE: in the legacy JSON tracing format, this section corresponds to b/e/n
 events with the associated M (metadata) events.
 
 Process-scoped slices are useful to trace execution of a "piece of work" across
-multiple threads of a process. A process-scoped slice can start on a thread
-A and end on a thread B. Examples include work submitted to thread pools
-and coroutines.
+multiple threads of a process. A process-scoped slice can start on a thread A
+and end on a thread B. Examples include work submitted to thread pools and
+coroutines.
 
 Process tracks can be named corresponding to the executor and can also have
 child slices in an identical way to thread-scoped slices. Importantly, this
-means slices on a single track must **strictly nest** inside each other
-without overlapping.
+means slices on a single track must **strictly nest** inside each other without
+overlapping.
 
-As separating each track in the UI can cause a lot of clutter, the UI
-visually merges process tracks with the same name in each process. Note that
-this **does not** change the data model (e.g. in trace processor
-tracks remain separated) as this is simply a visual grouping.
+As separating each track in the UI can cause a lot of clutter, the UI visually
+merges process tracks with the same name in each process. Note that this **does
+not** change the data model (e.g. in trace processor tracks remain separated) as
+this is simply a visual grouping.
 
 ![Process track event in UI](/docs/images/synthetic-track-event-process.png)
 
 This is corresponds to the following protos:
+
 ```
 # The first track associated with this process.
 packet {
@@ -219,17 +224,304 @@
 }
 ```
 
+## Custom-scoped slices
+
+NOTE: there is no equivalent in the JSON tracing format.
+
+As well as thread-scoped and process-scoped slices, Perfetto supports creating
+tracks which are not scoped to any OS-level concept. Moreover, these tracks can
+be recursively nested in a tree structure. This is useful to model the timeline
+of execution of GPUs, network traffic, IRQs etc.
+
+Note: in the past, modelling such slices may have been done by abusing
+processes/threads slices, due to limitations with the data model and the
+Perfetto UI. This is no longer necessary and we _strongly_ discourage continued
+use of this hack.
+
+![Process track event in UI](/docs/images/synthetic-track-event-custom-tree.png)
+
+This is corresponds to the following protos:
+
+```
+packet {
+  track_descriptor {
+    uuid: 48948                         # 64-bit random number.
+    name: "Root"
+  }
+}
+packet {
+  track_descriptor {
+    uuid: 50001                         # 64-bit random number.
+    parent_uuid: 48948                  # UUID of root track.
+    name: "Parent B"
+  }
+}
+packet {
+  track_descriptor {
+    uuid: 50000                         # 64-bit random number.
+    parent_uuid: 48948                  # UUID of root track.
+    name: "Parent A"
+  }
+}
+packet {
+  track_descriptor {
+    uuid: 60000                         # 64-bit random number.
+    parent_uuid: 50000                  # UUID of Parent A track.
+    name: "Child A1"
+  }
+}
+packet {
+  track_descriptor {
+    uuid: 60001                         # 64-bit random number.
+    parent_uuid: 50000                  # UUID of Parent A track.
+    name: "Child A2"
+  }
+}
+packet {
+  track_descriptor {
+    uuid: 70000                         # 64-bit random number.
+    parent_uuid: 50001                  # UUID of Parent B track.
+    name: "Child B1"
+  }
+}
+
+# The events for the Child A1 track.
+packet {
+  timestamp: 200
+  track_event {
+    type: TYPE_SLICE_BEGIN
+    track_uuid: 60000                   # Same random number from above.
+    name: "A1"
+  }
+  trusted_packet_sequence_id: 3903809   # Generate *once*, use throughout.
+}
+packet {
+  timestamp: 250
+  track_event {
+    type: TYPE_SLICE_END
+    track_uuid: 60000
+  }
+  trusted_packet_sequence_id: 3903809
+}
+
+# The events for the Child A2 track.
+packet {
+  timestamp: 220
+  track_event {
+    type: TYPE_SLICE_BEGIN
+    track_uuid: 60001                   # Same random number from above.
+    name: "A2"
+  }
+  trusted_packet_sequence_id: 3903809   # Generate *once*, use throughout.
+}
+packet {
+  timestamp: 240
+  track_event {
+    type: TYPE_SLICE_END
+    track_uuid: 60001
+  }
+  trusted_packet_sequence_id: 3903809
+}
+
+# The events for the Child B1 track.
+packet {
+  timestamp: 210
+  track_event {
+    type: TYPE_SLICE_BEGIN
+    track_uuid: 70000                   # Same random number from above.
+    name: "B1"
+  }
+  trusted_packet_sequence_id: 3903809   # Generate *once*, use throughout.
+}
+packet {
+  timestamp: 230
+  track_event {
+    type: TYPE_SLICE_END
+    track_uuid: 70000
+  }
+  trusted_packet_sequence_id: 3903809
+}
+```
+
+## Track sorting order
+
+NOTE: the closest equivalent to this in the JSON format is `process_sort_index`
+but the Perfetto approach is significantly more flexible.
+
+Perfetto also supports specifying of how the tracks should be visualized in the
+UI by default. This is done via the use of the `child_ordering` field which can
+be set on `TrackDescriptor`.
+
+For example, to sort the tracks lexicographically (i.e. in alphabetical order):
+
+```
+packet {
+  track_descriptor {
+    uuid: 10
+    name: "Root"
+    # Any children of the `Root` track will appear in alphabetical order. This
+    # does *not* propogate to any indirect descendants, just the direct
+    # children.
+    child_ordering: LEXICOGRAPHIC
+  }
+}
+# B will appear nested under `Root` but *after* `A` in the UI, even though it
+# appears first in the trace and has a smaller UUID.
+packet {
+  track_descriptor {
+    uuid: 11
+    parent_uuid: 10
+    name: "B"
+  }
+}
+packet {
+  track_descriptor {
+    uuid: 12
+    parent_uuid: 10
+    name: "A"
+  }
+}
+```
+
+Chronological order is also supported, this sorts the tracks with the earliest
+event first:
+
+```
+packet {
+  track_descriptor {
+    uuid: 10
+    name: "Root"
+    # Any children of the `Root` track will appear in the order based on the
+    # timestamp of the first event on the trace: earlier timestamps will appear
+    # higher in the trace. This does *not* propogate to any indirect
+    # descendants, just the direct children.
+    child_ordering: CHRONOLOGICAL
+  }
+}
+
+# B will appear before A because B's first slice starts earlier than A's first
+# slice.
+packet {
+  track_descriptor {
+    uuid: 11
+    parent_uuid: 10
+    name: "A"
+  }
+}
+packet {
+  timestamp: 220
+  track_event {
+    type: TYPE_SLICE_BEGIN
+    track_uuid: 11
+    name: "A1"
+  }
+  trusted_packet_sequence_id: 3903809
+}
+packet {
+  timestamp: 230
+  track_event {
+    type: TYPE_SLICE_END
+    track_uuid: 60000
+  }
+  trusted_packet_sequence_id: 3903809
+}
+
+packet {
+  track_descriptor {
+    uuid: 12
+    parent_uuid: 10
+    name: "B"
+  }
+}
+packet {
+  timestamp: 210
+  track_event {
+    type: TYPE_SLICE_BEGIN
+    track_uuid: 12
+    name: "B1"
+  }
+  trusted_packet_sequence_id: 3903809
+}
+packet {
+  timestamp: 240
+  track_event {
+    type: TYPE_SLICE_END
+    track_uuid: 12
+  }
+  trusted_packet_sequence_id: 3903809
+}
+```
+
+Finally, for exact control, you can use the `EXPLICIT` ordering and specify
+`sibling_order_rank` on each child track:
+
+```
+packet {
+  track_descriptor {
+    uuid: 10
+    name: "Root"
+    # Any children of the `Root` track will appear in order specified by
+    # `sibling_order_rank` exactly: any unspecified rank is treated as 0
+    # implicitly.
+    child_ordering: EXPLICIT
+  }
+}
+# C will appear first, then B then A following the order specified by
+# `sibling_order_rank`.
+packet {
+  track_descriptor {
+    uuid: 11
+    parent_uuid: 10
+    name: "B"
+    sibling_order_rank: 1
+  }
+}
+packet {
+  track_descriptor {
+    uuid: 12
+    parent_uuid: 10
+    name: "A"
+    sibling_order_rank: 100
+  }
+}
+packet {
+  track_descriptor {
+    uuid: 13
+    parent_uuid: 10
+    name: "C"
+    sibling_order_rank: -100
+  }
+}
+```
+
+NOTE: using `EXPLICIT` is strongly discouraged where there is another option.
+Other orders are significantly more efficient and also allows for trace
+processor and the UI to better understand what you want to do with those tracks.
+Moreover, it gives the flexibility for having custom visualization (e.g. Gannt
+charts for CHRONOLOGICAL view) based on the type specified.
+
+Further documentation about the sorting order is available on the protos for
+[TrackDescriptor](/docs/reference/trace-packet-proto.autogen#TrackDescriptor)
+and
+[ChildTracksOrdering](/docs/reference/trace-packet-proto.autogen#TrackDescriptor.ChildTracksOrdering).
+
+NOTE: the order specified in the trace is a treated as a hint in the UI not a
+gurantee. The UI reserves the right to change the ordering as it sees fit.
+
 ## Flows
+
 NOTE: in the legacy JSON tracing format, this section correspond to s/t/f
 events.
 
-Flows allow connecting any number of slices with arrows. The semantic meaning
-of the arrow varies across different applications but most commonly it is used
-to track work passing between threads or processes: e.g. the UI thread asks a
+Flows allow connecting any number of slices with arrows. The semantic meaning of
+the arrow varies across different applications but most commonly it is used to
+track work passing between threads or processes: e.g. the UI thread asks a
 background thread to do some work and notify when the result is available.
 
-NOTE: a single flow *cannot* fork ands imply represents a single stream of
-arrows from one slice to the next. See [this](https://source.chromium.org/chromium/chromium/src/+/main:third_party/perfetto/protos/perfetto/trace/perfetto_trace.proto;drc=ba05b783d9c29fe334a02913cf157ea1d415d37c;l=9604) comment for information.
+NOTE: a single flow _cannot_ fork ands imply represents a single stream of
+arrows from one slice to the next. See
+[this](https://source.chromium.org/chromium/chromium/src/+/main:third_party/perfetto/protos/perfetto/trace/perfetto_trace.proto;drc=ba05b783d9c29fe334a02913cf157ea1d415d37c;l=9604)
+comment for information.
 
 ![TrackEvent flows in UI](/docs/images/synthetic-track-event-flow.png)
 
@@ -315,6 +607,7 @@
 ```
 
 ## Counters
+
 NOTE: in the legacy JSON tracing format, this section correspond to C events.
 
 Counters are useful to represent continuous values which change with time.
@@ -323,6 +616,7 @@
 ![TrackEvent counter in UI](/docs/images/synthetic-track-event-counter.png)
 
 This corresponds to the following protos:
+
 ```
 # Counter track scoped to a process.
 packet {
@@ -381,22 +675,23 @@
 ```
 
 ## Interning
+
 NOTE: there is no equivalent to interning in the JSON tracing format.
 
 Interning is an advanced but powerful feature of the protobuf tracing format
-which allows allows for reducing the number of times long strings are emitted
-in the trace.
+which allows allows for reducing the number of times long strings are emitted in
+the trace.
 
 Specifically, certain fields in the protobuf format allow associating an "iid"
 (interned id) to a string and using the iid to reference the string in all
-future packets. The most commonly used cases are slice names and category
-names
+future packets. The most commonly used cases are slice names and category names
 
-Here is an example of a trace which makes use of interning to reduce the
-number of times a very long slice name is emitted:
+Here is an example of a trace which makes use of interning to reduce the number
+of times a very long slice name is emitted:
 ![TrackEvent interning](/docs/images/synthetic-track-event-interned.png)
 
 This corresponds to the following protos:
+
 ```
 packet {
   track_descriptor {
@@ -431,7 +726,7 @@
 
   first_packet_on_sequence: true        # Indicates to trace processor that
                                         # this is the first packet on the
-                                        # sequence.   
+                                        # sequence.
   previous_packet_dropped: true         # Same as |first_packet_on_sequence|.
 
   # Indicates to trace processor that this sequence resets the incremental state but
diff --git a/docs/toc.md b/docs/toc.md
index f828874..deaeb5d 100644
--- a/docs/toc.md
+++ b/docs/toc.md
@@ -44,13 +44,15 @@
     * [C++ library](analysis/trace-processor.md)
     * [Python library](analysis/trace-processor-python.md)
     * [Trace-based metrics](analysis/metrics.md)
-    * [Batch Trace Processor](analysis/batch-trace-processor.md)
   * [PerfettoSQL](#)
     * [Standard Library](analysis/stdlib-docs.autogen)
     * [Syntax](analysis/perfetto-sql-syntax.md)
     * [Prelude tables](analysis/sql-tables.autogen)
-    * [Common Queries](analysis/common-queries.md)
     * [Built-ins](analysis/builtin.md)
+  * [Analysis at scale](#)
+    * [Batch Trace Processor](analysis/batch-trace-processor.md)
+    * [Bigtrace](deployment/deploying-bigtrace-on-a-single-machine.md)
+    * [Bigtrace on Kubernetes](deployment/deploying-bigtrace-on-kubernetes.md)
 
 * [Trace visualization](#)
   * [Perfetto UI](visualization/perfetto-ui.md)
@@ -73,7 +75,6 @@
   * [Trace Packet proto](reference/trace-packet-proto.autogen)
   * [perfetto cmdline](reference/perfetto-cli.md)
   * [heap_profile cmdline](reference/heap_profile-cli.md)
-  * [UI Plugin API](reference/ui-plugin-api.autogen)
   * [Synthetic TrackEvent](reference/synthetic-track-event.md)
   * [Android Version Notes](reference/android-version-notes.md)
   * [Stats table](analysis/sql-stats.autogen)
diff --git a/examples/sdk/README.md b/examples/sdk/README.md
index 404647d..042b7b5 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 v47.0
+git clone https://android.googlesource.com/platform/external/perfetto -b v48.1
 ```
 
 Then, build using CMake:
diff --git a/examples/shared_lib/example_shlib_track_event.c b/examples/shared_lib/example_shlib_track_event.c
index 143180c..a63666a 100644
--- a/examples/shared_lib/example_shlib_track_event.c
+++ b/examples/shared_lib/example_shlib_track_event.c
@@ -93,6 +93,10 @@
                     perfetto_protos_TrackEvent_source_location_field_number,
                     PERFETTO_TE_PROTO_FIELD_CSTR(2, __FILE__),
                     PERFETTO_TE_PROTO_FIELD_VARINT(4, __LINE__))));
+    PERFETTO_TE(
+        physics, PERFETTO_TE_COUNTER(),
+        PERFETTO_TE_COUNTER_TRACK("mycounter", PerfettoTeProcessTrackUuid()),
+        PERFETTO_TE_INT_COUNTER(89));
     PERFETTO_TE(PERFETTO_TE_DYNAMIC_CATEGORY, PERFETTO_TE_COUNTER(),
                 PERFETTO_TE_DOUBLE_COUNTER(3.14),
                 PERFETTO_TE_REGISTERED_TRACK(&mycounter),
diff --git a/gn/BUILD.gn b/gn/BUILD.gn
index ed055ee..f19df9b 100644
--- a/gn/BUILD.gn
+++ b/gn/BUILD.gn
@@ -87,6 +87,7 @@
     "PERFETTO_TP_LINENOISE=$enable_perfetto_trace_processor_linenoise",
     "PERFETTO_TP_HTTPD=$enable_perfetto_trace_processor_httpd",
     "PERFETTO_TP_JSON=$enable_perfetto_trace_processor_json",
+    "PERFETTO_TP_INSTRUMENTS=$enable_perfetto_trace_processor_mac_instruments",
     "PERFETTO_LOCAL_SYMBOLIZER=$perfetto_local_symbolizer",
     "PERFETTO_ZLIB=$enable_perfetto_zlib",
     "PERFETTO_TRACED_PERF=$enable_perfetto_traced_perf",
@@ -95,6 +96,7 @@
     "PERFETTO_X64_CPU_OPT=$enable_perfetto_x64_cpu_opt",
     "PERFETTO_LLVM_DEMANGLE=$enable_perfetto_llvm_demangle",
     "PERFETTO_SYSTEM_CONSUMER=$enable_perfetto_system_consumer",
+    "PERFETTO_THREAD_SAFETY_ANNOTATIONS=$perfetto_thread_safety_annotations",
   ]
 
   rel_out_path = rebase_path(gen_header_path, "$root_build_dir")
@@ -363,6 +365,16 @@
   }
 }
 
+if (enable_perfetto_trace_processor_mac_instruments) {
+  group("expat") {
+    if (perfetto_root_path == "//") {
+      public_deps = [ "//buildtools:expat" ]
+    } else {
+      public_deps = [ "//third_party/expat:expat" ]
+    }
+  }
+}
+
 if (enable_perfetto_trace_processor_json) {
   group("jsoncpp") {
     if (perfetto_root_path == "//") {
@@ -379,6 +391,10 @@
     public_configs = [ "//buildtools:grpc_public_config" ]
     public_deps = [ "//buildtools/grpc:grpc++" ]
   }
+
+  group("cpp_httplib") {
+    public_deps = [ "//buildtools:cpp_httplib" ]
+  }
 }
 
 if (enable_perfetto_trace_processor_linenoise) {
diff --git a/gn/perfetto.gni b/gn/perfetto.gni
index ce106c5..987d347 100644
--- a/gn/perfetto.gni
+++ b/gn/perfetto.gni
@@ -108,6 +108,7 @@
 if (perfetto_build_standalone || is_perfetto_build_generator) {
   perfetto_root_path = "//"
   import("//gn/standalone/android.gni")  # For android_api_level
+  import("//gn/standalone/libc++/libc++.gni")  # For use_custom_libcxx
   import("//gn/standalone/sanitizers/vars.gni")  # For is_fuzzer
 } else if (!defined(perfetto_root_path)) {
   perfetto_root_path = "//third_party/perfetto/"
@@ -246,6 +247,11 @@
   enable_perfetto_x64_cpu_opt =
       current_cpu == "x64" && is_linux && !is_wasm &&
       perfetto_build_standalone && !is_perfetto_build_generator
+
+  # Enables complie-time thread safety analysis.
+  perfetto_thread_safety_annotations =
+      perfetto_build_standalone && !is_perfetto_build_generator &&
+      defined(use_custom_libcxx) && use_custom_libcxx
 }
 
 declare_args() {
@@ -298,6 +304,14 @@
   enable_perfetto_trace_processor_json =
       enable_perfetto_trace_processor && !perfetto_build_with_android
 
+  # Enables the support for importing profiles from the MacOS Instruments app.
+  # Requires a dependency on libexpat for XML parsing.
+  # Disabled in chromium due to some fuzzer related build failure (b/363347029).
+  enable_perfetto_trace_processor_mac_instruments =
+      enable_perfetto_trace_processor &&
+      (perfetto_build_standalone || perfetto_build_with_android ||
+       is_perfetto_build_generator)
+
   # Enables httpd RPC support in the trace processor.
   # Further per-OS conditionals are applied in gn/BUILD.gn.
   # Chromium+Win: httpd support depends on enable_perfetto_ipc, which is not
diff --git a/gn/perfetto_cc_proto_descriptor.gni b/gn/perfetto_cc_proto_descriptor.gni
index 5b0a58e..ec7db88 100644
--- a/gn/perfetto_cc_proto_descriptor.gni
+++ b/gn/perfetto_cc_proto_descriptor.gni
@@ -45,6 +45,14 @@
       rebase_path(root_gen_dir, root_build_dir),
       "--cpp_out",
       rebase_path(generated_header, root_build_dir),
+    ]
+    if (defined(invoker.namespace)) {
+      args += [
+        "--namespace",
+        invoker.namespace,
+      ]
+    }
+    args += [
       rebase_path(descriptor_file_path, root_build_dir),
     ]
     inputs = [ descriptor_file_path ]
diff --git a/gn/perfetto_integrationtests.gni b/gn/perfetto_integrationtests.gni
index f9237fa..7dfdece 100644
--- a/gn/perfetto_integrationtests.gni
+++ b/gn/perfetto_integrationtests.gni
@@ -15,7 +15,6 @@
 import("perfetto.gni")
 
 perfetto_integrationtests_targets = [
-  "gn:default_deps",
   "src/tracing/test:client_api_integrationtests",
   "src/shared_lib/test:integrationtests",
 ]
diff --git a/gn/proto_library.gni b/gn/proto_library.gni
index e5c865b..af540c2 100644
--- a/gn/proto_library.gni
+++ b/gn/proto_library.gni
@@ -79,10 +79,11 @@
       component_build_force_source_set = true
     }
 
+    # Convert deps to link_deps: the proto_library rule requires that C++ files
+    # are passed in as link_deps wheras in Perfetto, we always works with deps.
+    link_deps = []
     if (defined(invoker.deps)) {
-      deps = invoker.deps
-    } else {
-      deps = []
+      link_deps += invoker.deps
     }
 
     # omit_protozero_dep is intended to be used when protozero_library
@@ -93,7 +94,11 @@
     #
     # TODO(b/173041866): use fine-grained components instead when available
     if (!(defined(invoker.omit_protozero_dep) && invoker.omit_protozero_dep)) {
-      deps += [ perfetto_root_path + "src/protozero" ]
+      link_deps += [ perfetto_root_path + "src/protozero" ]
+    }
+
+    if (defined(invoker.link_deps)) {
+      link_deps += invoker.link_deps
     }
 
     forward_variables_from(invoker,
@@ -101,6 +106,8 @@
                              "defines",
                              "generator_plugin_options",
                              "include_dirs",
+                             "proto_data_sources",
+                             "proto_deps",
                              "proto_in_dir",
                              "proto_out_dir",
                              "sources",
@@ -131,21 +138,23 @@
       component_build_force_source_set = true
     }
 
-    deps = [
+    link_deps = [
       "$perfetto_root_path/gn:default_deps",
       "$perfetto_root_path/include/perfetto/base",
       "$perfetto_root_path/src/protozero",
     ]
 
+    # Convert deps to link_deps: the proto_library rule requires that C++ files
+    # are passed in as link_deps wheras in Perfetto, we always works with deps.
     if (defined(invoker.deps)) {
-      deps += invoker.deps
+      link_deps += invoker.deps
     }
-
     forward_variables_from(invoker,
                            [
                              "defines",
                              "generator_plugin_options",
                              "include_dirs",
+                             "proto_deps",
                              "proto_in_dir",
                              "proto_out_dir",
                              "sources",
@@ -167,11 +176,11 @@
     generator_plugin_label =
         "$perfetto_root_path/src/ipc/protoc_plugin:ipc_plugin"
     generator_plugin_suffix = ".ipc"
-    deps = [ "$perfetto_root_path/gn:default_deps" ]
+    link_deps = [ "$perfetto_root_path/gn:default_deps" ]
     if (perfetto_component_type == "static_library") {
-      deps += [ "$perfetto_root_path/src/ipc:perfetto_ipc" ]
+      link_deps += [ "$perfetto_root_path/src/ipc:perfetto_ipc" ]
     } else {
-      deps += [ "$perfetto_root_path/src/ipc:common" ]
+      link_deps += [ "$perfetto_root_path/src/ipc:common" ]
     }
     if (is_win) {
       # TODO(primiano): investigate this. In Windows standalone builds, some
@@ -182,20 +191,23 @@
       # client-side IPC library. Perhaps we just should do this unconditionally
       # on all platforms?
       if (perfetto_component_type == "static_library") {
-        deps += [ "$perfetto_root_path/src/ipc:perfetto_ipc" ]
+        link_deps += [ "$perfetto_root_path/src/ipc:perfetto_ipc" ]
       } else {
-        deps += [ "$perfetto_root_path/src/ipc:client" ]
+        link_deps += [ "$perfetto_root_path/src/ipc:client" ]
       }
     }
 
+    # Convert deps to link_deps: the proto_library rule requires that C++ files
+    # are passed in as link_deps wheras in Perfetto, we always works with deps.
     if (defined(invoker.deps)) {
-      deps += invoker.deps
+      link_deps += invoker.deps
     }
     forward_variables_from(invoker,
                            [
                              "defines",
                              "extra_configs",
                              "include_dirs",
+                             "proto_deps",
                              "proto_in_dir",
                              "proto_out_dir",
                              "generator_plugin_options",
@@ -224,10 +236,14 @@
       generator_plugin_label =
           "$perfetto_root_path/buildtools/grpc:grpc_cpp_plugin"
       generator_plugin_suffix = ".grpc.pb"
-      deps = [ "$perfetto_root_path/buildtools/grpc:grpc++" ]
+      link_deps = [ "$perfetto_root_path/buildtools/grpc:grpc++" ]
       public_configs = [ "$perfetto_root_path/buildtools:grpc_gen_config" ]
+
+      # Convert deps to link_deps: the proto_library rule requires that C++
+      # files are passed in as link_deps wheras in Perfetto, we always works
+      # with deps.
       if (defined(invoker.deps)) {
-        deps += invoker.deps
+        link_deps += invoker.deps
       }
       forward_variables_from(invoker,
                              [
@@ -251,7 +267,6 @@
       "zero",
       "lite",
       "cpp",
-      "source_set",
     ]
   }
 
@@ -268,46 +283,114 @@
     import_dirs_ = []
   }
 
-  vars_to_forward = [
-    "sources",
-    "visibility",
-    "testonly",
-    "exclude_imports",
-  ]
   expansion_token = "@TYPE@"
 
-  # gn:public_config propagates the gen dir as include directory. We
-  # disable the proto_library's public_config to avoid duplicate include
-  # directory command line flags (crbug.com/1043279, crbug.com/gn/142).
-  propagate_imports_configs_ = false
+  # The source set target should always be generated as it is used by the
+  # build generators and for generating descriptors.
+  source_set_target_name =
+      string_replace(target_name, expansion_token, "source_set")
+
+  # This config is necessary for Chrome proto_library build rule to work
+  # correctly.
+  source_set_input_config_name = "${source_set_target_name}_input_config"
+  config(source_set_input_config_name) {
+    inputs = invoker.sources
+  }
+
+  group(source_set_target_name) {
+    # To propagate indirect inputs dependencies to descendant tareget, we use
+    # public_deps and public_configs in this target.
+    public_deps = []
+    exports_ = []
+    if (defined(invoker.public_deps)) {
+      foreach(dep, invoker.public_deps) {
+        # Get the absolute target path
+        mapped_dep = string_replace(dep, expansion_token, "source_set")
+        public_deps += [ mapped_dep ]
+        exports_ += [ get_label_info(mapped_dep, "dir") + ":" +
+                      get_label_info(mapped_dep, "name") ]
+      }
+    }
+
+    if (defined(invoker.deps)) {
+      foreach(dep, invoker.deps) {
+        mapped_dep = string_replace(dep, expansion_token, "source_set")
+        public_deps += [ mapped_dep ]
+      }
+    }
+
+    sources = []
+    foreach(source, invoker.sources) {
+      sources += [ get_path_info(source, "abspath") ]
+    }
+
+    public_configs = [ ":${source_set_input_config_name}" ]
+
+    metadata = {
+      proto_library_sources = sources
+      proto_import_dirs = import_dirs_
+      exports = exports_
+    }
+  }
+
+  # Generate the descriptor if the option is set.
+  if (defined(invoker.generate_descriptor)) {
+    target_name_ = string_replace(target_name, expansion_token, "descriptor")
+    proto_library(target_name_) {
+      proto_in_dir = proto_path
+      proto_out_dir = proto_path
+      generate_python = false
+      generate_cc = false
+      generate_descriptor = rebase_path(invoker.generate_descriptor, proto_path)
+      sources = [ invoker.descriptor_root_source ]
+      import_dirs = import_dirs_
+      deps = [ ":${source_set_target_name}" ]
+      forward_variables_from(invoker,
+                             [
+                               "visibility",
+                               "testonly",
+                               "exclude_imports",
+                             ])
+    }
+  }
 
   foreach(gen_type, proto_generators) {
     target_name_ = string_replace(target_name, expansion_token, gen_type)
 
     # Translate deps from xxx:@TYPE@ to xxx:lite/zero.
-    deps_ = []
+    all_deps_ = []
     if (defined(invoker.deps)) {
       foreach(dep, invoker.deps) {
-        deps_ += [ string_replace(dep, expansion_token, gen_type) ]
+        all_deps_ += [ string_replace(dep, expansion_token, gen_type) ]
       }
     }
 
     # The distinction between deps and public_deps does not matter for GN
     # but Bazel cares about the difference so we distinguish between the two.
-    public_deps_ = []
     if (defined(invoker.public_deps)) {
       foreach(dep, invoker.public_deps) {
-        public_deps_ = [ string_replace(dep, expansion_token, gen_type) ]
+        all_deps_ += [ string_replace(dep, expansion_token, gen_type) ]
       }
     }
-    deps_ += public_deps_
+
+    # gn:public_config propagates the gen dir as include directory. We
+    # disable the proto_library's public_config to avoid duplicate include
+    # directory command line flags (crbug.com/1043279, crbug.com/gn/142).
+    propagate_imports_configs_ = false
+    vars_to_forward = []
+    vars_to_forward += [
+      "sources",
+      "visibility",
+      "testonly",
+    ]
 
     if (gen_type == "zero") {
       protozero_library(target_name_) {
         proto_in_dir = proto_path
         proto_out_dir = proto_path
         generator_plugin_options = "wrapper_namespace=pbzero"
-        deps = deps_
+        deps = all_deps_
+        proto_deps = [ ":$source_set_target_name" ]
         propagate_imports_configs = propagate_imports_configs_
         import_dirs = import_dirs_
         forward_variables_from(invoker, vars_to_forward)
@@ -317,7 +400,8 @@
         proto_in_dir = proto_path
         proto_out_dir = proto_path
         generator_plugin_options = "wrapper_namespace=gen"
-        deps = deps_
+        deps = all_deps_
+        proto_deps = [ ":$source_set_target_name" ]
         propagate_imports_configs = propagate_imports_configs_
         import_dirs = import_dirs_
         forward_variables_from(invoker, vars_to_forward)
@@ -328,7 +412,8 @@
         proto_in_dir = proto_path
         proto_out_dir = proto_path
         generator_plugin_options = "wrapper_namespace=gen"
-        deps = deps_ + [ ":$cpp_target_name_" ]
+        proto_deps = [ ":$source_set_target_name" ]
+        deps = all_deps_ + [ ":$cpp_target_name_" ]
         propagate_imports_configs = propagate_imports_configs_
         import_dirs = import_dirs_
         forward_variables_from(invoker, vars_to_forward)
@@ -338,59 +423,13 @@
         proto_in_dir = proto_path
         proto_out_dir = proto_path
         generate_python = false
-        deps = deps_
+        link_deps = all_deps_
         cc_generator_options = "lite=true:"
         propagate_imports_configs = propagate_imports_configs_
         import_dirs = import_dirs_
+        proto_deps = [ ":${source_set_target_name}" ]
         forward_variables_from(invoker, vars_to_forward)
       }
-    } else if (gen_type == "descriptor") {
-      proto_library(target_name_) {
-        proto_in_dir = proto_path
-        proto_out_dir = proto_path
-        generate_python = false
-        generate_cc = false
-        generate_descriptor =
-            rebase_path(invoker.generate_descriptor, proto_path)
-        deps = deps_
-        import_dirs = import_dirs_
-        forward_variables_from(invoker, vars_to_forward)
-      }
-
-      # Not needed for descriptor proto_library target.
-      not_needed([ "propagate_imports_configs_" ])
-    } else if (gen_type == "source_set") {
-      action(target_name_) {
-        out_path = "$target_gen_dir/" + target_name_
-        rebased_out_path =
-            rebase_path(target_gen_dir, root_build_dir) + "/" + target_name_
-
-        script = "$perfetto_root_path/tools/touch_file.py"
-        args = [
-          "--output",
-          rebased_out_path,
-        ]
-        outputs = [ out_path ]
-        deps = deps_
-
-        metadata = {
-          proto_library_sources = invoker.sources
-          proto_import_dirs = import_dirs_
-          exports = []
-          foreach(i, public_deps_) {
-            # Get the absolute target path
-            exports +=
-                [ get_label_info(i, "dir") + ":" + get_label_info(i, "name") ]
-          }
-        }
-        forward_variables_from(invoker, vars_to_forward)
-      }
-
-      # Not needed for source_set proto_library target.
-      not_needed([
-                   "propagate_imports_configs_",
-                   "proto_path",
-                 ])
     } else {
       assert(false, "Invalid 'proto_generators' value.")
     }
diff --git a/gn/standalone/BUILD.gn b/gn/standalone/BUILD.gn
index 36fb637..a6455ef 100644
--- a/gn/standalone/BUILD.gn
+++ b/gn/standalone/BUILD.gn
@@ -95,6 +95,13 @@
       # codebase cleanup.
       "-Wno-switch-default",
     ]
+
+    if (perfetto_thread_safety_annotations) {
+      cflags += [
+        "-Wthread-safety",
+        "-Wno-thread-safety-negative",
+      ]
+    }
   } else if (is_gcc) {
     # Use return std::move(...) for compatibility with old GCC compilers.
     cflags_cc = [ "-Wno-redundant-move" ]
diff --git a/gn/standalone/proto_library.gni b/gn/standalone/proto_library.gni
index 1a23a97..ed86c02 100644
--- a/gn/standalone/proto_library.gni
+++ b/gn/standalone/proto_library.gni
@@ -22,6 +22,10 @@
 
 template("proto_library") {
   assert(defined(invoker.sources))
+
+  # This is used in chromium build.
+  not_needed(invoker, [ "proto_deps" ])
+
   proto_sources = invoker.sources
 
   # All the proto imports should be relative to the project root.
@@ -222,13 +226,6 @@
       deps = []
     }
 
-    # TODO(hjd): Avoid adding to deps here this.
-    # When we generate BUILD files we need find the transitive proto,
-    # dependencies, so also add link_deps to actual deps so they show up
-    # in gn desc.
-    if (defined(invoker.link_deps)) {
-      deps += invoker.link_deps
-    }
     if (generate_with_plugin) {
       inputs += [ plugin_path ]
       if (defined(plugin_host_label)) {
@@ -237,9 +234,12 @@
       }
     }
 
-    if (defined(invoker.deps)) {
+    if (generate_descriptor != "") {
       deps += invoker.deps
     }
+    if (defined(invoker.link_deps)) {
+      deps += invoker.link_deps
+    }
   }  # action(action_name)
 
   # The source_set that builds the generated .pb.cc files.
@@ -299,6 +299,9 @@
       if (defined(invoker.deps)) {
         deps += invoker.deps
       }
+      if (defined(invoker.link_deps)) {
+        deps += invoker.link_deps
+      }
     }  # source_set(source_set_name)
   }
 }  # template
diff --git a/include/perfetto/base/BUILD.gn b/include/perfetto/base/BUILD.gn
index 357f89a..1fa5c9d 100644
--- a/include/perfetto/base/BUILD.gn
+++ b/include/perfetto/base/BUILD.gn
@@ -26,6 +26,7 @@
     "status.h",
     "task_runner.h",
     "template_util.h",
+    "thread_annotations.h",
     "thread_utils.h",
     "time.h",
   ]
diff --git a/include/perfetto/base/build_configs/android_tree/perfetto_build_flags.h b/include/perfetto/base/build_configs/android_tree/perfetto_build_flags.h
index c36d04b..6cb3e3d 100644
--- a/include/perfetto/base/build_configs/android_tree/perfetto_build_flags.h
+++ b/include/perfetto/base/build_configs/android_tree/perfetto_build_flags.h
@@ -38,6 +38,7 @@
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_LINENOISE() (0)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_HTTPD() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_JSON() (0)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_INSTRUMENTS() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_LOCAL_SYMBOLIZER() (PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_LINUX() || PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_MAC() ||PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_WIN())
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_ZLIB() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TRACED_PERF() (1)
@@ -46,6 +47,7 @@
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_X64_CPU_OPT() (0)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_LLVM_DEMANGLE() (0)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_SYSTEM_CONSUMER() (1)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_THREAD_SAFETY_ANNOTATIONS() (0)
 
 // clang-format on
 #endif  // GEN_BUILD_CONFIG_PERFETTO_BUILD_FLAGS_H_
diff --git a/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h b/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h
index 54fe273..c27eaf8 100644
--- a/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h
+++ b/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h
@@ -38,6 +38,7 @@
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_LINENOISE() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_HTTPD() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_JSON() (1)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_INSTRUMENTS() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_LOCAL_SYMBOLIZER() (PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_LINUX() || PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_MAC() ||PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_WIN())
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_ZLIB() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TRACED_PERF() (0)
@@ -46,6 +47,7 @@
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_X64_CPU_OPT() (0)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_LLVM_DEMANGLE() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_SYSTEM_CONSUMER() (1)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_THREAD_SAFETY_ANNOTATIONS() (0)
 
 // clang-format on
 #endif  // GEN_BUILD_CONFIG_PERFETTO_BUILD_FLAGS_H_
diff --git a/include/perfetto/base/compiler.h b/include/perfetto/base/compiler.h
index 22ebb8c..2bdc158 100644
--- a/include/perfetto/base/compiler.h
+++ b/include/perfetto/base/compiler.h
@@ -17,8 +17,9 @@
 #ifndef INCLUDE_PERFETTO_BASE_COMPILER_H_
 #define INCLUDE_PERFETTO_BASE_COMPILER_H_
 
-#include <stddef.h>
+#include <cstddef>
 #include <type_traits>
+#include <variant>
 
 #include "perfetto/public/compiler.h"
 
@@ -107,15 +108,6 @@
 #define PERFETTO_EXPORT_ENTRYPOINT
 #endif
 
-// Disables thread safety analysis for functions where the compiler can't
-// accurate figure out which locks are being held.
-#if defined(__clang__)
-#define PERFETTO_NO_THREAD_SAFETY_ANALYSIS \
-  __attribute__((no_thread_safety_analysis))
-#else
-#define PERFETTO_NO_THREAD_SAFETY_ANALYSIS
-#endif
-
 // Disables undefined behavior analysis for a function.
 #if defined(__clang__)
 #define PERFETTO_NO_SANITIZE_UNDEFINED __attribute__((no_sanitize("undefined")))
@@ -135,13 +127,23 @@
 // Macro for telling -Wimplicit-fallthrough that a fallthrough is intentional.
 #define PERFETTO_FALLTHROUGH [[fallthrough]]
 
-namespace perfetto {
-namespace base {
+namespace perfetto::base {
 
 template <typename... T>
 inline void ignore_result(const T&...) {}
 
-}  // namespace base
-}  // namespace perfetto
+// Given a std::variant and a type T, returns the index of the T in the variant.
+template <typename VariantType, typename T, size_t i = 0>
+constexpr size_t variant_index() {
+  static_assert(i < std::variant_size_v<VariantType>,
+                "Type not found in variant");
+  if constexpr (std::is_same_v<std::variant_alternative_t<i, VariantType>, T>) {
+    return i;
+  } else {
+    return variant_index<VariantType, T, i + 1>();
+  }
+}
+
+}  // namespace perfetto::base
 
 #endif  // INCLUDE_PERFETTO_BASE_COMPILER_H_
diff --git a/include/perfetto/base/thread_annotations.h b/include/perfetto/base/thread_annotations.h
new file mode 100644
index 0000000..3d91269
--- /dev/null
+++ b/include/perfetto/base/thread_annotations.h
@@ -0,0 +1,280 @@
+/*
+ * 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 INCLUDE_PERFETTO_BASE_THREAD_ANNOTATIONS_H_
+#define INCLUDE_PERFETTO_BASE_THREAD_ANNOTATIONS_H_
+
+// This header file contains macro definitions for thread safety annotations
+// that allow developers to document the locking policies of multi-threaded
+// code. The annotations can also help program analysis tools to identify
+// potential thread safety issues.
+//
+// These macro definitions are copied from the Chromium code base:
+// https://source.chromium.org/chromium/chromium/src/+/main:base/thread_annotations.h;drc=10d865767e72f494da1e4e868eb6ae9befe87422
+// with the 'PERFETTO_' prefix added.
+//
+// Note that no analysis is done inside constructors and destructors,
+// regardless of what attributes are used. See
+// https://clang.llvm.org/docs/ThreadSafetyAnalysis.html#no-checking-inside-constructors-and-destructors
+// for details.
+//
+// Note that the annotations we use are described as deprecated in the Clang
+// documentation, linked below. E.g. we use PERFETTO_EXCLUSIVE_LOCKS_REQUIRED
+// where the Clang docs use REQUIRES.
+//
+// http://clang.llvm.org/docs/ThreadSafetyAnalysis.html
+//
+// We use the deprecated Clang annotations to match Abseil (relevant header
+// linked below) and its ecosystem of libraries. We will follow Abseil with
+// respect to upgrading to more modern annotations.
+//
+// https://github.com/abseil/abseil-cpp/blob/master/absl/base/thread_annotations.h
+//
+// These annotations are implemented using compiler attributes. Using the macros
+// defined here instead of raw attributes allow for portability and future
+// compatibility.
+//
+// When referring to mutexes in the arguments of the attributes, you should
+// use variable names or more complex expressions (e.g. my_object->mutex_)
+// that evaluate to a concrete mutex object whenever possible. If the mutex
+// you want to refer to is not in scope, you may use a member pointer
+// (e.g. &MyClass::mutex_) to refer to a mutex in some (unknown) object.
+
+#include "perfetto/base/build_config.h"
+
+#if defined(__clang__) && PERFETTO_BUILDFLAG(PERFETTO_THREAD_SAFETY_ANNOTATIONS)
+#define PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(x) __attribute__((x))
+#else
+#define PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(x)  // no-op
+#endif
+
+// PERFETTO_GUARDED_BY()
+//
+// Documents if a shared field or global variable needs to be protected by a
+// mutex. PERFETTO_GUARDED_BY() allows the user to specify a particular mutex
+// that should be held when accessing the annotated variable.
+//
+// Example:
+//
+//   Mutex mu;
+//   int p1 PERFETTO_GUARDED_BY(mu);
+#define PERFETTO_GUARDED_BY(x) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x))
+
+// PERFETTO_PT_GUARDED_BY()
+//
+// Documents if the memory location pointed to by a pointer should be guarded
+// by a mutex when dereferencing the pointer.
+//
+// Example:
+//   Mutex mu;
+//   int *p1 PERFETTO_PT_GUARDED_BY(mu);
+//
+// Note that a pointer variable to a shared memory location could itself be a
+// shared variable.
+//
+// Example:
+//
+//     // `q`, guarded by `mu1`, points to a shared memory location that is
+//     // guarded by `mu2`:
+//     int *q PERFETTO_GUARDED_BY(mu1) PERFETTO_PT_GUARDED_BY(mu2);
+#define PERFETTO_PT_GUARDED_BY(x) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(pt_guarded_by(x))
+
+// PERFETTO_ACQUIRED_AFTER() / PERFETTO_ACQUIRED_BEFORE()
+//
+// Documents the acquisition order between locks that can be held
+// simultaneously by a thread. For any two locks that need to be annotated
+// to establish an acquisition order, only one of them needs the annotation.
+// (i.e. You don't have to annotate both locks with both PERFETTO_ACQUIRED_AFTER
+// and PERFETTO_ACQUIRED_BEFORE.)
+//
+// Example:
+//
+//   Mutex m1;
+//   Mutex m2 PERFETTO_ACQUIRED_AFTER(m1);
+#define PERFETTO_ACQUIRED_AFTER(...) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(acquired_after(__VA_ARGS__))
+
+#define PERFETTO_ACQUIRED_BEFORE(...) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(acquired_before(__VA_ARGS__))
+
+// PERFETTO_EXCLUSIVE_LOCKS_REQUIRED() / PERFETTO_SHARED_LOCKS_REQUIRED()
+//
+// Documents a function that expects a mutex to be held prior to entry.
+// The mutex is expected to be held both on entry to, and exit from, the
+// function.
+//
+// Example:
+//
+//   Mutex mu1, mu2;
+//   int a PERFETTO_GUARDED_BY(mu1);
+//   int b PERFETTO_GUARDED_BY(mu2);
+//
+//   void foo() PERFETTO_EXCLUSIVE_LOCKS_REQUIRED(mu1, mu2) { ... };
+#define PERFETTO_EXCLUSIVE_LOCKS_REQUIRED(...) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(exclusive_locks_required(__VA_ARGS__))
+
+#define PERFETTO_SHARED_LOCKS_REQUIRED(...) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(shared_locks_required(__VA_ARGS__))
+
+// PERFETTO_LOCKS_EXCLUDED()
+//
+// Documents the locks acquired in the body of the function. These locks
+// cannot be held when calling this function (as Abseil's `Mutex` locks are
+// non-reentrant).
+#define PERFETTO_LOCKS_EXCLUDED(...) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(locks_excluded(__VA_ARGS__))
+
+// PERFETTO_LOCK_RETURNED()
+//
+// Documents a function that returns a mutex without acquiring it.  For example,
+// a public getter method that returns a pointer to a private mutex should
+// be annotated with PERFETTO_LOCK_RETURNED.
+#define PERFETTO_LOCK_RETURNED(x) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(lock_returned(x))
+
+// PERFETTO_LOCKABLE
+//
+// Documents if a class/type is a lockable type (such as the `Mutex` class).
+#define PERFETTO_LOCKABLE PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(lockable)
+
+// PERFETTO_SCOPED_LOCKABLE
+//
+// Documents if a class does RAII locking (such as the `MutexLock` class).
+// The constructor should use `LOCK_FUNCTION()` to specify the mutex that is
+// acquired, and the destructor should use `PERFETTO_UNLOCK_FUNCTION()` with no
+// arguments; the analysis will assume that the destructor unlocks whatever the
+// constructor locked.
+#define PERFETTO_SCOPED_LOCKABLE \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(scoped_lockable)
+
+// PERFETTO_EXCLUSIVE_LOCK_FUNCTION()
+//
+// Documents functions that acquire a lock in the body of a function, and do
+// not release it.
+#define PERFETTO_EXCLUSIVE_LOCK_FUNCTION(...) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(exclusive_lock_function(__VA_ARGS__))
+
+// PERFETTO_SHARED_LOCK_FUNCTION()
+//
+// Documents functions that acquire a shared (reader) lock in the body of a
+// function, and do not release it.
+#define PERFETTO_SHARED_LOCK_FUNCTION(...) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(shared_lock_function(__VA_ARGS__))
+
+// PERFETTO_UNLOCK_FUNCTION()
+//
+// Documents functions that expect a lock to be held on entry to the function,
+// and release it in the body of the function.
+#define PERFETTO_UNLOCK_FUNCTION(...) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(unlock_function(__VA_ARGS__))
+
+// PERFETTO_EXCLUSIVE_TRYLOCK_FUNCTION() / PERFETTO_SHARED_TRYLOCK_FUNCTION()
+//
+// Documents functions that try to acquire a lock, and return success or failure
+// (or a non-boolean value that can be interpreted as a boolean).
+// The first argument should be `true` for functions that return `true` on
+// success, or `false` for functions that return `false` on success. The second
+// argument specifies the mutex that is locked on success. If unspecified, this
+// mutex is assumed to be `this`.
+#define PERFETTO_EXCLUSIVE_TRYLOCK_FUNCTION(...) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(        \
+      exclusive_trylock_function(__VA_ARGS__))
+
+#define PERFETTO_SHARED_TRYLOCK_FUNCTION(...) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(shared_trylock_function(__VA_ARGS__))
+
+// PERFETTO_ASSERT_EXCLUSIVE_LOCK() / PERFETTO_ASSERT_SHARED_LOCK()
+//
+// Documents functions that dynamically check to see if a lock is held, and fail
+// if it is not held.
+#define PERFETTO_ASSERT_EXCLUSIVE_LOCK(...) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(assert_exclusive_lock(__VA_ARGS__))
+
+#define PERFETTO_ASSERT_SHARED_LOCK(...) \
+  PERFETTO_THREAD_ANNOTATION_ATTRIBUTE__(assert_shared_lock(__VA_ARGS__))
+
+// PERFETTO_NO_THREAD_SAFETY_ANALYSIS is special and differs from other
+// macros defined in this file, it was defined in `compiler.h` and used before
+// we introduce Thread Safety Analysis. Therefore, we define it here even if
+// 'PERFETTO_ENABLE_THREAD_SAFETY_ANNOTATIONS' macro is not defined.
+
+#if defined(__clang__)
+// PERFETTO_NO_THREAD_SAFETY_ANALYSIS
+//
+// Turns off thread safety checking within the body of a particular function.
+// This annotation is used to mark functions that are known to be correct, but
+// the locking behavior is more complicated than the analyzer can handle.
+#define PERFETTO_NO_THREAD_SAFETY_ANALYSIS \
+  __attribute__((no_thread_safety_analysis))
+#else
+#define PERFETTO_NO_THREAD_SAFETY_ANALYSIS
+#endif
+
+//------------------------------------------------------------------------------
+// Tool-Supplied Annotations
+//------------------------------------------------------------------------------
+
+// PERFETTO_TS_UNCHECKED should be placed around lock expressions that are not
+// valid C++ syntax, but which are present for documentation purposes.  These
+// annotations will be ignored by the analysis.
+#define PERFETTO_TS_UNCHECKED(x) ""
+
+// TS_FIXME is used to mark lock expressions that are not valid C++ syntax.
+// It is used by automated tools to mark and disable invalid expressions.
+// The annotation should either be fixed, or changed to PERFETTO_TS_UNCHECKED.
+#define PERFETTO_TS_FIXME(x) ""
+
+// Like NO_THREAD_SAFETY_ANALYSIS, this turns off checking within the body of
+// a particular function.  However, this attribute is used to mark functions
+// that are incorrect and need to be fixed.  It is used by automated tools to
+// avoid breaking the build when the analysis is updated.
+// Code owners are expected to eventually fix the routine.
+#define PERFETTO_NO_THREAD_SAFETY_ANALYSIS_FIXME \
+  PERFETTO_NO_THREAD_SAFETY_ANALYSIS
+
+// Similar to NO_THREAD_SAFETY_ANALYSIS_FIXME, this macro marks a
+// PERFETTO_GUARDED_BY annotation that needs to be fixed, because it is
+// producing thread safety warning.  It disables the PERFETTO_GUARDED_BY.
+#define PERFETTO_PERFETTO_GUARDED_BY_FIXME(x)
+
+// Disables warnings for a single read operation.  This can be used to avoid
+// warnings when it is known that the read is not actually involved in a race,
+// but the compiler cannot confirm that.
+#define PERFETTO_TS_UNCHECKED_READ(x) \
+  perfetto::thread_safety_analysis::ts_unchecked_read(x)
+
+namespace perfetto {
+namespace thread_safety_analysis {
+
+// Takes a reference to a guarded data member, and returns an unguarded
+// reference.
+template <typename T>
+inline const T& ts_unchecked_read(const T& v)
+    PERFETTO_NO_THREAD_SAFETY_ANALYSIS {
+  return v;
+}
+
+template <typename T>
+inline T& ts_unchecked_read(T& v) PERFETTO_NO_THREAD_SAFETY_ANALYSIS {
+  return v;
+}
+
+}  // namespace thread_safety_analysis
+}  // namespace perfetto
+
+#endif  // INCLUDE_PERFETTO_BASE_THREAD_ANNOTATIONS_H_
diff --git a/include/perfetto/ext/base/BUILD.gn b/include/perfetto/ext/base/BUILD.gn
index 2418346..b485000 100644
--- a/include/perfetto/ext/base/BUILD.gn
+++ b/include/perfetto/ext/base/BUILD.gn
@@ -19,6 +19,7 @@
     "android_utils.h",
     "base64.h",
     "circular_queue.h",
+    "clock_snapshots.h",
     "container_annotations.h",
     "crash_keys.h",
     "ctrl_c_handler.h",
diff --git a/include/perfetto/ext/base/clock_snapshots.h b/include/perfetto/ext/base/clock_snapshots.h
new file mode 100644
index 0000000..704d0c8
--- /dev/null
+++ b/include/perfetto/ext/base/clock_snapshots.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef INCLUDE_PERFETTO_EXT_BASE_CLOCK_SNAPSHOTS_H_
+#define INCLUDE_PERFETTO_EXT_BASE_CLOCK_SNAPSHOTS_H_
+
+#include <cstdint>
+#include <vector>
+
+namespace perfetto::base {
+
+struct ClockReading {
+  ClockReading(uint32_t _clock_id, uint64_t _timestamp)
+      : clock_id(_clock_id), timestamp(_timestamp) {}
+  ClockReading() = default;
+
+  // Identifier of the clock domain (of type protos::pbzero::BuiltinClock).
+  uint32_t clock_id = 0;
+  // Clock reading as uint64_t.
+  uint64_t timestamp = 0;
+};
+
+using ClockSnapshotVector = std::vector<ClockReading>;
+
+// Takes snapshots of clock readings of all supported built-in clocks.
+ClockSnapshotVector CaptureClockSnapshots();
+
+}  // namespace perfetto::base
+
+#endif  // INCLUDE_PERFETTO_EXT_BASE_CLOCK_SNAPSHOTS_H_
diff --git a/include/perfetto/ext/base/small_set.h b/include/perfetto/ext/base/small_set.h
index 5d8d8bc..8ad41c7 100644
--- a/include/perfetto/ext/base/small_set.h
+++ b/include/perfetto/ext/base/small_set.h
@@ -18,6 +18,7 @@
 #define INCLUDE_PERFETTO_EXT_BASE_SMALL_SET_H_
 
 #include <array>
+#include <cstdlib>
 
 namespace perfetto {
 
diff --git a/include/perfetto/ext/base/threading/channel.h b/include/perfetto/ext/base/threading/channel.h
index 9bf42f5..3659d28 100644
--- a/include/perfetto/ext/base/threading/channel.h
+++ b/include/perfetto/ext/base/threading/channel.h
@@ -22,6 +22,7 @@
 
 #include "perfetto/base/compiler.h"
 #include "perfetto/base/platform_handle.h"
+#include "perfetto/base/thread_annotations.h"
 #include "perfetto/ext/base/circular_queue.h"
 #include "perfetto/ext/base/event_fd.h"
 
@@ -167,8 +168,8 @@
 
  private:
   std::mutex mutex_;
-  base::CircularQueue<T> elements_;
-  bool is_closed_ = false;
+  base::CircularQueue<T> elements_ PERFETTO_GUARDED_BY(mutex_);
+  bool is_closed_ PERFETTO_GUARDED_BY(mutex_) = false;
 
   base::EventFd read_fd_;
   base::EventFd write_fd_;
diff --git a/include/perfetto/ext/base/threading/thread_pool.h b/include/perfetto/ext/base/threading/thread_pool.h
index e08baaa..44402af 100644
--- a/include/perfetto/ext/base/threading/thread_pool.h
+++ b/include/perfetto/ext/base/threading/thread_pool.h
@@ -26,6 +26,7 @@
 #include <vector>
 
 #include "perfetto/base/task_runner.h"
+#include "perfetto/base/thread_annotations.h"
 
 namespace perfetto {
 namespace base {
@@ -63,13 +64,11 @@
   ThreadPool(ThreadPool&&) = delete;
   ThreadPool& operator=(ThreadPool&&) = delete;
 
-  // Start of mutex protected members.
   std::mutex mutex_;
-  std::list<std::function<void()>> pending_tasks_;
-  std::condition_variable thread_waiter_;
-  uint32_t thread_waiting_count_ = 0;
-  bool quit_ = false;
-  // End of mutex protected members.
+  std::list<std::function<void()>> pending_tasks_ PERFETTO_GUARDED_BY(mutex_);
+  std::condition_variable thread_waiter_ PERFETTO_GUARDED_BY(mutex_);
+  uint32_t thread_waiting_count_ PERFETTO_GUARDED_BY(mutex_) = 0;
+  bool quit_ PERFETTO_GUARDED_BY(mutex_) = false;
 
   std::vector<std::thread> threads_;
 };
diff --git a/include/perfetto/ext/base/unix_task_runner.h b/include/perfetto/ext/base/unix_task_runner.h
index ecde733..5f42161 100644
--- a/include/perfetto/ext/base/unix_task_runner.h
+++ b/include/perfetto/ext/base/unix_task_runner.h
@@ -19,6 +19,7 @@
 
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/task_runner.h"
+#include "perfetto/base/thread_annotations.h"
 #include "perfetto/base/thread_utils.h"
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/event_fd.h"
@@ -69,6 +70,10 @@
   // delayed tasks don't count even if they are due to run.
   bool IsIdleForTesting();
 
+  // Pretends (for the purposes of running delayed tasks) that time advanced by
+  // `ms`.
+  void AdvanceTimeForTesting(uint32_t ms);
+
   // TaskRunner implementation:
   void PostTask(std::function<void()>) override;
   void PostDelayedTask(std::function<void()>, uint32_t delay_ms) override;
@@ -83,8 +88,9 @@
 
  private:
   void WakeUp();
-  void UpdateWatchTasksLocked();
-  int GetDelayMsToNextTaskLocked() const;
+  void UpdateWatchTasksLocked() PERFETTO_EXCLUSIVE_LOCKS_REQUIRED(lock_);
+  int GetDelayMsToNextTaskLocked() const
+      PERFETTO_EXCLUSIVE_LOCKS_REQUIRED(lock_);
   void RunImmediateAndDelayedTask();
   void PostFileDescriptorWatches(uint64_t windows_wait_result);
   void RunFileDescriptorWatch(PlatformHandle);
@@ -101,13 +107,14 @@
   std::vector<struct pollfd> poll_fds_;
 #endif
 
-  // --- Begin lock-protected members ---
-
   std::mutex lock_;
 
-  std::deque<std::function<void()>> immediate_tasks_;
-  std::multimap<TimeMillis, std::function<void()>> delayed_tasks_;
-  bool quit_ = false;
+  std::deque<std::function<void()>> immediate_tasks_ PERFETTO_GUARDED_BY(lock_);
+  std::multimap<TimeMillis, std::function<void()>> delayed_tasks_
+      PERFETTO_GUARDED_BY(lock_);
+  bool quit_ PERFETTO_GUARDED_BY(lock_) = false;
+  TimeMillis advanced_time_for_testing_ PERFETTO_GUARDED_BY(lock_) =
+      TimeMillis(0);
 
   struct WatchTask {
     std::function<void()> callback;
@@ -121,10 +128,8 @@
 #endif
   };
 
-  std::map<PlatformHandle, WatchTask> watch_tasks_;
-  bool watch_tasks_changed_ = false;
-
-  // --- End lock-protected members ---
+  std::map<PlatformHandle, WatchTask> watch_tasks_ PERFETTO_GUARDED_BY(lock_);
+  bool watch_tasks_changed_ PERFETTO_GUARDED_BY(lock_) = false;
 };
 
 }  // namespace base
diff --git a/include/perfetto/ext/base/uuid.h b/include/perfetto/ext/base/uuid.h
index 2bf5f5b..55a2c32 100644
--- a/include/perfetto/ext/base/uuid.h
+++ b/include/perfetto/ext/base/uuid.h
@@ -23,10 +23,12 @@
 #include <optional>
 #include <string>
 
+#include "perfetto/base/export.h"
+
 namespace perfetto {
 namespace base {
 
-class Uuid {
+class PERFETTO_EXPORT_COMPONENT Uuid {
  public:
   explicit Uuid(const std::string& s);
   explicit Uuid(int64_t lsb, int64_t msb);
diff --git a/include/perfetto/ext/base/waitable_event.h b/include/perfetto/ext/base/waitable_event.h
index 0e78619..a55b34b 100644
--- a/include/perfetto/ext/base/waitable_event.h
+++ b/include/perfetto/ext/base/waitable_event.h
@@ -17,6 +17,8 @@
 #ifndef INCLUDE_PERFETTO_EXT_BASE_WAITABLE_EVENT_H_
 #define INCLUDE_PERFETTO_EXT_BASE_WAITABLE_EVENT_H_
 
+#include "perfetto/base/thread_annotations.h"
+
 #include <condition_variable>
 #include <mutex>
 
@@ -40,8 +42,8 @@
 
  private:
   std::mutex mutex_;
-  std::condition_variable event_;
-  uint64_t notifications_ = 0;
+  std::condition_variable event_ PERFETTO_GUARDED_BY(mutex_);
+  uint64_t notifications_ PERFETTO_GUARDED_BY(mutex_) = 0;
 };
 
 }  // namespace base
diff --git a/include/perfetto/ext/base/watchdog.h b/include/perfetto/ext/base/watchdog.h
index 9bb4227..ead3a9c 100644
--- a/include/perfetto/ext/base/watchdog.h
+++ b/include/perfetto/ext/base/watchdog.h
@@ -58,9 +58,9 @@
 constexpr uint32_t kWatchdogDefaultMemoryWindow = 30 * 1000;  // 30 seconds.
 
 inline void RunTaskWithWatchdogGuard(const std::function<void()>& task) {
-  // Maximum time a single task can take in a TaskRunner before the
-  // program suicides.
-  constexpr int64_t kWatchdogMillis = 30000;  // 30s
+  // The longest duration allowed for a single task within the TaskRunner.
+  // Exceeding this limit will trigger program termination.
+  constexpr int64_t kWatchdogMillis = 180000;  // 180s
 
   Watchdog::Timer handle = base::Watchdog::GetInstance()->CreateFatalTimer(
       kWatchdogMillis, WatchdogCrashReason::kTaskRunnerHung);
diff --git a/include/perfetto/ext/base/watchdog_posix.h b/include/perfetto/ext/base/watchdog_posix.h
index fef4074..3210c14 100644
--- a/include/perfetto/ext/base/watchdog_posix.h
+++ b/include/perfetto/ext/base/watchdog_posix.h
@@ -17,6 +17,7 @@
 #ifndef INCLUDE_PERFETTO_EXT_BASE_WATCHDOG_POSIX_H_
 #define INCLUDE_PERFETTO_EXT_BASE_WATCHDOG_POSIX_H_
 
+#include "perfetto/base/thread_annotations.h"
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/scoped_file.h"
 
@@ -154,12 +155,14 @@
 
   // Check each type of resource every |polling_interval_ms_| miillis.
   // Returns true if the threshold is exceeded and the process should be killed.
-  bool CheckMemory_Locked(uint64_t rss_bytes);
-  bool CheckCpu_Locked(uint64_t cpu_time);
+  bool CheckMemory_Locked(uint64_t rss_bytes)
+      PERFETTO_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+  bool CheckCpu_Locked(uint64_t cpu_time)
+      PERFETTO_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
   void AddFatalTimer(TimerData);
   void RemoveFatalTimer(TimerData);
-  void RearmTimerFd_Locked();
+  void RearmTimerFd_Locked() PERFETTO_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
   void SerializeLogsAndKillThread(int tid, WatchdogCrashReason);
 
   // Computes the time interval spanned by a given ring buffer with respect
@@ -171,24 +174,20 @@
   std::thread thread_;
   ScopedPlatformHandle timer_fd_;
 
-  // --- Begin lock-protected members ---
-
   std::mutex mutex_;
 
-  uint64_t memory_limit_bytes_ = 0;
-  WindowedInterval memory_window_bytes_;
+  uint64_t memory_limit_bytes_ PERFETTO_GUARDED_BY(mutex_) = 0;
+  WindowedInterval memory_window_bytes_ PERFETTO_GUARDED_BY(mutex_);
 
-  uint32_t cpu_limit_percentage_ = 0;
-  WindowedInterval cpu_window_time_ticks_;
+  uint32_t cpu_limit_percentage_ PERFETTO_GUARDED_BY(mutex_) = 0;
+  WindowedInterval cpu_window_time_ticks_ PERFETTO_GUARDED_BY(mutex_);
 
   // Outstanding timers created via CreateFatalTimer() and not yet destroyed.
   // The vector is not sorted. In most cases there are only 1-2 timers, we can
   // afford O(N) operations.
   // All the timers in the list share the same |timer_fd_|, which is keeped
   // armed on the min(timers_) through RearmTimerFd_Locked().
-  std::vector<TimerData> timers_;
-
-  // --- End lock-protected members ---
+  std::vector<TimerData> timers_ PERFETTO_GUARDED_BY(mutex_);
 
  protected:
   // Protected for testing.
diff --git a/include/perfetto/ext/tracing/core/null_consumer_endpoint_for_testing.h b/include/perfetto/ext/tracing/core/null_consumer_endpoint_for_testing.h
index 2e85c92..795f8b8 100644
--- a/include/perfetto/ext/tracing/core/null_consumer_endpoint_for_testing.h
+++ b/include/perfetto/ext/tracing/core/null_consumer_endpoint_for_testing.h
@@ -34,7 +34,7 @@
   void ChangeTraceConfig(const perfetto::TraceConfig&) override {}
   void StartTracing() override {}
   void DisableTracing() override {}
-  void CloneSession(TracingSessionID, CloneSessionArgs) override {}
+  void CloneSession(CloneSessionArgs) override {}
   void Flush(uint32_t, FlushCallback, FlushFlags) override {}
   void ReadBuffers() override {}
   void FreeBuffers() override {}
diff --git a/include/perfetto/ext/tracing/core/shared_memory.h b/include/perfetto/ext/tracing/core/shared_memory.h
index f2e5274..4773c34 100644
--- a/include/perfetto/ext/tracing/core/shared_memory.h
+++ b/include/perfetto/ext/tracing/core/shared_memory.h
@@ -20,6 +20,7 @@
 #include <stddef.h>
 
 #include <memory>
+#include <utility>
 
 #include "perfetto/base/export.h"
 #include "perfetto/base/platform_handle.h"
@@ -45,7 +46,20 @@
   // this object region when destroyed.
   virtual ~SharedMemory();
 
-  virtual void* start() const = 0;
+  // Read/write and read-only access to underlying buffer. The non-const method
+  // is implemented in terms of the const one so subclasses need only provide a
+  // single implementation; implementing in the opposite order would be unsafe
+  // since subclasses could effectively mutate state from inside a const method.
+  //
+  // N.B. This signature implements "deep const" that ties the constness of this
+  // object to the constness of the underlying buffer, as opposed to "shallow
+  // const" that would have the signature `void* start() const;`; this is less
+  // flexible for callers but prevents corner cases where it's transitively
+  // possible to change this object's state via the controlled memory.
+  void* start() { return const_cast<void*>(std::as_const(*this).start()); }
+  virtual const void* start() const = 0;
+
+
   virtual size_t size() const = 0;
 };
 
diff --git a/include/perfetto/ext/tracing/core/tracing_service.h b/include/perfetto/ext/tracing/core/tracing_service.h
index 2b33fee..e3beaff 100644
--- a/include/perfetto/ext/tracing/core/tracing_service.h
+++ b/include/perfetto/ext/tracing/core/tracing_service.h
@@ -24,13 +24,13 @@
 #include <vector>
 
 #include "perfetto/base/export.h"
+#include "perfetto/ext/base/clock_snapshots.h"
 #include "perfetto/ext/base/scoped_file.h"
 #include "perfetto/ext/base/sys_types.h"
 #include "perfetto/ext/tracing/core/basic_types.h"
 #include "perfetto/ext/tracing/core/shared_memory.h"
 #include "perfetto/ext/tracing/core/trace_packet.h"
 #include "perfetto/tracing/buffer_exhausted_policy.h"
-#include "perfetto/tracing/core/clock_snapshots.h"
 #include "perfetto/tracing/core/flush_flags.h"
 #include "perfetto/tracing/core/forward_decls.h"
 
@@ -193,9 +193,17 @@
   // Clones an existing tracing session and attaches to it. The session is
   // cloned in read-only mode and can only be used to read a snapshot of an
   // existing tracing session. Will invoke Consumer::OnSessionCloned().
-  // If TracingSessionID == kBugreportSessionId (0xff...ff) the session with the
-  // highest bugreport score is cloned (if any exists).
   struct CloneSessionArgs {
+    // Exactly one between tsid and unique_session_name should be set.
+
+    // The id of the tracing session that should be cloned. If
+    // kBugreportSessionId (0xff...ff) the session with the highest bugreport
+    // score is cloned (if any exists).
+    TracingSessionID tsid = 0;
+
+    // The unique_session_name of the session that should be cloned.
+    std::string unique_session_name;
+
     // If set, the trace filter will not have effect on the cloned session.
     // Used for bugreports.
     bool skip_trace_filter = false;
@@ -204,7 +212,7 @@
     // to kBugreport when requesting the flush to the producers.
     bool for_bugreport = false;
   };
-  virtual void CloneSession(TracingSessionID, CloneSessionArgs) = 0;
+  virtual void CloneSession(CloneSessionArgs) = 0;
 
   // Requests all data sources to flush their data immediately and invokes the
   // passed callback once all of them have acked the flush (in which case
@@ -298,14 +306,14 @@
 
   // A snapshot of client and host clocks.
   struct SyncClockSnapshot {
-    ClockSnapshotVector client_clock_snapshots;
-    ClockSnapshotVector host_clock_snapshots;
+    base::ClockSnapshotVector client_clock_snapshots;
+    base::ClockSnapshotVector host_clock_snapshots;
   };
 
   enum class SyncMode : uint32_t { PING = 1, UPDATE = 2 };
   virtual void SyncClocks(SyncMode sync_mode,
-                          ClockSnapshotVector client_clocks,
-                          ClockSnapshotVector host_clocks) = 0;
+                          base::ClockSnapshotVector client_clocks,
+                          base::ClockSnapshotVector host_clocks) = 0;
   virtual void Disconnect() = 0;
 };
 
diff --git a/include/perfetto/public/abi/track_event_hl_abi.h b/include/perfetto/public/abi/track_event_hl_abi.h
index 3d8720c..f214596 100644
--- a/include/perfetto/public/abi/track_event_hl_abi.h
+++ b/include/perfetto/public/abi/track_event_hl_abi.h
@@ -125,6 +125,7 @@
   PERFETTO_TE_HL_EXTRA_TYPE_FLUSH = 15,
   PERFETTO_TE_HL_EXTRA_TYPE_NO_INTERN = 16,
   PERFETTO_TE_HL_EXTRA_TYPE_PROTO_FIELDS = 17,
+  PERFETTO_TE_HL_EXTRA_TYPE_PROTO_TRACK = 18,
 };
 
 // An extra event parameter. Each type of parameter should embed this as its
@@ -250,6 +251,14 @@
   struct PerfettoTeHlProtoField* const* fields;
 };
 
+// PERFETTO_TE_HL_EXTRA_TYPE_PROTO_TRACK
+struct PerfettoTeHlExtraProtoTrack {
+  struct PerfettoTeHlExtra header;
+  uint64_t uuid;
+  // Array of pointers to the fields. The last pointer should be NULL.
+  struct PerfettoTeHlProtoField* const* fields;
+};
+
 // Emits an event on all active instances of the track event data source.
 // * `cat`: The registered category of the event, it knows on which data source
 //          instances the event should be emitted. Use
diff --git a/include/perfetto/public/protos/trace/track_event/track_descriptor.pzc.h b/include/perfetto/public/protos/trace/track_event/track_descriptor.pzc.h
index 7f0a5ad..7c3b0f3 100644
--- a/include/perfetto/public/protos/trace/track_event/track_descriptor.pzc.h
+++ b/include/perfetto/public/protos/trace/track_event/track_descriptor.pzc.h
@@ -49,6 +49,11 @@
                   static_name,
                   10);
 PERFETTO_PB_FIELD(perfetto_protos_TrackDescriptor,
+                  STRING,
+                  const char*,
+                  atrace_name,
+                  13);
+PERFETTO_PB_FIELD(perfetto_protos_TrackDescriptor,
                   MSG,
                   perfetto_protos_ProcessDescriptor,
                   process,
diff --git a/include/perfetto/public/te_macros.h b/include/perfetto/public/te_macros.h
index 9ab741d..e8f4a03 100644
--- a/include/perfetto/public/te_macros.h
+++ b/include/perfetto/public/te_macros.h
@@ -320,6 +320,38 @@
        PERFETTO_I_TE_COMPOUND_LITERAL_ARRAY(struct PerfettoTeHlProtoField*, \
                                             {__VA_ARGS__, PERFETTO_NULL})})
 
+// Specifies (manually) the track for this event
+// * `UUID` can be computed with e.g.:
+//   * PerfettoTeCounterTrackUuid()
+//   * PerfettoTeNamedTrackUuid()
+// * `...` the rest of the params should be PERFETTO_TE_PROTO_FIELD_* macros
+//   and should be fields of the perfetto.protos.TrackDescriptor protobuf
+//   message.
+#define PERFETTO_TE_PROTO_TRACK(UUID, ...)                                  \
+  PERFETTO_I_TE_EXTRA(                                                      \
+      PerfettoTeHlExtraProtoTrack,                                          \
+      {{PERFETTO_TE_HL_EXTRA_TYPE_PROTO_TRACK},                             \
+       UUID,                                                                \
+       PERFETTO_I_TE_COMPOUND_LITERAL_ARRAY(struct PerfettoTeHlProtoField*, \
+                                            {__VA_ARGS__, PERFETTO_NULL})})
+
+// Specifies that the current track for this event is a counter track named
+// `const char *NAME`, child of a track whose uuid is `PARENT_UUID`. `NAME`
+// and `PARENT_UUID` uniquely identify a track. Common values for `PARENT_UUID`
+// include PerfettoTeProcessTrackUuid(), PerfettoTeThreadTrackUuid() or
+// PerfettoTeGlobalTrackUuid().
+#define PERFETTO_TE_COUNTER_TRACK(NAME, PARENT_UUID)                           \
+  PERFETTO_TE_PROTO_TRACK(                                                     \
+      PerfettoTeCounterTrackUuid(NAME, PARENT_UUID),                           \
+      PERFETTO_TE_PROTO_FIELD_VARINT(                                          \
+          perfetto_protos_TrackDescriptor_parent_uuid_field_number,            \
+          PARENT_UUID),                                                        \
+      PERFETTO_TE_PROTO_FIELD_CSTR(                                            \
+          perfetto_protos_TrackDescriptor_name_field_number, NAME),            \
+      PERFETTO_TE_PROTO_FIELD_BYTES(                                           \
+          perfetto_protos_TrackDescriptor_counter_field_number, PERFETTO_NULL, \
+          0))
+
 // ----------------------------------
 // The main PERFETTO_TE tracing macro
 // ----------------------------------
diff --git a/include/perfetto/trace_processor/basic_types.h b/include/perfetto/trace_processor/basic_types.h
index ace17de..716e73a 100644
--- a/include/perfetto/trace_processor/basic_types.h
+++ b/include/perfetto/trace_processor/basic_types.h
@@ -17,9 +17,9 @@
 #ifndef INCLUDE_PERFETTO_TRACE_PROCESSOR_BASIC_TYPES_H_
 #define INCLUDE_PERFETTO_TRACE_PROCESSOR_BASIC_TYPES_H_
 
-#include <assert.h>
-#include <math.h>
-#include <stdarg.h>
+#include <cassert>
+#include <cstdarg>
+#include <cstddef>
 #include <cstdint>
 
 #include <string>
@@ -30,16 +30,41 @@
 #include "perfetto/base/export.h"
 #include "perfetto/base/logging.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 // All metrics protos are in this directory. When loading metric extensions, the
 // protos are mounted onto a virtual path inside this directory.
 constexpr char kMetricProtoRoot[] = "protos/perfetto/metrics/";
 
+// Enum which encodes how trace processor should parse the ingested data.
+enum class ParsingMode {
+  // This option causes trace processor to tokenize the raw trace bytes, sort
+  // the events into timestamp order and parse the events into tables.
+  //
+  // This is the default mode.
+  kDefault = 0,
+
+  // This option causes trace processor to skip the sorting and parsing
+  // steps of ingesting a trace, only retaining any information which could be
+  // gathered during tokenization of the trace files.
+  //
+  // Note the exact information available with this option is left intentionally
+  // undefined as it relies heavily on implementation details of trace
+  // processor. It is mainly intended for use by the Perfetto UI which
+  // integrates very closely with trace processor. General users should use
+  // `kDefault` unless they know what they are doing.
+  kTokenizeOnly = 1,
+
+  // This option causes trace processor to skip the parsing step of ingesting
+  // a trace.
+  //
+  // Note this option does not offer any visible benefits over `kTokenizeOnly`
+  // but has the downside of being slower. It mainly exists for use by
+  // developers debugging performance of trace processor.
+  kTokenizeAndSort = 2,
+};
+
 // Enum which encodes how trace processor should try to sort the ingested data.
-// Note that these options are only applicable to proto traces; other trace
-// types (e.g. JSON, Fuchsia) use full sorts.
 enum class SortingMode {
   // This option allows trace processor to use built-in heuristics about how to
   // sort the data. Generally, this option is correct for most embedders as
@@ -51,26 +76,11 @@
   // This is the default mode.
   kDefaultHeuristics = 0,
 
-  // This option forces trace processor to wait for all trace packets to be
-  // passed to it before doing a full sort of all the packets. This causes any
+  // This option forces trace processor to wait for all events to be passed to
+  // it before doing a full sort of all the events. This causes any
   // heuristics trace processor would normally use to ingest partially sorted
   // data to be skipped.
   kForceFullSort = 1,
-
-  // This option is deprecated in v18; trace processor will ignore it and
-  // use |kDefaultHeuristics|.
-  //
-  // Rationale for deprecation:
-  // The new windowed sorting logic in trace processor uses a combination of
-  // flush and buffer-read lifecycle events inside the trace instead of
-  // using time-periods from the config.
-  //
-  // Recommended migration:
-  // Users of this option should switch to using |kDefaultHeuristics| which
-  // will act very similarly to the pre-v20 behaviour of this option.
-  //
-  // This option is scheduled to be removed in v21.
-  kForceFlushPeriodWindowedSort = 2
 };
 
 // Enum which encodes which event (if any) should be used to drop ftrace data
@@ -128,8 +138,13 @@
 
 // Struct for configuring a TraceProcessor instance (see trace_processor.h).
 struct PERFETTO_EXPORT_COMPONENT Config {
-  // Indicates the sortinng mode that trace processor should use on the passed
-  // trace packets. See the enum documentation for more details.
+  // Indicates the parsing mode trace processor should use to extract
+  // information from the raw trace bytes. See the enum documentation for more
+  // details.
+  ParsingMode parsing_mode = ParsingMode::kDefault;
+
+  // Indicates the sortinng mode that trace processor should use on the
+  // passed trace packets. See the enum documentation for more details.
   SortingMode sorting_mode = SortingMode::kDefaultHeuristics;
 
   // When set to false, this option makes the trace processor not include ftrace
@@ -262,28 +277,38 @@
   Type type = kNull;
 };
 
-// Data used to register a new SQL module.
-struct SqlModule {
-  // Must be unique among modules, or can be used to override existing module if
-  // |allow_module_override| is set.
+// Data used to register a new SQL package.
+struct SqlPackage {
+  // Must be unique among package, or can be used to override existing package
+  // if |allow_override| is set.
   std::string name;
 
-  // Pairs of strings used for |IMPORT| with the contents of SQL files being
-  // run. Strings should only contain alphanumeric characters and '.', where
-  // string before the first dot has to be module name.
+  // Pairs of strings mapping from the name of the module used by `INCLUDE
+  // PERFETTO MODULE` statements to the contents of SQL files being executed.
+  // Module names should only contain alphanumeric characters and '.', where
+  // string before the first dot must be the package name.
   //
-  // It is encouraged that import key should be the path to the SQL file being
+  // It is encouraged that include key should be the path to the SQL file being
   // run, with slashes replaced by dots and without the SQL extension. For
-  // example, 'android/camera/jank.sql' would be imported by
-  // 'android.camera.jank'.
-  std::vector<std::pair<std::string, std::string>> files;
+  // example, 'android/camera/jank.sql' would be included by
+  // 'android.camera.jank'. This conforms to user expectations of how modules
+  // behave in other languages (e.g. Java, Python etc).
+  std::vector<std::pair<std::string, std::string>> modules;
 
-  // If true, SqlModule will override registered module with the same name. Can
-  // only be set if enable_dev_features is true, otherwise will throw an error.
+  // If true, will allow overriding a package which already exists with `name.
+  // Can only be set if enable_dev_features (in the TraceProcessorConfig object
+  // when creating TraceProcessor) is true. Otherwise, this option will throw an
+  // error.
+  bool allow_override = false;
+};
+
+// Deprecated. Please use `RegisterSqlPackage` and `SqlPackage` instead.
+struct SqlModule {
+  std::string name;
+  std::vector<std::pair<std::string, std::string>> files;
   bool allow_module_override = false;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // INCLUDE_PERFETTO_TRACE_PROCESSOR_BASIC_TYPES_H_
diff --git a/include/perfetto/trace_processor/trace_processor.h b/include/perfetto/trace_processor/trace_processor.h
index e46d1a7..7f70653 100644
--- a/include/perfetto/trace_processor/trace_processor.h
+++ b/include/perfetto/trace_processor/trace_processor.h
@@ -55,15 +55,15 @@
   // the returned iterator.
   virtual Iterator ExecuteQuery(const std::string& sql) = 0;
 
-  // Registers SQL files with the associated path under the module named
-  // |sql_module.name|. These modules can be run by using the |IMPORT| SQL
-  // function.
+  // Registers SQL files with the associated path under the package named
+  // |sql_package.name|.
   //
-  // For example, if you registered a module called "camera" with a file path
-  // "camera/cpu/metrics.sql" you can import it (run the file) using "SELECT
-  // IMPORT('camera.cpu.metrics');". The first word of the string has to be a
-  // module name and there can be only one module registered with a given name.
-  virtual base::Status RegisterSqlModule(SqlModule sql_module) = 0;
+  // For example, if you registered a package called "camera" with a file path
+  // "camera/cpu/metrics.sql" you can include it (run the file) using "INCLUDE
+  // PERFETTO MODULE camera.cpu.metrics". The first word of the string has to be
+  // a package name and there can be only one package registered with a given
+  // name.
+  virtual base::Status RegisterSqlPackage(SqlPackage) = 0;
 
   // Registers a metric at the given path which will run the specified SQL.
   virtual base::Status RegisterMetric(const std::string& path,
@@ -138,6 +138,11 @@
   // 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;
 };
 
 }  // namespace trace_processor
diff --git a/include/perfetto/tracing/core/BUILD.gn b/include/perfetto/tracing/core/BUILD.gn
index 63c77ca..9ea0e2c 100644
--- a/include/perfetto/tracing/core/BUILD.gn
+++ b/include/perfetto/tracing/core/BUILD.gn
@@ -20,7 +20,6 @@
   ]
   sources = [
     "chrome_config.h",
-    "clock_snapshots.h",
     "data_source_config.h",
     "data_source_descriptor.h",
     "flush_flags.h",
diff --git a/include/perfetto/tracing/core/clock_snapshots.h b/include/perfetto/tracing/core/clock_snapshots.h
deleted file mode 100644
index 49d4c67..0000000
--- a/include/perfetto/tracing/core/clock_snapshots.h
+++ /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.
- */
-
-#ifndef INCLUDE_PERFETTO_TRACING_CORE_CLOCK_SNAPSHOTS_H_
-#define INCLUDE_PERFETTO_TRACING_CORE_CLOCK_SNAPSHOTS_H_
-
-#include <cstdint>
-#include <vector>
-
-namespace perfetto {
-struct ClockReading {
-  ClockReading(uint32_t _clock_id, uint64_t _timestamp)
-      : clock_id(_clock_id), timestamp(_timestamp) {}
-  ClockReading() = default;
-
-  // Identifier of the clock domain (of type protos::pbzero::BuiltinClock).
-  uint32_t clock_id = 0;
-  // Clock reading as uint64_t.
-  uint64_t timestamp = 0;
-};
-
-using ClockSnapshotVector = std::vector<ClockReading>;
-
-// Takes snapshots of clock readings of all supported built-in clocks.
-ClockSnapshotVector CaptureClockSnapshots();
-
-}  // namespace perfetto
-
-#endif  // INCLUDE_PERFETTO_TRACING_CORE_CLOCK_SNAPSHOTS_H_
diff --git a/include/perfetto/tracing/internal/data_source_internal.h b/include/perfetto/tracing/internal/data_source_internal.h
index a62e909..85bf234 100644
--- a/include/perfetto/tracing/internal/data_source_internal.h
+++ b/include/perfetto/tracing/internal/data_source_internal.h
@@ -152,11 +152,16 @@
   // this data source.
   std::atomic<uint32_t> incremental_state_generation{};
 
+  // The caller must be sure that `n` was a valid instance at some point (either
+  // through a previous read of `valid_instances` or because the instance lock
+  // is held).
+  DataSourceState* GetUnsafe(size_t n) {
+    return reinterpret_cast<DataSourceState*>(&instances[n]);
+  }
+
   // Can be used with a cached |valid_instances| bitmap.
   DataSourceState* TryGetCached(uint32_t cached_bitmap, size_t n) {
-    return cached_bitmap & (1 << n)
-               ? reinterpret_cast<DataSourceState*>(&instances[n])
-               : nullptr;
+    return cached_bitmap & (1 << n) ? GetUnsafe(n) : nullptr;
   }
 
   DataSourceState* TryGet(size_t n) {
diff --git a/include/perfetto/tracing/internal/track_event_data_source.h b/include/perfetto/tracing/internal/track_event_data_source.h
index 70b0dee..204f798 100644
--- a/include/perfetto/tracing/internal/track_event_data_source.h
+++ b/include/perfetto/tracing/internal/track_event_data_source.h
@@ -17,8 +17,8 @@
 #ifndef INCLUDE_PERFETTO_TRACING_INTERNAL_TRACK_EVENT_DATA_SOURCE_H_
 #define INCLUDE_PERFETTO_TRACING_INTERNAL_TRACK_EVENT_DATA_SOURCE_H_
 
-#include "perfetto/base/compiler.h"
 #include "perfetto/base/template_util.h"
+#include "perfetto/base/thread_annotations.h"
 #include "perfetto/protozero/message_handle.h"
 #include "perfetto/tracing/core/data_source_config.h"
 #include "perfetto/tracing/data_source.h"
diff --git a/include/perfetto/tracing/internal/track_event_macros.h b/include/perfetto/tracing/internal/track_event_macros.h
index 68da366..01db0b1 100644
--- a/include/perfetto/tracing/internal/track_event_macros.h
+++ b/include/perfetto/tracing/internal/track_event_macros.h
@@ -21,7 +21,7 @@
 // implementation. Perfetto API users typically don't need to use anything here
 // directly.
 
-#include "perfetto/base/compiler.h"
+#include "perfetto/base/thread_annotations.h"
 #include "perfetto/tracing/internal/track_event_data_source.h"
 #include "perfetto/tracing/string_helpers.h"
 #include "perfetto/tracing/track_event_category_registry.h"
@@ -144,12 +144,6 @@
     }                                                                          \
   } while (false)
 
-// This internal macro is unused from the repo now, but some improper usage
-// remain outside of the repo.
-// TODO(b/294800182): Remove this.
-#define PERFETTO_INTERNAL_TRACK_EVENT(...) \
-  PERFETTO_INTERNAL_TRACK_EVENT_WITH_METHOD(TraceForCategory, ##__VA_ARGS__)
-
 // C++17 doesn't like a move constructor being defined for the EventFinalizer
 // class but C++11 and MSVC doesn't compile without it being defined so support
 // both.
diff --git a/include/perfetto/tracing/tracing.h b/include/perfetto/tracing/tracing.h
index 6f28e8d..03b9fec 100644
--- a/include/perfetto/tracing/tracing.h
+++ b/include/perfetto/tracing/tracing.h
@@ -344,6 +344,30 @@
   // started.
   virtual void StartBlocking() = 0;
 
+  // Struct passed as argument to the callback passed to CloneTrace().
+  struct CloneTraceCallbackArgs {
+    bool success;
+    std::string error;
+    // UUID of the cloned session.
+    int64_t uuid_msb;
+    int64_t uuid_lsb;
+  };
+
+  // Struct passed as argument to CloneTrace().
+  struct CloneTraceArgs {
+    // The unique_session_name of the session that should be cloned.
+    std::string unique_session_name;
+  };
+
+  // Clones an existing initialized tracing session from the same `BackendType`
+  // as this tracing session, and attaches to it. The session is cloned in
+  // read-only mode and can only be used to read a snapshot of an existing
+  // tracing session. For each session, only one CloneTrace call can be pending
+  // at the same time; subsequent calls after the callback is executed are
+  // supported.
+  using CloneTraceCallback = std::function<void(CloneTraceCallbackArgs)>;
+  virtual void CloneTrace(CloneTraceArgs args, CloneTraceCallback);
+
   // This callback will be invoked when all data sources have acknowledged that
   // tracing has started. This callback will be invoked on an internal perfetto
   // thread.
diff --git a/include/perfetto/tracing/track.h b/include/perfetto/tracing/track.h
index 831c290..ffd92c1 100644
--- a/include/perfetto/tracing/track.h
+++ b/include/perfetto/tracing/track.h
@@ -26,11 +26,15 @@
 #include "perfetto/tracing/internal/tracing_muxer.h"
 #include "perfetto/tracing/platform.h"
 #include "perfetto/tracing/string_helpers.h"
-#include "protos/perfetto/trace/trace_packet.pbzero.h"
-#include "protos/perfetto/trace/track_event/counter_descriptor.gen.h"
-#include "protos/perfetto/trace/track_event/counter_descriptor.pbzero.h"
-#include "protos/perfetto/trace/track_event/track_descriptor.gen.h"
-#include "protos/perfetto/trace/track_event/track_descriptor.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"  // IWYU pragma: export
+#include "protos/perfetto/trace/track_event/counter_descriptor.gen.h"  // IWYU pragma: export
+#include "protos/perfetto/trace/track_event/counter_descriptor.pbzero.h"  // IWYU pragma: export
+#include "protos/perfetto/trace/track_event/process_descriptor.gen.h"  // IWYU pragma: export
+#include "protos/perfetto/trace/track_event/process_descriptor.pbzero.h"  // IWYU pragma: export
+#include "protos/perfetto/trace/track_event/thread_descriptor.gen.h"  // IWYU pragma: export
+#include "protos/perfetto/trace/track_event/thread_descriptor.pbzero.h"  // IWYU pragma: export
+#include "protos/perfetto/trace/track_event/track_descriptor.gen.h"  // IWYU pragma: export
+#include "protos/perfetto/trace/track_event/track_descriptor.pbzero.h"  // IWYU pragma: export
 
 #include <stdint.h>
 #include <map>
@@ -188,6 +192,52 @@
             disallow_merging_with_system_tracks_) {}
 };
 
+// A track that's identified by an explcit name, id and its parent.
+class PERFETTO_EXPORT_COMPONENT NamedTrack : public Track {
+  // A random value mixed into named track uuids to avoid collisions with
+  // other types of tracks.
+  static constexpr uint64_t kNamedTrackMagic = 0xCD571EC5EAD37024ul;
+
+ public:
+  // `name` is hashed to get a uuid identifying the track. Optionally specify
+  // `id` to differentiate between multiple tracks with the same `name` and
+  // `parent`.
+  NamedTrack(DynamicString name,
+             uint64_t id = 0,
+             Track parent = MakeProcessTrack())
+      : Track(id ^ internal::Fnv1a(name.value, name.length) ^ kNamedTrackMagic,
+              parent),
+        static_name_(nullptr),
+        dynamic_name_(name) {}
+
+  constexpr NamedTrack(StaticString name,
+                       uint64_t id = 0,
+                       Track parent = MakeProcessTrack())
+      : Track(id ^ internal::Fnv1a(name.value) ^ kNamedTrackMagic, parent),
+        static_name_(name) {}
+
+  // Construct a track using `name` and `id` as identifier within thread-scope.
+  // Shorthand for `Track::NamedTrack("name", id, ThreadTrack::Current())`
+  // Usage: TRACE_EVENT_BEGIN("...", "...",
+  // perfetto::NamedTrack::ThreadScoped("rendering"))
+  template <class TrackEventName>
+  static Track ThreadScoped(TrackEventName name,
+                            uint64_t id = 0,
+                            Track parent = Track()) {
+    if (parent.uuid == 0)
+      return NamedTrack(std::forward<TrackEventName>(name), id,
+                        ThreadTrack::Current());
+    return NamedTrack(std::forward<TrackEventName>(name), id, parent);
+  }
+
+  void Serialize(protos::pbzero::TrackDescriptor*) const;
+  protos::gen::TrackDescriptor Serialize() const;
+
+ private:
+  StaticString static_name_;
+  DynamicString dynamic_name_;
+};
+
 // A track for recording counter values with the TRACE_COUNTER macro. Counter
 // tracks can optionally be given units and other metadata. See
 // /protos/perfetto/trace/track_event/counter_descriptor.proto for details.
diff --git a/infra/bigtrace/clickhouse/Dockerfile b/infra/bigtrace/clickhouse/Dockerfile
new file mode 100644
index 0000000..0b70ccb
--- /dev/null
+++ b/infra/bigtrace/clickhouse/Dockerfile
@@ -0,0 +1,33 @@
+# 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.
+
+# Install Python and necessary libraries for gRPC on top of Clickhouse image
+
+FROM clickhouse/clickhouse-server
+
+RUN apt update && apt install python3 pip curl git -y
+RUN pip install grpcio grpcio-tools protobuf perfetto==0.7.0 pandas numpy
+
+WORKDIR /tmp
+RUN git clone --depth 1 https://android.googlesource.com/platform/external/perfetto/
+WORKDIR /tmp/perfetto
+RUN tools/install-build-deps --grpc
+RUN tools/gn gen out/dist '--args=is_clang=true enable_perfetto_grpc=true'
+RUN tools/ninja -C out/dist trace_processor_shell
+RUN tools/gen_clickhouse_bigtrace_protos.py
+
+RUN mkdir /var/lib/perfetto-clickhouse/
+RUN cp /tmp/perfetto/out/dist/trace_processor_shell /var/lib/perfetto-clickhouse/
+RUN cp -r /tmp/perfetto/python/perfetto/bigtrace_clickhouse/. /var/lib/perfetto-clickhouse/
+RUN cp -r /tmp/perfetto/python/perfetto/bigtrace_clickhouse/protos/ /var/lib/perfetto-clickhouse/
\ No newline at end of file
diff --git a/infra/bigtrace/clickhouse/clickhouse-config.yaml b/infra/bigtrace/clickhouse/clickhouse-config.yaml
new file mode 100644
index 0000000..b39c359
--- /dev/null
+++ b/infra/bigtrace/clickhouse/clickhouse-config.yaml
@@ -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.
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: clickhouse-configmap
+data:
+  config.xml: |
+    <clickhouse>
+      <listen_host>0.0.0.0</listen_host>
+      <path>/data/clickhouse</path>
+      <user_directories>
+        <local_directory>
+          <path>/data/</path>
+        </local_directory>
+      </user_directories>
+      <user_scripts_path>/var/lib/perfetto-clickhouse</user_scripts_path>
+    </clickhouse>
\ No newline at end of file
diff --git a/infra/bigtrace/clickhouse/clickhouse-deployment.yaml b/infra/bigtrace/clickhouse/clickhouse-deployment.yaml
new file mode 100644
index 0000000..40ee247
--- /dev/null
+++ b/infra/bigtrace/clickhouse/clickhouse-deployment.yaml
@@ -0,0 +1,57 @@
+# 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.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: clickhouse
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: clickhouse
+  template:
+    metadata:
+      labels:
+        app: clickhouse
+    spec:
+      volumes:
+        - name: clickhouse-configmap
+          configMap:
+            name: clickhouse-configmap
+        - name: clickhouse-storage
+          persistentVolumeClaim:
+            claimName: clickhouse
+      containers:
+      - name: clickhouse
+        image: # [ZONE]-docker.pkg.dev/[PROJECT_NAME]/[REPO_NAME]/clickhouse
+        env:
+        - name: BIGTRACE_ORCHESTRATOR_ADDRESS
+          value: # Address of Orchestrator service
+        ports:
+        - containerPort: 8123
+        - containerPort: 9000
+        resources:
+          limits:
+            cpu: 600m
+            memory: 4Gi
+          requests:
+            cpu: 300m
+            memory: 2Gi
+        volumeMounts:
+            - name: clickhouse-configmap
+              mountPath: /etc/clickhouse-server/config.d/default_config.xml
+              subPath: config.xml
+            - name: clickhouse-storage
+              mountPath: /data
\ No newline at end of file
diff --git a/infra/bigtrace/clickhouse/clickhouse-ilb.yaml b/infra/bigtrace/clickhouse/clickhouse-ilb.yaml
new file mode 100644
index 0000000..70ffe92
--- /dev/null
+++ b/infra/bigtrace/clickhouse/clickhouse-ilb.yaml
@@ -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.
+
+apiVersion: v1
+kind: Service
+metadata:
+  name: clickhouse-ilb
+  annotations:
+    networking.gke.io/load-balancer-type: "Internal"
+spec:
+  type: LoadBalancer
+  externalTrafficPolicy: Cluster
+  selector:
+    app: clickhouse
+  ports:
+  - name: tcp-port
+    protocol: TCP
+    port: 80
+    targetPort: 9000
\ No newline at end of file
diff --git a/infra/bigtrace/clickhouse/clickhouse-pv.yaml b/infra/bigtrace/clickhouse/clickhouse-pv.yaml
new file mode 100644
index 0000000..76bf132
--- /dev/null
+++ b/infra/bigtrace/clickhouse/clickhouse-pv.yaml
@@ -0,0 +1,29 @@
+# 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.
+
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+  name: clickhouse-pv
+  labels:
+    type: local
+spec:
+  capacity:
+    storage: 6Gi
+  accessModes:
+    - ReadWriteOnce
+  storageClassName: standard-rwo
+  hostPath:
+    path: /mnt/clickhouse
+  persistentVolumeReclaimPolicy: Retain
\ No newline at end of file
diff --git a/infra/bigtrace/clickhouse/clickhouse-pvc.yaml b/infra/bigtrace/clickhouse/clickhouse-pvc.yaml
new file mode 100644
index 0000000..bec54d9
--- /dev/null
+++ b/infra/bigtrace/clickhouse/clickhouse-pvc.yaml
@@ -0,0 +1,24 @@
+# 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.
+
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  name: clickhouse
+spec:
+  accessModes:
+    - ReadWriteOnce
+  resources:
+    requests:
+      storage: 64Gi
\ No newline at end of file
diff --git a/src/bigtrace/compose.yaml b/infra/bigtrace/docker/compose.yaml
similarity index 100%
rename from src/bigtrace/compose.yaml
rename to infra/bigtrace/docker/compose.yaml
diff --git a/src/bigtrace/orchestrator/Dockerfile b/infra/bigtrace/docker/orchestrator/Dockerfile
similarity index 100%
rename from src/bigtrace/orchestrator/Dockerfile
rename to infra/bigtrace/docker/orchestrator/Dockerfile
diff --git a/src/bigtrace/worker/Dockerfile b/infra/bigtrace/docker/worker/Dockerfile
similarity index 100%
rename from src/bigtrace/worker/Dockerfile
rename to infra/bigtrace/docker/worker/Dockerfile
diff --git a/infra/bigtrace/gke/orchestrator-deployment.yaml b/infra/bigtrace/gke/orchestrator-deployment.yaml
new file mode 100644
index 0000000..68d193e
--- /dev/null
+++ b/infra/bigtrace/gke/orchestrator-deployment.yaml
@@ -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.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: orchestrator
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: orchestrator
+  template:
+    metadata:
+      labels:
+        app: orchestrator
+    spec:
+      containers:
+      - image: # [ZONE]-docker.pkg.dev/[PROJECT_NAME]/[REPO_NAME]/bigtrace_orchestrator
+        command: ["/perfetto/out/dist/orchestrator_main"]
+        args: ["-l", "worker:5052", "-r", "dns:///", "-s", "0.0.0.0:5051", "-t", "100"]
+        imagePullPolicy: IfNotPresent
+        name: orchestrator
+        ports:
+        - containerPort: 5051
+          protocol: TCP
+        resources:
+          requests:
+            cpu: "7"
+          limits:
+            cpu: "7"
\ No newline at end of file
diff --git a/infra/bigtrace/gke/orchestrator-ilb.yaml b/infra/bigtrace/gke/orchestrator-ilb.yaml
new file mode 100644
index 0000000..77058df
--- /dev/null
+++ b/infra/bigtrace/gke/orchestrator-ilb.yaml
@@ -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.
+
+apiVersion: v1
+kind: Service
+metadata:
+  name: orchestrator-ilb
+  annotations:
+    networking.gke.io/load-balancer-type: "Internal"
+spec:
+  type: LoadBalancer
+  externalTrafficPolicy: Cluster
+  selector:
+    app: orchestrator
+  ports:
+  - name: tcp-port
+    protocol: TCP
+    port: 80
+    targetPort: 5051
\ No newline at end of file
diff --git a/infra/bigtrace/gke/worker-deployment.yaml b/infra/bigtrace/gke/worker-deployment.yaml
new file mode 100644
index 0000000..3806fb9
--- /dev/null
+++ b/infra/bigtrace/gke/worker-deployment.yaml
@@ -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.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: worker
+spec:
+  replicas: 5
+  selector:
+    matchLabels:
+      app: worker
+  template:
+    metadata:
+      labels:
+        app: worker
+    spec:
+      containers:
+      - name: worker
+        image: # [ZONE]-docker.pkg.dev/[PROJECT_NAME]/[REPO_NAME]/bigtrace_worker
+        imagePullPolicy: IfNotPresent
+        ports:
+        - containerPort: 5052
+        resources:
+          requests:
+            cpu: "7"
\ No newline at end of file
diff --git a/infra/bigtrace/gke/worker-service.yaml b/infra/bigtrace/gke/worker-service.yaml
new file mode 100644
index 0000000..e632203
--- /dev/null
+++ b/infra/bigtrace/gke/worker-service.yaml
@@ -0,0 +1,25 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+apiVersion: v1
+kind: Service
+metadata:
+  name: worker
+spec:
+  clusterIP: None
+  ports:
+  - port: 5052
+    targetPort: 5052
+  selector:
+    app: worker
\ No newline at end of file
diff --git a/src/bigtrace/orchestrator-deployment.yaml b/infra/bigtrace/minikube/orchestrator-deployment.yaml
similarity index 100%
rename from src/bigtrace/orchestrator-deployment.yaml
rename to infra/bigtrace/minikube/orchestrator-deployment.yaml
diff --git a/src/bigtrace/orchestrator-service.yaml b/infra/bigtrace/minikube/orchestrator-service.yaml
similarity index 100%
rename from src/bigtrace/orchestrator-service.yaml
rename to infra/bigtrace/minikube/orchestrator-service.yaml
diff --git a/src/bigtrace/worker-deployment.yaml b/infra/bigtrace/minikube/worker-deployment.yaml
similarity index 100%
rename from src/bigtrace/worker-deployment.yaml
rename to infra/bigtrace/minikube/worker-deployment.yaml
diff --git a/src/bigtrace/worker-service.yaml b/infra/bigtrace/minikube/worker-service.yaml
similarity index 100%
rename from src/bigtrace/worker-service.yaml
rename to infra/bigtrace/minikube/worker-service.yaml
diff --git a/infra/ci/config.py b/infra/ci/config.py
index a96ebfc..8994865 100755
--- a/infra/ci/config.py
+++ b/infra/ci/config.py
@@ -88,9 +88,8 @@
         'PERFETTO_TEST_SCRIPT': 'test/ci/linux_tests.sh',
         'PERFETTO_INSTALL_BUILD_DEPS_ARGS': '',
     },
-    'linux-clang-x86-asan_lsan': {
-        'PERFETTO_TEST_GN_ARGS': 'is_debug=false is_asan=true is_lsan=true '
-                                 'target_cpu="x86"',
+    'linux-clang-x86-release': {
+        'PERFETTO_TEST_GN_ARGS': 'is_debug=false target_cpu="x86"',
         'PERFETTO_TEST_SCRIPT': 'test/ci/linux_tests.sh',
         'PERFETTO_INSTALL_BUILD_DEPS_ARGS': '',
     },
diff --git a/infra/ci/frontend/static/script.js b/infra/ci/frontend/static/script.js
index 017ca6e..6aaf84e 100644
--- a/infra/ci/frontend/static/script.js
+++ b/infra/ci/frontend/static/script.js
@@ -23,7 +23,7 @@
   { id: 'linux-clang-x86_64-tsan', label: 'tsan' },
   { id: 'linux-clang-x86_64-msan', label: 'msan' },
   { id: 'linux-clang-x86_64-asan_lsan', label: '{a,l}san' },
-  { id: 'linux-clang-x86-asan_lsan', label: 'x86 {a,l}san' },
+  { id: 'linux-clang-x86-release', label: 'x86 rel' },
   { id: 'linux-clang-x86_64-libfuzzer', label: 'fuzzer' },
   { id: 'linux-clang-x86_64-bazel', label: 'bazel' },
   { id: 'ui-clang-x86_64-release', label: 'rel' },
diff --git a/infra/perfetto.dev/BUILD.gn b/infra/perfetto.dev/BUILD.gn
index c15cb3b..b81fa3d 100644
--- a/infra/perfetto.dev/BUILD.gn
+++ b/infra/perfetto.dev/BUILD.gn
@@ -37,7 +37,6 @@
     ":gen_toc",
     ":gen_trace_config_proto",
     ":gen_trace_packet_proto",
-    ":gen_ui_plugin_api_html",
     ":node_assets",
     ":readme",
     ":style_scss",
@@ -193,31 +192,6 @@
   out_html = "docs/analysis/sql-stats"
 }
 
-ui_plugin_api_md = "${target_gen_dir}/ui-plugin-api.md"
-
-nodejs_script("gen_ui_plugin_api_md") {
-  script = "src/gen_ui_reference.js"
-  input = "../../ui/src/public/index.ts"
-  inputs = [ input ]
-  outputs = [ ui_plugin_api_md ]
-  args = [
-    "-i",
-    rebase_path(input, root_build_dir),
-    "-o",
-    rebase_path(ui_plugin_api_md, root_build_dir),
-  ]
-}
-
-md_to_html("gen_ui_plugin_api_html") {
-  markdown = ui_plugin_api_md
-  html_template = "src/template_markdown.html"
-  deps = [
-    ":gen_toc",
-    ":gen_ui_plugin_api_md",
-  ]
-  out_html = "docs/reference/ui-plugin-api"
-}
-
 # Generates a html reference for a proto
 # Args:
 # * proto: The path to a .proto file
@@ -384,6 +358,22 @@
   mdtargets += [ ":mdfile_${source}" ]
 }
 
+# Files which have been removed/renamed/moved and now have HTTP redirections in
+# src/assets/script.js
+removed_renamed_moved_files = [ "analysis/common-queries.md" ]
+
+foreach(source, removed_renamed_moved_files) {
+  filename = rebase_path(string_replace(source, ".md", ""),
+                         rebase_path("../../docs", root_build_dir))
+  md_to_html("mdfile_${source}") {
+    markdown = "src/empty.md"
+    html_template = "src/template_markdown.html"
+    out_html = "docs/${filename}"
+    deps = [ ":gen_toc" ]
+  }
+  mdtargets += [ ":mdfile_${source}" ]
+}
+
 group("all_mdfiles") {
   deps = mdtargets
 }
diff --git a/infra/perfetto.dev/cloudbuild.yaml b/infra/perfetto.dev/cloudbuild.yaml
index f8e0889..be05455 100644
--- a/infra/perfetto.dev/cloudbuild.yaml
+++ b/infra/perfetto.dev/cloudbuild.yaml
@@ -1,7 +1,10 @@
 # See go/perfetto-ui-autopush for docs on how this works end-to-end.
 # Reuse the same Docker container of the UI autopusher.
+# If this file is modified, the inline YAML must be copy-pasted into the
+# trigger configs inline YAML in Google Cloud Console > Cloud Build for the
+# "perfetto-site" project (zone: global)
 steps:
-- name: gcr.io/perfetto-ui/perfetto-ui-builder
+- name: europe-docker.pkg.dev/perfetto-ui/builder/perfetto-ui-builder
   args:
   - 'infra/perfetto.dev/cloud_build_entrypoint.sh'
 # Timeout: 15 min (last measured time in Feb 2021: 2 min)
diff --git a/infra/perfetto.dev/src/assets/script.js b/infra/perfetto.dev/src/assets/script.js
index aa368bc..44adbf6 100644
--- a/infra/perfetto.dev/src/assets/script.js
+++ b/infra/perfetto.dev/src/assets/script.js
@@ -21,26 +21,6 @@
 let tocEventHandlersInstalled = false;
 let resizeObserver = undefined;
 
-// Handles redirects from the old docs.perfetto.dev.
-const legacyRedirectMap = {
-  '#/contributing': '/docs/contributing/getting-started#community',
-  '#/build-instructions': '/docs/contributing/build-instructions',
-  '#/testing': '/docs/contributing/testing',
-  '#/app-instrumentation': '/docs/instrumentation/tracing-sdk',
-  '#/recording-traces': '/docs/instrumentation/tracing-sdk#recording',
-  '#/running': '/docs/quickstart/android-tracing',
-  '#/long-traces': '/docs/concepts/config#long-traces',
-  '#/detached-mode': '/docs/concepts/detached-mode',
-  '#/heapprofd': '/docs/data-sources/native-heap-profiler',
-  '#/java-hprof': '/docs/data-sources/java-heap-profiler',
-  '#/trace-processor': '/docs/analysis/trace-processor',
-  '#/analysis': '/docs/analysis/trace-processor#annotations',
-  '#/metrics': '/docs/analysis/metrics',
-  '#/traceconv': '/docs/quickstart/traceconv',
-  '#/clock-sync': '/docs/concepts/clock-sync',
-  '#/architecture': '/docs/concepts/service-model',
-};
-
 function doAfterLoadEvent(action) {
   if (onloadFired) {
     return action();
@@ -345,7 +325,40 @@
   document.documentElement.style.setProperty('--anim-enabled', '1')
 });
 
+// Handles redirects from the old docs.perfetto.dev.
+const legacyRedirectMap = {
+  '#/contributing': '/docs/contributing/getting-started#community',
+  '#/build-instructions': '/docs/contributing/build-instructions',
+  '#/testing': '/docs/contributing/testing',
+  '#/app-instrumentation': '/docs/instrumentation/tracing-sdk',
+  '#/recording-traces': '/docs/instrumentation/tracing-sdk#recording',
+  '#/running': '/docs/quickstart/android-tracing',
+  '#/long-traces': '/docs/concepts/config#long-traces',
+  '#/detached-mode': '/docs/concepts/detached-mode',
+  '#/heapprofd': '/docs/data-sources/native-heap-profiler',
+  '#/java-hprof': '/docs/data-sources/java-heap-profiler',
+  '#/trace-processor': '/docs/analysis/trace-processor',
+  '#/analysis': '/docs/analysis/trace-processor#annotations',
+  '#/metrics': '/docs/analysis/metrics',
+  '#/traceconv': '/docs/quickstart/traceconv',
+  '#/clock-sync': '/docs/concepts/clock-sync',
+  '#/architecture': '/docs/concepts/service-model',
+};
+
 const fragment = location.hash.split('?')[0].replace('.md', '');
 if (fragment in legacyRedirectMap) {
   location.replace(legacyRedirectMap[fragment]);
-}
\ No newline at end of file
+}
+
+// Pages which have been been removed/renamed/moved and need to be redirected
+// to their new home.
+const redirectMap = {
+  // stdlib docs is not a perfect replacement but is good enough until we write
+  // a proper, Android specific query codelab page.
+  // TODO(lalitm): switch to that page when it's ready.
+  '/docs/analysis/common-queries': '/docs/analysis/stdlib-docs',
+};
+
+if (location.pathname in redirectMap) {
+  location.replace(redirectMap[location.pathname]);
+}
diff --git a/infra/perfetto.dev/src/empty.md b/infra/perfetto.dev/src/empty.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/infra/perfetto.dev/src/empty.md
diff --git a/infra/perfetto.dev/src/gen_stdlib_docs_md.py b/infra/perfetto.dev/src/gen_stdlib_docs_md.py
index e1e60bd..13f51db 100644
--- a/infra/perfetto.dev/src/gen_stdlib_docs_md.py
+++ b/infra/perfetto.dev/src/gen_stdlib_docs_md.py
@@ -52,7 +52,7 @@
 FROM android_startups;
 ```
 
-Prelude is a special module is automatically imported. It contains key helper
+Prelude is a special module is automatically included. It contains key helper
 tables, views and functions which are universally useful.
 
 More information on importing modules is available in the
@@ -60,211 +60,189 @@
 for the `INCLUDE PERFETTO MODULE` statement.
 
 <!-- TODO(b/290185551): talk about experimental module and contributions. -->
-
-## Summary
 '''
 
 
-def _escape_in_table(desc: str):
+def _escape(desc: str) -> str:
   """Escapes special characters in a markdown table."""
   return desc.replace('|', '\\|')
 
 
-def _md_table(cols: List[str]):
+def _md_table_header(cols: List[str]) -> str:
   col_str = ' | '.join(cols) + '\n'
   lines = ['-' * len(col) for col in cols]
   underlines = ' | '.join(lines)
   return col_str + underlines
 
 
-def _write_summary(sql_type: str, table_cols: List[str],
-                   summary_objs: List[str]) -> str:
-  table_data = '\n'.join(s.strip() for s in summary_objs if s)
-  return f"""
-### {sql_type}
+def _md_rolldown(summary: str, content: str) -> str:
+  return f"""<details>
+  <summary style="cursor: pointer;">{summary}</summary>
 
-{_md_table(table_cols)}
-{table_data}
+  {content}
 
-"""
+  </details>
+  """
 
 
-class FileMd:
-  """Responsible for file level markdown generation."""
-
-  def __init__(self, module_name, file_dict):
-    self.import_key = file_dict['import_key']
-    import_key_name = self.import_key if module_name != 'prelude' else 'N/A'
-    self.objs, self.funs, self.view_funs, self.macros = [], [], [], []
-    summary_objs_list, summary_funs_list, summary_view_funs_list, summary_macros_list = [], [], [], []
-
-    # Add imports if in file.
-    for data in file_dict['imports']:
-      # Anchor
-      anchor = f'''obj/{module_name}/{data['name']}'''
-
-      # Add summary of imported view/table
-      summary_objs_list.append(f'''[{data['name']}](#{anchor})|'''
-                               f'''{import_key_name}|'''
-                               f'''{_escape_in_table(data['summary_desc'])}''')
-
-      self.objs.append(f'''\n\n<a name="{anchor}"></a>'''
-                       f'''**{data['name']}**, {data['type']}\n\n'''
-                       f'''{_escape_in_table(data['desc'])}\n''')
-
-      self.objs.append(_md_table(['Column', 'Type', 'Description']))
-      for name, info in data['cols'].items():
-        self.objs.append(
-            f'{name} | {info["type"]} | {_escape_in_table(info["desc"])}')
-
-      self.objs.append('\n\n')
-
-    # Add functions if in file
-    for data in file_dict['functions']:
-      # Anchor
-      anchor = f'''fun/{module_name}/{data['name']}'''
-
-      # Add summary of imported function
-      summary_funs_list.append(f'''[{data['name']}](#{anchor})|'''
-                               f'''{import_key_name}|'''
-                               f'''{data['return_type']}|'''
-                               f'''{_escape_in_table(data['summary_desc'])}''')
-      self.funs.append(
-          f'''\n\n<a name="{anchor}"></a>'''
-          f'''**{data['name']}**\n\n'''
-          f'''{data['desc']}\n\n'''
-          f'''Returns: {data['return_type']}, {data['return_desc']}\n\n''')
-      if data['args']:
-        self.funs.append(_md_table(['Argument', 'Type', 'Description']))
-        for name, arg_dict in data['args'].items():
-          self.funs.append(
-              f'''{name} | {arg_dict['type']} | {_escape_in_table(arg_dict['desc'])}'''
-          )
-
-        self.funs.append('\n\n')
-
-    # Add table functions if in file
-    for data in file_dict['table_functions']:
-      # Anchor
-      anchor = rf'''view_fun/{module_name}/{data['name']}'''
-      # Add summary of imported view function
-      summary_view_funs_list.append(
-          f'''[{data['name']}](#{anchor})|'''
-          f'''{import_key_name}|'''
-          f'''{_escape_in_table(data['summary_desc'])}''')
-
-      self.view_funs.append(f'''\n\n<a name="{anchor}"></a>'''
-                            f'''**{data['name']}**\n'''
-                            f'''{data['desc']}\n\n''')
-      if data['args']:
-        self.funs.append(_md_table(['Argument', 'Type', 'Description']))
-        for name, arg_dict in data['args'].items():
-          self.view_funs.append(
-              f'''{name} | {arg_dict['type']} | {_escape_in_table(arg_dict['desc'])}'''
-          )
-        self.view_funs.append('\n')
-        self.view_funs.append(_md_table(['Column', 'Type', 'Description']))
-      for name, column in data['cols'].items():
-        self.view_funs.append(f'{name} | {column["type"]} | {column["desc"]}')
-
-      self.view_funs.append('\n\n')
-
-    # Add macros if in file
-    for data in file_dict['macros']:
-      # Anchor
-      anchor = rf'''macro/{module_name}/{data['name']}'''
-      # Add summary of imported view function
-      summary_macros_list.append(
-          f'''[{data['name']}](#{anchor})|'''
-          f'''{import_key_name}|'''
-          f'''{_escape_in_table(data['summary_desc'])}''')
-
-      self.macros.append(
-          f'''\n\n<a name="{anchor}"></a>'''
-          f'''**{data['name']}**\n'''
-          f'''{data['desc']}\n\n'''
-          f'''Returns: {data['return_type']}, {data['return_desc']}\n\n''')
-      if data['args']:
-        self.macros.append(_md_table(['Argument', 'Type', 'Description']))
-        for name, arg_dict in data['args'].items():
-          self.macros.append(
-              f'''{name} | {arg_dict['type']} | {_escape_in_table(arg_dict['desc'])}'''
-          )
-        self.macros.append('\n')
-      self.macros.append('\n\n')
-
-    self.summary_objs = '\n'.join(summary_objs_list)
-    self.summary_funs = '\n'.join(summary_funs_list)
-    self.summary_view_funs = '\n'.join(summary_view_funs_list)
-    self.summary_macros = '\n'.join(summary_macros_list)
+def _bold(s: str) -> str:
+  return f"<strong>{s}</strong>"
 
 
 class ModuleMd:
   """Responsible for module level markdown generation."""
 
-  def __init__(self, module_name: str, module_files: List[Dict[str,
-                                                               Any]]) -> None:
-    self.module_name = module_name
-    self.files_md = sorted(
-        [FileMd(module_name, file_dict) for file_dict in module_files],
-        key=lambda x: x.import_key)
-    self.summary_objs = '\n'.join(
-        file.summary_objs for file in self.files_md if file.summary_objs)
-    self.summary_funs = '\n'.join(
-        file.summary_funs for file in self.files_md if file.summary_funs)
-    self.summary_view_funs = '\n'.join(file.summary_view_funs
-                                       for file in self.files_md
-                                       if file.summary_view_funs)
-    self.summary_macros = '\n'.join(
-        file.summary_macros for file in self.files_md if file.summary_macros)
+  def __init__(self, package_name: str, module_dict: Dict):
+    self.module_name = module_dict['module_name']
+    self.include_str = self.module_name if package_name != 'prelude' else 'N/A'
+    self.objs, self.funs, self.view_funs, self.macros = [], [], [], []
+
+    # Views/tables
+    for data in module_dict['data_objects']:
+      if not data['cols']:
+        continue
+
+      obj_summary = (
+          f'''{_bold(data['name'])}. {data['summary_desc']}\n'''
+      )
+      content = [f"{data['type']}"]
+      if (data['summary_desc'] != data['desc']):
+        content.append(data['desc'])
+
+      table = [_md_table_header(['Column', 'Type', 'Description'])]
+      for info in data['cols']:
+        name = info["name"]
+        table.append(
+            f'{name} | {info["type"]} | {_escape(info["desc"])}')
+      content.append('\n\n')
+      content.append('\n'.join(table))
+
+      self.objs.append(_md_rolldown(obj_summary, '\n'.join(content)))
+
+      self.objs.append('\n\n')
+
+    # Functions
+    for d in module_dict['functions']:
+      summary = f'''{_bold(d['name'])} -> {d['return_type']}. {d['summary_desc']}\n\n'''
+      content = []
+      if (d['summary_desc'] != d['desc']):
+        content.append(d['desc'])
+
+      content.append(
+          f"Returns {d['return_type']}: {d['return_desc']}\n\n")
+      if d['args']:
+        content.append(_md_table_header(['Argument', 'Type', 'Description']))
+        for arg_dict in d['args']:
+          content.append(
+              f'''{arg_dict['name']} | {arg_dict['type']} | {_escape(arg_dict['desc'])}'''
+          )
+
+      self.funs.append(_md_rolldown(summary, '\n'.join(content)))
+      self.funs.append('\n\n')
+
+    # Table functions
+    for data in module_dict['table_functions']:
+      obj_summary = f'''{_bold(data['name'])}. {data['summary_desc']}\n\n'''
+      content = []
+      if (data['summary_desc'] != data['desc']):
+        content.append(data['desc'])
+
+      if data['args']:
+        args_table = [_md_table_header(['Argument', 'Type', 'Description'])]
+        for arg_dict in data['args']:
+          args_table.append(
+              f'''{arg_dict['name']} | {arg_dict['type']} | {_escape(arg_dict['desc'])}'''
+          )
+        content.append('\n'.join(args_table))
+        content.append('\n\n')
+
+      content.append(_md_table_header(['Column', 'Type', 'Description']))
+      for column in data['cols']:
+        content.append(
+            f'{column["name"]} | {column["type"]} | {column["desc"]}')
+
+      self.view_funs.append(_md_rolldown(obj_summary, '\n'.join(content)))
+      self.view_funs.append('\n\n')
+
+    # Macros
+    for data in module_dict['macros']:
+      obj_summary = f'''{_bold(data['name'])}. {data['summary_desc']}\n\n'''
+      content = []
+      if (data['summary_desc'] != data['desc']):
+        content.append(data['desc'])
+
+      content.append(
+          f'''Returns: {data['return_type']}, {data['return_desc']}\n\n''')
+      if data['args']:
+        table = [_md_table_header(['Argument', 'Type', 'Description'])]
+        for arg_dict in data['args']:
+          table.append(
+              f'''{arg_dict['name']} | {arg_dict['type']} | {_escape(arg_dict['desc'])}'''
+          )
+        content.append('\n'.join(table))
+
+      self.macros.append(_md_rolldown(obj_summary, '\n'.join(content)))
+      self.macros.append('\n\n')
+
+
+class PackageMd:
+  """Responsible for package level markdown generation."""
+
+  def __init__(self, package_name: str, module_files: List[Dict[str,
+                                                                Any]]) -> None:
+    self.package_name = package_name
+    self.modules_md = sorted(
+        [ModuleMd(package_name, file_dict) for file_dict in module_files],
+        key=lambda x: x.module_name)
 
   def get_prelude_description(self) -> str:
-    if not self.module_name == 'prelude':
+    if not self.package_name == 'prelude':
       raise ValueError("Only callable on prelude module")
 
     lines = []
-    lines.append(f'## Module: {self.module_name}')
+    lines.append(f'## Package: {self.package_name}')
 
     # Prelude is a special module which is automatically imported and doesn't
     # have any include keys.
-    objs = '\n'.join(obj for file in self.files_md for obj in file.objs)
+    objs = '\n'.join(obj for module in self.modules_md for obj in module.objs)
     if objs:
       lines.append('#### Views/Tables')
       lines.append(objs)
 
-    funs = '\n'.join(fun for file in self.files_md for fun in file.funs)
+    funs = '\n'.join(fun for module in self.modules_md for fun in module.funs)
     if funs:
       lines.append('#### Functions')
       lines.append(funs)
 
     table_funs = '\n'.join(
-        view_fun for file in self.files_md for view_fun in file.view_funs)
+        view_fun for module in self.modules_md for view_fun in module.view_funs)
     if table_funs:
       lines.append('#### Table Functions')
       lines.append(table_funs)
 
-    macros = '\n'.join(macro for file in self.files_md for macro in file.macros)
+    macros = '\n'.join(
+        macro for module in self.modules_md for macro in module.macros)
     if macros:
       lines.append('#### Macros')
       lines.append(macros)
 
     return '\n'.join(lines)
 
-  def get_description(self) -> str:
-    if not self.files_md:
+  def get_md(self) -> str:
+    if not self.modules_md:
       return ''
 
-    if self.module_name == 'prelude':
+    if self.package_name == 'prelude':
       raise ValueError("Can't be called with prelude module")
 
     lines = []
-    lines.append(f'## Module: {self.module_name}')
+    lines.append(f'## Package: {self.package_name}')
 
-    for file in self.files_md:
+    for file in self.modules_md:
       if not any((file.objs, file.funs, file.view_funs, file.macros)):
         continue
 
-      lines.append(f'### {file.import_key}')
+      lines.append(f'### {file.module_name}')
       if file.objs:
         lines.append('#### Views/Tables')
         lines.append('\n'.join(file.objs))
@@ -280,6 +258,12 @@
 
     return '\n'.join(lines)
 
+  def is_empty(self) -> bool:
+    for file in self.modules_md:
+      if any((file.objs, file.funs, file.view_funs, file.macros)):
+        return False
+    return True
+
 
 def main():
   parser = argparse.ArgumentParser()
@@ -288,68 +272,26 @@
   args = parser.parse_args()
 
   with open(args.input) as f:
-    modules_json_dict = json.load(f)
+    stdlib_json = json.load(f)
 
   # Fetch the modules from json documentation.
-  modules_dict: Dict[str, ModuleMd] = {}
-  for module_name, module_files in modules_json_dict.items():
+  packages: Dict[str, PackageMd] = {}
+  for package in stdlib_json:
+    package_name = package["name"]
+    modules = package["modules"]
     # Remove 'common' when it has been removed from the code.
-    if module_name not in ['deprecated', 'common']:
-      modules_dict[module_name] = ModuleMd(module_name, module_files)
+    if package_name not in ['deprecated', 'common']:
+      package = PackageMd(package_name, modules)
+      if (not package.is_empty()):
+        packages[package_name] = package
 
-  prelude_module = modules_dict.pop('prelude')
+  prelude = packages.pop('prelude')
 
   with open(args.output, 'w') as f:
     f.write(INTRODUCTION)
-
-    summary_objs = [prelude_module.summary_objs
-                   ] if prelude_module.summary_objs else []
-    summary_objs += [
-        module.summary_objs
-        for module in modules_dict.values()
-        if (module.summary_objs)
-    ]
-
-    summary_funs = [prelude_module.summary_funs
-                   ] if prelude_module.summary_funs else []
-    summary_funs += [module.summary_funs for module in modules_dict.values()]
-    summary_view_funs = [prelude_module.summary_view_funs
-                        ] if prelude_module.summary_view_funs else []
-    summary_view_funs += [
-        module.summary_view_funs for module in modules_dict.values()
-    ]
-    summary_macros = [prelude_module.summary_macros
-                     ] if prelude_module.summary_macros else []
-    summary_macros += [
-        module.summary_macros for module in modules_dict.values()
-    ]
-
-    if summary_objs:
-      f.write(
-          _write_summary('Views/tables', ['Name', 'Import', 'Description'],
-                         summary_objs))
-
-    if summary_funs:
-      f.write(
-          _write_summary('Functions',
-                         ['Name', 'Import', 'Return type', 'Description'],
-                         summary_funs))
-
-    if summary_view_funs:
-      f.write(
-          _write_summary('Table functions', ['Name', 'Import', 'Description'],
-                         summary_view_funs))
-
-    if summary_macros:
-      f.write(
-          _write_summary('Macros', ['Name', 'Import', 'Description'],
-                         summary_macros))
-
-    f.write('\n\n')
-    f.write(prelude_module.get_prelude_description())
+    f.write(prelude.get_prelude_description())
     f.write('\n')
-    f.write('\n'.join(
-        module.get_description() for module in modules_dict.values()))
+    f.write('\n'.join(module.get_md() for module in packages.values()))
 
   return 0
 
diff --git a/infra/ui.perfetto.dev/cloudbuild_release.yaml b/infra/ui.perfetto.dev/cloudbuild_release.yaml
index 1dfe79a..8ad05f3 100644
--- a/infra/ui.perfetto.dev/cloudbuild_release.yaml
+++ b/infra/ui.perfetto.dev/cloudbuild_release.yaml
@@ -1,7 +1,8 @@
 # See go/perfetto-ui-autopush for docs on how this works end-to-end.
 # If this file is modified, the inline YAML must be copy-pasted
 # FROM: infra/ui.perfetto.dev/cloudbuild.yaml
-# TO: TWO trigger configs inline YAML in Google Cloud Console > Cloud Build.
+# TO: TWO trigger configs inline YAML in Google Cloud Console > Cloud Build
+# for the project "perfetto-ui" (zone: europe-west2).
 steps:
 - name: europe-docker.pkg.dev/perfetto-ui/builder/perfetto-ui-builder
   args:
diff --git a/persistent_cfg.pbtxt b/persistent_cfg.pbtxt
index 7d802c0..ef47c51 100644
--- a/persistent_cfg.pbtxt
+++ b/persistent_cfg.pbtxt
@@ -21,9 +21,9 @@
   clear_before_clone: true
 }
 
-# Buffer 2: for com.android.wm.shell.transition
+# Buffer 2: for other Winscope traces
 buffers {
-  size_kb: 32
+  size_kb: 2048
   fill_policy: RING_BUFFER
 }
 
@@ -57,3 +57,12 @@
   }
 }
 
+data_sources: {
+  config {
+    name: "android.protolog"
+    protolog_config {
+      tracing_mode: ENABLE_ALL
+    }
+    target_buffer: 2
+  }
+}
diff --git a/protos/perfetto/bigtrace/BUILD.gn b/protos/perfetto/bigtrace/BUILD.gn
index 1ba673b..c1b9fba 100644
--- a/protos/perfetto/bigtrace/BUILD.gn
+++ b/protos/perfetto/bigtrace/BUILD.gn
@@ -18,7 +18,6 @@
   proto_generators = [
     "lite",
     "zero",
-    "source_set",
   ]
   deps = [ "../trace_processor:@TYPE@" ]
   sources = [ "worker.proto" ]
@@ -28,7 +27,6 @@
   proto_generators = [
     "lite",
     "zero",
-    "source_set",
   ]
   deps = [ "../trace_processor:@TYPE@" ]
   sources = [ "orchestrator.proto" ]
diff --git a/protos/perfetto/common/BUILD.gn b/protos/perfetto/common/BUILD.gn
index 1a250b2..e92b631 100644
--- a/protos/perfetto/common/BUILD.gn
+++ b/protos/perfetto/common/BUILD.gn
@@ -41,6 +41,5 @@
     "lite",
     "zero",
     "cpp",
-    "source_set",
   ]
 }
diff --git a/protos/perfetto/common/builtin_clock.proto b/protos/perfetto/common/builtin_clock.proto
index 6b1ecc1..559d163 100644
--- a/protos/perfetto/common/builtin_clock.proto
+++ b/protos/perfetto/common/builtin_clock.proto
@@ -27,6 +27,7 @@
   BUILTIN_CLOCK_MONOTONIC_RAW = 5;
   BUILTIN_CLOCK_BOOTTIME = 6;
   BUILTIN_CLOCK_TSC = 9;
+  BUILTIN_CLOCK_PERF = 10;
   BUILTIN_CLOCK_MAX_ID = 63;
 
   reserved 7, 8;
diff --git a/protos/perfetto/common/perf_events.proto b/protos/perfetto/common/perf_events.proto
index 270bacb..c9f96ac 100644
--- a/protos/perfetto/common/perf_events.proto
+++ b/protos/perfetto/common/perf_events.proto
@@ -151,3 +151,16 @@
     PERF_CLOCK_BOOTTIME = 4;
   }
 }
+
+// Additional events associated with a leader.
+// Configuration is similar to Timebase event. Because data acquisition is
+// driven by the leader there is no option to configure the clock or the
+// frequency.
+message FollowerEvent {
+  oneof event {
+    PerfEvents.Counter counter = 1;
+    PerfEvents.Tracepoint tracepoint = 2;
+    PerfEvents.RawEvent raw_event = 3;
+  }
+  optional string name = 4;
+}
diff --git a/protos/perfetto/config/BUILD.gn b/protos/perfetto/config/BUILD.gn
index 22d052a..2013e3b 100644
--- a/protos/perfetto/config/BUILD.gn
+++ b/protos/perfetto/config/BUILD.gn
@@ -38,6 +38,7 @@
   sources = [
     "chrome/chrome_config.proto",
     "chrome/scenario_config.proto",
+    "chrome/system_metrics.proto",
     "chrome/v8_config.proto",
     "data_source_config.proto",
     "etw/etw_config.proto",
@@ -46,11 +47,7 @@
     "test_config.proto",
     "trace_config.proto",
   ]
-}
 
-perfetto_proto_library("descriptor") {
-  proto_generators = [ "descriptor" ]
   generate_descriptor = "config.descriptor"
-  deps = [ ":source_set" ]
-  sources = [ "trace_config.proto" ]
+  descriptor_root_source = "trace_config.proto"
 }
diff --git a/protos/perfetto/config/chrome/BUILD.gn b/protos/perfetto/config/chrome/BUILD.gn
index 4397cfc..f367848 100644
--- a/protos/perfetto/config/chrome/BUILD.gn
+++ b/protos/perfetto/config/chrome/BUILD.gn
@@ -15,8 +15,10 @@
 import("../../../../gn/perfetto.gni")
 import("../../../../gn/proto_library.gni")
 
-perfetto_proto_library("scenario_descriptor") {
-  proto_generators = [ "descriptor" ]
-  generate_descriptor = "scenario_config.descriptor"
+perfetto_proto_library("scenario_@TYPE@") {
+  proto_generators = []
+  deps = [ "..:@TYPE@" ]
   sources = [ "scenario_config.proto" ]
+  generate_descriptor = "scenario_config.descriptor"
+  descriptor_root_source = "scenario_config.proto"
 }
diff --git a/protos/perfetto/config/chrome/system_metrics.proto b/protos/perfetto/config/chrome/system_metrics.proto
new file mode 100644
index 0000000..daca421
--- /dev/null
+++ b/protos/perfetto/config/chrome/system_metrics.proto
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+message ChromiumSystemMetricsConfig {
+  // Samples counters every X ms.
+  optional uint32 sampling_interval_ms = 1;
+}
diff --git a/protos/perfetto/config/data_source_config.proto b/protos/perfetto/config/data_source_config.proto
index 5249100..75947d7 100644
--- a/protos/perfetto/config/data_source_config.proto
+++ b/protos/perfetto/config/data_source_config.proto
@@ -34,6 +34,7 @@
 import "protos/perfetto/config/chrome/chrome_config.proto";
 import "protos/perfetto/config/chrome/v8_config.proto";
 import "protos/perfetto/config/etw/etw_config.proto";
+import "protos/perfetto/config/chrome/system_metrics.proto";
 import "protos/perfetto/config/ftrace/ftrace_config.proto";
 import "protos/perfetto/config/gpu/gpu_counter_config.proto";
 import "protos/perfetto/config/gpu/vulkan_memory_config.proto";
@@ -51,7 +52,7 @@
 import "protos/perfetto/config/system_info/system_info.proto";
 
 // The configuration that is passed to each data source when starting tracing.
-// Next id: 131
+// Next id: 132
 message DataSourceConfig {
   enum SessionInitiator {
     SESSION_INITIATOR_UNSPECIFIED = 0;
@@ -206,6 +207,9 @@
   // Data source name: android.windowmanager
   optional WindowManagerConfig windowmanager_config = 130 [lazy = true];
 
+  // Data source name: org.chromium.system_metrics
+  optional ChromiumSystemMetricsConfig chromium_system_metrics = 131 [lazy = true];
+
   // This is a fallback mechanism to send a free-form text config to the
   // producer. In theory this should never be needed. All the code that
   // is part of the platform (i.e. traced service) is supposed to *not* truncate
diff --git a/protos/perfetto/config/ftrace/ftrace_config.proto b/protos/perfetto/config/ftrace/ftrace_config.proto
index eb9bffd..b720b21 100644
--- a/protos/perfetto/config/ftrace/ftrace_config.proto
+++ b/protos/perfetto/config/ftrace/ftrace_config.proto
@@ -18,10 +18,26 @@
 
 package perfetto.protos;
 
-// Next id: 29
+// Next id: 31
 message FtraceConfig {
   // Ftrace events to record, example: "sched/sched_switch".
   repeated string ftrace_events = 1;
+
+  message KprobeEvent {
+    enum KprobeType {
+      KPROBE_TYPE_UNKNOWN = 0;
+      KPROBE_TYPE_KPROBE = 1;
+      KPROBE_TYPE_KRETPROBE = 2;
+      KPROBE_TYPE_BOTH = 3;
+    }
+    // Kernel function name to attach to, for example "fuse_file_write_iter"
+    optional string probe = 1;
+    optional KprobeType type = 2;
+  }
+
+  // Ftrace events to record, specific for kprobes and kretprobes
+  repeated KprobeEvent kprobe_events = 30;
+
   // Android-specific event categories:
   repeated string atrace_categories = 2;
   repeated string atrace_apps = 3;
@@ -45,9 +61,9 @@
   // of 50 means that a read pass is triggered as soon as any per-cpu buffer is
   // half-full. Not guaranteed if there are multiple concurrent tracing
   // sessions.
-  // Currently does nothing on Linux kernels below v6.1.
-  // Introduced in: perfetto v43.
-  optional uint32 drain_buffer_percent = 26;
+  // Currently does nothing on Linux kernels below v6.9.
+  // Introduced in: perfetto v48.
+  optional uint32 drain_buffer_percent = 29;
 
   // Configuration for compact encoding of scheduler events. When enabled (and
   // recording the relevant ftrace events), specific high-volume events are
@@ -237,4 +253,7 @@
   // the recording in the kernel.
   // Introduced in: perfetto v43.
   optional bool buffer_size_lower_bound = 27;
+
+  // Previously drain_buffer_percent, perfetto v43-v47.
+  reserved 26;
 }
diff --git a/protos/perfetto/config/perfetto_config.proto b/protos/perfetto/config/perfetto_config.proto
index 702cb98..40259f2 100644
--- a/protos/perfetto/config/perfetto_config.proto
+++ b/protos/perfetto/config/perfetto_config.proto
@@ -340,6 +340,7 @@
   BUILTIN_CLOCK_MONOTONIC_RAW = 5;
   BUILTIN_CLOCK_BOOTTIME = 6;
   BUILTIN_CLOCK_TSC = 9;
+  BUILTIN_CLOCK_PERF = 10;
   BUILTIN_CLOCK_MAX_ID = 63;
 
   reserved 7, 8;
@@ -887,6 +888,15 @@
 
 // End of protos/perfetto/config/chrome/chrome_config.proto
 
+// Begin of protos/perfetto/config/chrome/system_metrics.proto
+
+message ChromiumSystemMetricsConfig {
+  // Samples counters every X ms.
+  optional uint32 sampling_interval_ms = 1;
+}
+
+// End of protos/perfetto/config/chrome/system_metrics.proto
+
 // Begin of protos/perfetto/config/chrome/v8_config.proto
 
 message V8Config {
@@ -926,10 +936,26 @@
 
 // Begin of protos/perfetto/config/ftrace/ftrace_config.proto
 
-// Next id: 29
+// Next id: 31
 message FtraceConfig {
   // Ftrace events to record, example: "sched/sched_switch".
   repeated string ftrace_events = 1;
+
+  message KprobeEvent {
+    enum KprobeType {
+      KPROBE_TYPE_UNKNOWN = 0;
+      KPROBE_TYPE_KPROBE = 1;
+      KPROBE_TYPE_KRETPROBE = 2;
+      KPROBE_TYPE_BOTH = 3;
+    }
+    // Kernel function name to attach to, for example "fuse_file_write_iter"
+    optional string probe = 1;
+    optional KprobeType type = 2;
+  }
+
+  // Ftrace events to record, specific for kprobes and kretprobes
+  repeated KprobeEvent kprobe_events = 30;
+
   // Android-specific event categories:
   repeated string atrace_categories = 2;
   repeated string atrace_apps = 3;
@@ -953,9 +979,9 @@
   // of 50 means that a read pass is triggered as soon as any per-cpu buffer is
   // half-full. Not guaranteed if there are multiple concurrent tracing
   // sessions.
-  // Currently does nothing on Linux kernels below v6.1.
-  // Introduced in: perfetto v43.
-  optional uint32 drain_buffer_percent = 26;
+  // Currently does nothing on Linux kernels below v6.9.
+  // Introduced in: perfetto v48.
+  optional uint32 drain_buffer_percent = 29;
 
   // Configuration for compact encoding of scheduler events. When enabled (and
   // recording the relevant ftrace events), specific high-volume events are
@@ -1145,6 +1171,9 @@
   // the recording in the kernel.
   // Introduced in: perfetto v43.
   optional bool buffer_size_lower_bound = 27;
+
+  // Previously drain_buffer_percent, perfetto v43-v47.
+  reserved 26;
 }
 
 // End of protos/perfetto/config/ftrace/ftrace_config.proto
@@ -1739,6 +1768,19 @@
   }
 }
 
+// Additional events associated with a leader.
+// Configuration is similar to Timebase event. Because data acquisition is
+// driven by the leader there is no option to configure the clock or the
+// frequency.
+message FollowerEvent {
+  oneof event {
+    PerfEvents.Counter counter = 1;
+    PerfEvents.Tracepoint tracepoint = 2;
+    PerfEvents.RawEvent raw_event = 3;
+  }
+  optional string name = 4;
+}
+
 // End of protos/perfetto/common/perf_events.proto
 
 // Begin of protos/perfetto/config/profiling/perf_event_config.proto
@@ -1759,12 +1801,15 @@
 //     }
 //   }
 //
-// Next id: 19
+// Next id: 20
 message PerfEventConfig {
   // What event to sample on, and how often.
   // Defined in common/perf_events.proto.
   optional PerfEvents.Timebase timebase = 15;
 
+  // Other events associated with the leader described in the timebase.
+  repeated FollowerEvent followers = 19;
+
   // If set, the profiler will sample userspace processes' callstacks at the
   // interval specified by the |timebase|.
   // If unset, the profiler will record only the event counts.
@@ -1937,6 +1982,8 @@
     UNWIND_SKIP = 1;
     // Use libunwindstack (default):
     UNWIND_DWARF = 2;
+    // Use userspace frame pointer unwinder:
+    UNWIND_FRAME_POINTER = 3;
   }
 }
 
@@ -2002,11 +2049,9 @@
   ATOM_LMK_KILL_OCCURRED = 51;
   ATOM_PICTURE_IN_PICTURE_STATE_CHANGED = 52;
   ATOM_WIFI_MULTICAST_LOCK_STATE_CHANGED = 53;
-  ATOM_LMK_STATE_CHANGED = 54;
   ATOM_APP_START_MEMORY_STATE_CAPTURED = 55;
   ATOM_SHUTDOWN_SEQUENCE_REPORTED = 56;
   ATOM_BOOT_SEQUENCE_REPORTED = 57;
-  ATOM_DAVEY_OCCURRED = 58;
   ATOM_OVERLAY_STATE_CHANGED = 59;
   ATOM_FOREGROUND_SERVICE_STATE_CHANGED = 60;
   ATOM_CALL_STATE_CHANGED = 61;
@@ -2323,7 +2368,6 @@
   ATOM_PRIVACY_TOGGLE_DIALOG_INTERACTION = 382;
   ATOM_APP_SEARCH_OPTIMIZE_STATS_REPORTED = 383;
   ATOM_NON_A11Y_TOOL_SERVICE_WARNING_REPORT = 384;
-  ATOM_APP_SEARCH_SET_SCHEMA_STATS_REPORTED = 385;
   ATOM_APP_COMPAT_STATE_CHANGED = 386;
   ATOM_SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED = 387;
   ATOM_SPLITSCREEN_UI_CHANGED = 388;
@@ -2372,8 +2416,6 @@
   ATOM_HOTWORD_DETECTION_SERVICE_RESTARTED = 432;
   ATOM_HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED = 433;
   ATOM_HOTWORD_DETECTOR_EVENTS = 434;
-  ATOM_AD_SERVICES_API_CALLED = 435;
-  ATOM_AD_SERVICES_MESUREMENT_REPORTS_UPLOADED = 436;
   ATOM_BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED = 437;
   ATOM_CONTACTS_INDEXER_UPDATE_STATS_REPORTED = 440;
   ATOM_APP_BACKGROUND_RESTRICTIONS_INFO = 441;
@@ -2417,25 +2459,14 @@
   ATOM_CB_MODULE_ERROR_REPORTED = 480;
   ATOM_CB_SERVICE_FEATURE_CHANGED = 481;
   ATOM_CB_RECEIVER_FEATURE_CHANGED = 482;
-  ATOM_JSSCRIPTENGINE_LATENCY_REPORTED = 483;
   ATOM_PRIVACY_SIGNAL_NOTIFICATION_INTERACTION = 484;
   ATOM_PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION = 485;
   ATOM_PRIVACY_SIGNALS_JOB_FAILURE = 486;
   ATOM_VIBRATION_REPORTED = 487;
   ATOM_UWB_RANGING_START = 489;
-  ATOM_MOBILE_DATA_DOWNLOAD_FILE_GROUP_STATUS_REPORTED = 490;
   ATOM_APP_COMPACTED_V2 = 491;
-  ATOM_AD_SERVICES_SETTINGS_USAGE_REPORTED = 493;
   ATOM_DISPLAY_BRIGHTNESS_CHANGED = 494;
   ATOM_ACTIVITY_ACTION_BLOCKED = 495;
-  ATOM_BACKGROUND_FETCH_PROCESS_REPORTED = 496;
-  ATOM_UPDATE_CUSTOM_AUDIENCE_PROCESS_REPORTED = 497;
-  ATOM_RUN_AD_BIDDING_PROCESS_REPORTED = 498;
-  ATOM_RUN_AD_SCORING_PROCESS_REPORTED = 499;
-  ATOM_RUN_AD_SELECTION_PROCESS_REPORTED = 500;
-  ATOM_RUN_AD_BIDDING_PER_CA_PROCESS_REPORTED = 501;
-  ATOM_MOBILE_DATA_DOWNLOAD_DOWNLOAD_RESULT_REPORTED = 502;
-  ATOM_MOBILE_DATA_DOWNLOAD_FILE_GROUP_STORAGE_STATS_REPORTED = 503;
   ATOM_NETWORK_DNS_SERVER_SUPPORT_REPORTED = 504;
   ATOM_VM_BOOTED = 505;
   ATOM_VM_EXITED = 506;
@@ -2444,7 +2475,6 @@
   ATOM_MEDIAMETRICS_SPATIALIZERDEVICEENABLED_REPORTED = 509;
   ATOM_MEDIAMETRICS_HEADTRACKERDEVICEENABLED_REPORTED = 510;
   ATOM_MEDIAMETRICS_HEADTRACKERDEVICESUPPORTED_REPORTED = 511;
-  ATOM_AD_SERVICES_MEASUREMENT_REGISTRATIONS = 512;
   ATOM_HEARING_AID_INFO_REPORTED = 513;
   ATOM_DEVICE_WIDE_JOB_CONSTRAINT_CHANGED = 514;
   ATOM_AMBIENT_MODE_CHANGED = 515;
@@ -2466,9 +2496,6 @@
   ATOM_BLUETOOTH_LOCAL_SUPPORTED_FEATURES_REPORTED = 532;
   ATOM_BLUETOOTH_GATT_APP_INFO = 533;
   ATOM_BRIGHTNESS_CONFIGURATION_UPDATED = 534;
-  ATOM_AD_SERVICES_GET_TOPICS_REPORTED = 535;
-  ATOM_AD_SERVICES_EPOCH_COMPUTATION_GET_TOP_TOPICS_REPORTED = 536;
-  ATOM_AD_SERVICES_EPOCH_COMPUTATION_CLASSIFIER_REPORTED = 537;
   ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_LAUNCHED = 538;
   ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_FINISHED = 539;
   ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_CONNECTION_REPORTED = 540;
@@ -2506,12 +2533,10 @@
   ATOM_MEDIAMETRICS_MIDI_DEVICE_CLOSE_REPORTED = 576;
   ATOM_BIOMETRIC_TOUCH_REPORTED = 577;
   ATOM_HOTWORD_AUDIO_EGRESS_EVENT_REPORTED = 578;
-  ATOM_APP_SEARCH_SCHEMA_MIGRATION_STATS_REPORTED = 579;
   ATOM_LOCATION_ENABLED_STATE_CHANGED = 580;
   ATOM_IME_REQUEST_FINISHED = 581;
   ATOM_USB_COMPLIANCE_WARNINGS_REPORTED = 582;
   ATOM_APP_SUPPORTED_LOCALES_CHANGED = 583;
-  ATOM_GRAMMATICAL_INFLECTION_CHANGED = 584;
   ATOM_MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED = 586;
   ATOM_BIOMETRIC_PROPERTIES_COLLECTED = 587;
   ATOM_KERNEL_WAKEUP_ATTRIBUTED = 588;
@@ -2524,7 +2549,11 @@
   ATOM_WS_NOTIFICATION_UPDATED = 596;
   ATOM_NETWORK_VALIDATION_FAILURE_STATS_DAILY_REPORTED = 601;
   ATOM_WS_COMPLICATION_TAPPED = 602;
-  ATOM_WS_WEAR_TIME_SESSION = 610;
+  ATOM_WS_NOTIFICATION_BLOCKING = 780;
+  ATOM_WS_NOTIFICATION_BRIDGEMODE_UPDATED = 822;
+  ATOM_WS_NOTIFICATION_DISMISSAL_ACTIONED = 823;
+  ATOM_WS_NOTIFICATION_ACTIONED = 824;
+  ATOM_WS_NOTIFICATION_LATENCY = 880;
   ATOM_WIFI_BYTES_TRANSFER = 10000;
   ATOM_WIFI_BYTES_TRANSFER_BY_FG_BG = 10001;
   ATOM_MOBILE_BYTES_TRANSFER = 10002;
@@ -2697,6 +2726,186 @@
   ATOM_NOTIFICATION_MEMORY_USE = 10174;
   ATOM_HDR_CAPABILITIES = 10175;
   ATOM_WS_FAVOURITE_WATCH_FACE_LIST_SNAPSHOT = 10176;
+  ATOM_WS_WEAR_TIME_SESSION = 610;
+  ATOM_WS_INCOMING_CALL_ACTION_REPORTED = 626;
+  ATOM_WS_CALL_DISCONNECTION_REPORTED = 627;
+  ATOM_WS_CALL_DURATION_REPORTED = 628;
+  ATOM_WS_CALL_USER_EXPERIENCE_LATENCY_REPORTED = 629;
+  ATOM_WS_CALL_INTERACTION_REPORTED = 630;
+  ATOM_WS_ON_BODY_STATE_CHANGED = 787;
+  ATOM_WS_WATCH_FACE_RESTRICTED_COMPLICATIONS_IMPACTED = 802;
+  ATOM_WS_WATCH_FACE_DEFAULT_RESTRICTED_COMPLICATIONS_REMOVED = 803;
+  ATOM_WS_COMPLICATIONS_IMPACTED_NOTIFICATION_EVENT_REPORTED = 804;
+  ATOM_WS_STANDALONE_MODE_SNAPSHOT = 10197;
+  ATOM_WS_FAVORITE_WATCH_FACE_SNAPSHOT = 10206;
+  ATOM_SETTINGS_SPA_REPORTED = 622;
+  ATOM_PDF_LOAD_REPORTED = 859;
+  ATOM_PDF_API_USAGE_REPORTED = 860;
+  ATOM_PDF_SEARCH_REPORTED = 861;
+  ATOM_HDMI_EARC_STATUS_REPORTED = 701;
+  ATOM_HDMI_SOUNDBAR_MODE_STATUS_REPORTED = 724;
+  ATOM_MEDIA_PROVIDER_DATABASE_ROLLBACK_REPORTED = 784;
+  ATOM_BACKUP_SETUP_STATUS_REPORTED = 785;
+  ATOM_PHOTOPICKER_SESSION_INFO_REPORTED = 886;
+  ATOM_PHOTOPICKER_API_INFO_REPORTED = 887;
+  ATOM_PHOTOPICKER_UI_EVENT_LOGGED = 888;
+  ATOM_PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED = 889;
+  ATOM_PHOTOPICKER_PREVIEW_INFO_LOGGED = 890;
+  ATOM_PHOTOPICKER_MENU_INTERACTION_LOGGED = 891;
+  ATOM_PHOTOPICKER_BANNER_INTERACTION_LOGGED = 892;
+  ATOM_PHOTOPICKER_MEDIA_LIBRARY_INFO_LOGGED = 893;
+  ATOM_PHOTOPICKER_PAGE_INFO_LOGGED = 894;
+  ATOM_PHOTOPICKER_MEDIA_GRID_SYNC_INFO_REPORTED = 895;
+  ATOM_PHOTOPICKER_ALBUM_SYNC_INFO_REPORTED = 896;
+  ATOM_PHOTOPICKER_SEARCH_INFO_REPORTED = 897;
+  ATOM_SEARCH_DATA_EXTRACTION_DETAILS_REPORTED = 898;
+  ATOM_EMBEDDED_PHOTOPICKER_INFO_REPORTED = 899;
+  ATOM_WEAR_POWER_MENU_OPENED = 731;
+  ATOM_WEAR_ASSISTANT_OPENED = 755;
+  ATOM_KERNEL_OOM_KILL_OCCURRED = 754;
+  ATOM_AUTOFILL_UI_EVENT_REPORTED = 603;
+  ATOM_AUTOFILL_FILL_REQUEST_REPORTED = 604;
+  ATOM_AUTOFILL_FILL_RESPONSE_REPORTED = 605;
+  ATOM_AUTOFILL_SAVE_EVENT_REPORTED = 606;
+  ATOM_AUTOFILL_SESSION_COMMITTED = 607;
+  ATOM_AUTOFILL_FIELD_CLASSIFICATION_EVENT_REPORTED = 659;
+  ATOM_TV_LOW_POWER_STANDBY_POLICY = 679;
+  ATOM_EXTERNAL_TV_INPUT_EVENT = 717;
+  ATOM_COMPONENT_STATE_CHANGED_REPORTED = 863;
+  ATOM_AI_WALLPAPERS_BUTTON_PRESSED = 706;
+  ATOM_AI_WALLPAPERS_TEMPLATE_SELECTED = 707;
+  ATOM_AI_WALLPAPERS_TERM_SELECTED = 708;
+  ATOM_AI_WALLPAPERS_WALLPAPER_SET = 709;
+  ATOM_AI_WALLPAPERS_SESSION_SUMMARY = 710;
+  ATOM_APF_SESSION_INFO_REPORTED = 777;
+  ATOM_IP_CLIENT_RA_INFO_REPORTED = 778;
+  ATOM_VPN_CONNECTION_STATE_CHANGED = 850;
+  ATOM_VPN_CONNECTION_REPORTED = 851;
+  ATOM_NETWORK_STATS_RECORDER_FILE_OPERATED = 783;
+  ATOM_DAILY_KEEPALIVE_INFO_REPORTED = 650;
+  ATOM_NETWORK_REQUEST_STATE_CHANGED = 779;
+  ATOM_TETHERING_ACTIVE_SESSIONS_REPORTED = 925;
+  ATOM_ART_DATUM_REPORTED = 332;
+  ATOM_ART_DEVICE_DATUM_REPORTED = 550;
+  ATOM_ART_DATUM_DELTA_REPORTED = 565;
+  ATOM_ART_DEX2OAT_REPORTED = 929;
+  ATOM_ART_DEVICE_STATUS = 10205;
+  ATOM_ODREFRESH_REPORTED = 366;
+  ATOM_ODSIGN_REPORTED = 548;
+  ATOM_BACKGROUND_DEXOPT_JOB_ENDED = 467;
+  ATOM_PREREBOOT_DEXOPT_JOB_ENDED = 883;
+  ATOM_PERMISSION_RATIONALE_DIALOG_VIEWED = 645;
+  ATOM_PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED = 646;
+  ATOM_APP_DATA_SHARING_UPDATES_NOTIFICATION_INTERACTION = 647;
+  ATOM_APP_DATA_SHARING_UPDATES_FRAGMENT_VIEWED = 648;
+  ATOM_APP_DATA_SHARING_UPDATES_FRAGMENT_ACTION_REPORTED = 649;
+  ATOM_ENHANCED_CONFIRMATION_DIALOG_RESULT_REPORTED = 827;
+  ATOM_ENHANCED_CONFIRMATION_RESTRICTION_CLEARED = 828;
+  ATOM_EMERGENCY_STATE_CHANGED = 633;
+  ATOM_CHRE_SIGNIFICANT_MOTION_STATE_CHANGED = 868;
+  ATOM_HEALTH_CONNECT_UI_IMPRESSION = 623;
+  ATOM_HEALTH_CONNECT_UI_INTERACTION = 624;
+  ATOM_HEALTH_CONNECT_APP_OPENED_REPORTED = 625;
+  ATOM_HEALTH_CONNECT_API_CALLED = 616;
+  ATOM_HEALTH_CONNECT_USAGE_STATS = 617;
+  ATOM_HEALTH_CONNECT_STORAGE_STATS = 618;
+  ATOM_HEALTH_CONNECT_API_INVOKED = 643;
+  ATOM_EXERCISE_ROUTE_API_CALLED = 654;
+  ATOM_SELINUX_AUDIT_LOG = 799;
+  ATOM_ONDEVICEPERSONALIZATION_API_CALLED = 711;
+  ATOM_CELLULAR_RADIO_POWER_STATE_CHANGED = 713;
+  ATOM_EMERGENCY_NUMBERS_INFO = 10180;
+  ATOM_DATA_NETWORK_VALIDATION = 10207;
+  ATOM_DATA_RAT_STATE_CHANGED = 854;
+  ATOM_CONNECTED_CHANNEL_CHANGED = 882;
+  ATOM_QUALIFIED_RAT_LIST_CHANGED = 634;
+  ATOM_QNS_IMS_CALL_DROP_STATS = 635;
+  ATOM_QNS_FALLBACK_RESTRICTION_CHANGED = 636;
+  ATOM_QNS_RAT_PREFERENCE_MISMATCH_INFO = 10177;
+  ATOM_QNS_HANDOVER_TIME_MILLIS = 10178;
+  ATOM_QNS_HANDOVER_PINGPONG = 10179;
+  ATOM_SATELLITE_CONTROLLER = 10182;
+  ATOM_SATELLITE_SESSION = 10183;
+  ATOM_SATELLITE_INCOMING_DATAGRAM = 10184;
+  ATOM_SATELLITE_OUTGOING_DATAGRAM = 10185;
+  ATOM_SATELLITE_PROVISION = 10186;
+  ATOM_SATELLITE_SOS_MESSAGE_RECOMMENDER = 10187;
+  ATOM_CARRIER_ROAMING_SATELLITE_SESSION = 10211;
+  ATOM_CARRIER_ROAMING_SATELLITE_CONTROLLER_STATS = 10212;
+  ATOM_CONTROLLER_STATS_PER_PACKAGE = 10213;
+  ATOM_SATELLITE_ENTITLEMENT = 10214;
+  ATOM_SATELLITE_CONFIG_UPDATER = 10215;
+  ATOM_SATELLITE_ACCESS_CONTROLLER = 10219;
+  ATOM_CELLULAR_IDENTIFIER_DISCLOSED = 800;
+  ATOM_KEYBOARD_CONFIGURED = 682;
+  ATOM_KEYBOARD_SYSTEMS_EVENT_REPORTED = 683;
+  ATOM_INPUTDEVICE_USAGE_REPORTED = 686;
+  ATOM_TOUCHPAD_USAGE = 10191;
+  ATOM_THREADNETWORK_TELEMETRY_DATA_REPORTED = 738;
+  ATOM_THREADNETWORK_TOPO_ENTRY_REPEATED = 739;
+  ATOM_THREADNETWORK_DEVICE_INFO_REPORTED = 740;
+  ATOM_CRONET_ENGINE_CREATED = 703;
+  ATOM_CRONET_TRAFFIC_REPORTED = 704;
+  ATOM_CRONET_ENGINE_BUILDER_INITIALIZED = 762;
+  ATOM_CRONET_HTTP_FLAGS_INITIALIZED = 763;
+  ATOM_CRONET_INITIALIZED = 764;
+  ATOM_WEAR_MODE_STATE_CHANGED = 715;
+  ATOM_RENDERER_INITIALIZED = 736;
+  ATOM_SCHEMA_VERSION_RECEIVED = 737;
+  ATOM_LAYOUT_INSPECTED = 741;
+  ATOM_LAYOUT_EXPRESSION_INSPECTED = 742;
+  ATOM_LAYOUT_ANIMATIONS_INSPECTED = 743;
+  ATOM_MATERIAL_COMPONENTS_INSPECTED = 744;
+  ATOM_TILE_REQUESTED = 745;
+  ATOM_STATE_RESPONSE_RECEIVED = 746;
+  ATOM_TILE_RESPONSE_RECEIVED = 747;
+  ATOM_INFLATION_FINISHED = 748;
+  ATOM_INFLATION_FAILED = 749;
+  ATOM_IGNORED_INFLATION_FAILURES_REPORTED = 750;
+  ATOM_DRAWABLE_RENDERED = 751;
+  ATOM_MEDIA_ACTION_REPORTED = 608;
+  ATOM_MEDIA_CONTROLS_LAUNCHED = 609;
+  ATOM_MEDIA_SESSION_STATE_CHANGED = 677;
+  ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_DEVICE_SCAN_API_LATENCY = 757;
+  ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_SASS_DEVICE_UNAVAILABLE = 758;
+  ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_FASTPAIR_API_TIMEOUT = 759;
+  ATOM_MEDIATOR_UPDATED = 721;
+  ATOM_SYSPROXY_BLUETOOTH_BYTES_TRANSFER = 10196;
+  ATOM_SYSPROXY_CONNECTION_UPDATED = 786;
+  ATOM_ADAPTIVE_AUTH_UNLOCK_AFTER_LOCK_REPORTED = 820;
+  ATOM_FEDERATED_COMPUTE_API_CALLED = 712;
+  ATOM_FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED = 771;
+  ATOM_EXAMPLE_ITERATOR_NEXT_LATENCY_REPORTED = 838;
+  ATOM_RKPD_POOL_STATS = 664;
+  ATOM_RKPD_CLIENT_OPERATION = 665;
+  ATOM_CPU_POLICY = 10199;
+  ATOM_ATOM_9999 = 9999;
+  ATOM_ATOM_99999 = 99999;
+  ATOM_SCREEN_OFF_REPORTED = 776;
+  ATOM_SCREEN_TIMEOUT_OVERRIDE_REPORTED = 836;
+  ATOM_SCREEN_INTERACTIVE_SESSION_REPORTED = 837;
+  ATOM_SCREEN_DIM_REPORTED = 867;
+  ATOM_FULL_SCREEN_INTENT_LAUNCHED = 631;
+  ATOM_BAL_ALLOWED = 632;
+  ATOM_IN_TASK_ACTIVITY_STARTED = 685;
+  ATOM_CACHED_APPS_HIGH_WATERMARK = 10189;
+  ATOM_STYLUS_PREDICTION_METRICS_REPORTED = 718;
+  ATOM_USER_RISK_EVENT_REPORTED = 725;
+  ATOM_MEDIA_PROJECTION_STATE_CHANGED = 729;
+  ATOM_MEDIA_PROJECTION_TARGET_CHANGED = 730;
+  ATOM_EXCESSIVE_BINDER_PROXY_COUNT_REPORTED = 853;
+  ATOM_PROXY_BYTES_TRANSFER_BY_FG_BG = 10200;
+  ATOM_MOBILE_BYTES_TRANSFER_BY_PROC_STATE = 10204;
+  ATOM_BIOMETRIC_FRR_NOTIFICATION = 817;
+  ATOM_SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION = 830;
+  ATOM_SENSITIVE_NOTIFICATION_APP_PROTECTION_SESSION = 831;
+  ATOM_SENSITIVE_NOTIFICATION_APP_PROTECTION_APPLIED = 832;
+  ATOM_SENSITIVE_NOTIFICATION_REDACTION = 833;
+  ATOM_SENSITIVE_CONTENT_APP_PROTECTION = 835;
+  ATOM_APP_RESTRICTION_STATE_CHANGED = 866;
+  ATOM_DREAM_SETTING_CHANGED = 705;
+  ATOM_DREAM_SETTING_SNAPSHOT = 10192;
+  ATOM_BOOT_INTEGRITY_INFO_REPORTED = 775;
   ATOM_WIFI_AWARE_NDP_REPORTED = 638;
   ATOM_WIFI_AWARE_ATTACH_REPORTED = 639;
   ATOM_WIFI_SELF_RECOVERY_TRIGGERED = 661;
@@ -2711,39 +2920,99 @@
   ATOM_WIFI_LOCAL_ONLY_REQUEST_SCAN_TRIGGERED = 693;
   ATOM_WIFI_THREAD_TASK_EXECUTED = 694;
   ATOM_WIFI_STATE_CHANGED = 700;
+  ATOM_PNO_SCAN_STARTED = 719;
+  ATOM_PNO_SCAN_STOPPED = 720;
+  ATOM_WIFI_IS_UNUSABLE_REPORTED = 722;
+  ATOM_WIFI_AP_CAPABILITIES_REPORTED = 723;
+  ATOM_SOFT_AP_STATE_CHANGED = 805;
+  ATOM_SCORER_PREDICTION_RESULT_REPORTED = 884;
   ATOM_WIFI_AWARE_CAPABILITIES = 10190;
   ATOM_WIFI_MODULE_INFO = 10193;
-  ATOM_SETTINGS_SPA_REPORTED = 622;
+  ATOM_WIFI_SETTING_INFO = 10194;
+  ATOM_WIFI_COMPLEX_SETTING_INFO = 10195;
+  ATOM_WIFI_CONFIGURED_NETWORK_INFO = 10198;
+  ATOM_MTE_STATE = 10181;
+  ATOM_HOTWORD_EGRESS_SIZE_ATOM_REPORTED = 761;
+  ATOM_SANDBOX_API_CALLED = 488;
+  ATOM_SANDBOX_ACTIVITY_EVENT_OCCURRED = 735;
+  ATOM_SDK_SANDBOX_RESTRICTED_ACCESS_IN_SESSION = 796;
+  ATOM_SANDBOX_SDK_STORAGE = 10159;
   ATOM_EXPRESS_EVENT_REPORTED = 528;
   ATOM_EXPRESS_HISTOGRAM_SAMPLE_REPORTED = 593;
   ATOM_EXPRESS_UID_EVENT_REPORTED = 644;
   ATOM_EXPRESS_UID_HISTOGRAM_SAMPLE_REPORTED = 658;
-  ATOM_PERMISSION_RATIONALE_DIALOG_VIEWED = 645;
-  ATOM_PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED = 646;
-  ATOM_APP_DATA_SHARING_UPDATES_NOTIFICATION_INTERACTION = 647;
-  ATOM_APP_DATA_SHARING_UPDATES_FRAGMENT_VIEWED = 648;
-  ATOM_APP_DATA_SHARING_UPDATES_FRAGMENT_ACTION_REPORTED = 649;
-  ATOM_WS_INCOMING_CALL_ACTION_REPORTED = 626;
-  ATOM_WS_CALL_DISCONNECTION_REPORTED = 627;
-  ATOM_WS_CALL_DURATION_REPORTED = 628;
-  ATOM_WS_CALL_USER_EXPERIENCE_LATENCY_REPORTED = 629;
-  ATOM_WS_CALL_INTERACTION_REPORTED = 630;
-  ATOM_FULL_SCREEN_INTENT_LAUNCHED = 631;
-  ATOM_BAL_ALLOWED = 632;
-  ATOM_IN_TASK_ACTIVITY_STARTED = 685;
-  ATOM_CACHED_APPS_HIGH_WATERMARK = 10189;
-  ATOM_ODREFRESH_REPORTED = 366;
-  ATOM_ODSIGN_REPORTED = 548;
-  ATOM_ART_DATUM_REPORTED = 332;
-  ATOM_ART_DEVICE_DATUM_REPORTED = 550;
-  ATOM_ART_DATUM_DELTA_REPORTED = 565;
-  ATOM_BACKGROUND_DEXOPT_JOB_ENDED = 467;
-  ATOM_WEAR_ADAPTIVE_SUSPEND_STATS_REPORTED = 619;
-  ATOM_WEAR_POWER_ANOMALY_SERVICE_OPERATIONAL_STATS_REPORTED = 620;
-  ATOM_WEAR_POWER_ANOMALY_SERVICE_EVENT_STATS_REPORTED = 621;
-  ATOM_EMERGENCY_STATE_CHANGED = 633;
-  ATOM_DND_STATE_CHANGED = 657;
-  ATOM_MTE_STATE = 10181;
+  ATOM_IKE_SESSION_TERMINATED = 678;
+  ATOM_IKE_LIVENESS_CHECK_SESSION_VALIDATED = 760;
+  ATOM_NEGOTIATED_SECURITY_ASSOCIATION = 821;
+  ATOM_APP_SEARCH_SET_SCHEMA_STATS_REPORTED = 385;
+  ATOM_APP_SEARCH_SCHEMA_MIGRATION_STATS_REPORTED = 579;
+  ATOM_APP_SEARCH_USAGE_SEARCH_INTENT_STATS_REPORTED = 825;
+  ATOM_APP_SEARCH_USAGE_SEARCH_INTENT_RAW_QUERY_STATS_REPORTED = 826;
+  ATOM_DEVICE_POLICY_MANAGEMENT_MODE = 10216;
+  ATOM_DEVICE_POLICY_STATE = 10217;
+  ATOM_DESKTOP_MODE_UI_CHANGED = 818;
+  ATOM_DESKTOP_MODE_SESSION_TASK_UPDATE = 819;
+  ATOM_MEDIA_CODEC_RECLAIM_REQUEST_COMPLETED = 600;
+  ATOM_MEDIA_CODEC_STARTED = 641;
+  ATOM_MEDIA_CODEC_STOPPED = 642;
+  ATOM_MEDIA_CODEC_RENDERED = 684;
+  ATOM_MEDIA_EDITING_ENDED_REPORTED = 798;
+  ATOM_CAR_WAKEUP_FROM_SUSPEND_REPORTED = 852;
+  ATOM_PLUGIN_INITIALIZED = 655;
+  ATOM_CAR_RECENTS_EVENT_REPORTED = 770;
+  ATOM_CAR_CALM_MODE_EVENT_REPORTED = 797;
+  ATOM_CAMERA_FEATURE_COMBINATION_QUERY_EVENT = 900;
+  ATOM_THERMAL_STATUS_CALLED = 772;
+  ATOM_THERMAL_HEADROOM_CALLED = 773;
+  ATOM_THERMAL_HEADROOM_THRESHOLDS_CALLED = 774;
+  ATOM_ADPF_HINT_SESSION_TID_CLEANUP = 839;
+  ATOM_THERMAL_HEADROOM_THRESHOLDS = 10201;
+  ATOM_ADPF_SESSION_SNAPSHOT = 10218;
+  ATOM_BLUETOOTH_HASHED_DEVICE_NAME_REPORTED = 613;
+  ATOM_BLUETOOTH_L2CAP_COC_CLIENT_CONNECTION = 614;
+  ATOM_BLUETOOTH_L2CAP_COC_SERVER_CONNECTION = 615;
+  ATOM_BLUETOOTH_LE_SESSION_CONNECTED = 656;
+  ATOM_RESTRICTED_BLUETOOTH_DEVICE_NAME_REPORTED = 666;
+  ATOM_BLUETOOTH_PROFILE_CONNECTION_ATTEMPTED = 696;
+  ATOM_BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED = 781;
+  ATOM_BLUETOOTH_RFCOMM_CONNECTION_ATTEMPTED = 782;
+  ATOM_REMOTE_DEVICE_INFORMATION_WITH_METRIC_ID = 862;
+  ATOM_LE_APP_SCAN_STATE_CHANGED = 870;
+  ATOM_LE_RADIO_SCAN_STOPPED = 871;
+  ATOM_LE_SCAN_RESULT_RECEIVED = 872;
+  ATOM_LE_SCAN_ABUSED = 873;
+  ATOM_LE_ADV_STATE_CHANGED = 874;
+  ATOM_LE_ADV_ERROR_REPORTED = 875;
+  ATOM_A2DP_SESSION_REPORTED = 904;
+  ATOM_BLUETOOTH_CROSS_LAYER_EVENT_REPORTED = 916;
+  ATOM_BROADCAST_AUDIO_SESSION_REPORTED = 927;
+  ATOM_BROADCAST_AUDIO_SYNC_REPORTED = 928;
+  ATOM_DEVICE_LOCK_CHECK_IN_REQUEST_REPORTED = 726;
+  ATOM_DEVICE_LOCK_PROVISIONING_COMPLETE_REPORTED = 727;
+  ATOM_DEVICE_LOCK_KIOSK_APP_REQUEST_REPORTED = 728;
+  ATOM_DEVICE_LOCK_CHECK_IN_RETRY_REPORTED = 789;
+  ATOM_DEVICE_LOCK_PROVISION_FAILURE_REPORTED = 790;
+  ATOM_DEVICE_LOCK_LOCK_UNLOCK_DEVICE_FAILURE_REPORTED = 791;
+  ATOM_APPLICATION_GRAMMATICAL_INFLECTION_CHANGED = 584;
+  ATOM_SYSTEM_GRAMMATICAL_INFLECTION_CHANGED = 816;
+  ATOM_EMERGENCY_NUMBER_DIALED = 637;
+  ATOM_JSSCRIPTENGINE_LATENCY_REPORTED = 483;
+  ATOM_AD_SERVICES_API_CALLED = 435;
+  ATOM_AD_SERVICES_MESUREMENT_REPORTS_UPLOADED = 436;
+  ATOM_MOBILE_DATA_DOWNLOAD_FILE_GROUP_STATUS_REPORTED = 490;
+  ATOM_MOBILE_DATA_DOWNLOAD_DOWNLOAD_RESULT_REPORTED = 502;
+  ATOM_AD_SERVICES_SETTINGS_USAGE_REPORTED = 493;
+  ATOM_BACKGROUND_FETCH_PROCESS_REPORTED = 496;
+  ATOM_UPDATE_CUSTOM_AUDIENCE_PROCESS_REPORTED = 497;
+  ATOM_RUN_AD_BIDDING_PROCESS_REPORTED = 498;
+  ATOM_RUN_AD_SCORING_PROCESS_REPORTED = 499;
+  ATOM_RUN_AD_SELECTION_PROCESS_REPORTED = 500;
+  ATOM_RUN_AD_BIDDING_PER_CA_PROCESS_REPORTED = 501;
+  ATOM_MOBILE_DATA_DOWNLOAD_FILE_GROUP_STORAGE_STATS_REPORTED = 503;
+  ATOM_AD_SERVICES_MEASUREMENT_REGISTRATIONS = 512;
+  ATOM_AD_SERVICES_GET_TOPICS_REPORTED = 535;
+  ATOM_AD_SERVICES_EPOCH_COMPUTATION_GET_TOP_TOPICS_REPORTED = 536;
+  ATOM_AD_SERVICES_EPOCH_COMPUTATION_CLASSIFIER_REPORTED = 537;
   ATOM_AD_SERVICES_BACK_COMPAT_GET_TOPICS_REPORTED = 598;
   ATOM_AD_SERVICES_BACK_COMPAT_EPOCH_COMPUTATION_CLASSIFIER_REPORTED = 599;
   ATOM_AD_SERVICES_MEASUREMENT_DEBUG_KEYS = 640;
@@ -2753,68 +3022,68 @@
   ATOM_AD_SERVICES_MEASUREMENT_ATTRIBUTION = 674;
   ATOM_AD_SERVICES_MEASUREMENT_JOBS = 675;
   ATOM_AD_SERVICES_MEASUREMENT_WIPEOUT = 676;
+  ATOM_AD_SERVICES_MEASUREMENT_AD_ID_MATCH_FOR_DEBUG_KEYS = 695;
+  ATOM_AD_SERVICES_ENROLLMENT_DATA_STORED = 697;
+  ATOM_AD_SERVICES_ENROLLMENT_FILE_DOWNLOADED = 698;
+  ATOM_AD_SERVICES_ENROLLMENT_MATCHED = 699;
   ATOM_AD_SERVICES_CONSENT_MIGRATED = 702;
-  ATOM_RKPD_POOL_STATS = 664;
-  ATOM_RKPD_CLIENT_OPERATION = 665;
-  ATOM_AUTOFILL_UI_EVENT_REPORTED = 603;
-  ATOM_AUTOFILL_FILL_REQUEST_REPORTED = 604;
-  ATOM_AUTOFILL_FILL_RESPONSE_REPORTED = 605;
-  ATOM_AUTOFILL_SAVE_EVENT_REPORTED = 606;
-  ATOM_AUTOFILL_SESSION_COMMITTED = 607;
-  ATOM_AUTOFILL_FIELD_CLASSIFICATION_EVENT_REPORTED = 659;
+  ATOM_AD_SERVICES_ENROLLMENT_FAILED = 714;
+  ATOM_AD_SERVICES_MEASUREMENT_CLICK_VERIFICATION = 756;
+  ATOM_AD_SERVICES_ENCRYPTION_KEY_FETCHED = 765;
+  ATOM_AD_SERVICES_ENCRYPTION_KEY_DB_TRANSACTION_ENDED = 766;
+  ATOM_DESTINATION_REGISTERED_BEACONS = 767;
+  ATOM_REPORT_INTERACTION_API_CALLED = 768;
+  ATOM_INTERACTION_REPORTING_TABLE_CLEARED = 769;
+  ATOM_APP_MANIFEST_CONFIG_HELPER_CALLED = 788;
+  ATOM_AD_FILTERING_PROCESS_JOIN_CA_REPORTED = 793;
+  ATOM_AD_FILTERING_PROCESS_AD_SELECTION_REPORTED = 794;
+  ATOM_AD_COUNTER_HISTOGRAM_UPDATER_REPORTED = 795;
+  ATOM_SIGNATURE_VERIFICATION = 807;
+  ATOM_K_ANON_IMMEDIATE_SIGN_JOIN_STATUS_REPORTED = 808;
+  ATOM_K_ANON_BACKGROUND_JOB_STATUS_REPORTED = 809;
+  ATOM_K_ANON_INITIALIZE_STATUS_REPORTED = 810;
+  ATOM_K_ANON_SIGN_STATUS_REPORTED = 811;
+  ATOM_K_ANON_JOIN_STATUS_REPORTED = 812;
+  ATOM_K_ANON_KEY_ATTESTATION_STATUS_REPORTED = 813;
+  ATOM_GET_AD_SELECTION_DATA_API_CALLED = 814;
+  ATOM_GET_AD_SELECTION_DATA_BUYER_INPUT_GENERATED = 815;
+  ATOM_BACKGROUND_JOB_SCHEDULING_REPORTED = 834;
+  ATOM_TOPICS_ENCRYPTION_EPOCH_COMPUTATION_REPORTED = 840;
+  ATOM_TOPICS_ENCRYPTION_GET_TOPICS_REPORTED = 841;
+  ATOM_ADSERVICES_SHELL_COMMAND_CALLED = 842;
+  ATOM_UPDATE_SIGNALS_API_CALLED = 843;
+  ATOM_ENCODING_JOB_RUN = 844;
+  ATOM_ENCODING_JS_FETCH = 845;
+  ATOM_ENCODING_JS_EXECUTION = 846;
+  ATOM_PERSIST_AD_SELECTION_RESULT_CALLED = 847;
+  ATOM_SERVER_AUCTION_KEY_FETCH_CALLED = 848;
+  ATOM_SERVER_AUCTION_BACKGROUND_KEY_FETCH_ENABLED = 849;
+  ATOM_AD_SERVICES_MEASUREMENT_PROCESS_ODP_REGISTRATION = 864;
+  ATOM_AD_SERVICES_MEASUREMENT_NOTIFY_REGISTRATION_TO_ODP = 865;
+  ATOM_SELECT_ADS_FROM_OUTCOMES_API_CALLED = 876;
+  ATOM_REPORT_IMPRESSION_API_CALLED = 877;
+  ATOM_AD_SERVICES_ENROLLMENT_TRANSACTION_STATS = 885;
+  ATOM_EXTERNAL_DISPLAY_STATE_CHANGED = 806;
+  ATOM_DISPLAY_MODE_DIRECTOR_VOTE_CHANGED = 792;
   ATOM_TEST_EXTENSION_ATOM_REPORTED = 660;
   ATOM_TEST_RESTRICTED_ATOM_REPORTED = 672;
   ATOM_STATS_SOCKET_LOSS_REPORTED = 752;
-  ATOM_PLUGIN_INITIALIZED = 655;
-  ATOM_TV_LOW_POWER_STANDBY_POLICY = 679;
+  ATOM_NFC_OBSERVE_MODE_STATE_CHANGED = 855;
+  ATOM_NFC_FIELD_CHANGED = 856;
+  ATOM_NFC_POLLING_LOOP_NOTIFICATION_REPORTED = 857;
+  ATOM_NFC_PROPRIETARY_CAPABILITIES_REPORTED = 858;
   ATOM_LOCKSCREEN_SHORTCUT_SELECTED = 611;
   ATOM_LOCKSCREEN_SHORTCUT_TRIGGERED = 612;
-  ATOM_EMERGENCY_NUMBERS_INFO = 10180;
-  ATOM_QUALIFIED_RAT_LIST_CHANGED = 634;
-  ATOM_QNS_IMS_CALL_DROP_STATS = 635;
-  ATOM_QNS_FALLBACK_RESTRICTION_CHANGED = 636;
-  ATOM_QNS_RAT_PREFERENCE_MISMATCH_INFO = 10177;
-  ATOM_QNS_HANDOVER_TIME_MILLIS = 10178;
-  ATOM_QNS_HANDOVER_PINGPONG = 10179;
-  ATOM_SATELLITE_CONTROLLER = 10182;
-  ATOM_SATELLITE_SESSION = 10183;
-  ATOM_SATELLITE_INCOMING_DATAGRAM = 10184;
-  ATOM_SATELLITE_OUTGOING_DATAGRAM = 10185;
-  ATOM_SATELLITE_PROVISION = 10186;
-  ATOM_SATELLITE_SOS_MESSAGE_RECOMMENDER = 10187;
-  ATOM_IKE_SESSION_TERMINATED = 678;
-  ATOM_IKE_LIVENESS_CHECK_SESSION_VALIDATED = 760;
-  ATOM_BLUETOOTH_HASHED_DEVICE_NAME_REPORTED = 613;
-  ATOM_BLUETOOTH_L2CAP_COC_CLIENT_CONNECTION = 614;
-  ATOM_BLUETOOTH_L2CAP_COC_SERVER_CONNECTION = 615;
-  ATOM_BLUETOOTH_LE_SESSION_CONNECTED = 656;
-  ATOM_RESTRICTED_BLUETOOTH_DEVICE_NAME_REPORTED = 666;
-  ATOM_BLUETOOTH_PROFILE_CONNECTION_ATTEMPTED = 696;
-  ATOM_HEALTH_CONNECT_UI_IMPRESSION = 623;
-  ATOM_HEALTH_CONNECT_UI_INTERACTION = 624;
-  ATOM_HEALTH_CONNECT_APP_OPENED_REPORTED = 625;
-  ATOM_HEALTH_CONNECT_API_CALLED = 616;
-  ATOM_HEALTH_CONNECT_USAGE_STATS = 617;
-  ATOM_HEALTH_CONNECT_STORAGE_STATS = 618;
-  ATOM_HEALTH_CONNECT_API_INVOKED = 643;
-  ATOM_EXERCISE_ROUTE_API_CALLED = 654;
-  ATOM_ATOM_9999 = 9999;
-  ATOM_ATOM_99999 = 99999;
-  ATOM_THREADNETWORK_TELEMETRY_DATA_REPORTED = 738;
-  ATOM_THREADNETWORK_TOPO_ENTRY_REPEATED = 739;
-  ATOM_THREADNETWORK_DEVICE_INFO_REPORTED = 740;
-  ATOM_EMERGENCY_NUMBER_DIALED = 637;
-  ATOM_SANDBOX_API_CALLED = 488;
-  ATOM_SANDBOX_ACTIVITY_EVENT_OCCURRED = 735;
-  ATOM_SANDBOX_SDK_STORAGE = 10159;
-  ATOM_CRONET_ENGINE_CREATED = 703;
-  ATOM_CRONET_TRAFFIC_REPORTED = 704;
-  ATOM_CRONET_ENGINE_BUILDER_INITIALIZED = 762;
-  ATOM_CRONET_HTTP_FLAGS_INITIALIZED = 763;
-  ATOM_CRONET_INITIALIZED = 764;
-  ATOM_DAILY_KEEPALIVE_INFO_REPORTED = 650;
-  ATOM_IP_CLIENT_RA_INFO_REPORTED = 778;
-  ATOM_APF_SESSION_INFO_REPORTED = 777;
+  ATOM_LAUNCHER_IMPRESSION_EVENT_V2 = 716;
+  ATOM_DISPLAY_SWITCH_LATENCY_TRACKED = 753;
+  ATOM_NOTIFICATION_LISTENER_SERVICE = 829;
+  ATOM_NAV_HANDLE_TOUCH_POINTS = 869;
+  ATOM_WEAR_ADAPTIVE_SUSPEND_STATS_REPORTED = 619;
+  ATOM_WEAR_POWER_ANOMALY_SERVICE_OPERATIONAL_STATS_REPORTED = 620;
+  ATOM_WEAR_POWER_ANOMALY_SERVICE_EVENT_STATS_REPORTED = 621;
+  ATOM_APEX_INSTALLATION_REQUESTED = 732;
+  ATOM_APEX_INSTALLATION_STAGED = 733;
+  ATOM_APEX_INSTALLATION_ENDED = 734;
   ATOM_CREDENTIAL_MANAGER_API_CALLED = 585;
   ATOM_CREDENTIAL_MANAGER_INIT_PHASE_REPORTED = 651;
   ATOM_CREDENTIAL_MANAGER_CANDIDATE_PHASE_REPORTED = 652;
@@ -2825,12 +3094,7 @@
   ATOM_CREDENTIAL_MANAGER_AUTH_CLICK_REPORTED = 670;
   ATOM_CREDENTIAL_MANAGER_APIV2_CALLED = 671;
   ATOM_UWB_ACTIVITY_INFO = 10188;
-  ATOM_MEDIA_ACTION_REPORTED = 608;
-  ATOM_MEDIA_CONTROLS_LAUNCHED = 609;
-  ATOM_MEDIA_CODEC_RECLAIM_REQUEST_COMPLETED = 600;
-  ATOM_MEDIA_CODEC_STARTED = 641;
-  ATOM_MEDIA_CODEC_STOPPED = 642;
-  ATOM_MEDIA_CODEC_RENDERED = 684;
+  ATOM_DND_STATE_CHANGED = 657;
 }
 // End of protos/perfetto/config/statsd/atom_ids.proto
 
@@ -3173,6 +3437,10 @@
   // Polls /sys/devices/system/cpu/cpu*/cpuidle/state* every X ms, if non-zero.
   // This is required to be > 10ms to avoid excessive CPU usage.
   optional uint32 cpuidle_period_ms = 13;
+
+  // Polls device-specific GPU frequency info every X ms, if non-zero.
+  // This is required to be > 10ms to avoid excessive CPU usage.
+  optional uint32 gpufreq_period_ms = 14;
 }
 
 // End of protos/perfetto/config/sys_stats/sys_stats_config.proto
@@ -3327,7 +3595,7 @@
 // Begin of protos/perfetto/config/data_source_config.proto
 
 // The configuration that is passed to each data source when starting tracing.
-// Next id: 131
+// Next id: 132
 message DataSourceConfig {
   enum SessionInitiator {
     SESSION_INITIATOR_UNSPECIFIED = 0;
@@ -3482,6 +3750,9 @@
   // Data source name: android.windowmanager
   optional WindowManagerConfig windowmanager_config = 130 [lazy = true];
 
+  // Data source name: org.chromium.system_metrics
+  optional ChromiumSystemMetricsConfig chromium_system_metrics = 131 [lazy = true];
+
   // This is a fallback mechanism to send a free-form text config to the
   // producer. In theory this should never be needed. All the code that
   // is part of the platform (i.e. traced service) is supposed to *not* truncate
@@ -3919,11 +4190,8 @@
   }
   optional IncrementalStateConfig incremental_state_config = 21;
 
-  // Additional guardrail used by the Perfetto command line client.
-  // On user builds when --dropbox is set perfetto will refuse to trace unless
-  // this is also set.
-  // Added in Q.
-  optional bool allow_user_build_tracing = 19;
+  // No longer needed as we unconditionally allow tracing on user builds.
+  optional bool allow_user_build_tracing = 19 [deprecated = true];
 
   // If set the tracing service will ensure there is at most one tracing session
   // with this key.
diff --git a/protos/perfetto/config/profiling/perf_event_config.proto b/protos/perfetto/config/profiling/perf_event_config.proto
index 3c9af8a..d3cc51c 100644
--- a/protos/perfetto/config/profiling/perf_event_config.proto
+++ b/protos/perfetto/config/profiling/perf_event_config.proto
@@ -36,12 +36,15 @@
 //     }
 //   }
 //
-// Next id: 19
+// Next id: 20
 message PerfEventConfig {
   // What event to sample on, and how often.
   // Defined in common/perf_events.proto.
   optional PerfEvents.Timebase timebase = 15;
 
+  // Other events associated with the leader described in the timebase.
+  repeated FollowerEvent followers = 19;
+
   // If set, the profiler will sample userspace processes' callstacks at the
   // interval specified by the |timebase|.
   // If unset, the profiler will record only the event counts.
@@ -214,5 +217,7 @@
     UNWIND_SKIP = 1;
     // Use libunwindstack (default):
     UNWIND_DWARF = 2;
+    // Use userspace frame pointer unwinder:
+    UNWIND_FRAME_POINTER = 3;
   }
 }
diff --git a/protos/perfetto/config/statsd/atom_ids.proto b/protos/perfetto/config/statsd/atom_ids.proto
index c436571..0d0af24 100644
--- a/protos/perfetto/config/statsd/atom_ids.proto
+++ b/protos/perfetto/config/statsd/atom_ids.proto
@@ -75,11 +75,9 @@
   ATOM_LMK_KILL_OCCURRED = 51;
   ATOM_PICTURE_IN_PICTURE_STATE_CHANGED = 52;
   ATOM_WIFI_MULTICAST_LOCK_STATE_CHANGED = 53;
-  ATOM_LMK_STATE_CHANGED = 54;
   ATOM_APP_START_MEMORY_STATE_CAPTURED = 55;
   ATOM_SHUTDOWN_SEQUENCE_REPORTED = 56;
   ATOM_BOOT_SEQUENCE_REPORTED = 57;
-  ATOM_DAVEY_OCCURRED = 58;
   ATOM_OVERLAY_STATE_CHANGED = 59;
   ATOM_FOREGROUND_SERVICE_STATE_CHANGED = 60;
   ATOM_CALL_STATE_CHANGED = 61;
@@ -396,7 +394,6 @@
   ATOM_PRIVACY_TOGGLE_DIALOG_INTERACTION = 382;
   ATOM_APP_SEARCH_OPTIMIZE_STATS_REPORTED = 383;
   ATOM_NON_A11Y_TOOL_SERVICE_WARNING_REPORT = 384;
-  ATOM_APP_SEARCH_SET_SCHEMA_STATS_REPORTED = 385;
   ATOM_APP_COMPAT_STATE_CHANGED = 386;
   ATOM_SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED = 387;
   ATOM_SPLITSCREEN_UI_CHANGED = 388;
@@ -445,8 +442,6 @@
   ATOM_HOTWORD_DETECTION_SERVICE_RESTARTED = 432;
   ATOM_HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED = 433;
   ATOM_HOTWORD_DETECTOR_EVENTS = 434;
-  ATOM_AD_SERVICES_API_CALLED = 435;
-  ATOM_AD_SERVICES_MESUREMENT_REPORTS_UPLOADED = 436;
   ATOM_BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED = 437;
   ATOM_CONTACTS_INDEXER_UPDATE_STATS_REPORTED = 440;
   ATOM_APP_BACKGROUND_RESTRICTIONS_INFO = 441;
@@ -490,25 +485,14 @@
   ATOM_CB_MODULE_ERROR_REPORTED = 480;
   ATOM_CB_SERVICE_FEATURE_CHANGED = 481;
   ATOM_CB_RECEIVER_FEATURE_CHANGED = 482;
-  ATOM_JSSCRIPTENGINE_LATENCY_REPORTED = 483;
   ATOM_PRIVACY_SIGNAL_NOTIFICATION_INTERACTION = 484;
   ATOM_PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION = 485;
   ATOM_PRIVACY_SIGNALS_JOB_FAILURE = 486;
   ATOM_VIBRATION_REPORTED = 487;
   ATOM_UWB_RANGING_START = 489;
-  ATOM_MOBILE_DATA_DOWNLOAD_FILE_GROUP_STATUS_REPORTED = 490;
   ATOM_APP_COMPACTED_V2 = 491;
-  ATOM_AD_SERVICES_SETTINGS_USAGE_REPORTED = 493;
   ATOM_DISPLAY_BRIGHTNESS_CHANGED = 494;
   ATOM_ACTIVITY_ACTION_BLOCKED = 495;
-  ATOM_BACKGROUND_FETCH_PROCESS_REPORTED = 496;
-  ATOM_UPDATE_CUSTOM_AUDIENCE_PROCESS_REPORTED = 497;
-  ATOM_RUN_AD_BIDDING_PROCESS_REPORTED = 498;
-  ATOM_RUN_AD_SCORING_PROCESS_REPORTED = 499;
-  ATOM_RUN_AD_SELECTION_PROCESS_REPORTED = 500;
-  ATOM_RUN_AD_BIDDING_PER_CA_PROCESS_REPORTED = 501;
-  ATOM_MOBILE_DATA_DOWNLOAD_DOWNLOAD_RESULT_REPORTED = 502;
-  ATOM_MOBILE_DATA_DOWNLOAD_FILE_GROUP_STORAGE_STATS_REPORTED = 503;
   ATOM_NETWORK_DNS_SERVER_SUPPORT_REPORTED = 504;
   ATOM_VM_BOOTED = 505;
   ATOM_VM_EXITED = 506;
@@ -517,7 +501,6 @@
   ATOM_MEDIAMETRICS_SPATIALIZERDEVICEENABLED_REPORTED = 509;
   ATOM_MEDIAMETRICS_HEADTRACKERDEVICEENABLED_REPORTED = 510;
   ATOM_MEDIAMETRICS_HEADTRACKERDEVICESUPPORTED_REPORTED = 511;
-  ATOM_AD_SERVICES_MEASUREMENT_REGISTRATIONS = 512;
   ATOM_HEARING_AID_INFO_REPORTED = 513;
   ATOM_DEVICE_WIDE_JOB_CONSTRAINT_CHANGED = 514;
   ATOM_AMBIENT_MODE_CHANGED = 515;
@@ -539,9 +522,6 @@
   ATOM_BLUETOOTH_LOCAL_SUPPORTED_FEATURES_REPORTED = 532;
   ATOM_BLUETOOTH_GATT_APP_INFO = 533;
   ATOM_BRIGHTNESS_CONFIGURATION_UPDATED = 534;
-  ATOM_AD_SERVICES_GET_TOPICS_REPORTED = 535;
-  ATOM_AD_SERVICES_EPOCH_COMPUTATION_GET_TOP_TOPICS_REPORTED = 536;
-  ATOM_AD_SERVICES_EPOCH_COMPUTATION_CLASSIFIER_REPORTED = 537;
   ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_LAUNCHED = 538;
   ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_FINISHED = 539;
   ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_CONNECTION_REPORTED = 540;
@@ -579,12 +559,10 @@
   ATOM_MEDIAMETRICS_MIDI_DEVICE_CLOSE_REPORTED = 576;
   ATOM_BIOMETRIC_TOUCH_REPORTED = 577;
   ATOM_HOTWORD_AUDIO_EGRESS_EVENT_REPORTED = 578;
-  ATOM_APP_SEARCH_SCHEMA_MIGRATION_STATS_REPORTED = 579;
   ATOM_LOCATION_ENABLED_STATE_CHANGED = 580;
   ATOM_IME_REQUEST_FINISHED = 581;
   ATOM_USB_COMPLIANCE_WARNINGS_REPORTED = 582;
   ATOM_APP_SUPPORTED_LOCALES_CHANGED = 583;
-  ATOM_GRAMMATICAL_INFLECTION_CHANGED = 584;
   ATOM_MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED = 586;
   ATOM_BIOMETRIC_PROPERTIES_COLLECTED = 587;
   ATOM_KERNEL_WAKEUP_ATTRIBUTED = 588;
@@ -597,7 +575,11 @@
   ATOM_WS_NOTIFICATION_UPDATED = 596;
   ATOM_NETWORK_VALIDATION_FAILURE_STATS_DAILY_REPORTED = 601;
   ATOM_WS_COMPLICATION_TAPPED = 602;
-  ATOM_WS_WEAR_TIME_SESSION = 610;
+  ATOM_WS_NOTIFICATION_BLOCKING = 780;
+  ATOM_WS_NOTIFICATION_BRIDGEMODE_UPDATED = 822;
+  ATOM_WS_NOTIFICATION_DISMISSAL_ACTIONED = 823;
+  ATOM_WS_NOTIFICATION_ACTIONED = 824;
+  ATOM_WS_NOTIFICATION_LATENCY = 880;
   ATOM_WIFI_BYTES_TRANSFER = 10000;
   ATOM_WIFI_BYTES_TRANSFER_BY_FG_BG = 10001;
   ATOM_MOBILE_BYTES_TRANSFER = 10002;
@@ -770,6 +752,186 @@
   ATOM_NOTIFICATION_MEMORY_USE = 10174;
   ATOM_HDR_CAPABILITIES = 10175;
   ATOM_WS_FAVOURITE_WATCH_FACE_LIST_SNAPSHOT = 10176;
+  ATOM_WS_WEAR_TIME_SESSION = 610;
+  ATOM_WS_INCOMING_CALL_ACTION_REPORTED = 626;
+  ATOM_WS_CALL_DISCONNECTION_REPORTED = 627;
+  ATOM_WS_CALL_DURATION_REPORTED = 628;
+  ATOM_WS_CALL_USER_EXPERIENCE_LATENCY_REPORTED = 629;
+  ATOM_WS_CALL_INTERACTION_REPORTED = 630;
+  ATOM_WS_ON_BODY_STATE_CHANGED = 787;
+  ATOM_WS_WATCH_FACE_RESTRICTED_COMPLICATIONS_IMPACTED = 802;
+  ATOM_WS_WATCH_FACE_DEFAULT_RESTRICTED_COMPLICATIONS_REMOVED = 803;
+  ATOM_WS_COMPLICATIONS_IMPACTED_NOTIFICATION_EVENT_REPORTED = 804;
+  ATOM_WS_STANDALONE_MODE_SNAPSHOT = 10197;
+  ATOM_WS_FAVORITE_WATCH_FACE_SNAPSHOT = 10206;
+  ATOM_SETTINGS_SPA_REPORTED = 622;
+  ATOM_PDF_LOAD_REPORTED = 859;
+  ATOM_PDF_API_USAGE_REPORTED = 860;
+  ATOM_PDF_SEARCH_REPORTED = 861;
+  ATOM_HDMI_EARC_STATUS_REPORTED = 701;
+  ATOM_HDMI_SOUNDBAR_MODE_STATUS_REPORTED = 724;
+  ATOM_MEDIA_PROVIDER_DATABASE_ROLLBACK_REPORTED = 784;
+  ATOM_BACKUP_SETUP_STATUS_REPORTED = 785;
+  ATOM_PHOTOPICKER_SESSION_INFO_REPORTED = 886;
+  ATOM_PHOTOPICKER_API_INFO_REPORTED = 887;
+  ATOM_PHOTOPICKER_UI_EVENT_LOGGED = 888;
+  ATOM_PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED = 889;
+  ATOM_PHOTOPICKER_PREVIEW_INFO_LOGGED = 890;
+  ATOM_PHOTOPICKER_MENU_INTERACTION_LOGGED = 891;
+  ATOM_PHOTOPICKER_BANNER_INTERACTION_LOGGED = 892;
+  ATOM_PHOTOPICKER_MEDIA_LIBRARY_INFO_LOGGED = 893;
+  ATOM_PHOTOPICKER_PAGE_INFO_LOGGED = 894;
+  ATOM_PHOTOPICKER_MEDIA_GRID_SYNC_INFO_REPORTED = 895;
+  ATOM_PHOTOPICKER_ALBUM_SYNC_INFO_REPORTED = 896;
+  ATOM_PHOTOPICKER_SEARCH_INFO_REPORTED = 897;
+  ATOM_SEARCH_DATA_EXTRACTION_DETAILS_REPORTED = 898;
+  ATOM_EMBEDDED_PHOTOPICKER_INFO_REPORTED = 899;
+  ATOM_WEAR_POWER_MENU_OPENED = 731;
+  ATOM_WEAR_ASSISTANT_OPENED = 755;
+  ATOM_KERNEL_OOM_KILL_OCCURRED = 754;
+  ATOM_AUTOFILL_UI_EVENT_REPORTED = 603;
+  ATOM_AUTOFILL_FILL_REQUEST_REPORTED = 604;
+  ATOM_AUTOFILL_FILL_RESPONSE_REPORTED = 605;
+  ATOM_AUTOFILL_SAVE_EVENT_REPORTED = 606;
+  ATOM_AUTOFILL_SESSION_COMMITTED = 607;
+  ATOM_AUTOFILL_FIELD_CLASSIFICATION_EVENT_REPORTED = 659;
+  ATOM_TV_LOW_POWER_STANDBY_POLICY = 679;
+  ATOM_EXTERNAL_TV_INPUT_EVENT = 717;
+  ATOM_COMPONENT_STATE_CHANGED_REPORTED = 863;
+  ATOM_AI_WALLPAPERS_BUTTON_PRESSED = 706;
+  ATOM_AI_WALLPAPERS_TEMPLATE_SELECTED = 707;
+  ATOM_AI_WALLPAPERS_TERM_SELECTED = 708;
+  ATOM_AI_WALLPAPERS_WALLPAPER_SET = 709;
+  ATOM_AI_WALLPAPERS_SESSION_SUMMARY = 710;
+  ATOM_APF_SESSION_INFO_REPORTED = 777;
+  ATOM_IP_CLIENT_RA_INFO_REPORTED = 778;
+  ATOM_VPN_CONNECTION_STATE_CHANGED = 850;
+  ATOM_VPN_CONNECTION_REPORTED = 851;
+  ATOM_NETWORK_STATS_RECORDER_FILE_OPERATED = 783;
+  ATOM_DAILY_KEEPALIVE_INFO_REPORTED = 650;
+  ATOM_NETWORK_REQUEST_STATE_CHANGED = 779;
+  ATOM_TETHERING_ACTIVE_SESSIONS_REPORTED = 925;
+  ATOM_ART_DATUM_REPORTED = 332;
+  ATOM_ART_DEVICE_DATUM_REPORTED = 550;
+  ATOM_ART_DATUM_DELTA_REPORTED = 565;
+  ATOM_ART_DEX2OAT_REPORTED = 929;
+  ATOM_ART_DEVICE_STATUS = 10205;
+  ATOM_ODREFRESH_REPORTED = 366;
+  ATOM_ODSIGN_REPORTED = 548;
+  ATOM_BACKGROUND_DEXOPT_JOB_ENDED = 467;
+  ATOM_PREREBOOT_DEXOPT_JOB_ENDED = 883;
+  ATOM_PERMISSION_RATIONALE_DIALOG_VIEWED = 645;
+  ATOM_PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED = 646;
+  ATOM_APP_DATA_SHARING_UPDATES_NOTIFICATION_INTERACTION = 647;
+  ATOM_APP_DATA_SHARING_UPDATES_FRAGMENT_VIEWED = 648;
+  ATOM_APP_DATA_SHARING_UPDATES_FRAGMENT_ACTION_REPORTED = 649;
+  ATOM_ENHANCED_CONFIRMATION_DIALOG_RESULT_REPORTED = 827;
+  ATOM_ENHANCED_CONFIRMATION_RESTRICTION_CLEARED = 828;
+  ATOM_EMERGENCY_STATE_CHANGED = 633;
+  ATOM_CHRE_SIGNIFICANT_MOTION_STATE_CHANGED = 868;
+  ATOM_HEALTH_CONNECT_UI_IMPRESSION = 623;
+  ATOM_HEALTH_CONNECT_UI_INTERACTION = 624;
+  ATOM_HEALTH_CONNECT_APP_OPENED_REPORTED = 625;
+  ATOM_HEALTH_CONNECT_API_CALLED = 616;
+  ATOM_HEALTH_CONNECT_USAGE_STATS = 617;
+  ATOM_HEALTH_CONNECT_STORAGE_STATS = 618;
+  ATOM_HEALTH_CONNECT_API_INVOKED = 643;
+  ATOM_EXERCISE_ROUTE_API_CALLED = 654;
+  ATOM_SELINUX_AUDIT_LOG = 799;
+  ATOM_ONDEVICEPERSONALIZATION_API_CALLED = 711;
+  ATOM_CELLULAR_RADIO_POWER_STATE_CHANGED = 713;
+  ATOM_EMERGENCY_NUMBERS_INFO = 10180;
+  ATOM_DATA_NETWORK_VALIDATION = 10207;
+  ATOM_DATA_RAT_STATE_CHANGED = 854;
+  ATOM_CONNECTED_CHANNEL_CHANGED = 882;
+  ATOM_QUALIFIED_RAT_LIST_CHANGED = 634;
+  ATOM_QNS_IMS_CALL_DROP_STATS = 635;
+  ATOM_QNS_FALLBACK_RESTRICTION_CHANGED = 636;
+  ATOM_QNS_RAT_PREFERENCE_MISMATCH_INFO = 10177;
+  ATOM_QNS_HANDOVER_TIME_MILLIS = 10178;
+  ATOM_QNS_HANDOVER_PINGPONG = 10179;
+  ATOM_SATELLITE_CONTROLLER = 10182;
+  ATOM_SATELLITE_SESSION = 10183;
+  ATOM_SATELLITE_INCOMING_DATAGRAM = 10184;
+  ATOM_SATELLITE_OUTGOING_DATAGRAM = 10185;
+  ATOM_SATELLITE_PROVISION = 10186;
+  ATOM_SATELLITE_SOS_MESSAGE_RECOMMENDER = 10187;
+  ATOM_CARRIER_ROAMING_SATELLITE_SESSION = 10211;
+  ATOM_CARRIER_ROAMING_SATELLITE_CONTROLLER_STATS = 10212;
+  ATOM_CONTROLLER_STATS_PER_PACKAGE = 10213;
+  ATOM_SATELLITE_ENTITLEMENT = 10214;
+  ATOM_SATELLITE_CONFIG_UPDATER = 10215;
+  ATOM_SATELLITE_ACCESS_CONTROLLER = 10219;
+  ATOM_CELLULAR_IDENTIFIER_DISCLOSED = 800;
+  ATOM_KEYBOARD_CONFIGURED = 682;
+  ATOM_KEYBOARD_SYSTEMS_EVENT_REPORTED = 683;
+  ATOM_INPUTDEVICE_USAGE_REPORTED = 686;
+  ATOM_TOUCHPAD_USAGE = 10191;
+  ATOM_THREADNETWORK_TELEMETRY_DATA_REPORTED = 738;
+  ATOM_THREADNETWORK_TOPO_ENTRY_REPEATED = 739;
+  ATOM_THREADNETWORK_DEVICE_INFO_REPORTED = 740;
+  ATOM_CRONET_ENGINE_CREATED = 703;
+  ATOM_CRONET_TRAFFIC_REPORTED = 704;
+  ATOM_CRONET_ENGINE_BUILDER_INITIALIZED = 762;
+  ATOM_CRONET_HTTP_FLAGS_INITIALIZED = 763;
+  ATOM_CRONET_INITIALIZED = 764;
+  ATOM_WEAR_MODE_STATE_CHANGED = 715;
+  ATOM_RENDERER_INITIALIZED = 736;
+  ATOM_SCHEMA_VERSION_RECEIVED = 737;
+  ATOM_LAYOUT_INSPECTED = 741;
+  ATOM_LAYOUT_EXPRESSION_INSPECTED = 742;
+  ATOM_LAYOUT_ANIMATIONS_INSPECTED = 743;
+  ATOM_MATERIAL_COMPONENTS_INSPECTED = 744;
+  ATOM_TILE_REQUESTED = 745;
+  ATOM_STATE_RESPONSE_RECEIVED = 746;
+  ATOM_TILE_RESPONSE_RECEIVED = 747;
+  ATOM_INFLATION_FINISHED = 748;
+  ATOM_INFLATION_FAILED = 749;
+  ATOM_IGNORED_INFLATION_FAILURES_REPORTED = 750;
+  ATOM_DRAWABLE_RENDERED = 751;
+  ATOM_MEDIA_ACTION_REPORTED = 608;
+  ATOM_MEDIA_CONTROLS_LAUNCHED = 609;
+  ATOM_MEDIA_SESSION_STATE_CHANGED = 677;
+  ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_DEVICE_SCAN_API_LATENCY = 757;
+  ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_SASS_DEVICE_UNAVAILABLE = 758;
+  ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_FASTPAIR_API_TIMEOUT = 759;
+  ATOM_MEDIATOR_UPDATED = 721;
+  ATOM_SYSPROXY_BLUETOOTH_BYTES_TRANSFER = 10196;
+  ATOM_SYSPROXY_CONNECTION_UPDATED = 786;
+  ATOM_ADAPTIVE_AUTH_UNLOCK_AFTER_LOCK_REPORTED = 820;
+  ATOM_FEDERATED_COMPUTE_API_CALLED = 712;
+  ATOM_FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED = 771;
+  ATOM_EXAMPLE_ITERATOR_NEXT_LATENCY_REPORTED = 838;
+  ATOM_RKPD_POOL_STATS = 664;
+  ATOM_RKPD_CLIENT_OPERATION = 665;
+  ATOM_CPU_POLICY = 10199;
+  ATOM_ATOM_9999 = 9999;
+  ATOM_ATOM_99999 = 99999;
+  ATOM_SCREEN_OFF_REPORTED = 776;
+  ATOM_SCREEN_TIMEOUT_OVERRIDE_REPORTED = 836;
+  ATOM_SCREEN_INTERACTIVE_SESSION_REPORTED = 837;
+  ATOM_SCREEN_DIM_REPORTED = 867;
+  ATOM_FULL_SCREEN_INTENT_LAUNCHED = 631;
+  ATOM_BAL_ALLOWED = 632;
+  ATOM_IN_TASK_ACTIVITY_STARTED = 685;
+  ATOM_CACHED_APPS_HIGH_WATERMARK = 10189;
+  ATOM_STYLUS_PREDICTION_METRICS_REPORTED = 718;
+  ATOM_USER_RISK_EVENT_REPORTED = 725;
+  ATOM_MEDIA_PROJECTION_STATE_CHANGED = 729;
+  ATOM_MEDIA_PROJECTION_TARGET_CHANGED = 730;
+  ATOM_EXCESSIVE_BINDER_PROXY_COUNT_REPORTED = 853;
+  ATOM_PROXY_BYTES_TRANSFER_BY_FG_BG = 10200;
+  ATOM_MOBILE_BYTES_TRANSFER_BY_PROC_STATE = 10204;
+  ATOM_BIOMETRIC_FRR_NOTIFICATION = 817;
+  ATOM_SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION = 830;
+  ATOM_SENSITIVE_NOTIFICATION_APP_PROTECTION_SESSION = 831;
+  ATOM_SENSITIVE_NOTIFICATION_APP_PROTECTION_APPLIED = 832;
+  ATOM_SENSITIVE_NOTIFICATION_REDACTION = 833;
+  ATOM_SENSITIVE_CONTENT_APP_PROTECTION = 835;
+  ATOM_APP_RESTRICTION_STATE_CHANGED = 866;
+  ATOM_DREAM_SETTING_CHANGED = 705;
+  ATOM_DREAM_SETTING_SNAPSHOT = 10192;
+  ATOM_BOOT_INTEGRITY_INFO_REPORTED = 775;
   ATOM_WIFI_AWARE_NDP_REPORTED = 638;
   ATOM_WIFI_AWARE_ATTACH_REPORTED = 639;
   ATOM_WIFI_SELF_RECOVERY_TRIGGERED = 661;
@@ -784,39 +946,99 @@
   ATOM_WIFI_LOCAL_ONLY_REQUEST_SCAN_TRIGGERED = 693;
   ATOM_WIFI_THREAD_TASK_EXECUTED = 694;
   ATOM_WIFI_STATE_CHANGED = 700;
+  ATOM_PNO_SCAN_STARTED = 719;
+  ATOM_PNO_SCAN_STOPPED = 720;
+  ATOM_WIFI_IS_UNUSABLE_REPORTED = 722;
+  ATOM_WIFI_AP_CAPABILITIES_REPORTED = 723;
+  ATOM_SOFT_AP_STATE_CHANGED = 805;
+  ATOM_SCORER_PREDICTION_RESULT_REPORTED = 884;
   ATOM_WIFI_AWARE_CAPABILITIES = 10190;
   ATOM_WIFI_MODULE_INFO = 10193;
-  ATOM_SETTINGS_SPA_REPORTED = 622;
+  ATOM_WIFI_SETTING_INFO = 10194;
+  ATOM_WIFI_COMPLEX_SETTING_INFO = 10195;
+  ATOM_WIFI_CONFIGURED_NETWORK_INFO = 10198;
+  ATOM_MTE_STATE = 10181;
+  ATOM_HOTWORD_EGRESS_SIZE_ATOM_REPORTED = 761;
+  ATOM_SANDBOX_API_CALLED = 488;
+  ATOM_SANDBOX_ACTIVITY_EVENT_OCCURRED = 735;
+  ATOM_SDK_SANDBOX_RESTRICTED_ACCESS_IN_SESSION = 796;
+  ATOM_SANDBOX_SDK_STORAGE = 10159;
   ATOM_EXPRESS_EVENT_REPORTED = 528;
   ATOM_EXPRESS_HISTOGRAM_SAMPLE_REPORTED = 593;
   ATOM_EXPRESS_UID_EVENT_REPORTED = 644;
   ATOM_EXPRESS_UID_HISTOGRAM_SAMPLE_REPORTED = 658;
-  ATOM_PERMISSION_RATIONALE_DIALOG_VIEWED = 645;
-  ATOM_PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED = 646;
-  ATOM_APP_DATA_SHARING_UPDATES_NOTIFICATION_INTERACTION = 647;
-  ATOM_APP_DATA_SHARING_UPDATES_FRAGMENT_VIEWED = 648;
-  ATOM_APP_DATA_SHARING_UPDATES_FRAGMENT_ACTION_REPORTED = 649;
-  ATOM_WS_INCOMING_CALL_ACTION_REPORTED = 626;
-  ATOM_WS_CALL_DISCONNECTION_REPORTED = 627;
-  ATOM_WS_CALL_DURATION_REPORTED = 628;
-  ATOM_WS_CALL_USER_EXPERIENCE_LATENCY_REPORTED = 629;
-  ATOM_WS_CALL_INTERACTION_REPORTED = 630;
-  ATOM_FULL_SCREEN_INTENT_LAUNCHED = 631;
-  ATOM_BAL_ALLOWED = 632;
-  ATOM_IN_TASK_ACTIVITY_STARTED = 685;
-  ATOM_CACHED_APPS_HIGH_WATERMARK = 10189;
-  ATOM_ODREFRESH_REPORTED = 366;
-  ATOM_ODSIGN_REPORTED = 548;
-  ATOM_ART_DATUM_REPORTED = 332;
-  ATOM_ART_DEVICE_DATUM_REPORTED = 550;
-  ATOM_ART_DATUM_DELTA_REPORTED = 565;
-  ATOM_BACKGROUND_DEXOPT_JOB_ENDED = 467;
-  ATOM_WEAR_ADAPTIVE_SUSPEND_STATS_REPORTED = 619;
-  ATOM_WEAR_POWER_ANOMALY_SERVICE_OPERATIONAL_STATS_REPORTED = 620;
-  ATOM_WEAR_POWER_ANOMALY_SERVICE_EVENT_STATS_REPORTED = 621;
-  ATOM_EMERGENCY_STATE_CHANGED = 633;
-  ATOM_DND_STATE_CHANGED = 657;
-  ATOM_MTE_STATE = 10181;
+  ATOM_IKE_SESSION_TERMINATED = 678;
+  ATOM_IKE_LIVENESS_CHECK_SESSION_VALIDATED = 760;
+  ATOM_NEGOTIATED_SECURITY_ASSOCIATION = 821;
+  ATOM_APP_SEARCH_SET_SCHEMA_STATS_REPORTED = 385;
+  ATOM_APP_SEARCH_SCHEMA_MIGRATION_STATS_REPORTED = 579;
+  ATOM_APP_SEARCH_USAGE_SEARCH_INTENT_STATS_REPORTED = 825;
+  ATOM_APP_SEARCH_USAGE_SEARCH_INTENT_RAW_QUERY_STATS_REPORTED = 826;
+  ATOM_DEVICE_POLICY_MANAGEMENT_MODE = 10216;
+  ATOM_DEVICE_POLICY_STATE = 10217;
+  ATOM_DESKTOP_MODE_UI_CHANGED = 818;
+  ATOM_DESKTOP_MODE_SESSION_TASK_UPDATE = 819;
+  ATOM_MEDIA_CODEC_RECLAIM_REQUEST_COMPLETED = 600;
+  ATOM_MEDIA_CODEC_STARTED = 641;
+  ATOM_MEDIA_CODEC_STOPPED = 642;
+  ATOM_MEDIA_CODEC_RENDERED = 684;
+  ATOM_MEDIA_EDITING_ENDED_REPORTED = 798;
+  ATOM_CAR_WAKEUP_FROM_SUSPEND_REPORTED = 852;
+  ATOM_PLUGIN_INITIALIZED = 655;
+  ATOM_CAR_RECENTS_EVENT_REPORTED = 770;
+  ATOM_CAR_CALM_MODE_EVENT_REPORTED = 797;
+  ATOM_CAMERA_FEATURE_COMBINATION_QUERY_EVENT = 900;
+  ATOM_THERMAL_STATUS_CALLED = 772;
+  ATOM_THERMAL_HEADROOM_CALLED = 773;
+  ATOM_THERMAL_HEADROOM_THRESHOLDS_CALLED = 774;
+  ATOM_ADPF_HINT_SESSION_TID_CLEANUP = 839;
+  ATOM_THERMAL_HEADROOM_THRESHOLDS = 10201;
+  ATOM_ADPF_SESSION_SNAPSHOT = 10218;
+  ATOM_BLUETOOTH_HASHED_DEVICE_NAME_REPORTED = 613;
+  ATOM_BLUETOOTH_L2CAP_COC_CLIENT_CONNECTION = 614;
+  ATOM_BLUETOOTH_L2CAP_COC_SERVER_CONNECTION = 615;
+  ATOM_BLUETOOTH_LE_SESSION_CONNECTED = 656;
+  ATOM_RESTRICTED_BLUETOOTH_DEVICE_NAME_REPORTED = 666;
+  ATOM_BLUETOOTH_PROFILE_CONNECTION_ATTEMPTED = 696;
+  ATOM_BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED = 781;
+  ATOM_BLUETOOTH_RFCOMM_CONNECTION_ATTEMPTED = 782;
+  ATOM_REMOTE_DEVICE_INFORMATION_WITH_METRIC_ID = 862;
+  ATOM_LE_APP_SCAN_STATE_CHANGED = 870;
+  ATOM_LE_RADIO_SCAN_STOPPED = 871;
+  ATOM_LE_SCAN_RESULT_RECEIVED = 872;
+  ATOM_LE_SCAN_ABUSED = 873;
+  ATOM_LE_ADV_STATE_CHANGED = 874;
+  ATOM_LE_ADV_ERROR_REPORTED = 875;
+  ATOM_A2DP_SESSION_REPORTED = 904;
+  ATOM_BLUETOOTH_CROSS_LAYER_EVENT_REPORTED = 916;
+  ATOM_BROADCAST_AUDIO_SESSION_REPORTED = 927;
+  ATOM_BROADCAST_AUDIO_SYNC_REPORTED = 928;
+  ATOM_DEVICE_LOCK_CHECK_IN_REQUEST_REPORTED = 726;
+  ATOM_DEVICE_LOCK_PROVISIONING_COMPLETE_REPORTED = 727;
+  ATOM_DEVICE_LOCK_KIOSK_APP_REQUEST_REPORTED = 728;
+  ATOM_DEVICE_LOCK_CHECK_IN_RETRY_REPORTED = 789;
+  ATOM_DEVICE_LOCK_PROVISION_FAILURE_REPORTED = 790;
+  ATOM_DEVICE_LOCK_LOCK_UNLOCK_DEVICE_FAILURE_REPORTED = 791;
+  ATOM_APPLICATION_GRAMMATICAL_INFLECTION_CHANGED = 584;
+  ATOM_SYSTEM_GRAMMATICAL_INFLECTION_CHANGED = 816;
+  ATOM_EMERGENCY_NUMBER_DIALED = 637;
+  ATOM_JSSCRIPTENGINE_LATENCY_REPORTED = 483;
+  ATOM_AD_SERVICES_API_CALLED = 435;
+  ATOM_AD_SERVICES_MESUREMENT_REPORTS_UPLOADED = 436;
+  ATOM_MOBILE_DATA_DOWNLOAD_FILE_GROUP_STATUS_REPORTED = 490;
+  ATOM_MOBILE_DATA_DOWNLOAD_DOWNLOAD_RESULT_REPORTED = 502;
+  ATOM_AD_SERVICES_SETTINGS_USAGE_REPORTED = 493;
+  ATOM_BACKGROUND_FETCH_PROCESS_REPORTED = 496;
+  ATOM_UPDATE_CUSTOM_AUDIENCE_PROCESS_REPORTED = 497;
+  ATOM_RUN_AD_BIDDING_PROCESS_REPORTED = 498;
+  ATOM_RUN_AD_SCORING_PROCESS_REPORTED = 499;
+  ATOM_RUN_AD_SELECTION_PROCESS_REPORTED = 500;
+  ATOM_RUN_AD_BIDDING_PER_CA_PROCESS_REPORTED = 501;
+  ATOM_MOBILE_DATA_DOWNLOAD_FILE_GROUP_STORAGE_STATS_REPORTED = 503;
+  ATOM_AD_SERVICES_MEASUREMENT_REGISTRATIONS = 512;
+  ATOM_AD_SERVICES_GET_TOPICS_REPORTED = 535;
+  ATOM_AD_SERVICES_EPOCH_COMPUTATION_GET_TOP_TOPICS_REPORTED = 536;
+  ATOM_AD_SERVICES_EPOCH_COMPUTATION_CLASSIFIER_REPORTED = 537;
   ATOM_AD_SERVICES_BACK_COMPAT_GET_TOPICS_REPORTED = 598;
   ATOM_AD_SERVICES_BACK_COMPAT_EPOCH_COMPUTATION_CLASSIFIER_REPORTED = 599;
   ATOM_AD_SERVICES_MEASUREMENT_DEBUG_KEYS = 640;
@@ -826,68 +1048,68 @@
   ATOM_AD_SERVICES_MEASUREMENT_ATTRIBUTION = 674;
   ATOM_AD_SERVICES_MEASUREMENT_JOBS = 675;
   ATOM_AD_SERVICES_MEASUREMENT_WIPEOUT = 676;
+  ATOM_AD_SERVICES_MEASUREMENT_AD_ID_MATCH_FOR_DEBUG_KEYS = 695;
+  ATOM_AD_SERVICES_ENROLLMENT_DATA_STORED = 697;
+  ATOM_AD_SERVICES_ENROLLMENT_FILE_DOWNLOADED = 698;
+  ATOM_AD_SERVICES_ENROLLMENT_MATCHED = 699;
   ATOM_AD_SERVICES_CONSENT_MIGRATED = 702;
-  ATOM_RKPD_POOL_STATS = 664;
-  ATOM_RKPD_CLIENT_OPERATION = 665;
-  ATOM_AUTOFILL_UI_EVENT_REPORTED = 603;
-  ATOM_AUTOFILL_FILL_REQUEST_REPORTED = 604;
-  ATOM_AUTOFILL_FILL_RESPONSE_REPORTED = 605;
-  ATOM_AUTOFILL_SAVE_EVENT_REPORTED = 606;
-  ATOM_AUTOFILL_SESSION_COMMITTED = 607;
-  ATOM_AUTOFILL_FIELD_CLASSIFICATION_EVENT_REPORTED = 659;
+  ATOM_AD_SERVICES_ENROLLMENT_FAILED = 714;
+  ATOM_AD_SERVICES_MEASUREMENT_CLICK_VERIFICATION = 756;
+  ATOM_AD_SERVICES_ENCRYPTION_KEY_FETCHED = 765;
+  ATOM_AD_SERVICES_ENCRYPTION_KEY_DB_TRANSACTION_ENDED = 766;
+  ATOM_DESTINATION_REGISTERED_BEACONS = 767;
+  ATOM_REPORT_INTERACTION_API_CALLED = 768;
+  ATOM_INTERACTION_REPORTING_TABLE_CLEARED = 769;
+  ATOM_APP_MANIFEST_CONFIG_HELPER_CALLED = 788;
+  ATOM_AD_FILTERING_PROCESS_JOIN_CA_REPORTED = 793;
+  ATOM_AD_FILTERING_PROCESS_AD_SELECTION_REPORTED = 794;
+  ATOM_AD_COUNTER_HISTOGRAM_UPDATER_REPORTED = 795;
+  ATOM_SIGNATURE_VERIFICATION = 807;
+  ATOM_K_ANON_IMMEDIATE_SIGN_JOIN_STATUS_REPORTED = 808;
+  ATOM_K_ANON_BACKGROUND_JOB_STATUS_REPORTED = 809;
+  ATOM_K_ANON_INITIALIZE_STATUS_REPORTED = 810;
+  ATOM_K_ANON_SIGN_STATUS_REPORTED = 811;
+  ATOM_K_ANON_JOIN_STATUS_REPORTED = 812;
+  ATOM_K_ANON_KEY_ATTESTATION_STATUS_REPORTED = 813;
+  ATOM_GET_AD_SELECTION_DATA_API_CALLED = 814;
+  ATOM_GET_AD_SELECTION_DATA_BUYER_INPUT_GENERATED = 815;
+  ATOM_BACKGROUND_JOB_SCHEDULING_REPORTED = 834;
+  ATOM_TOPICS_ENCRYPTION_EPOCH_COMPUTATION_REPORTED = 840;
+  ATOM_TOPICS_ENCRYPTION_GET_TOPICS_REPORTED = 841;
+  ATOM_ADSERVICES_SHELL_COMMAND_CALLED = 842;
+  ATOM_UPDATE_SIGNALS_API_CALLED = 843;
+  ATOM_ENCODING_JOB_RUN = 844;
+  ATOM_ENCODING_JS_FETCH = 845;
+  ATOM_ENCODING_JS_EXECUTION = 846;
+  ATOM_PERSIST_AD_SELECTION_RESULT_CALLED = 847;
+  ATOM_SERVER_AUCTION_KEY_FETCH_CALLED = 848;
+  ATOM_SERVER_AUCTION_BACKGROUND_KEY_FETCH_ENABLED = 849;
+  ATOM_AD_SERVICES_MEASUREMENT_PROCESS_ODP_REGISTRATION = 864;
+  ATOM_AD_SERVICES_MEASUREMENT_NOTIFY_REGISTRATION_TO_ODP = 865;
+  ATOM_SELECT_ADS_FROM_OUTCOMES_API_CALLED = 876;
+  ATOM_REPORT_IMPRESSION_API_CALLED = 877;
+  ATOM_AD_SERVICES_ENROLLMENT_TRANSACTION_STATS = 885;
+  ATOM_EXTERNAL_DISPLAY_STATE_CHANGED = 806;
+  ATOM_DISPLAY_MODE_DIRECTOR_VOTE_CHANGED = 792;
   ATOM_TEST_EXTENSION_ATOM_REPORTED = 660;
   ATOM_TEST_RESTRICTED_ATOM_REPORTED = 672;
   ATOM_STATS_SOCKET_LOSS_REPORTED = 752;
-  ATOM_PLUGIN_INITIALIZED = 655;
-  ATOM_TV_LOW_POWER_STANDBY_POLICY = 679;
+  ATOM_NFC_OBSERVE_MODE_STATE_CHANGED = 855;
+  ATOM_NFC_FIELD_CHANGED = 856;
+  ATOM_NFC_POLLING_LOOP_NOTIFICATION_REPORTED = 857;
+  ATOM_NFC_PROPRIETARY_CAPABILITIES_REPORTED = 858;
   ATOM_LOCKSCREEN_SHORTCUT_SELECTED = 611;
   ATOM_LOCKSCREEN_SHORTCUT_TRIGGERED = 612;
-  ATOM_EMERGENCY_NUMBERS_INFO = 10180;
-  ATOM_QUALIFIED_RAT_LIST_CHANGED = 634;
-  ATOM_QNS_IMS_CALL_DROP_STATS = 635;
-  ATOM_QNS_FALLBACK_RESTRICTION_CHANGED = 636;
-  ATOM_QNS_RAT_PREFERENCE_MISMATCH_INFO = 10177;
-  ATOM_QNS_HANDOVER_TIME_MILLIS = 10178;
-  ATOM_QNS_HANDOVER_PINGPONG = 10179;
-  ATOM_SATELLITE_CONTROLLER = 10182;
-  ATOM_SATELLITE_SESSION = 10183;
-  ATOM_SATELLITE_INCOMING_DATAGRAM = 10184;
-  ATOM_SATELLITE_OUTGOING_DATAGRAM = 10185;
-  ATOM_SATELLITE_PROVISION = 10186;
-  ATOM_SATELLITE_SOS_MESSAGE_RECOMMENDER = 10187;
-  ATOM_IKE_SESSION_TERMINATED = 678;
-  ATOM_IKE_LIVENESS_CHECK_SESSION_VALIDATED = 760;
-  ATOM_BLUETOOTH_HASHED_DEVICE_NAME_REPORTED = 613;
-  ATOM_BLUETOOTH_L2CAP_COC_CLIENT_CONNECTION = 614;
-  ATOM_BLUETOOTH_L2CAP_COC_SERVER_CONNECTION = 615;
-  ATOM_BLUETOOTH_LE_SESSION_CONNECTED = 656;
-  ATOM_RESTRICTED_BLUETOOTH_DEVICE_NAME_REPORTED = 666;
-  ATOM_BLUETOOTH_PROFILE_CONNECTION_ATTEMPTED = 696;
-  ATOM_HEALTH_CONNECT_UI_IMPRESSION = 623;
-  ATOM_HEALTH_CONNECT_UI_INTERACTION = 624;
-  ATOM_HEALTH_CONNECT_APP_OPENED_REPORTED = 625;
-  ATOM_HEALTH_CONNECT_API_CALLED = 616;
-  ATOM_HEALTH_CONNECT_USAGE_STATS = 617;
-  ATOM_HEALTH_CONNECT_STORAGE_STATS = 618;
-  ATOM_HEALTH_CONNECT_API_INVOKED = 643;
-  ATOM_EXERCISE_ROUTE_API_CALLED = 654;
-  ATOM_ATOM_9999 = 9999;
-  ATOM_ATOM_99999 = 99999;
-  ATOM_THREADNETWORK_TELEMETRY_DATA_REPORTED = 738;
-  ATOM_THREADNETWORK_TOPO_ENTRY_REPEATED = 739;
-  ATOM_THREADNETWORK_DEVICE_INFO_REPORTED = 740;
-  ATOM_EMERGENCY_NUMBER_DIALED = 637;
-  ATOM_SANDBOX_API_CALLED = 488;
-  ATOM_SANDBOX_ACTIVITY_EVENT_OCCURRED = 735;
-  ATOM_SANDBOX_SDK_STORAGE = 10159;
-  ATOM_CRONET_ENGINE_CREATED = 703;
-  ATOM_CRONET_TRAFFIC_REPORTED = 704;
-  ATOM_CRONET_ENGINE_BUILDER_INITIALIZED = 762;
-  ATOM_CRONET_HTTP_FLAGS_INITIALIZED = 763;
-  ATOM_CRONET_INITIALIZED = 764;
-  ATOM_DAILY_KEEPALIVE_INFO_REPORTED = 650;
-  ATOM_IP_CLIENT_RA_INFO_REPORTED = 778;
-  ATOM_APF_SESSION_INFO_REPORTED = 777;
+  ATOM_LAUNCHER_IMPRESSION_EVENT_V2 = 716;
+  ATOM_DISPLAY_SWITCH_LATENCY_TRACKED = 753;
+  ATOM_NOTIFICATION_LISTENER_SERVICE = 829;
+  ATOM_NAV_HANDLE_TOUCH_POINTS = 869;
+  ATOM_WEAR_ADAPTIVE_SUSPEND_STATS_REPORTED = 619;
+  ATOM_WEAR_POWER_ANOMALY_SERVICE_OPERATIONAL_STATS_REPORTED = 620;
+  ATOM_WEAR_POWER_ANOMALY_SERVICE_EVENT_STATS_REPORTED = 621;
+  ATOM_APEX_INSTALLATION_REQUESTED = 732;
+  ATOM_APEX_INSTALLATION_STAGED = 733;
+  ATOM_APEX_INSTALLATION_ENDED = 734;
   ATOM_CREDENTIAL_MANAGER_API_CALLED = 585;
   ATOM_CREDENTIAL_MANAGER_INIT_PHASE_REPORTED = 651;
   ATOM_CREDENTIAL_MANAGER_CANDIDATE_PHASE_REPORTED = 652;
@@ -898,10 +1120,5 @@
   ATOM_CREDENTIAL_MANAGER_AUTH_CLICK_REPORTED = 670;
   ATOM_CREDENTIAL_MANAGER_APIV2_CALLED = 671;
   ATOM_UWB_ACTIVITY_INFO = 10188;
-  ATOM_MEDIA_ACTION_REPORTED = 608;
-  ATOM_MEDIA_CONTROLS_LAUNCHED = 609;
-  ATOM_MEDIA_CODEC_RECLAIM_REQUEST_COMPLETED = 600;
-  ATOM_MEDIA_CODEC_STARTED = 641;
-  ATOM_MEDIA_CODEC_STOPPED = 642;
-  ATOM_MEDIA_CODEC_RENDERED = 684;
+  ATOM_DND_STATE_CHANGED = 657;
 }
\ No newline at end of file
diff --git a/protos/perfetto/config/sys_stats/sys_stats_config.proto b/protos/perfetto/config/sys_stats/sys_stats_config.proto
index 3466255..fc16456 100644
--- a/protos/perfetto/config/sys_stats/sys_stats_config.proto
+++ b/protos/perfetto/config/sys_stats/sys_stats_config.proto
@@ -87,4 +87,8 @@
   // Polls /sys/devices/system/cpu/cpu*/cpuidle/state* every X ms, if non-zero.
   // This is required to be > 10ms to avoid excessive CPU usage.
   optional uint32 cpuidle_period_ms = 13;
+
+  // Polls device-specific GPU frequency info every X ms, if non-zero.
+  // This is required to be > 10ms to avoid excessive CPU usage.
+  optional uint32 gpufreq_period_ms = 14;
 }
diff --git a/protos/perfetto/config/trace_config.proto b/protos/perfetto/config/trace_config.proto
index 8fe4424..255ae7e 100644
--- a/protos/perfetto/config/trace_config.proto
+++ b/protos/perfetto/config/trace_config.proto
@@ -438,11 +438,8 @@
   }
   optional IncrementalStateConfig incremental_state_config = 21;
 
-  // Additional guardrail used by the Perfetto command line client.
-  // On user builds when --dropbox is set perfetto will refuse to trace unless
-  // this is also set.
-  // Added in Q.
-  optional bool allow_user_build_tracing = 19;
+  // No longer needed as we unconditionally allow tracing on user builds.
+  optional bool allow_user_build_tracing = 19 [deprecated = true];
 
   // If set the tracing service will ensure there is at most one tracing session
   // with this key.
diff --git a/protos/perfetto/ipc/BUILD.gn b/protos/perfetto/ipc/BUILD.gn
index 6d7eccb..a2d3fac 100644
--- a/protos/perfetto/ipc/BUILD.gn
+++ b/protos/perfetto/ipc/BUILD.gn
@@ -21,7 +21,6 @@
   proto_generators = [
     "ipc",
     "cpp",
-    "source_set",
   ]
   sources = [
     "consumer_port.proto",
@@ -41,7 +40,6 @@
   proto_generators = [
     "zero",
     "cpp",
-    "source_set",
   ]
   sources = [ "wire_protocol.proto" ]
 }
diff --git a/protos/perfetto/ipc/consumer_port.proto b/protos/perfetto/ipc/consumer_port.proto
index d7a654c..a38e0eb 100644
--- a/protos/perfetto/ipc/consumer_port.proto
+++ b/protos/perfetto/ipc/consumer_port.proto
@@ -287,9 +287,15 @@
 
 // Arguments for rpc CloneSession.
 message CloneSessionRequest {
-  // The session ID to clone. If session_id == kBugreportSessionId (0xff...ff)
-  // the session with the highest bugreport score is cloned (if any exists).
-  optional uint64 session_id = 1;
+  oneof selector {
+    // The session ID to clone. If session_id == kBugreportSessionId (0xff...ff)
+    // the session with the highest bugreport score is cloned (if any exists).
+    uint64 session_id = 1;
+
+    // The unique_session_name of the tracing session to clone. Tracing sessions
+    // that are clones of other tracing sessions are ignored.
+    string unique_session_name = 4;
+  }
 
   // If set, the trace filter will not have effect on the cloned session.
   // Used for bugreports.
diff --git a/protos/perfetto/metrics/BUILD.gn b/protos/perfetto/metrics/BUILD.gn
index e09200c..d90ef8c 100644
--- a/protos/perfetto/metrics/BUILD.gn
+++ b/protos/perfetto/metrics/BUILD.gn
@@ -16,26 +16,15 @@
 import("../../../gn/proto_library.gni")
 
 perfetto_proto_library("@TYPE@") {
-  proto_generators = [
-    "lite",
-    "source_set",
-  ]
+  proto_generators = [ "lite" ]
   deps = [ "android:@TYPE@" ]
   sources = [ "metrics.proto" ]
+  generate_descriptor = "metrics.descriptor"
+  descriptor_root_source = "metrics.proto"
 }
 
 perfetto_proto_library("custom_options_@TYPE@") {
-  proto_generators = [
-    "lite",
-    "source_set",
-  ]
+  proto_generators = [ "lite" ]
   sources = [ "custom_options.proto" ]
   import_dirs = [ "${perfetto_protobuf_src_dir}" ]
 }
-
-perfetto_proto_library("descriptor") {
-  proto_generators = [ "descriptor" ]
-  generate_descriptor = "metrics.descriptor"
-  deps = [ ":source_set" ]
-  sources = [ "metrics.proto" ]
-}
diff --git a/protos/perfetto/metrics/android/BUILD.gn b/protos/perfetto/metrics/android/BUILD.gn
index fc3cfb4..ba0f60f 100644
--- a/protos/perfetto/metrics/android/BUILD.gn
+++ b/protos/perfetto/metrics/android/BUILD.gn
@@ -15,10 +15,7 @@
 import("../../../../gn/proto_library.gni")
 
 perfetto_proto_library("@TYPE@") {
-  proto_generators = [
-    "lite",
-    "source_set",
-  ]
+  proto_generators = [ "lite" ]
   sources = [
     "ad_services_metric.proto",
     "android_anomaly_metric.proto",
@@ -32,7 +29,6 @@
     "android_garbage_collection_unagg_metric.proto",
     "android_oom_adjuster_metric.proto",
     "android_sysui_notifications_blocking_calls_metric.proto",
-    "android_trusty_workqueues.proto",
     "anr_metric.proto",
     "app_process_starts_metric.proto",
     "auto_metric.proto",
@@ -66,7 +62,6 @@
     "monitor_contention_metric.proto",
     "multiuser_metric.proto",
     "network_metric.proto",
-    "other_traces.proto",
     "package_list.proto",
     "powrails_metric.proto",
     "process_metadata.proto",
diff --git a/protos/perfetto/metrics/android/android_trusty_workqueues.proto b/protos/perfetto/metrics/android/android_trusty_workqueues.proto
deleted file mode 100644
index b7a1d5a..0000000
--- a/protos/perfetto/metrics/android/android_trusty_workqueues.proto
+++ /dev/null
@@ -1,22 +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.
- */
-
-syntax = "proto2";
-
-package perfetto.protos;
-
-// Metric used to generate a simplified view of the Trusty kworker events.
-message AndroidTrustyWorkqueues {}
diff --git a/protos/perfetto/metrics/android/batt_metric.proto b/protos/perfetto/metrics/android/batt_metric.proto
index 015a2a8..792dc0d 100644
--- a/protos/perfetto/metrics/android/batt_metric.proto
+++ b/protos/perfetto/metrics/android/batt_metric.proto
@@ -42,6 +42,10 @@
     optional int64 sleep_screen_off_ns = 6;
     optional int64 sleep_screen_on_ns = 7;
     optional int64 sleep_screen_doze_ns = 8;
+    // Average power over the duration of the trace.
+    optional double avg_power_mw = 9;
+    // Energy usage estimate in joules.
+    optional double energy_usage_estimate = 10;
   }
 
   // Period of time during the trace that the device went to sleep completely.
diff --git a/protos/perfetto/metrics/android/codec_metrics.proto b/protos/perfetto/metrics/android/codec_metrics.proto
index 2f8b94d..b95dbe6 100644
--- a/protos/perfetto/metrics/android/codec_metrics.proto
+++ b/protos/perfetto/metrics/android/codec_metrics.proto
@@ -34,6 +34,8 @@
     optional int64 total_cpu_ns = 2;
     // CPU time ( time 'Running' on cpu)
     optional int64 running_cpu_ns = 3;
+    // CPU cycles
+    optional int64 cpu_cycles = 4;
   }
 
   // These are traces and could indicate framework queue latency
@@ -67,7 +69,34 @@
     repeated AndroidCpuMetric.CoreTypeData core_data = 5;
   }
 
+  // Rail details
+  message Rail {
+    // name of rail
+    optional string name = 1;
+    // energy and power details of this rail
+    message Info {
+      // energy from this rail for codec use
+      optional double energy = 1;
+      // power consumption in this rail for codec use
+      optional double power_mw = 2;
+    }
+    optional Info info = 2;
+  }
+
+  // have the energy usage for the codec running time
+  message Energy {
+    // total energy taken by the system during this time
+    optional double total_energy = 1;
+    // total time for this energy is calculated
+    optional int64 duration = 2;
+    //  for this session
+    optional double power_mw = 3;
+    // enery breakdown by subsystem
+    repeated Rail rail = 4;
+  }
+
   repeated CpuUsage cpu_usage = 1;
   repeated CodecFunction codec_function = 2;
+  optional Energy energy = 3;
 
 }
diff --git a/protos/perfetto/metrics/android/other_traces.proto b/protos/perfetto/metrics/android/other_traces.proto
deleted file mode 100644
index 1795ee2..0000000
--- a/protos/perfetto/metrics/android/other_traces.proto
+++ /dev/null
@@ -1,25 +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.
- */
-
-syntax = "proto2";
-
-package perfetto.protos;
-
-message AndroidOtherTracesMetric {
-  // Uuids of other traces being finalized while the current trace was being
-  // recorded.
-  repeated string finalized_traces_uuid = 1;
-}
diff --git a/protos/perfetto/metrics/android/startup_metric.proto b/protos/perfetto/metrics/android/startup_metric.proto
index f228036..86c206c 100644
--- a/protos/perfetto/metrics/android/startup_metric.proto
+++ b/protos/perfetto/metrics/android/startup_metric.proto
@@ -47,7 +47,7 @@
 
   // Timing information spanning the intent received by the
   // activity manager to the first frame drawn.
-  // Next id: 36.
+  // Next id: 38
   message ToFirstFrame {
     // The duration between the intent received and first frame.
     optional int64 dur_ns = 1;
@@ -96,6 +96,8 @@
 
     optional Slice time_post_fork = 16;
 
+    // Total time on class initialization during app startup.
+    optional Slice time_class_initialization = 36;
     // The total time spent on opening dex files.
     optional Slice time_dex_open = 24;
     // Total time spent verifying classes during app startup.
@@ -104,6 +106,9 @@
     // Number of methods that were compiled by JIT during app startup.
     optional uint32 jit_compiled_methods = 27;
 
+    // Number of class initializations during app startup.
+    optional uint32 class_initialization_count = 37;
+
     // Time spent running CPU on jit thread pool.
     optional Slice time_jit_thread_pool_on_cpu = 28;
 
@@ -298,8 +303,8 @@
     // sorted by the duration in descending order.
     // By checking out the top slices/threads, developers can identify specific
     // slices or threads for further investigation.
-    repeated TraceSliceSection trace_slice_sections = 7;
-    repeated TraceThreadSection trace_thread_sections = 8;
+    optional TraceSliceSectionInfo trace_slice_sections = 7;
+    optional TraceThreadSectionInfo trace_thread_sections = 8;
 
     // Details specific for a reason.
     optional string additional_info = 9;
@@ -344,6 +349,17 @@
     optional uint32 slice_id = 3;
 
     optional string slice_name = 4;
+
+    optional uint32 process_pid = 5;
+
+    optional uint32 thread_tid = 6;
+  }
+
+  // Information for the SliceSections
+  message TraceSliceSectionInfo {
+    repeated TraceSliceSection slice_section = 1;
+    optional int64 start_timestamp = 2;
+    optional int64 end_timestamp = 3;
   }
 
   // Contains information for a section of a thread.
@@ -352,12 +368,24 @@
 
     optional int64 end_timestamp = 2;
 
+    // Deprecated as of 09/2024
     optional uint32 thread_utid = 3;
 
     optional string thread_name = 4;
+
+    optional uint32 process_pid = 5;
+
+    optional uint32 thread_tid = 6;
   }
 
-  // Next id: 25
+  // Information for the ThreadSections
+  message TraceThreadSectionInfo {
+    repeated TraceThreadSection thread_section = 1;
+    optional int64 start_timestamp  = 2;
+    optional int64 end_timestamp = 3;
+  }
+
+  // Next id: 26
   message Startup {
     // Random id uniquely identifying an app startup in this trace.
     optional uint32 startup_id = 1;
@@ -365,6 +393,9 @@
     // Startup type (cold / warm / hot)
     optional string startup_type = 16;
 
+    // Number of CPUs the device has
+    optional uint32 cpu_count = 25;
+
     // Name of the package launched
     optional string package_name = 2;
 
diff --git a/protos/perfetto/metrics/android/wattson_in_time_period.proto b/protos/perfetto/metrics/android/wattson_in_time_period.proto
index 2f02e4d..495de8c 100644
--- a/protos/perfetto/metrics/android/wattson_in_time_period.proto
+++ b/protos/perfetto/metrics/android/wattson_in_time_period.proto
@@ -19,8 +19,13 @@
 package perfetto.protos;
 
 message AndroidWattsonTimePeriodMetric {
+  // Each version increment means updated structure format or field
   optional int32 metric_version = 1;
-  repeated AndroidWattsonEstimateInfo period_info = 2;
+  // Each version increment means power model has been updated and estimates
+  // might change for the exact same input. Don't compare estimates across
+  // different power model versions.
+  optional int32 power_model_version = 2;
+  repeated AndroidWattsonEstimateInfo period_info = 3;
 }
 
 message AndroidWattsonEstimateInfo {
@@ -31,34 +36,38 @@
 
 message AndroidWattsonCpuSubsystemEstimate {
   // estimates and estimates of subrails
-  optional float estimate_mw = 1;
-  optional AndroidWattsonPolicyEstimate policy0 = 2;
-  optional AndroidWattsonPolicyEstimate policy1 = 3;
-  optional AndroidWattsonPolicyEstimate policy2 = 4;
-  optional AndroidWattsonPolicyEstimate policy3 = 5;
-  optional AndroidWattsonPolicyEstimate policy4 = 6;
-  optional AndroidWattsonPolicyEstimate policy5 = 7;
-  optional AndroidWattsonPolicyEstimate policy6 = 8;
-  optional AndroidWattsonPolicyEstimate policy7 = 9;
-  optional AndroidWattsonDsuScuEstimate dsu_scu = 10;
+  optional float estimated_mw = 1;
+  optional float estimated_mws = 2;
+  optional AndroidWattsonPolicyEstimate policy0 = 3;
+  optional AndroidWattsonPolicyEstimate policy1 = 4;
+  optional AndroidWattsonPolicyEstimate policy2 = 5;
+  optional AndroidWattsonPolicyEstimate policy3 = 6;
+  optional AndroidWattsonPolicyEstimate policy4 = 7;
+  optional AndroidWattsonPolicyEstimate policy5 = 8;
+  optional AndroidWattsonPolicyEstimate policy6 = 9;
+  optional AndroidWattsonPolicyEstimate policy7 = 10;
+  optional AndroidWattsonDsuScuEstimate dsu_scu = 11;
 }
 
 message AndroidWattsonPolicyEstimate {
-  optional float estimate_mw = 1;
-  optional AndroidWattsonCpuEstimate cpu0 = 2;
-  optional AndroidWattsonCpuEstimate cpu1 = 3;
-  optional AndroidWattsonCpuEstimate cpu2 = 4;
-  optional AndroidWattsonCpuEstimate cpu3 = 5;
-  optional AndroidWattsonCpuEstimate cpu4 = 6;
-  optional AndroidWattsonCpuEstimate cpu5 = 7;
-  optional AndroidWattsonCpuEstimate cpu6 = 8;
-  optional AndroidWattsonCpuEstimate cpu7 = 9;
+  optional float estimated_mw = 1;
+  optional float estimated_mws = 2;
+  optional AndroidWattsonCpuEstimate cpu0 = 3;
+  optional AndroidWattsonCpuEstimate cpu1 = 4;
+  optional AndroidWattsonCpuEstimate cpu2 = 5;
+  optional AndroidWattsonCpuEstimate cpu3 = 6;
+  optional AndroidWattsonCpuEstimate cpu4 = 7;
+  optional AndroidWattsonCpuEstimate cpu5 = 8;
+  optional AndroidWattsonCpuEstimate cpu6 = 9;
+  optional AndroidWattsonCpuEstimate cpu7 = 10;
 }
 
 message AndroidWattsonCpuEstimate {
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
+  optional float estimated_mws = 2;
 }
 
 message AndroidWattsonDsuScuEstimate {
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
+  optional float estimated_mws = 2;
 }
diff --git a/protos/perfetto/metrics/android/wattson_tasks_attribution.proto b/protos/perfetto/metrics/android/wattson_tasks_attribution.proto
index f505c04..d3b91f4 100644
--- a/protos/perfetto/metrics/android/wattson_tasks_attribution.proto
+++ b/protos/perfetto/metrics/android/wattson_tasks_attribution.proto
@@ -19,19 +19,32 @@
 package perfetto.protos;
 
 message AndroidWattsonTasksAttributionMetric {
+  // Each version increment means updated structure format or field
   optional int32 metric_version = 1;
+  // Each version increment means power model has been updated and estimates
+  // might change for the exact same input. Don't compare estimates across
+  // different power model versions.
+  optional int32 power_model_version = 2;
   // Lists tasks (e.g. threads, process, package) and associated estimates
+  repeated AndroidWattsonTaskPeriodInfo period_info = 3;
+}
+
+// Groups of power per task for each period
+message AndroidWattsonTaskPeriodInfo {
+  optional int32 period_id = 1;
   repeated AndroidWattsonTaskInfo task_info = 2;
 }
 
 message AndroidWattsonTaskInfo {
   // Average estimated power for wall duration in mW
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
   // Total energy over wall duration across CPUs in mWs
-  optional float estimate_mws = 2;
-  optional string thread_name = 3;
-  optional string process_name = 4;
-  optional string package_name = 5;
-  optional int32 thread_id = 6;
-  optional int32 process_id = 7;
+  optional float estimated_mws = 2;
+  // Energy attributed to a thread for causing CPU idle exit
+  optional float idle_transitions_mws = 3;
+  optional string thread_name = 4;
+  optional string process_name = 5;
+  optional string package_name = 6;
+  optional int32 thread_id = 7;
+  optional int32 process_id = 8;
 }
diff --git a/protos/perfetto/metrics/chrome/BUILD.gn b/protos/perfetto/metrics/chrome/BUILD.gn
index a8b6bc2..bf47885 100644
--- a/protos/perfetto/metrics/chrome/BUILD.gn
+++ b/protos/perfetto/metrics/chrome/BUILD.gn
@@ -15,10 +15,7 @@
 import("../../../../gn/proto_library.gni")
 
 perfetto_proto_library("@TYPE@") {
-  proto_generators = [
-    "lite",
-    "source_set",
-  ]
+  proto_generators = [ "lite" ]
   import_dirs = [ "${perfetto_protobuf_src_dir}" ]
   deps = [
     "..:@TYPE@",
@@ -42,12 +39,6 @@
     "unsymbolized_args.proto",
     "user_event_hashes.proto",
   ]
-}
-
-perfetto_proto_library("descriptor") {
-  proto_generators = [ "descriptor" ]
-  import_dirs = [ "${perfetto_protobuf_src_dir}" ]
   generate_descriptor = "all_chrome_metrics.descriptor"
-  deps = [ ":source_set" ]
-  sources = [ "all_chrome_metrics.proto" ]
+  descriptor_root_source = "all_chrome_metrics.proto"
 }
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index 9efe6fd..9ab7eda 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -57,7 +57,6 @@
 import "protos/perfetto/metrics/android/mem_unagg_metric.proto";
 import "protos/perfetto/metrics/android/multiuser_metric.proto";
 import "protos/perfetto/metrics/android/network_metric.proto";
-import "protos/perfetto/metrics/android/other_traces.proto";
 import "protos/perfetto/metrics/android/package_list.proto";
 import "protos/perfetto/metrics/android/powrails_metric.proto";
 import "protos/perfetto/metrics/android/profiler_smaps.proto";
@@ -67,7 +66,6 @@
 import "protos/perfetto/metrics/android/surfaceflinger.proto";
 import "protos/perfetto/metrics/android/task_names.proto";
 import "protos/perfetto/metrics/android/trace_quality.proto";
-import "protos/perfetto/metrics/android/android_trusty_workqueues.proto";
 import "protos/perfetto/metrics/android/unsymbolized_frames.proto";
 import "protos/perfetto/metrics/android/binder_metric.proto";
 import "protos/perfetto/metrics/android/monitor_contention_metric.proto";
@@ -80,11 +78,13 @@
 import "protos/perfetto/metrics/android/wattson_tasks_attribution.proto";
 
 // Trace processor metadata
+// Next id: 17
 message TraceMetadata {
   reserved 1;
   optional int64 trace_duration_ns = 2;
   optional string trace_uuid = 3;
   optional string android_build_fingerprint = 4;
+  optional string android_device_manufacturer = 16;
   optional int64 statsd_triggering_subscription_id = 5;
   optional int64 trace_size_bytes = 6;
   repeated string trace_trigger = 7;
@@ -92,6 +92,10 @@
   optional string trace_config_pbtxt = 9;
   optional int64 sched_duration_ns = 10;
   optional int64 tracing_started_ns = 11;
+  optional int64 android_sdk_version = 12;
+  optional int64 suspend_count = 13;
+  optional int64 data_loss_count = 14;
+  optional int64 error_count = 15;
 }
 
 // Stats counters for the trace.
@@ -124,7 +128,7 @@
 
 // Root message for all Perfetto-based metrics.
 //
-// Next id: 73
+// Next id: 75
 message TraceMetrics {
   reserved 4, 10, 13, 14, 16, 19;
 
@@ -247,11 +251,13 @@
   // Metrics for IRQ runtime.
   optional AndroidIrqRuntimeMetric android_irq_runtime = 43;
 
-  // Metrics for the Trusty team.
-  optional AndroidTrustyWorkqueues android_trusty_workqueues = 44;
+  // Was metrics for the Trusty team.
+  reserved 44;
+  reserved 'android_trusty_workqueues';
 
-  // Summary of other concurrent trace recording.
-  optional AndroidOtherTracesMetric android_other_traces = 45;
+  // Was summary of concurrent trace recording.
+  reserved 45;
+  reserved 'android_other_traces';
 
   // Per-process Binder transaction metrics.
   optional AndroidBinderMetric android_binder = 46;
@@ -319,8 +325,8 @@
   // Android Broadcasts aggregated metrics
   optional AndroidBroadcastsMetric android_broadcasts = 68;
 
-  // Android Wattson app startup metrics.
-  optional AndroidWattsonTimePeriodMetric wattson_app_startup = 69;
+  // Android Wattson rail estimate for each app startup.
+  optional AndroidWattsonTimePeriodMetric wattson_app_startup_rails = 69;
 
   // Android Wattson rail estimate for duration of entire trace.
   optional AndroidWattsonTimePeriodMetric wattson_trace_rails = 70;
@@ -331,6 +337,12 @@
   // Android Wattson app startup metrics.
   optional AndroidWattsonTasksAttributionMetric wattson_trace_threads = 72;
 
+  // Android Wattson thread attribution during markers time window.
+  optional AndroidWattsonTasksAttributionMetric wattson_markers_threads = 73;
+
+  // Android Wattson estimate during markers time window.
+  optional AndroidWattsonTimePeriodMetric wattson_markers_rails = 74;
+
   // 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 f620981..e2fc3a3 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -527,13 +527,6 @@
 
 // End of protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto
 
-// Begin of protos/perfetto/metrics/android/android_trusty_workqueues.proto
-
-// Metric used to generate a simplified view of the Trusty kworker events.
-message AndroidTrustyWorkqueues {}
-
-// End of protos/perfetto/metrics/android/android_trusty_workqueues.proto
-
 // Begin of protos/perfetto/metrics/android/anr_metric.proto
 
  message AndroidAnrMetric {
@@ -621,6 +614,10 @@
     optional int64 sleep_screen_off_ns = 6;
     optional int64 sleep_screen_on_ns = 7;
     optional int64 sleep_screen_doze_ns = 8;
+    // Average power over the duration of the trace.
+    optional double avg_power_mw = 9;
+    // Energy usage estimate in joules.
+    optional double energy_usage_estimate = 10;
   }
 
   // Period of time during the trace that the device went to sleep completely.
@@ -861,6 +858,8 @@
     optional int64 total_cpu_ns = 2;
     // CPU time ( time 'Running' on cpu)
     optional int64 running_cpu_ns = 3;
+    // CPU cycles
+    optional int64 cpu_cycles = 4;
   }
 
   // These are traces and could indicate framework queue latency
@@ -894,8 +893,35 @@
     repeated AndroidCpuMetric.CoreTypeData core_data = 5;
   }
 
+  // Rail details
+  message Rail {
+    // name of rail
+    optional string name = 1;
+    // energy and power details of this rail
+    message Info {
+      // energy from this rail for codec use
+      optional double energy = 1;
+      // power consumption in this rail for codec use
+      optional double power_mw = 2;
+    }
+    optional Info info = 2;
+  }
+
+  // have the energy usage for the codec running time
+  message Energy {
+    // total energy taken by the system during this time
+    optional double total_energy = 1;
+    // total time for this energy is calculated
+    optional int64 duration = 2;
+    //  for this session
+    optional double power_mw = 3;
+    // enery breakdown by subsystem
+    repeated Rail rail = 4;
+  }
+
   repeated CpuUsage cpu_usage = 1;
   repeated CodecFunction codec_function = 2;
+  optional Energy energy = 3;
 
 }
 
@@ -2097,16 +2123,6 @@
 
 // End of protos/perfetto/metrics/android/network_metric.proto
 
-// Begin of protos/perfetto/metrics/android/other_traces.proto
-
-message AndroidOtherTracesMetric {
-  // Uuids of other traces being finalized while the current trace was being
-  // recorded.
-  repeated string finalized_traces_uuid = 1;
-}
-
-// End of protos/perfetto/metrics/android/other_traces.proto
-
 // Begin of protos/perfetto/metrics/android/package_list.proto
 
 message AndroidPackageList {
@@ -2269,7 +2285,7 @@
 
   // Timing information spanning the intent received by the
   // activity manager to the first frame drawn.
-  // Next id: 36.
+  // Next id: 38
   message ToFirstFrame {
     // The duration between the intent received and first frame.
     optional int64 dur_ns = 1;
@@ -2318,6 +2334,8 @@
 
     optional Slice time_post_fork = 16;
 
+    // Total time on class initialization during app startup.
+    optional Slice time_class_initialization = 36;
     // The total time spent on opening dex files.
     optional Slice time_dex_open = 24;
     // Total time spent verifying classes during app startup.
@@ -2326,6 +2344,9 @@
     // Number of methods that were compiled by JIT during app startup.
     optional uint32 jit_compiled_methods = 27;
 
+    // Number of class initializations during app startup.
+    optional uint32 class_initialization_count = 37;
+
     // Time spent running CPU on jit thread pool.
     optional Slice time_jit_thread_pool_on_cpu = 28;
 
@@ -2520,8 +2541,8 @@
     // sorted by the duration in descending order.
     // By checking out the top slices/threads, developers can identify specific
     // slices or threads for further investigation.
-    repeated TraceSliceSection trace_slice_sections = 7;
-    repeated TraceThreadSection trace_thread_sections = 8;
+    optional TraceSliceSectionInfo trace_slice_sections = 7;
+    optional TraceThreadSectionInfo trace_thread_sections = 8;
 
     // Details specific for a reason.
     optional string additional_info = 9;
@@ -2566,6 +2587,17 @@
     optional uint32 slice_id = 3;
 
     optional string slice_name = 4;
+
+    optional uint32 process_pid = 5;
+
+    optional uint32 thread_tid = 6;
+  }
+
+  // Information for the SliceSections
+  message TraceSliceSectionInfo {
+    repeated TraceSliceSection slice_section = 1;
+    optional int64 start_timestamp = 2;
+    optional int64 end_timestamp = 3;
   }
 
   // Contains information for a section of a thread.
@@ -2574,12 +2606,24 @@
 
     optional int64 end_timestamp = 2;
 
+    // Deprecated as of 09/2024
     optional uint32 thread_utid = 3;
 
     optional string thread_name = 4;
+
+    optional uint32 process_pid = 5;
+
+    optional uint32 thread_tid = 6;
   }
 
-  // Next id: 25
+  // Information for the ThreadSections
+  message TraceThreadSectionInfo {
+    repeated TraceThreadSection thread_section = 1;
+    optional int64 start_timestamp  = 2;
+    optional int64 end_timestamp = 3;
+  }
+
+  // Next id: 26
   message Startup {
     // Random id uniquely identifying an app startup in this trace.
     optional uint32 startup_id = 1;
@@ -2587,6 +2631,9 @@
     // Startup type (cold / warm / hot)
     optional string startup_type = 16;
 
+    // Number of CPUs the device has
+    optional uint32 cpu_count = 25;
+
     // Name of the package launched
     optional string package_name = 2;
 
@@ -2883,8 +2930,13 @@
 // Begin of protos/perfetto/metrics/android/wattson_in_time_period.proto
 
 message AndroidWattsonTimePeriodMetric {
+  // Each version increment means updated structure format or field
   optional int32 metric_version = 1;
-  repeated AndroidWattsonEstimateInfo period_info = 2;
+  // Each version increment means power model has been updated and estimates
+  // might change for the exact same input. Don't compare estimates across
+  // different power model versions.
+  optional int32 power_model_version = 2;
+  repeated AndroidWattsonEstimateInfo period_info = 3;
 }
 
 message AndroidWattsonEstimateInfo {
@@ -2895,36 +2947,40 @@
 
 message AndroidWattsonCpuSubsystemEstimate {
   // estimates and estimates of subrails
-  optional float estimate_mw = 1;
-  optional AndroidWattsonPolicyEstimate policy0 = 2;
-  optional AndroidWattsonPolicyEstimate policy1 = 3;
-  optional AndroidWattsonPolicyEstimate policy2 = 4;
-  optional AndroidWattsonPolicyEstimate policy3 = 5;
-  optional AndroidWattsonPolicyEstimate policy4 = 6;
-  optional AndroidWattsonPolicyEstimate policy5 = 7;
-  optional AndroidWattsonPolicyEstimate policy6 = 8;
-  optional AndroidWattsonPolicyEstimate policy7 = 9;
-  optional AndroidWattsonDsuScuEstimate dsu_scu = 10;
+  optional float estimated_mw = 1;
+  optional float estimated_mws = 2;
+  optional AndroidWattsonPolicyEstimate policy0 = 3;
+  optional AndroidWattsonPolicyEstimate policy1 = 4;
+  optional AndroidWattsonPolicyEstimate policy2 = 5;
+  optional AndroidWattsonPolicyEstimate policy3 = 6;
+  optional AndroidWattsonPolicyEstimate policy4 = 7;
+  optional AndroidWattsonPolicyEstimate policy5 = 8;
+  optional AndroidWattsonPolicyEstimate policy6 = 9;
+  optional AndroidWattsonPolicyEstimate policy7 = 10;
+  optional AndroidWattsonDsuScuEstimate dsu_scu = 11;
 }
 
 message AndroidWattsonPolicyEstimate {
-  optional float estimate_mw = 1;
-  optional AndroidWattsonCpuEstimate cpu0 = 2;
-  optional AndroidWattsonCpuEstimate cpu1 = 3;
-  optional AndroidWattsonCpuEstimate cpu2 = 4;
-  optional AndroidWattsonCpuEstimate cpu3 = 5;
-  optional AndroidWattsonCpuEstimate cpu4 = 6;
-  optional AndroidWattsonCpuEstimate cpu5 = 7;
-  optional AndroidWattsonCpuEstimate cpu6 = 8;
-  optional AndroidWattsonCpuEstimate cpu7 = 9;
+  optional float estimated_mw = 1;
+  optional float estimated_mws = 2;
+  optional AndroidWattsonCpuEstimate cpu0 = 3;
+  optional AndroidWattsonCpuEstimate cpu1 = 4;
+  optional AndroidWattsonCpuEstimate cpu2 = 5;
+  optional AndroidWattsonCpuEstimate cpu3 = 6;
+  optional AndroidWattsonCpuEstimate cpu4 = 7;
+  optional AndroidWattsonCpuEstimate cpu5 = 8;
+  optional AndroidWattsonCpuEstimate cpu6 = 9;
+  optional AndroidWattsonCpuEstimate cpu7 = 10;
 }
 
 message AndroidWattsonCpuEstimate {
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
+  optional float estimated_mws = 2;
 }
 
 message AndroidWattsonDsuScuEstimate {
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
+  optional float estimated_mws = 2;
 }
 
 // End of protos/perfetto/metrics/android/wattson_in_time_period.proto
@@ -2932,21 +2988,34 @@
 // Begin of protos/perfetto/metrics/android/wattson_tasks_attribution.proto
 
 message AndroidWattsonTasksAttributionMetric {
+  // Each version increment means updated structure format or field
   optional int32 metric_version = 1;
+  // Each version increment means power model has been updated and estimates
+  // might change for the exact same input. Don't compare estimates across
+  // different power model versions.
+  optional int32 power_model_version = 2;
   // Lists tasks (e.g. threads, process, package) and associated estimates
+  repeated AndroidWattsonTaskPeriodInfo period_info = 3;
+}
+
+// Groups of power per task for each period
+message AndroidWattsonTaskPeriodInfo {
+  optional int32 period_id = 1;
   repeated AndroidWattsonTaskInfo task_info = 2;
 }
 
 message AndroidWattsonTaskInfo {
   // Average estimated power for wall duration in mW
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
   // Total energy over wall duration across CPUs in mWs
-  optional float estimate_mws = 2;
-  optional string thread_name = 3;
-  optional string process_name = 4;
-  optional string package_name = 5;
-  optional int32 thread_id = 6;
-  optional int32 process_id = 7;
+  optional float estimated_mws = 2;
+  // Energy attributed to a thread for causing CPU idle exit
+  optional float idle_transitions_mws = 3;
+  optional string thread_name = 4;
+  optional string process_name = 5;
+  optional string package_name = 6;
+  optional int32 thread_id = 7;
+  optional int32 process_id = 8;
 }
 
 // End of protos/perfetto/metrics/android/wattson_tasks_attribution.proto
@@ -2954,11 +3023,13 @@
 // Begin of protos/perfetto/metrics/metrics.proto
 
 // Trace processor metadata
+// Next id: 17
 message TraceMetadata {
   reserved 1;
   optional int64 trace_duration_ns = 2;
   optional string trace_uuid = 3;
   optional string android_build_fingerprint = 4;
+  optional string android_device_manufacturer = 16;
   optional int64 statsd_triggering_subscription_id = 5;
   optional int64 trace_size_bytes = 6;
   repeated string trace_trigger = 7;
@@ -2966,6 +3037,10 @@
   optional string trace_config_pbtxt = 9;
   optional int64 sched_duration_ns = 10;
   optional int64 tracing_started_ns = 11;
+  optional int64 android_sdk_version = 12;
+  optional int64 suspend_count = 13;
+  optional int64 data_loss_count = 14;
+  optional int64 error_count = 15;
 }
 
 // Stats counters for the trace.
@@ -2998,7 +3073,7 @@
 
 // Root message for all Perfetto-based metrics.
 //
-// Next id: 73
+// Next id: 75
 message TraceMetrics {
   reserved 4, 10, 13, 14, 16, 19;
 
@@ -3121,11 +3196,13 @@
   // Metrics for IRQ runtime.
   optional AndroidIrqRuntimeMetric android_irq_runtime = 43;
 
-  // Metrics for the Trusty team.
-  optional AndroidTrustyWorkqueues android_trusty_workqueues = 44;
+  // Was metrics for the Trusty team.
+  reserved 44;
+  reserved 'android_trusty_workqueues';
 
-  // Summary of other concurrent trace recording.
-  optional AndroidOtherTracesMetric android_other_traces = 45;
+  // Was summary of concurrent trace recording.
+  reserved 45;
+  reserved 'android_other_traces';
 
   // Per-process Binder transaction metrics.
   optional AndroidBinderMetric android_binder = 46;
@@ -3193,8 +3270,8 @@
   // Android Broadcasts aggregated metrics
   optional AndroidBroadcastsMetric android_broadcasts = 68;
 
-  // Android Wattson app startup metrics.
-  optional AndroidWattsonTimePeriodMetric wattson_app_startup = 69;
+  // Android Wattson rail estimate for each app startup.
+  optional AndroidWattsonTimePeriodMetric wattson_app_startup_rails = 69;
 
   // Android Wattson rail estimate for duration of entire trace.
   optional AndroidWattsonTimePeriodMetric wattson_trace_rails = 70;
@@ -3205,6 +3282,12 @@
   // Android Wattson app startup metrics.
   optional AndroidWattsonTasksAttributionMetric wattson_trace_threads = 72;
 
+  // Android Wattson thread attribution during markers time window.
+  optional AndroidWattsonTasksAttributionMetric wattson_markers_threads = 73;
+
+  // Android Wattson estimate during markers time window.
+  optional AndroidWattsonTimePeriodMetric wattson_markers_rails = 74;
+
   // Android
   // Demo extensions.
   extensions 450 to 499;
diff --git a/protos/perfetto/metrics/webview/BUILD.gn b/protos/perfetto/metrics/webview/BUILD.gn
index 744794c..ec2c9c4 100644
--- a/protos/perfetto/metrics/webview/BUILD.gn
+++ b/protos/perfetto/metrics/webview/BUILD.gn
@@ -15,18 +15,13 @@
 import("../../../../gn/proto_library.gni")
 
 perfetto_proto_library("@TYPE@") {
-  proto_generators = [ "source_set" ]
-  deps = [ "..:@TYPE@" ]
+  proto_generators = []
+  import_dirs = [ "${perfetto_protobuf_src_dir}" ]
   sources = [
     "all_webview_metrics.proto",
     "webview_jank_approximation.proto",
   ]
-}
-
-perfetto_proto_library("descriptor") {
-  proto_generators = [ "descriptor" ]
-  import_dirs = [ "${perfetto_protobuf_src_dir}" ]
+  deps = [ "..:@TYPE@" ]
   generate_descriptor = "all_webview_metrics.descriptor"
-  deps = [ ":source_set" ]
-  sources = [ "all_webview_metrics.proto" ]
+  descriptor_root_source = "all_webview_metrics.proto"
 }
diff --git a/protos/perfetto/trace/BUILD.gn b/protos/perfetto/trace/BUILD.gn
index c3c58b4..4ecdb18 100644
--- a/protos/perfetto/trace/BUILD.gn
+++ b/protos/perfetto/trace/BUILD.gn
@@ -73,21 +73,11 @@
   ]
 }
 
-# This is only for Bazel build generation.
-group("source_set") {
-  testonly = true
-  public_deps = [
-    ":minimal_source_set",
-    ":non_minimal_source_set",
-  ]
-}
-
 perfetto_proto_library("non_minimal_@TYPE@") {
   proto_generators = [
     "cpp",
     "lite",
     "zero",
-    "source_set",
   ]
   deps = [
     ":minimal_@TYPE@",
@@ -106,7 +96,6 @@
     "statsd:@TYPE@",
     "sys_stats:@TYPE@",
     "system_info:@TYPE@",
-    "track_event:@TYPE@",
     "translation:@TYPE@",
   ]
   sources = proto_sources_non_minimal
@@ -118,11 +107,12 @@
   sources = proto_sources_minimal
 }
 
-perfetto_proto_library("descriptor") {
-  proto_generators = [ "descriptor" ]
+perfetto_proto_library("@TYPE@") {
+  proto_generators = []
+  deps = [ ":non_minimal_@TYPE@" ]
+  sources = []
   generate_descriptor = "trace.descriptor"
-  sources = [ "trace.proto" ]
-  deps = [ ":non_minimal_source_set" ]
+  descriptor_root_source = "trace.proto"
 }
 
 # This target exports perfetto trace protos allowing both host and device
@@ -132,11 +122,11 @@
   deps = [ ":lite" ]
 }
 
-perfetto_proto_library("test_extensions_descriptor") {
-  proto_generators = [ "descriptor" ]
-  generate_descriptor = "test_extensions.descriptor"
+perfetto_proto_library("test_extensions_@TYPE@") {
+  proto_generators = []
   sources = [ "test_extensions.proto" ]
-  deps = [ "track_event:source_set" ]
+  generate_descriptor = "test_extensions.descriptor"
+  descriptor_root_source = "test_extensions.proto"
 }
 
 if (enable_perfetto_merged_protos_check) {
diff --git a/protos/perfetto/trace/android/BUILD.gn b/protos/perfetto/trace/android/BUILD.gn
index 350e7e2..429bbeb 100644
--- a/protos/perfetto/trace/android/BUILD.gn
+++ b/protos/perfetto/trace/android/BUILD.gn
@@ -60,29 +60,26 @@
 
 # Winscope messages added to TracePacket as extensions
 perfetto_proto_library("winscope_extensions_@TYPE@") {
-  proto_generators = [
-    "zero",
-    "source_set",
-  ]
+  proto_generators = [ "zero" ]
   public_deps = [ ":winscope_common_@TYPE@" ]
   sources = [
     "android_input_event.proto",
-    "graphics/pixelformat.proto",
-    "inputmethodeditor.proto",
-    "inputmethodservice/inputmethodservice.proto",
-    "inputmethodservice/softinputwindow.proto",
-    "server/inputmethod/inputmethodmanagerservice.proto",
-    "typedef.proto",
     "app/statusbarmanager.proto",
     "app/window_configuration.proto",
     "content/activityinfo.proto",
     "content/configuration.proto",
     "content/locale.proto",
+    "graphics/pixelformat.proto",
+    "inputmethodeditor.proto",
+    "inputmethodservice/inputmethodservice.proto",
+    "inputmethodservice/softinputwindow.proto",
     "privacy.proto",
     "server/animationadapter.proto",
+    "server/inputmethod/inputmethodmanagerservice.proto",
     "server/surfaceanimator.proto",
     "server/windowcontainerthumbnail.proto",
     "server/windowmanagerservice.proto",
+    "typedef.proto",
     "view/display.proto",
     "view/displaycutout.proto",
     "view/displayinfo.proto",
@@ -115,27 +112,27 @@
   deps = [ ":winscope_extensions_zero" ]
 }
 
-perfetto_proto_library("winscope_descriptor") {
-  proto_generators = [ "descriptor" ]
-  generate_descriptor = "winscope.descriptor"
-  deps = [
-    ":winscope_extensions_source_set",
-    ":winscope_regular_source_set",
-  ]
+perfetto_proto_library("winscope_@TYPE@") {
+  proto_generators = []
   sources = [ "winscope.proto" ]
+  deps = [
+    ":winscope_extensions_@TYPE@",
+    ":winscope_regular_@TYPE@",
+  ]
   import_dirs = [ "${perfetto_protobuf_src_dir}" ]
+  generate_descriptor = "winscope.descriptor"
+  descriptor_root_source = "winscope.proto"
 }
 
 # Android track_event extensions
 perfetto_proto_library("android_track_event_@TYPE@") {
-  proto_generators = [ "source_set" ]
+  sources = [ "android_track_event.proto" ]
   public_deps = [ "../track_event:@TYPE@" ]
-  sources = [ "android_track_event.proto" ]
-}
 
-perfetto_proto_library("android_track_event_@TYPE@") {
-  proto_generators = [ "descriptor" ]
   generate_descriptor = "android_track_event.descriptor"
-  sources = [ "android_track_event.proto" ]
-  deps = [ ":android_track_event_source_set" ]
+  descriptor_root_source = "android_track_event.proto"
+
+  # This is the descriptor for an extension. It shouldn't include the descriptor
+  # for the base message as well.
+  exclude_imports = true
 }
diff --git a/protos/perfetto/trace/android/android_input_event.proto b/protos/perfetto/trace/android/android_input_event.proto
index 85a89c0..901b497 100644
--- a/protos/perfetto/trace/android/android_input_event.proto
+++ b/protos/perfetto/trace/android/android_input_event.proto
@@ -90,7 +90,7 @@
   optional int32 device_id = 6;
   // Use a signed int for display_id, because -1 (DISPLAY_IS_NONE) is a common value.
   optional sint32 display_id = 7;
-  optional int32 key_code = 8;
+  optional int32 key_code = 8 [(.perfetto.protos.typedef) = "android.view.KeyEvent.KeyCode"];
   optional uint32 scan_code = 9;
   optional uint32 meta_state = 10 [(.perfetto.protos.typedef) ="android.view.KeyEvent.MetaState"];
   optional int32 repeat_count = 11;
diff --git a/protos/perfetto/trace/etw/BUILD.gn b/protos/perfetto/trace/etw/BUILD.gn
index f22514c..fb4bf93 100644
--- a/protos/perfetto/trace/etw/BUILD.gn
+++ b/protos/perfetto/trace/etw/BUILD.gn
@@ -18,12 +18,8 @@
 
 perfetto_proto_library("@TYPE@") {
   sources = etw_proto_names
-}
-
-if (perfetto_build_standalone) {
-  perfetto_proto_library("descriptor") {
-    proto_generators = [ "descriptor" ]
+  if (perfetto_build_standalone) {
     generate_descriptor = "etw.descriptor"
-    sources = [ "etw_event_bundle.proto" ]
+    descriptor_root_source = "etw_event_bundle.proto"
   }
 }
diff --git a/protos/perfetto/trace/ftrace/BUILD.gn b/protos/perfetto/trace/ftrace/BUILD.gn
index 2b6182d..6418223 100644
--- a/protos/perfetto/trace/ftrace/BUILD.gn
+++ b/protos/perfetto/trace/ftrace/BUILD.gn
@@ -18,12 +18,8 @@
 
 perfetto_proto_library("@TYPE@") {
   sources = ftrace_proto_names
-}
-
-if (perfetto_build_standalone) {
-  perfetto_proto_library("descriptor") {
-    proto_generators = [ "descriptor" ]
+  if (perfetto_build_standalone) {
     generate_descriptor = "ftrace.descriptor"
-    sources = [ "ftrace_event_bundle.proto" ]
+    descriptor_root_source = "ftrace_event_bundle.proto"
   }
 }
diff --git a/protos/perfetto/trace/ftrace/all_protos.gni b/protos/perfetto/trace/ftrace/all_protos.gni
index 1cb6085..7319dd4 100644
--- a/protos/perfetto/trace/ftrace/all_protos.gni
+++ b/protos/perfetto/trace/ftrace/all_protos.gni
@@ -28,9 +28,11 @@
   "clk.proto",
   "cma.proto",
   "compaction.proto",
+  "cpm_trace.proto",
   "cpuhp.proto",
   "cros_ec.proto",
   "dcvsh.proto",
+  "devfreq.proto",
   "dma_fence.proto",
   "dmabuf_heap.proto",
   "dpu.proto",
@@ -63,6 +65,7 @@
   "oom.proto",
   "panel.proto",
   "perf_trace_counters.proto",
+  "pixel_mm.proto",
   "power.proto",
   "printk.proto",
   "raw_syscalls.proto",
diff --git a/protos/perfetto/trace/ftrace/cpm_trace.proto b/protos/perfetto/trace/ftrace/cpm_trace.proto
new file mode 100644
index 0000000..f19f0f8
--- /dev/null
+++ b/protos/perfetto/trace/ftrace/cpm_trace.proto
@@ -0,0 +1,12 @@
+// Autogenerated by:
+// ../../src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
+// Do not edit.
+
+syntax = "proto2";
+package perfetto.protos;
+
+message ParamSetValueCpmFtraceEvent {
+  optional string body = 1;
+  optional uint32 value = 2;
+  optional int64 timestamp = 3;
+}
diff --git a/protos/perfetto/trace/ftrace/devfreq.proto b/protos/perfetto/trace/ftrace/devfreq.proto
new file mode 100644
index 0000000..1533fc4
--- /dev/null
+++ b/protos/perfetto/trace/ftrace/devfreq.proto
@@ -0,0 +1,14 @@
+// Autogenerated by:
+// ../../src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
+// Do not edit.
+
+syntax = "proto2";
+package perfetto.protos;
+
+message DevfreqFrequencyFtraceEvent {
+  optional string dev_name = 1;
+  optional uint64 freq = 2;
+  optional uint64 prev_freq = 3;
+  optional uint64 busy_time = 4;
+  optional uint64 total_time = 5;
+}
diff --git a/protos/perfetto/trace/ftrace/ftrace_event.proto b/protos/perfetto/trace/ftrace/ftrace_event.proto
index 4a0a589..b363eb0 100644
--- a/protos/perfetto/trace/ftrace/ftrace_event.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_event.proto
@@ -28,9 +28,11 @@
 import "protos/perfetto/trace/ftrace/clk.proto";
 import "protos/perfetto/trace/ftrace/cma.proto";
 import "protos/perfetto/trace/ftrace/compaction.proto";
+import "protos/perfetto/trace/ftrace/cpm_trace.proto";
 import "protos/perfetto/trace/ftrace/cpuhp.proto";
 import "protos/perfetto/trace/ftrace/cros_ec.proto";
 import "protos/perfetto/trace/ftrace/dcvsh.proto";
+import "protos/perfetto/trace/ftrace/devfreq.proto";
 import "protos/perfetto/trace/ftrace/dma_fence.proto";
 import "protos/perfetto/trace/ftrace/dmabuf_heap.proto";
 import "protos/perfetto/trace/ftrace/dpu.proto";
@@ -63,6 +65,7 @@
 import "protos/perfetto/trace/ftrace/oom.proto";
 import "protos/perfetto/trace/ftrace/panel.proto";
 import "protos/perfetto/trace/ftrace/perf_trace_counters.proto";
+import "protos/perfetto/trace/ftrace/pixel_mm.proto";
 import "protos/perfetto/trace/ftrace/power.proto";
 import "protos/perfetto/trace/ftrace/printk.proto";
 import "protos/perfetto/trace/ftrace/raw_syscalls.proto";
@@ -674,5 +677,11 @@
     KgslAdrenoCmdbatchSubmittedFtraceEvent kgsl_adreno_cmdbatch_submitted = 535;
     KgslAdrenoCmdbatchSyncFtraceEvent kgsl_adreno_cmdbatch_sync = 536;
     KgslAdrenoCmdbatchRetiredFtraceEvent kgsl_adreno_cmdbatch_retired = 537;
+    PixelMmKswapdWakeFtraceEvent pixel_mm_kswapd_wake = 538;
+    PixelMmKswapdDoneFtraceEvent pixel_mm_kswapd_done = 539;
+    SchedWakeupTaskAttrFtraceEvent sched_wakeup_task_attr = 540;
+    DevfreqFrequencyFtraceEvent devfreq_frequency = 541;
+    KprobeEvent kprobe_event = 542;
+    ParamSetValueCpmFtraceEvent param_set_value_cpm = 543;
   }
 }
diff --git a/protos/perfetto/trace/ftrace/ftrace_event_bundle.proto b/protos/perfetto/trace/ftrace/ftrace_event_bundle.proto
index 2bc2cd3..71a6d75 100644
--- a/protos/perfetto/trace/ftrace/ftrace_event_bundle.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_event_bundle.proto
@@ -113,15 +113,22 @@
   }
   repeated FtraceError error = 8;
 
-  // The timestamp (ftrace clock) of the last event consumed from this per-cpu
-  // kernel buffer prior to starting this bundle. In other words: the last
-  // event in the previous bundle.
+  // Superseded by |previous_bundle_end_timestamp| in perfetto v47+. The
+  // primary difference is that this field tracked the last timestamp read from
+  // the per-cpu buffer, while the newer field tracks events that get
+  // serialised into the trace.
+  // Added in: perfetto v44.
+  optional uint64 last_read_event_timestamp = 9;
+
+  // The timestamp (using ftrace clock) of the last event written into this
+  // data source on this cpu. In other words: the last event in the previous
+  // bundle.
   // Lets the trace processing find an initial timestamp after which ftrace
   // data is known to be valid across all cpus. Of particular importance when
   // the perfetto trace buffer is a ring buffer as well, as the overwriting of
   // oldest bundles can skew the first valid timestamp per cpu significantly.
-  // Added in: perfetto v44.
-  optional uint64 last_read_event_timestamp = 9;
+  // Added in: perfetto v47.
+  optional uint64 previous_bundle_end_timestamp = 10;
 }
 
 enum FtraceClock {
diff --git a/protos/perfetto/trace/ftrace/ftrace_stats.proto b/protos/perfetto/trace/ftrace/ftrace_stats.proto
index f921779..dd531d4 100644
--- a/protos/perfetto/trace/ftrace/ftrace_stats.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_stats.proto
@@ -61,6 +61,17 @@
   optional uint64 read_events = 9;
 }
 
+// Kprobe statistical data, gathered from /sys/kernel/tracing/kprobe_profile.
+message FtraceKprobeStats {
+  // Cumulative number of kprobe events generated for this function
+  optional int64 hits = 1;
+  // Cumulative number of kprobe events that could not be generated for this
+  // function and were missed.  This happens when too much nesting
+  // happens between a kprobe and its kretprobe, overflowing the
+  // maxactives buffer.
+  optional int64 misses = 2;
+}
+
 // Errors and kernel buffer stats for the ftrace data source.
 message FtraceStats {
   enum Phase {
@@ -112,6 +123,9 @@
   // Any traces with entries in this field should be investigated, as they
   // indicate a bug in perfetto or the kernel.
   repeated FtraceParseStatus ftrace_parse_errors = 9;
+
+  // Kprobe profile stats for functions hits and misses
+  optional FtraceKprobeStats kprobe_stats = 10;
 }
 
 enum FtraceParseStatus {
diff --git a/protos/perfetto/trace/ftrace/generic.proto b/protos/perfetto/trace/ftrace/generic.proto
index 65d5c95..d7d4e36 100644
--- a/protos/perfetto/trace/ftrace/generic.proto
+++ b/protos/perfetto/trace/ftrace/generic.proto
@@ -33,3 +33,14 @@
   optional string event_name = 1;
   repeated Field field = 2;
 }
+
+message KprobeEvent {
+  enum KprobeType {
+    KPROBE_TYPE_UNKNOWN = 0;
+    KPROBE_TYPE_BEGIN = 1;
+    KPROBE_TYPE_END = 2;
+    KPROBE_TYPE_INSTANT = 3;
+  }
+  optional string name = 1;
+  optional KprobeType type = 2;
+}
diff --git a/protos/perfetto/trace/ftrace/perf_trace_counters.proto b/protos/perfetto/trace/ftrace/perf_trace_counters.proto
index 0e3531c..337e492 100644
--- a/protos/perfetto/trace/ftrace/perf_trace_counters.proto
+++ b/protos/perfetto/trace/ftrace/perf_trace_counters.proto
@@ -8,19 +8,25 @@
 message SchedSwitchWithCtrsFtraceEvent {
   optional int32 old_pid = 1;
   optional int32 new_pid = 2;
-  optional uint32 cctr = 3;
-  optional uint32 ctr0 = 4;
-  optional uint32 ctr1 = 5;
-  optional uint32 ctr2 = 6;
-  optional uint32 ctr3 = 7;
+  optional uint64 cctr = 3;
+  optional uint64 ctr0 = 4;
+  optional uint64 ctr1 = 5;
+  optional uint64 ctr2 = 6;
+  optional uint64 ctr3 = 7;
   optional uint32 lctr0 = 8;
   optional uint32 lctr1 = 9;
-  optional uint32 ctr4 = 10;
-  optional uint32 ctr5 = 11;
+  optional uint64 ctr4 = 10;
+  optional uint64 ctr5 = 11;
   optional string prev_comm = 12;
   optional int32 prev_pid = 13;
   optional uint32 cyc = 14;
   optional uint32 inst = 15;
   optional uint32 stallbm = 16;
   optional uint32 l3dm = 17;
+  optional int32 next_pid = 18;
+  optional string next_comm = 19;
+  optional int64 prev_state = 20;
+  optional uint64 amu0 = 21;
+  optional uint64 amu1 = 22;
+  optional uint64 amu2 = 23;
 }
diff --git a/protos/perfetto/trace/ftrace/pixel_mm.proto b/protos/perfetto/trace/ftrace/pixel_mm.proto
new file mode 100644
index 0000000..5c323e4
--- /dev/null
+++ b/protos/perfetto/trace/ftrace/pixel_mm.proto
@@ -0,0 +1,14 @@
+// Autogenerated by:
+// ../../src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
+// Do not edit.
+
+syntax = "proto2";
+package perfetto.protos;
+
+message PixelMmKswapdWakeFtraceEvent {
+  optional int32 whatever = 1;
+}
+message PixelMmKswapdDoneFtraceEvent {
+  optional uint64 delta_nr_scanned = 1;
+  optional uint64 delta_nr_reclaimed = 2;
+}
diff --git a/protos/perfetto/trace/ftrace/sched.proto b/protos/perfetto/trace/ftrace/sched.proto
index fb4e2fb..70b7c8d 100644
--- a/protos/perfetto/trace/ftrace/sched.proto
+++ b/protos/perfetto/trace/ftrace/sched.proto
@@ -108,3 +108,10 @@
   optional int32 running = 6;
   optional uint32 load = 7;
 }
+message SchedWakeupTaskAttrFtraceEvent {
+  optional int32 pid = 1;
+  optional uint64 cpu_affinity = 2;
+  optional uint64 task_util = 3;
+  optional uint64 uclamp_min = 4;
+  optional uint64 vruntime = 5;
+}
diff --git a/protos/perfetto/trace/perfetto/tracing_service_event.proto b/protos/perfetto/trace/perfetto/tracing_service_event.proto
index 0573be0..dbc4df1 100644
--- a/protos/perfetto/trace/perfetto/tracing_service_event.proto
+++ b/protos/perfetto/trace/perfetto/tracing_service_event.proto
@@ -20,6 +20,13 @@
 
 // Events emitted by the tracing service.
 message TracingServiceEvent {
+  message DataSources {
+    message DataSource {
+      optional string producer_name = 1;
+      optional string data_source_name = 2;
+    }
+    repeated DataSource data_source = 1;
+  }
   oneof event_type {
     // When each of the following booleans are set to true, they report the
     // point in time (through TracePacket's timestamp) where the condition
@@ -39,6 +46,9 @@
     // sources have been recording events.
     bool all_data_sources_started = 1;
 
+    // Emitted when a flush is started.
+    bool flush_started = 9;
+
     // Emitted when all data sources have been flushed successfully or with an
     // error (including timeouts). This can generally happen many times over the
     // course of the trace.
@@ -63,5 +73,13 @@
     // Deprecated since Android U, where --save-for-bugreport uses
     // non-destructive cloning.
     bool seized_for_bugreport = 6;
+
+    // Emitted when not all data sources in all producers reply to a start
+    // request after some time.
+    DataSources slow_starting_data_sources = 7;
+
+    // Emitted when the last flush request has failed. Lists data sources that
+    // did not reply on time.
+    DataSources last_flush_slow_data_sources = 8;
   }
 }
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index eeeacc5..5e07a0c 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -340,6 +340,7 @@
   BUILTIN_CLOCK_MONOTONIC_RAW = 5;
   BUILTIN_CLOCK_BOOTTIME = 6;
   BUILTIN_CLOCK_TSC = 9;
+  BUILTIN_CLOCK_PERF = 10;
   BUILTIN_CLOCK_MAX_ID = 63;
 
   reserved 7, 8;
@@ -887,6 +888,15 @@
 
 // End of protos/perfetto/config/chrome/chrome_config.proto
 
+// Begin of protos/perfetto/config/chrome/system_metrics.proto
+
+message ChromiumSystemMetricsConfig {
+  // Samples counters every X ms.
+  optional uint32 sampling_interval_ms = 1;
+}
+
+// End of protos/perfetto/config/chrome/system_metrics.proto
+
 // Begin of protos/perfetto/config/chrome/v8_config.proto
 
 message V8Config {
@@ -926,10 +936,26 @@
 
 // Begin of protos/perfetto/config/ftrace/ftrace_config.proto
 
-// Next id: 29
+// Next id: 31
 message FtraceConfig {
   // Ftrace events to record, example: "sched/sched_switch".
   repeated string ftrace_events = 1;
+
+  message KprobeEvent {
+    enum KprobeType {
+      KPROBE_TYPE_UNKNOWN = 0;
+      KPROBE_TYPE_KPROBE = 1;
+      KPROBE_TYPE_KRETPROBE = 2;
+      KPROBE_TYPE_BOTH = 3;
+    }
+    // Kernel function name to attach to, for example "fuse_file_write_iter"
+    optional string probe = 1;
+    optional KprobeType type = 2;
+  }
+
+  // Ftrace events to record, specific for kprobes and kretprobes
+  repeated KprobeEvent kprobe_events = 30;
+
   // Android-specific event categories:
   repeated string atrace_categories = 2;
   repeated string atrace_apps = 3;
@@ -953,9 +979,9 @@
   // of 50 means that a read pass is triggered as soon as any per-cpu buffer is
   // half-full. Not guaranteed if there are multiple concurrent tracing
   // sessions.
-  // Currently does nothing on Linux kernels below v6.1.
-  // Introduced in: perfetto v43.
-  optional uint32 drain_buffer_percent = 26;
+  // Currently does nothing on Linux kernels below v6.9.
+  // Introduced in: perfetto v48.
+  optional uint32 drain_buffer_percent = 29;
 
   // Configuration for compact encoding of scheduler events. When enabled (and
   // recording the relevant ftrace events), specific high-volume events are
@@ -1145,6 +1171,9 @@
   // the recording in the kernel.
   // Introduced in: perfetto v43.
   optional bool buffer_size_lower_bound = 27;
+
+  // Previously drain_buffer_percent, perfetto v43-v47.
+  reserved 26;
 }
 
 // End of protos/perfetto/config/ftrace/ftrace_config.proto
@@ -1739,6 +1768,19 @@
   }
 }
 
+// Additional events associated with a leader.
+// Configuration is similar to Timebase event. Because data acquisition is
+// driven by the leader there is no option to configure the clock or the
+// frequency.
+message FollowerEvent {
+  oneof event {
+    PerfEvents.Counter counter = 1;
+    PerfEvents.Tracepoint tracepoint = 2;
+    PerfEvents.RawEvent raw_event = 3;
+  }
+  optional string name = 4;
+}
+
 // End of protos/perfetto/common/perf_events.proto
 
 // Begin of protos/perfetto/config/profiling/perf_event_config.proto
@@ -1759,12 +1801,15 @@
 //     }
 //   }
 //
-// Next id: 19
+// Next id: 20
 message PerfEventConfig {
   // What event to sample on, and how often.
   // Defined in common/perf_events.proto.
   optional PerfEvents.Timebase timebase = 15;
 
+  // Other events associated with the leader described in the timebase.
+  repeated FollowerEvent followers = 19;
+
   // If set, the profiler will sample userspace processes' callstacks at the
   // interval specified by the |timebase|.
   // If unset, the profiler will record only the event counts.
@@ -1937,6 +1982,8 @@
     UNWIND_SKIP = 1;
     // Use libunwindstack (default):
     UNWIND_DWARF = 2;
+    // Use userspace frame pointer unwinder:
+    UNWIND_FRAME_POINTER = 3;
   }
 }
 
@@ -2002,11 +2049,9 @@
   ATOM_LMK_KILL_OCCURRED = 51;
   ATOM_PICTURE_IN_PICTURE_STATE_CHANGED = 52;
   ATOM_WIFI_MULTICAST_LOCK_STATE_CHANGED = 53;
-  ATOM_LMK_STATE_CHANGED = 54;
   ATOM_APP_START_MEMORY_STATE_CAPTURED = 55;
   ATOM_SHUTDOWN_SEQUENCE_REPORTED = 56;
   ATOM_BOOT_SEQUENCE_REPORTED = 57;
-  ATOM_DAVEY_OCCURRED = 58;
   ATOM_OVERLAY_STATE_CHANGED = 59;
   ATOM_FOREGROUND_SERVICE_STATE_CHANGED = 60;
   ATOM_CALL_STATE_CHANGED = 61;
@@ -2323,7 +2368,6 @@
   ATOM_PRIVACY_TOGGLE_DIALOG_INTERACTION = 382;
   ATOM_APP_SEARCH_OPTIMIZE_STATS_REPORTED = 383;
   ATOM_NON_A11Y_TOOL_SERVICE_WARNING_REPORT = 384;
-  ATOM_APP_SEARCH_SET_SCHEMA_STATS_REPORTED = 385;
   ATOM_APP_COMPAT_STATE_CHANGED = 386;
   ATOM_SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED = 387;
   ATOM_SPLITSCREEN_UI_CHANGED = 388;
@@ -2372,8 +2416,6 @@
   ATOM_HOTWORD_DETECTION_SERVICE_RESTARTED = 432;
   ATOM_HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED = 433;
   ATOM_HOTWORD_DETECTOR_EVENTS = 434;
-  ATOM_AD_SERVICES_API_CALLED = 435;
-  ATOM_AD_SERVICES_MESUREMENT_REPORTS_UPLOADED = 436;
   ATOM_BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED = 437;
   ATOM_CONTACTS_INDEXER_UPDATE_STATS_REPORTED = 440;
   ATOM_APP_BACKGROUND_RESTRICTIONS_INFO = 441;
@@ -2417,25 +2459,14 @@
   ATOM_CB_MODULE_ERROR_REPORTED = 480;
   ATOM_CB_SERVICE_FEATURE_CHANGED = 481;
   ATOM_CB_RECEIVER_FEATURE_CHANGED = 482;
-  ATOM_JSSCRIPTENGINE_LATENCY_REPORTED = 483;
   ATOM_PRIVACY_SIGNAL_NOTIFICATION_INTERACTION = 484;
   ATOM_PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION = 485;
   ATOM_PRIVACY_SIGNALS_JOB_FAILURE = 486;
   ATOM_VIBRATION_REPORTED = 487;
   ATOM_UWB_RANGING_START = 489;
-  ATOM_MOBILE_DATA_DOWNLOAD_FILE_GROUP_STATUS_REPORTED = 490;
   ATOM_APP_COMPACTED_V2 = 491;
-  ATOM_AD_SERVICES_SETTINGS_USAGE_REPORTED = 493;
   ATOM_DISPLAY_BRIGHTNESS_CHANGED = 494;
   ATOM_ACTIVITY_ACTION_BLOCKED = 495;
-  ATOM_BACKGROUND_FETCH_PROCESS_REPORTED = 496;
-  ATOM_UPDATE_CUSTOM_AUDIENCE_PROCESS_REPORTED = 497;
-  ATOM_RUN_AD_BIDDING_PROCESS_REPORTED = 498;
-  ATOM_RUN_AD_SCORING_PROCESS_REPORTED = 499;
-  ATOM_RUN_AD_SELECTION_PROCESS_REPORTED = 500;
-  ATOM_RUN_AD_BIDDING_PER_CA_PROCESS_REPORTED = 501;
-  ATOM_MOBILE_DATA_DOWNLOAD_DOWNLOAD_RESULT_REPORTED = 502;
-  ATOM_MOBILE_DATA_DOWNLOAD_FILE_GROUP_STORAGE_STATS_REPORTED = 503;
   ATOM_NETWORK_DNS_SERVER_SUPPORT_REPORTED = 504;
   ATOM_VM_BOOTED = 505;
   ATOM_VM_EXITED = 506;
@@ -2444,7 +2475,6 @@
   ATOM_MEDIAMETRICS_SPATIALIZERDEVICEENABLED_REPORTED = 509;
   ATOM_MEDIAMETRICS_HEADTRACKERDEVICEENABLED_REPORTED = 510;
   ATOM_MEDIAMETRICS_HEADTRACKERDEVICESUPPORTED_REPORTED = 511;
-  ATOM_AD_SERVICES_MEASUREMENT_REGISTRATIONS = 512;
   ATOM_HEARING_AID_INFO_REPORTED = 513;
   ATOM_DEVICE_WIDE_JOB_CONSTRAINT_CHANGED = 514;
   ATOM_AMBIENT_MODE_CHANGED = 515;
@@ -2466,9 +2496,6 @@
   ATOM_BLUETOOTH_LOCAL_SUPPORTED_FEATURES_REPORTED = 532;
   ATOM_BLUETOOTH_GATT_APP_INFO = 533;
   ATOM_BRIGHTNESS_CONFIGURATION_UPDATED = 534;
-  ATOM_AD_SERVICES_GET_TOPICS_REPORTED = 535;
-  ATOM_AD_SERVICES_EPOCH_COMPUTATION_GET_TOP_TOPICS_REPORTED = 536;
-  ATOM_AD_SERVICES_EPOCH_COMPUTATION_CLASSIFIER_REPORTED = 537;
   ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_LAUNCHED = 538;
   ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_FINISHED = 539;
   ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_CONNECTION_REPORTED = 540;
@@ -2506,12 +2533,10 @@
   ATOM_MEDIAMETRICS_MIDI_DEVICE_CLOSE_REPORTED = 576;
   ATOM_BIOMETRIC_TOUCH_REPORTED = 577;
   ATOM_HOTWORD_AUDIO_EGRESS_EVENT_REPORTED = 578;
-  ATOM_APP_SEARCH_SCHEMA_MIGRATION_STATS_REPORTED = 579;
   ATOM_LOCATION_ENABLED_STATE_CHANGED = 580;
   ATOM_IME_REQUEST_FINISHED = 581;
   ATOM_USB_COMPLIANCE_WARNINGS_REPORTED = 582;
   ATOM_APP_SUPPORTED_LOCALES_CHANGED = 583;
-  ATOM_GRAMMATICAL_INFLECTION_CHANGED = 584;
   ATOM_MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED = 586;
   ATOM_BIOMETRIC_PROPERTIES_COLLECTED = 587;
   ATOM_KERNEL_WAKEUP_ATTRIBUTED = 588;
@@ -2524,7 +2549,11 @@
   ATOM_WS_NOTIFICATION_UPDATED = 596;
   ATOM_NETWORK_VALIDATION_FAILURE_STATS_DAILY_REPORTED = 601;
   ATOM_WS_COMPLICATION_TAPPED = 602;
-  ATOM_WS_WEAR_TIME_SESSION = 610;
+  ATOM_WS_NOTIFICATION_BLOCKING = 780;
+  ATOM_WS_NOTIFICATION_BRIDGEMODE_UPDATED = 822;
+  ATOM_WS_NOTIFICATION_DISMISSAL_ACTIONED = 823;
+  ATOM_WS_NOTIFICATION_ACTIONED = 824;
+  ATOM_WS_NOTIFICATION_LATENCY = 880;
   ATOM_WIFI_BYTES_TRANSFER = 10000;
   ATOM_WIFI_BYTES_TRANSFER_BY_FG_BG = 10001;
   ATOM_MOBILE_BYTES_TRANSFER = 10002;
@@ -2697,6 +2726,186 @@
   ATOM_NOTIFICATION_MEMORY_USE = 10174;
   ATOM_HDR_CAPABILITIES = 10175;
   ATOM_WS_FAVOURITE_WATCH_FACE_LIST_SNAPSHOT = 10176;
+  ATOM_WS_WEAR_TIME_SESSION = 610;
+  ATOM_WS_INCOMING_CALL_ACTION_REPORTED = 626;
+  ATOM_WS_CALL_DISCONNECTION_REPORTED = 627;
+  ATOM_WS_CALL_DURATION_REPORTED = 628;
+  ATOM_WS_CALL_USER_EXPERIENCE_LATENCY_REPORTED = 629;
+  ATOM_WS_CALL_INTERACTION_REPORTED = 630;
+  ATOM_WS_ON_BODY_STATE_CHANGED = 787;
+  ATOM_WS_WATCH_FACE_RESTRICTED_COMPLICATIONS_IMPACTED = 802;
+  ATOM_WS_WATCH_FACE_DEFAULT_RESTRICTED_COMPLICATIONS_REMOVED = 803;
+  ATOM_WS_COMPLICATIONS_IMPACTED_NOTIFICATION_EVENT_REPORTED = 804;
+  ATOM_WS_STANDALONE_MODE_SNAPSHOT = 10197;
+  ATOM_WS_FAVORITE_WATCH_FACE_SNAPSHOT = 10206;
+  ATOM_SETTINGS_SPA_REPORTED = 622;
+  ATOM_PDF_LOAD_REPORTED = 859;
+  ATOM_PDF_API_USAGE_REPORTED = 860;
+  ATOM_PDF_SEARCH_REPORTED = 861;
+  ATOM_HDMI_EARC_STATUS_REPORTED = 701;
+  ATOM_HDMI_SOUNDBAR_MODE_STATUS_REPORTED = 724;
+  ATOM_MEDIA_PROVIDER_DATABASE_ROLLBACK_REPORTED = 784;
+  ATOM_BACKUP_SETUP_STATUS_REPORTED = 785;
+  ATOM_PHOTOPICKER_SESSION_INFO_REPORTED = 886;
+  ATOM_PHOTOPICKER_API_INFO_REPORTED = 887;
+  ATOM_PHOTOPICKER_UI_EVENT_LOGGED = 888;
+  ATOM_PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED = 889;
+  ATOM_PHOTOPICKER_PREVIEW_INFO_LOGGED = 890;
+  ATOM_PHOTOPICKER_MENU_INTERACTION_LOGGED = 891;
+  ATOM_PHOTOPICKER_BANNER_INTERACTION_LOGGED = 892;
+  ATOM_PHOTOPICKER_MEDIA_LIBRARY_INFO_LOGGED = 893;
+  ATOM_PHOTOPICKER_PAGE_INFO_LOGGED = 894;
+  ATOM_PHOTOPICKER_MEDIA_GRID_SYNC_INFO_REPORTED = 895;
+  ATOM_PHOTOPICKER_ALBUM_SYNC_INFO_REPORTED = 896;
+  ATOM_PHOTOPICKER_SEARCH_INFO_REPORTED = 897;
+  ATOM_SEARCH_DATA_EXTRACTION_DETAILS_REPORTED = 898;
+  ATOM_EMBEDDED_PHOTOPICKER_INFO_REPORTED = 899;
+  ATOM_WEAR_POWER_MENU_OPENED = 731;
+  ATOM_WEAR_ASSISTANT_OPENED = 755;
+  ATOM_KERNEL_OOM_KILL_OCCURRED = 754;
+  ATOM_AUTOFILL_UI_EVENT_REPORTED = 603;
+  ATOM_AUTOFILL_FILL_REQUEST_REPORTED = 604;
+  ATOM_AUTOFILL_FILL_RESPONSE_REPORTED = 605;
+  ATOM_AUTOFILL_SAVE_EVENT_REPORTED = 606;
+  ATOM_AUTOFILL_SESSION_COMMITTED = 607;
+  ATOM_AUTOFILL_FIELD_CLASSIFICATION_EVENT_REPORTED = 659;
+  ATOM_TV_LOW_POWER_STANDBY_POLICY = 679;
+  ATOM_EXTERNAL_TV_INPUT_EVENT = 717;
+  ATOM_COMPONENT_STATE_CHANGED_REPORTED = 863;
+  ATOM_AI_WALLPAPERS_BUTTON_PRESSED = 706;
+  ATOM_AI_WALLPAPERS_TEMPLATE_SELECTED = 707;
+  ATOM_AI_WALLPAPERS_TERM_SELECTED = 708;
+  ATOM_AI_WALLPAPERS_WALLPAPER_SET = 709;
+  ATOM_AI_WALLPAPERS_SESSION_SUMMARY = 710;
+  ATOM_APF_SESSION_INFO_REPORTED = 777;
+  ATOM_IP_CLIENT_RA_INFO_REPORTED = 778;
+  ATOM_VPN_CONNECTION_STATE_CHANGED = 850;
+  ATOM_VPN_CONNECTION_REPORTED = 851;
+  ATOM_NETWORK_STATS_RECORDER_FILE_OPERATED = 783;
+  ATOM_DAILY_KEEPALIVE_INFO_REPORTED = 650;
+  ATOM_NETWORK_REQUEST_STATE_CHANGED = 779;
+  ATOM_TETHERING_ACTIVE_SESSIONS_REPORTED = 925;
+  ATOM_ART_DATUM_REPORTED = 332;
+  ATOM_ART_DEVICE_DATUM_REPORTED = 550;
+  ATOM_ART_DATUM_DELTA_REPORTED = 565;
+  ATOM_ART_DEX2OAT_REPORTED = 929;
+  ATOM_ART_DEVICE_STATUS = 10205;
+  ATOM_ODREFRESH_REPORTED = 366;
+  ATOM_ODSIGN_REPORTED = 548;
+  ATOM_BACKGROUND_DEXOPT_JOB_ENDED = 467;
+  ATOM_PREREBOOT_DEXOPT_JOB_ENDED = 883;
+  ATOM_PERMISSION_RATIONALE_DIALOG_VIEWED = 645;
+  ATOM_PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED = 646;
+  ATOM_APP_DATA_SHARING_UPDATES_NOTIFICATION_INTERACTION = 647;
+  ATOM_APP_DATA_SHARING_UPDATES_FRAGMENT_VIEWED = 648;
+  ATOM_APP_DATA_SHARING_UPDATES_FRAGMENT_ACTION_REPORTED = 649;
+  ATOM_ENHANCED_CONFIRMATION_DIALOG_RESULT_REPORTED = 827;
+  ATOM_ENHANCED_CONFIRMATION_RESTRICTION_CLEARED = 828;
+  ATOM_EMERGENCY_STATE_CHANGED = 633;
+  ATOM_CHRE_SIGNIFICANT_MOTION_STATE_CHANGED = 868;
+  ATOM_HEALTH_CONNECT_UI_IMPRESSION = 623;
+  ATOM_HEALTH_CONNECT_UI_INTERACTION = 624;
+  ATOM_HEALTH_CONNECT_APP_OPENED_REPORTED = 625;
+  ATOM_HEALTH_CONNECT_API_CALLED = 616;
+  ATOM_HEALTH_CONNECT_USAGE_STATS = 617;
+  ATOM_HEALTH_CONNECT_STORAGE_STATS = 618;
+  ATOM_HEALTH_CONNECT_API_INVOKED = 643;
+  ATOM_EXERCISE_ROUTE_API_CALLED = 654;
+  ATOM_SELINUX_AUDIT_LOG = 799;
+  ATOM_ONDEVICEPERSONALIZATION_API_CALLED = 711;
+  ATOM_CELLULAR_RADIO_POWER_STATE_CHANGED = 713;
+  ATOM_EMERGENCY_NUMBERS_INFO = 10180;
+  ATOM_DATA_NETWORK_VALIDATION = 10207;
+  ATOM_DATA_RAT_STATE_CHANGED = 854;
+  ATOM_CONNECTED_CHANNEL_CHANGED = 882;
+  ATOM_QUALIFIED_RAT_LIST_CHANGED = 634;
+  ATOM_QNS_IMS_CALL_DROP_STATS = 635;
+  ATOM_QNS_FALLBACK_RESTRICTION_CHANGED = 636;
+  ATOM_QNS_RAT_PREFERENCE_MISMATCH_INFO = 10177;
+  ATOM_QNS_HANDOVER_TIME_MILLIS = 10178;
+  ATOM_QNS_HANDOVER_PINGPONG = 10179;
+  ATOM_SATELLITE_CONTROLLER = 10182;
+  ATOM_SATELLITE_SESSION = 10183;
+  ATOM_SATELLITE_INCOMING_DATAGRAM = 10184;
+  ATOM_SATELLITE_OUTGOING_DATAGRAM = 10185;
+  ATOM_SATELLITE_PROVISION = 10186;
+  ATOM_SATELLITE_SOS_MESSAGE_RECOMMENDER = 10187;
+  ATOM_CARRIER_ROAMING_SATELLITE_SESSION = 10211;
+  ATOM_CARRIER_ROAMING_SATELLITE_CONTROLLER_STATS = 10212;
+  ATOM_CONTROLLER_STATS_PER_PACKAGE = 10213;
+  ATOM_SATELLITE_ENTITLEMENT = 10214;
+  ATOM_SATELLITE_CONFIG_UPDATER = 10215;
+  ATOM_SATELLITE_ACCESS_CONTROLLER = 10219;
+  ATOM_CELLULAR_IDENTIFIER_DISCLOSED = 800;
+  ATOM_KEYBOARD_CONFIGURED = 682;
+  ATOM_KEYBOARD_SYSTEMS_EVENT_REPORTED = 683;
+  ATOM_INPUTDEVICE_USAGE_REPORTED = 686;
+  ATOM_TOUCHPAD_USAGE = 10191;
+  ATOM_THREADNETWORK_TELEMETRY_DATA_REPORTED = 738;
+  ATOM_THREADNETWORK_TOPO_ENTRY_REPEATED = 739;
+  ATOM_THREADNETWORK_DEVICE_INFO_REPORTED = 740;
+  ATOM_CRONET_ENGINE_CREATED = 703;
+  ATOM_CRONET_TRAFFIC_REPORTED = 704;
+  ATOM_CRONET_ENGINE_BUILDER_INITIALIZED = 762;
+  ATOM_CRONET_HTTP_FLAGS_INITIALIZED = 763;
+  ATOM_CRONET_INITIALIZED = 764;
+  ATOM_WEAR_MODE_STATE_CHANGED = 715;
+  ATOM_RENDERER_INITIALIZED = 736;
+  ATOM_SCHEMA_VERSION_RECEIVED = 737;
+  ATOM_LAYOUT_INSPECTED = 741;
+  ATOM_LAYOUT_EXPRESSION_INSPECTED = 742;
+  ATOM_LAYOUT_ANIMATIONS_INSPECTED = 743;
+  ATOM_MATERIAL_COMPONENTS_INSPECTED = 744;
+  ATOM_TILE_REQUESTED = 745;
+  ATOM_STATE_RESPONSE_RECEIVED = 746;
+  ATOM_TILE_RESPONSE_RECEIVED = 747;
+  ATOM_INFLATION_FINISHED = 748;
+  ATOM_INFLATION_FAILED = 749;
+  ATOM_IGNORED_INFLATION_FAILURES_REPORTED = 750;
+  ATOM_DRAWABLE_RENDERED = 751;
+  ATOM_MEDIA_ACTION_REPORTED = 608;
+  ATOM_MEDIA_CONTROLS_LAUNCHED = 609;
+  ATOM_MEDIA_SESSION_STATE_CHANGED = 677;
+  ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_DEVICE_SCAN_API_LATENCY = 757;
+  ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_SASS_DEVICE_UNAVAILABLE = 758;
+  ATOM_WEAR_MEDIA_OUTPUT_SWITCHER_FASTPAIR_API_TIMEOUT = 759;
+  ATOM_MEDIATOR_UPDATED = 721;
+  ATOM_SYSPROXY_BLUETOOTH_BYTES_TRANSFER = 10196;
+  ATOM_SYSPROXY_CONNECTION_UPDATED = 786;
+  ATOM_ADAPTIVE_AUTH_UNLOCK_AFTER_LOCK_REPORTED = 820;
+  ATOM_FEDERATED_COMPUTE_API_CALLED = 712;
+  ATOM_FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED = 771;
+  ATOM_EXAMPLE_ITERATOR_NEXT_LATENCY_REPORTED = 838;
+  ATOM_RKPD_POOL_STATS = 664;
+  ATOM_RKPD_CLIENT_OPERATION = 665;
+  ATOM_CPU_POLICY = 10199;
+  ATOM_ATOM_9999 = 9999;
+  ATOM_ATOM_99999 = 99999;
+  ATOM_SCREEN_OFF_REPORTED = 776;
+  ATOM_SCREEN_TIMEOUT_OVERRIDE_REPORTED = 836;
+  ATOM_SCREEN_INTERACTIVE_SESSION_REPORTED = 837;
+  ATOM_SCREEN_DIM_REPORTED = 867;
+  ATOM_FULL_SCREEN_INTENT_LAUNCHED = 631;
+  ATOM_BAL_ALLOWED = 632;
+  ATOM_IN_TASK_ACTIVITY_STARTED = 685;
+  ATOM_CACHED_APPS_HIGH_WATERMARK = 10189;
+  ATOM_STYLUS_PREDICTION_METRICS_REPORTED = 718;
+  ATOM_USER_RISK_EVENT_REPORTED = 725;
+  ATOM_MEDIA_PROJECTION_STATE_CHANGED = 729;
+  ATOM_MEDIA_PROJECTION_TARGET_CHANGED = 730;
+  ATOM_EXCESSIVE_BINDER_PROXY_COUNT_REPORTED = 853;
+  ATOM_PROXY_BYTES_TRANSFER_BY_FG_BG = 10200;
+  ATOM_MOBILE_BYTES_TRANSFER_BY_PROC_STATE = 10204;
+  ATOM_BIOMETRIC_FRR_NOTIFICATION = 817;
+  ATOM_SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION = 830;
+  ATOM_SENSITIVE_NOTIFICATION_APP_PROTECTION_SESSION = 831;
+  ATOM_SENSITIVE_NOTIFICATION_APP_PROTECTION_APPLIED = 832;
+  ATOM_SENSITIVE_NOTIFICATION_REDACTION = 833;
+  ATOM_SENSITIVE_CONTENT_APP_PROTECTION = 835;
+  ATOM_APP_RESTRICTION_STATE_CHANGED = 866;
+  ATOM_DREAM_SETTING_CHANGED = 705;
+  ATOM_DREAM_SETTING_SNAPSHOT = 10192;
+  ATOM_BOOT_INTEGRITY_INFO_REPORTED = 775;
   ATOM_WIFI_AWARE_NDP_REPORTED = 638;
   ATOM_WIFI_AWARE_ATTACH_REPORTED = 639;
   ATOM_WIFI_SELF_RECOVERY_TRIGGERED = 661;
@@ -2711,39 +2920,99 @@
   ATOM_WIFI_LOCAL_ONLY_REQUEST_SCAN_TRIGGERED = 693;
   ATOM_WIFI_THREAD_TASK_EXECUTED = 694;
   ATOM_WIFI_STATE_CHANGED = 700;
+  ATOM_PNO_SCAN_STARTED = 719;
+  ATOM_PNO_SCAN_STOPPED = 720;
+  ATOM_WIFI_IS_UNUSABLE_REPORTED = 722;
+  ATOM_WIFI_AP_CAPABILITIES_REPORTED = 723;
+  ATOM_SOFT_AP_STATE_CHANGED = 805;
+  ATOM_SCORER_PREDICTION_RESULT_REPORTED = 884;
   ATOM_WIFI_AWARE_CAPABILITIES = 10190;
   ATOM_WIFI_MODULE_INFO = 10193;
-  ATOM_SETTINGS_SPA_REPORTED = 622;
+  ATOM_WIFI_SETTING_INFO = 10194;
+  ATOM_WIFI_COMPLEX_SETTING_INFO = 10195;
+  ATOM_WIFI_CONFIGURED_NETWORK_INFO = 10198;
+  ATOM_MTE_STATE = 10181;
+  ATOM_HOTWORD_EGRESS_SIZE_ATOM_REPORTED = 761;
+  ATOM_SANDBOX_API_CALLED = 488;
+  ATOM_SANDBOX_ACTIVITY_EVENT_OCCURRED = 735;
+  ATOM_SDK_SANDBOX_RESTRICTED_ACCESS_IN_SESSION = 796;
+  ATOM_SANDBOX_SDK_STORAGE = 10159;
   ATOM_EXPRESS_EVENT_REPORTED = 528;
   ATOM_EXPRESS_HISTOGRAM_SAMPLE_REPORTED = 593;
   ATOM_EXPRESS_UID_EVENT_REPORTED = 644;
   ATOM_EXPRESS_UID_HISTOGRAM_SAMPLE_REPORTED = 658;
-  ATOM_PERMISSION_RATIONALE_DIALOG_VIEWED = 645;
-  ATOM_PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED = 646;
-  ATOM_APP_DATA_SHARING_UPDATES_NOTIFICATION_INTERACTION = 647;
-  ATOM_APP_DATA_SHARING_UPDATES_FRAGMENT_VIEWED = 648;
-  ATOM_APP_DATA_SHARING_UPDATES_FRAGMENT_ACTION_REPORTED = 649;
-  ATOM_WS_INCOMING_CALL_ACTION_REPORTED = 626;
-  ATOM_WS_CALL_DISCONNECTION_REPORTED = 627;
-  ATOM_WS_CALL_DURATION_REPORTED = 628;
-  ATOM_WS_CALL_USER_EXPERIENCE_LATENCY_REPORTED = 629;
-  ATOM_WS_CALL_INTERACTION_REPORTED = 630;
-  ATOM_FULL_SCREEN_INTENT_LAUNCHED = 631;
-  ATOM_BAL_ALLOWED = 632;
-  ATOM_IN_TASK_ACTIVITY_STARTED = 685;
-  ATOM_CACHED_APPS_HIGH_WATERMARK = 10189;
-  ATOM_ODREFRESH_REPORTED = 366;
-  ATOM_ODSIGN_REPORTED = 548;
-  ATOM_ART_DATUM_REPORTED = 332;
-  ATOM_ART_DEVICE_DATUM_REPORTED = 550;
-  ATOM_ART_DATUM_DELTA_REPORTED = 565;
-  ATOM_BACKGROUND_DEXOPT_JOB_ENDED = 467;
-  ATOM_WEAR_ADAPTIVE_SUSPEND_STATS_REPORTED = 619;
-  ATOM_WEAR_POWER_ANOMALY_SERVICE_OPERATIONAL_STATS_REPORTED = 620;
-  ATOM_WEAR_POWER_ANOMALY_SERVICE_EVENT_STATS_REPORTED = 621;
-  ATOM_EMERGENCY_STATE_CHANGED = 633;
-  ATOM_DND_STATE_CHANGED = 657;
-  ATOM_MTE_STATE = 10181;
+  ATOM_IKE_SESSION_TERMINATED = 678;
+  ATOM_IKE_LIVENESS_CHECK_SESSION_VALIDATED = 760;
+  ATOM_NEGOTIATED_SECURITY_ASSOCIATION = 821;
+  ATOM_APP_SEARCH_SET_SCHEMA_STATS_REPORTED = 385;
+  ATOM_APP_SEARCH_SCHEMA_MIGRATION_STATS_REPORTED = 579;
+  ATOM_APP_SEARCH_USAGE_SEARCH_INTENT_STATS_REPORTED = 825;
+  ATOM_APP_SEARCH_USAGE_SEARCH_INTENT_RAW_QUERY_STATS_REPORTED = 826;
+  ATOM_DEVICE_POLICY_MANAGEMENT_MODE = 10216;
+  ATOM_DEVICE_POLICY_STATE = 10217;
+  ATOM_DESKTOP_MODE_UI_CHANGED = 818;
+  ATOM_DESKTOP_MODE_SESSION_TASK_UPDATE = 819;
+  ATOM_MEDIA_CODEC_RECLAIM_REQUEST_COMPLETED = 600;
+  ATOM_MEDIA_CODEC_STARTED = 641;
+  ATOM_MEDIA_CODEC_STOPPED = 642;
+  ATOM_MEDIA_CODEC_RENDERED = 684;
+  ATOM_MEDIA_EDITING_ENDED_REPORTED = 798;
+  ATOM_CAR_WAKEUP_FROM_SUSPEND_REPORTED = 852;
+  ATOM_PLUGIN_INITIALIZED = 655;
+  ATOM_CAR_RECENTS_EVENT_REPORTED = 770;
+  ATOM_CAR_CALM_MODE_EVENT_REPORTED = 797;
+  ATOM_CAMERA_FEATURE_COMBINATION_QUERY_EVENT = 900;
+  ATOM_THERMAL_STATUS_CALLED = 772;
+  ATOM_THERMAL_HEADROOM_CALLED = 773;
+  ATOM_THERMAL_HEADROOM_THRESHOLDS_CALLED = 774;
+  ATOM_ADPF_HINT_SESSION_TID_CLEANUP = 839;
+  ATOM_THERMAL_HEADROOM_THRESHOLDS = 10201;
+  ATOM_ADPF_SESSION_SNAPSHOT = 10218;
+  ATOM_BLUETOOTH_HASHED_DEVICE_NAME_REPORTED = 613;
+  ATOM_BLUETOOTH_L2CAP_COC_CLIENT_CONNECTION = 614;
+  ATOM_BLUETOOTH_L2CAP_COC_SERVER_CONNECTION = 615;
+  ATOM_BLUETOOTH_LE_SESSION_CONNECTED = 656;
+  ATOM_RESTRICTED_BLUETOOTH_DEVICE_NAME_REPORTED = 666;
+  ATOM_BLUETOOTH_PROFILE_CONNECTION_ATTEMPTED = 696;
+  ATOM_BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED = 781;
+  ATOM_BLUETOOTH_RFCOMM_CONNECTION_ATTEMPTED = 782;
+  ATOM_REMOTE_DEVICE_INFORMATION_WITH_METRIC_ID = 862;
+  ATOM_LE_APP_SCAN_STATE_CHANGED = 870;
+  ATOM_LE_RADIO_SCAN_STOPPED = 871;
+  ATOM_LE_SCAN_RESULT_RECEIVED = 872;
+  ATOM_LE_SCAN_ABUSED = 873;
+  ATOM_LE_ADV_STATE_CHANGED = 874;
+  ATOM_LE_ADV_ERROR_REPORTED = 875;
+  ATOM_A2DP_SESSION_REPORTED = 904;
+  ATOM_BLUETOOTH_CROSS_LAYER_EVENT_REPORTED = 916;
+  ATOM_BROADCAST_AUDIO_SESSION_REPORTED = 927;
+  ATOM_BROADCAST_AUDIO_SYNC_REPORTED = 928;
+  ATOM_DEVICE_LOCK_CHECK_IN_REQUEST_REPORTED = 726;
+  ATOM_DEVICE_LOCK_PROVISIONING_COMPLETE_REPORTED = 727;
+  ATOM_DEVICE_LOCK_KIOSK_APP_REQUEST_REPORTED = 728;
+  ATOM_DEVICE_LOCK_CHECK_IN_RETRY_REPORTED = 789;
+  ATOM_DEVICE_LOCK_PROVISION_FAILURE_REPORTED = 790;
+  ATOM_DEVICE_LOCK_LOCK_UNLOCK_DEVICE_FAILURE_REPORTED = 791;
+  ATOM_APPLICATION_GRAMMATICAL_INFLECTION_CHANGED = 584;
+  ATOM_SYSTEM_GRAMMATICAL_INFLECTION_CHANGED = 816;
+  ATOM_EMERGENCY_NUMBER_DIALED = 637;
+  ATOM_JSSCRIPTENGINE_LATENCY_REPORTED = 483;
+  ATOM_AD_SERVICES_API_CALLED = 435;
+  ATOM_AD_SERVICES_MESUREMENT_REPORTS_UPLOADED = 436;
+  ATOM_MOBILE_DATA_DOWNLOAD_FILE_GROUP_STATUS_REPORTED = 490;
+  ATOM_MOBILE_DATA_DOWNLOAD_DOWNLOAD_RESULT_REPORTED = 502;
+  ATOM_AD_SERVICES_SETTINGS_USAGE_REPORTED = 493;
+  ATOM_BACKGROUND_FETCH_PROCESS_REPORTED = 496;
+  ATOM_UPDATE_CUSTOM_AUDIENCE_PROCESS_REPORTED = 497;
+  ATOM_RUN_AD_BIDDING_PROCESS_REPORTED = 498;
+  ATOM_RUN_AD_SCORING_PROCESS_REPORTED = 499;
+  ATOM_RUN_AD_SELECTION_PROCESS_REPORTED = 500;
+  ATOM_RUN_AD_BIDDING_PER_CA_PROCESS_REPORTED = 501;
+  ATOM_MOBILE_DATA_DOWNLOAD_FILE_GROUP_STORAGE_STATS_REPORTED = 503;
+  ATOM_AD_SERVICES_MEASUREMENT_REGISTRATIONS = 512;
+  ATOM_AD_SERVICES_GET_TOPICS_REPORTED = 535;
+  ATOM_AD_SERVICES_EPOCH_COMPUTATION_GET_TOP_TOPICS_REPORTED = 536;
+  ATOM_AD_SERVICES_EPOCH_COMPUTATION_CLASSIFIER_REPORTED = 537;
   ATOM_AD_SERVICES_BACK_COMPAT_GET_TOPICS_REPORTED = 598;
   ATOM_AD_SERVICES_BACK_COMPAT_EPOCH_COMPUTATION_CLASSIFIER_REPORTED = 599;
   ATOM_AD_SERVICES_MEASUREMENT_DEBUG_KEYS = 640;
@@ -2753,68 +3022,68 @@
   ATOM_AD_SERVICES_MEASUREMENT_ATTRIBUTION = 674;
   ATOM_AD_SERVICES_MEASUREMENT_JOBS = 675;
   ATOM_AD_SERVICES_MEASUREMENT_WIPEOUT = 676;
+  ATOM_AD_SERVICES_MEASUREMENT_AD_ID_MATCH_FOR_DEBUG_KEYS = 695;
+  ATOM_AD_SERVICES_ENROLLMENT_DATA_STORED = 697;
+  ATOM_AD_SERVICES_ENROLLMENT_FILE_DOWNLOADED = 698;
+  ATOM_AD_SERVICES_ENROLLMENT_MATCHED = 699;
   ATOM_AD_SERVICES_CONSENT_MIGRATED = 702;
-  ATOM_RKPD_POOL_STATS = 664;
-  ATOM_RKPD_CLIENT_OPERATION = 665;
-  ATOM_AUTOFILL_UI_EVENT_REPORTED = 603;
-  ATOM_AUTOFILL_FILL_REQUEST_REPORTED = 604;
-  ATOM_AUTOFILL_FILL_RESPONSE_REPORTED = 605;
-  ATOM_AUTOFILL_SAVE_EVENT_REPORTED = 606;
-  ATOM_AUTOFILL_SESSION_COMMITTED = 607;
-  ATOM_AUTOFILL_FIELD_CLASSIFICATION_EVENT_REPORTED = 659;
+  ATOM_AD_SERVICES_ENROLLMENT_FAILED = 714;
+  ATOM_AD_SERVICES_MEASUREMENT_CLICK_VERIFICATION = 756;
+  ATOM_AD_SERVICES_ENCRYPTION_KEY_FETCHED = 765;
+  ATOM_AD_SERVICES_ENCRYPTION_KEY_DB_TRANSACTION_ENDED = 766;
+  ATOM_DESTINATION_REGISTERED_BEACONS = 767;
+  ATOM_REPORT_INTERACTION_API_CALLED = 768;
+  ATOM_INTERACTION_REPORTING_TABLE_CLEARED = 769;
+  ATOM_APP_MANIFEST_CONFIG_HELPER_CALLED = 788;
+  ATOM_AD_FILTERING_PROCESS_JOIN_CA_REPORTED = 793;
+  ATOM_AD_FILTERING_PROCESS_AD_SELECTION_REPORTED = 794;
+  ATOM_AD_COUNTER_HISTOGRAM_UPDATER_REPORTED = 795;
+  ATOM_SIGNATURE_VERIFICATION = 807;
+  ATOM_K_ANON_IMMEDIATE_SIGN_JOIN_STATUS_REPORTED = 808;
+  ATOM_K_ANON_BACKGROUND_JOB_STATUS_REPORTED = 809;
+  ATOM_K_ANON_INITIALIZE_STATUS_REPORTED = 810;
+  ATOM_K_ANON_SIGN_STATUS_REPORTED = 811;
+  ATOM_K_ANON_JOIN_STATUS_REPORTED = 812;
+  ATOM_K_ANON_KEY_ATTESTATION_STATUS_REPORTED = 813;
+  ATOM_GET_AD_SELECTION_DATA_API_CALLED = 814;
+  ATOM_GET_AD_SELECTION_DATA_BUYER_INPUT_GENERATED = 815;
+  ATOM_BACKGROUND_JOB_SCHEDULING_REPORTED = 834;
+  ATOM_TOPICS_ENCRYPTION_EPOCH_COMPUTATION_REPORTED = 840;
+  ATOM_TOPICS_ENCRYPTION_GET_TOPICS_REPORTED = 841;
+  ATOM_ADSERVICES_SHELL_COMMAND_CALLED = 842;
+  ATOM_UPDATE_SIGNALS_API_CALLED = 843;
+  ATOM_ENCODING_JOB_RUN = 844;
+  ATOM_ENCODING_JS_FETCH = 845;
+  ATOM_ENCODING_JS_EXECUTION = 846;
+  ATOM_PERSIST_AD_SELECTION_RESULT_CALLED = 847;
+  ATOM_SERVER_AUCTION_KEY_FETCH_CALLED = 848;
+  ATOM_SERVER_AUCTION_BACKGROUND_KEY_FETCH_ENABLED = 849;
+  ATOM_AD_SERVICES_MEASUREMENT_PROCESS_ODP_REGISTRATION = 864;
+  ATOM_AD_SERVICES_MEASUREMENT_NOTIFY_REGISTRATION_TO_ODP = 865;
+  ATOM_SELECT_ADS_FROM_OUTCOMES_API_CALLED = 876;
+  ATOM_REPORT_IMPRESSION_API_CALLED = 877;
+  ATOM_AD_SERVICES_ENROLLMENT_TRANSACTION_STATS = 885;
+  ATOM_EXTERNAL_DISPLAY_STATE_CHANGED = 806;
+  ATOM_DISPLAY_MODE_DIRECTOR_VOTE_CHANGED = 792;
   ATOM_TEST_EXTENSION_ATOM_REPORTED = 660;
   ATOM_TEST_RESTRICTED_ATOM_REPORTED = 672;
   ATOM_STATS_SOCKET_LOSS_REPORTED = 752;
-  ATOM_PLUGIN_INITIALIZED = 655;
-  ATOM_TV_LOW_POWER_STANDBY_POLICY = 679;
+  ATOM_NFC_OBSERVE_MODE_STATE_CHANGED = 855;
+  ATOM_NFC_FIELD_CHANGED = 856;
+  ATOM_NFC_POLLING_LOOP_NOTIFICATION_REPORTED = 857;
+  ATOM_NFC_PROPRIETARY_CAPABILITIES_REPORTED = 858;
   ATOM_LOCKSCREEN_SHORTCUT_SELECTED = 611;
   ATOM_LOCKSCREEN_SHORTCUT_TRIGGERED = 612;
-  ATOM_EMERGENCY_NUMBERS_INFO = 10180;
-  ATOM_QUALIFIED_RAT_LIST_CHANGED = 634;
-  ATOM_QNS_IMS_CALL_DROP_STATS = 635;
-  ATOM_QNS_FALLBACK_RESTRICTION_CHANGED = 636;
-  ATOM_QNS_RAT_PREFERENCE_MISMATCH_INFO = 10177;
-  ATOM_QNS_HANDOVER_TIME_MILLIS = 10178;
-  ATOM_QNS_HANDOVER_PINGPONG = 10179;
-  ATOM_SATELLITE_CONTROLLER = 10182;
-  ATOM_SATELLITE_SESSION = 10183;
-  ATOM_SATELLITE_INCOMING_DATAGRAM = 10184;
-  ATOM_SATELLITE_OUTGOING_DATAGRAM = 10185;
-  ATOM_SATELLITE_PROVISION = 10186;
-  ATOM_SATELLITE_SOS_MESSAGE_RECOMMENDER = 10187;
-  ATOM_IKE_SESSION_TERMINATED = 678;
-  ATOM_IKE_LIVENESS_CHECK_SESSION_VALIDATED = 760;
-  ATOM_BLUETOOTH_HASHED_DEVICE_NAME_REPORTED = 613;
-  ATOM_BLUETOOTH_L2CAP_COC_CLIENT_CONNECTION = 614;
-  ATOM_BLUETOOTH_L2CAP_COC_SERVER_CONNECTION = 615;
-  ATOM_BLUETOOTH_LE_SESSION_CONNECTED = 656;
-  ATOM_RESTRICTED_BLUETOOTH_DEVICE_NAME_REPORTED = 666;
-  ATOM_BLUETOOTH_PROFILE_CONNECTION_ATTEMPTED = 696;
-  ATOM_HEALTH_CONNECT_UI_IMPRESSION = 623;
-  ATOM_HEALTH_CONNECT_UI_INTERACTION = 624;
-  ATOM_HEALTH_CONNECT_APP_OPENED_REPORTED = 625;
-  ATOM_HEALTH_CONNECT_API_CALLED = 616;
-  ATOM_HEALTH_CONNECT_USAGE_STATS = 617;
-  ATOM_HEALTH_CONNECT_STORAGE_STATS = 618;
-  ATOM_HEALTH_CONNECT_API_INVOKED = 643;
-  ATOM_EXERCISE_ROUTE_API_CALLED = 654;
-  ATOM_ATOM_9999 = 9999;
-  ATOM_ATOM_99999 = 99999;
-  ATOM_THREADNETWORK_TELEMETRY_DATA_REPORTED = 738;
-  ATOM_THREADNETWORK_TOPO_ENTRY_REPEATED = 739;
-  ATOM_THREADNETWORK_DEVICE_INFO_REPORTED = 740;
-  ATOM_EMERGENCY_NUMBER_DIALED = 637;
-  ATOM_SANDBOX_API_CALLED = 488;
-  ATOM_SANDBOX_ACTIVITY_EVENT_OCCURRED = 735;
-  ATOM_SANDBOX_SDK_STORAGE = 10159;
-  ATOM_CRONET_ENGINE_CREATED = 703;
-  ATOM_CRONET_TRAFFIC_REPORTED = 704;
-  ATOM_CRONET_ENGINE_BUILDER_INITIALIZED = 762;
-  ATOM_CRONET_HTTP_FLAGS_INITIALIZED = 763;
-  ATOM_CRONET_INITIALIZED = 764;
-  ATOM_DAILY_KEEPALIVE_INFO_REPORTED = 650;
-  ATOM_IP_CLIENT_RA_INFO_REPORTED = 778;
-  ATOM_APF_SESSION_INFO_REPORTED = 777;
+  ATOM_LAUNCHER_IMPRESSION_EVENT_V2 = 716;
+  ATOM_DISPLAY_SWITCH_LATENCY_TRACKED = 753;
+  ATOM_NOTIFICATION_LISTENER_SERVICE = 829;
+  ATOM_NAV_HANDLE_TOUCH_POINTS = 869;
+  ATOM_WEAR_ADAPTIVE_SUSPEND_STATS_REPORTED = 619;
+  ATOM_WEAR_POWER_ANOMALY_SERVICE_OPERATIONAL_STATS_REPORTED = 620;
+  ATOM_WEAR_POWER_ANOMALY_SERVICE_EVENT_STATS_REPORTED = 621;
+  ATOM_APEX_INSTALLATION_REQUESTED = 732;
+  ATOM_APEX_INSTALLATION_STAGED = 733;
+  ATOM_APEX_INSTALLATION_ENDED = 734;
   ATOM_CREDENTIAL_MANAGER_API_CALLED = 585;
   ATOM_CREDENTIAL_MANAGER_INIT_PHASE_REPORTED = 651;
   ATOM_CREDENTIAL_MANAGER_CANDIDATE_PHASE_REPORTED = 652;
@@ -2825,12 +3094,7 @@
   ATOM_CREDENTIAL_MANAGER_AUTH_CLICK_REPORTED = 670;
   ATOM_CREDENTIAL_MANAGER_APIV2_CALLED = 671;
   ATOM_UWB_ACTIVITY_INFO = 10188;
-  ATOM_MEDIA_ACTION_REPORTED = 608;
-  ATOM_MEDIA_CONTROLS_LAUNCHED = 609;
-  ATOM_MEDIA_CODEC_RECLAIM_REQUEST_COMPLETED = 600;
-  ATOM_MEDIA_CODEC_STARTED = 641;
-  ATOM_MEDIA_CODEC_STOPPED = 642;
-  ATOM_MEDIA_CODEC_RENDERED = 684;
+  ATOM_DND_STATE_CHANGED = 657;
 }
 // End of protos/perfetto/config/statsd/atom_ids.proto
 
@@ -3173,6 +3437,10 @@
   // Polls /sys/devices/system/cpu/cpu*/cpuidle/state* every X ms, if non-zero.
   // This is required to be > 10ms to avoid excessive CPU usage.
   optional uint32 cpuidle_period_ms = 13;
+
+  // Polls device-specific GPU frequency info every X ms, if non-zero.
+  // This is required to be > 10ms to avoid excessive CPU usage.
+  optional uint32 gpufreq_period_ms = 14;
 }
 
 // End of protos/perfetto/config/sys_stats/sys_stats_config.proto
@@ -3327,7 +3595,7 @@
 // Begin of protos/perfetto/config/data_source_config.proto
 
 // The configuration that is passed to each data source when starting tracing.
-// Next id: 131
+// Next id: 132
 message DataSourceConfig {
   enum SessionInitiator {
     SESSION_INITIATOR_UNSPECIFIED = 0;
@@ -3482,6 +3750,9 @@
   // Data source name: android.windowmanager
   optional WindowManagerConfig windowmanager_config = 130 [lazy = true];
 
+  // Data source name: org.chromium.system_metrics
+  optional ChromiumSystemMetricsConfig chromium_system_metrics = 131 [lazy = true];
+
   // This is a fallback mechanism to send a free-form text config to the
   // producer. In theory this should never be needed. All the code that
   // is part of the platform (i.e. traced service) is supposed to *not* truncate
@@ -3919,11 +4190,8 @@
   }
   optional IncrementalStateConfig incremental_state_config = 21;
 
-  // Additional guardrail used by the Perfetto command line client.
-  // On user builds when --dropbox is set perfetto will refuse to trace unless
-  // this is also set.
-  // Added in Q.
-  optional bool allow_user_build_tracing = 19;
+  // No longer needed as we unconditionally allow tracing on user builds.
+  optional bool allow_user_build_tracing = 19 [deprecated = true];
 
   // If set the tracing service will ensure there is at most one tracing session
   // with this key.
@@ -7270,6 +7538,16 @@
 
 // End of protos/perfetto/trace/ftrace/compaction.proto
 
+// Begin of protos/perfetto/trace/ftrace/cpm_trace.proto
+
+message ParamSetValueCpmFtraceEvent {
+  optional string body = 1;
+  optional uint32 value = 2;
+  optional int64 timestamp = 3;
+}
+
+// End of protos/perfetto/trace/ftrace/cpm_trace.proto
+
 // Begin of protos/perfetto/trace/ftrace/cpuhp.proto
 
 message CpuhpExitFtraceEvent {
@@ -7327,6 +7605,18 @@
 
 // End of protos/perfetto/trace/ftrace/dcvsh.proto
 
+// Begin of protos/perfetto/trace/ftrace/devfreq.proto
+
+message DevfreqFrequencyFtraceEvent {
+  optional string dev_name = 1;
+  optional uint64 freq = 2;
+  optional uint64 prev_freq = 3;
+  optional uint64 busy_time = 4;
+  optional uint64 total_time = 5;
+}
+
+// End of protos/perfetto/trace/ftrace/devfreq.proto
+
 // Begin of protos/perfetto/trace/ftrace/dma_fence.proto
 
 message DmaFenceInitFtraceEvent {
@@ -8564,6 +8854,17 @@
   repeated Field field = 2;
 }
 
+message KprobeEvent {
+  enum KprobeType {
+    KPROBE_TYPE_UNKNOWN = 0;
+    KPROBE_TYPE_BEGIN = 1;
+    KPROBE_TYPE_END = 2;
+    KPROBE_TYPE_INSTANT = 3;
+  }
+  optional string name = 1;
+  optional KprobeType type = 2;
+}
+
 // End of protos/perfetto/trace/ftrace/generic.proto
 
 // Begin of protos/perfetto/trace/ftrace/google_icc_trace.proto
@@ -9662,25 +9963,43 @@
 message SchedSwitchWithCtrsFtraceEvent {
   optional int32 old_pid = 1;
   optional int32 new_pid = 2;
-  optional uint32 cctr = 3;
-  optional uint32 ctr0 = 4;
-  optional uint32 ctr1 = 5;
-  optional uint32 ctr2 = 6;
-  optional uint32 ctr3 = 7;
+  optional uint64 cctr = 3;
+  optional uint64 ctr0 = 4;
+  optional uint64 ctr1 = 5;
+  optional uint64 ctr2 = 6;
+  optional uint64 ctr3 = 7;
   optional uint32 lctr0 = 8;
   optional uint32 lctr1 = 9;
-  optional uint32 ctr4 = 10;
-  optional uint32 ctr5 = 11;
+  optional uint64 ctr4 = 10;
+  optional uint64 ctr5 = 11;
   optional string prev_comm = 12;
   optional int32 prev_pid = 13;
   optional uint32 cyc = 14;
   optional uint32 inst = 15;
   optional uint32 stallbm = 16;
   optional uint32 l3dm = 17;
+  optional int32 next_pid = 18;
+  optional string next_comm = 19;
+  optional int64 prev_state = 20;
+  optional uint64 amu0 = 21;
+  optional uint64 amu1 = 22;
+  optional uint64 amu2 = 23;
 }
 
 // End of protos/perfetto/trace/ftrace/perf_trace_counters.proto
 
+// Begin of protos/perfetto/trace/ftrace/pixel_mm.proto
+
+message PixelMmKswapdWakeFtraceEvent {
+  optional int32 whatever = 1;
+}
+message PixelMmKswapdDoneFtraceEvent {
+  optional uint64 delta_nr_scanned = 1;
+  optional uint64 delta_nr_reclaimed = 2;
+}
+
+// End of protos/perfetto/trace/ftrace/pixel_mm.proto
+
 // Begin of protos/perfetto/trace/ftrace/power.proto
 
 message CpuFrequencyFtraceEvent {
@@ -9926,6 +10245,13 @@
   optional int32 running = 6;
   optional uint32 load = 7;
 }
+message SchedWakeupTaskAttrFtraceEvent {
+  optional int32 pid = 1;
+  optional uint64 cpu_affinity = 2;
+  optional uint64 task_util = 3;
+  optional uint64 uclamp_min = 4;
+  optional uint64 vruntime = 5;
+}
 
 // End of protos/perfetto/trace/ftrace/sched.proto
 
@@ -11092,6 +11418,12 @@
     KgslAdrenoCmdbatchSubmittedFtraceEvent kgsl_adreno_cmdbatch_submitted = 535;
     KgslAdrenoCmdbatchSyncFtraceEvent kgsl_adreno_cmdbatch_sync = 536;
     KgslAdrenoCmdbatchRetiredFtraceEvent kgsl_adreno_cmdbatch_retired = 537;
+    PixelMmKswapdWakeFtraceEvent pixel_mm_kswapd_wake = 538;
+    PixelMmKswapdDoneFtraceEvent pixel_mm_kswapd_done = 539;
+    SchedWakeupTaskAttrFtraceEvent sched_wakeup_task_attr = 540;
+    DevfreqFrequencyFtraceEvent devfreq_frequency = 541;
+    KprobeEvent kprobe_event = 542;
+    ParamSetValueCpmFtraceEvent param_set_value_cpm = 543;
   }
 }
 
@@ -11142,6 +11474,17 @@
   optional uint64 read_events = 9;
 }
 
+// Kprobe statistical data, gathered from /sys/kernel/tracing/kprobe_profile.
+message FtraceKprobeStats {
+  // Cumulative number of kprobe events generated for this function
+  optional int64 hits = 1;
+  // Cumulative number of kprobe events that could not be generated for this
+  // function and were missed.  This happens when too much nesting
+  // happens between a kprobe and its kretprobe, overflowing the
+  // maxactives buffer.
+  optional int64 misses = 2;
+}
+
 // Errors and kernel buffer stats for the ftrace data source.
 message FtraceStats {
   enum Phase {
@@ -11193,6 +11536,9 @@
   // Any traces with entries in this field should be investigated, as they
   // indicate a bug in perfetto or the kernel.
   repeated FtraceParseStatus ftrace_parse_errors = 9;
+
+  // Kprobe profile stats for functions hits and misses
+  optional FtraceKprobeStats kprobe_stats = 10;
 }
 
 enum FtraceParseStatus {
@@ -11316,15 +11662,22 @@
   }
   repeated FtraceError error = 8;
 
-  // The timestamp (ftrace clock) of the last event consumed from this per-cpu
-  // kernel buffer prior to starting this bundle. In other words: the last
-  // event in the previous bundle.
+  // Superseded by |previous_bundle_end_timestamp| in perfetto v47+. The
+  // primary difference is that this field tracked the last timestamp read from
+  // the per-cpu buffer, while the newer field tracks events that get
+  // serialised into the trace.
+  // Added in: perfetto v44.
+  optional uint64 last_read_event_timestamp = 9;
+
+  // The timestamp (using ftrace clock) of the last event written into this
+  // data source on this cpu. In other words: the last event in the previous
+  // bundle.
   // Lets the trace processing find an initial timestamp after which ftrace
   // data is known to be valid across all cpus. Of particular importance when
   // the perfetto trace buffer is a ring buffer as well, as the overwriting of
   // oldest bundles can skew the first valid timestamp per cpu significantly.
-  // Added in: perfetto v44.
-  optional uint64 last_read_event_timestamp = 9;
+  // Added in: perfetto v47.
+  optional uint64 previous_bundle_end_timestamp = 10;
 }
 
 enum FtraceClock {
@@ -13344,6 +13697,13 @@
 
 // Events emitted by the tracing service.
 message TracingServiceEvent {
+  message DataSources {
+    message DataSource {
+      optional string producer_name = 1;
+      optional string data_source_name = 2;
+    }
+    repeated DataSource data_source = 1;
+  }
   oneof event_type {
     // When each of the following booleans are set to true, they report the
     // point in time (through TracePacket's timestamp) where the condition
@@ -13363,6 +13723,9 @@
     // sources have been recording events.
     bool all_data_sources_started = 1;
 
+    // Emitted when a flush is started.
+    bool flush_started = 9;
+
     // Emitted when all data sources have been flushed successfully or with an
     // error (including timeouts). This can generally happen many times over the
     // course of the trace.
@@ -13387,6 +13750,14 @@
     // Deprecated since Android U, where --save-for-bugreport uses
     // non-destructive cloning.
     bool seized_for_bugreport = 6;
+
+    // Emitted when not all data sources in all producers reply to a start
+    // request after some time.
+    DataSources slow_starting_data_sources = 7;
+
+    // Emitted when the last flush request has failed. Lists data sources that
+    // did not reply on time.
+    DataSources last_flush_slow_data_sources = 8;
   }
 }
 
@@ -13688,6 +14059,16 @@
   //
   // N.B. This is not the native size of this object.
   optional int64 native_allocation_registry_size_field = 8;
+
+  enum HeapType {
+    HEAP_TYPE_UNKNOWN = 0;
+    HEAP_TYPE_APP = 1;
+    HEAP_TYPE_ZYGOTE = 2;
+    HEAP_TYPE_BOOT_IMAGE = 3;
+  }
+  // To reduce the space required we only emit the heap type if it has changed
+  // from the previous object we recorded.
+  optional HeapType heap_type_delta = 9;
 }
 
 message HeapGraph {
@@ -14007,6 +14388,9 @@
   // Value of the timebase counter (since the event was configured, no deltas).
   optional uint64 timebase_count = 6;
 
+  // Value of the followers counter (since the event was configured, no deltas).
+  repeated uint64 follower_counts = 7;
+
   // Unwound callstack. Might be partial, in which case a synthetic "error"
   // frame is appended, and |unwind_error| is set accordingly.
   optional uint64 callstack_iid = 4;
@@ -14060,6 +14444,9 @@
   // the implementation decided to default/override some parameters.
   optional PerfEvents.Timebase timebase = 1;
 
+  // Description of followers event
+  repeated FollowerEvent followers = 4;
+
   // If the config requested process sharding, report back the count and which
   // of those bins was selected. Never changes for the duration of a trace.
   optional uint32 process_shard_count = 2;
@@ -14466,7 +14853,11 @@
     repeated CpuIdleStateEntry cpuidle_state_entry = 2;
   }
   repeated CpuIdleState cpuidle_state = 16;
+
+  // Read GPU frequency info on Intel/AMD devices.
+  repeated uint64 gpufreq_mhz = 17;
 }
+
 // End of protos/perfetto/trace/sys_stats/sys_stats.proto
 
 // Begin of protos/perfetto/trace/system_info.proto
@@ -14478,16 +14869,35 @@
   optional string machine = 4;
 }
 
+// Next id: 15;
 message SystemInfo {
   optional Utsname utsname = 1;
   optional string android_build_fingerprint = 2;
 
+  // The manufacturer of the product/hardware.
+  // Source : "ro.product.manufacturer"
+  // Introduced after Android W in Nov 2024 and is not supported on older
+  // versions.
+  optional string android_device_manufacturer = 14;
+
   // The SoC model from which trace is collected
   optional string android_soc_model = 9;
 
+  // The guest SoC model from which trace is collected in case of VMs
+  optional string android_guest_soc_model = 13;
+
   // The hardware reversion from android device
   optional string android_hardware_revision = 10;
 
+  // The storage component from android_device. This field has been introduced
+  // after Android W in Aug 2024 and is not supported on older versions.
+  optional string android_storage_model = 11;
+
+  // The RAM component information from android device. This field has been
+  // introduced after Android W in Aug 2024 and is not supported on older
+  // versions.
+  optional string android_ram_model = 12;
+
   // The version of traced (the same returned by `traced --version`).
   // This is a human readable string with and its format varies depending on
   // the build system and the repo (standalone vs AOSP).
@@ -14521,6 +14931,23 @@
 
 // Information about CPUs from procfs and sysfs.
 message CpuInfo {
+  message ArmCpuIdentifier {
+    // Implementer code
+    optional uint32 implementer = 1;
+
+    // Architecture code
+    optional uint32 architecture = 2;
+
+    // CPU variant
+    optional uint32 variant = 3;
+
+    // CPU part
+    optional uint32 part = 4;
+
+    // CPU revision
+    optional uint32 revision = 5;
+  }
+
   // Information about a single CPU.
   message Cpu {
     // Value of "Processor" field from /proc/cpuinfo for this CPU.
@@ -14535,6 +14962,11 @@
     // Cpu capacity from /sys/devices/system/cpu/cpuX/cpu_capacity where X is
     // the index of this CPU.
     optional uint32 capacity = 3;
+
+    // Code to identify the CPU
+    oneof identifier {
+      ArmCpuIdentifier arm_identifier = 4;
+    }
   }
 
   // Describes available CPUs, one entry per CPU.
@@ -14978,7 +15410,7 @@
 // |TrackEvent::track_uuid|. It is possible but not necessary to emit a
 // TrackDescriptor for this implicit track.
 //
-// Next id: 11.
+// Next id: 14.
 message TrackDescriptor {
   // Unique ID that identifies this track. This ID is global to the whole trace.
   // Producers should ensure that it is unlikely to clash with IDs emitted by
@@ -15003,6 +15435,9 @@
     // This field is only set by the SDK when perfetto::StaticString is
     // provided.
     string static_name = 10;
+    // Equivalent to name, used just to mark that the data is coming from
+    // android.os.Trace.
+    string atrace_name = 13;
   }
 
   // Associate the track with a process, making it the process-global track.
@@ -15037,6 +15472,35 @@
   // system events use nanoseconds. It results in broken event nesting when
   // track events and system events share a track.
   optional bool disallow_merging_with_system_tracks = 9;
+
+  // Specifies how the UI should display child tracks of this track (i.e. tracks
+  // where `parent_uuid` is specified to this track `uuid`). Note that this
+  // value is simply a *hint* to the UI: the UI is not guarnateed to respect
+  // this if it has a good reason not to do so.
+  enum ChildTracksOrdering {
+    // The default ordering, with no bearing on how the UI will visualise the
+    // tracks.
+    UNKNOWN = 0;
+
+    // Order tracks by `name` or `static_name` depending on which one has been
+    // specified.
+    LEXICOGRAPHIC = 1;
+
+    // Order tracks by the first `ts` event in a track.
+    CHRONOLOGICAL = 2;
+
+    // Order tracks by `sibling_order_rank` of child tracks. Child tracks with
+    // the lower values will be shown before tracks with higher values. Tracks
+    // with no value will be treated as having 0 rank.
+    EXPLICIT = 3;
+  }
+  optional ChildTracksOrdering child_ordering = 11;
+
+  // An opaque value which allows specifying how two sibling tracks should be
+  // ordered relative to each other: tracks with lower ranks will appear before
+  // tracks with higher ranks. An unspecified rank will be treated as a rank of
+  // 0.
+  optional int32 sibling_order_rank = 12;
 }
 
 // End of protos/perfetto/trace/track_event/track_descriptor.proto
diff --git a/protos/perfetto/trace/profiling/heap_graph.proto b/protos/perfetto/trace/profiling/heap_graph.proto
index d57568f..0713a9f 100644
--- a/protos/perfetto/trace/profiling/heap_graph.proto
+++ b/protos/perfetto/trace/profiling/heap_graph.proto
@@ -116,6 +116,16 @@
   //
   // N.B. This is not the native size of this object.
   optional int64 native_allocation_registry_size_field = 8;
+
+  enum HeapType {
+    HEAP_TYPE_UNKNOWN = 0;
+    HEAP_TYPE_APP = 1;
+    HEAP_TYPE_ZYGOTE = 2;
+    HEAP_TYPE_BOOT_IMAGE = 3;
+  }
+  // To reduce the space required we only emit the heap type if it has changed
+  // from the previous object we recorded.
+  optional HeapType heap_type_delta = 9;
 }
 
 message HeapGraph {
diff --git a/protos/perfetto/trace/profiling/profile_packet.proto b/protos/perfetto/trace/profiling/profile_packet.proto
index df466f7..52f6be2 100644
--- a/protos/perfetto/trace/profiling/profile_packet.proto
+++ b/protos/perfetto/trace/profiling/profile_packet.proto
@@ -303,6 +303,9 @@
   // Value of the timebase counter (since the event was configured, no deltas).
   optional uint64 timebase_count = 6;
 
+  // Value of the followers counter (since the event was configured, no deltas).
+  repeated uint64 follower_counts = 7;
+
   // Unwound callstack. Might be partial, in which case a synthetic "error"
   // frame is appended, and |unwind_error| is set accordingly.
   optional uint64 callstack_iid = 4;
@@ -356,6 +359,9 @@
   // the implementation decided to default/override some parameters.
   optional PerfEvents.Timebase timebase = 1;
 
+  // Description of followers event
+  repeated FollowerEvent followers = 4;
+
   // If the config requested process sharding, report back the count and which
   // of those bins was selected. Never changes for the duration of a trace.
   optional uint32 process_shard_count = 2;
diff --git a/protos/perfetto/trace/sys_stats/sys_stats.proto b/protos/perfetto/trace/sys_stats/sys_stats.proto
index d0ad10f..8532d78 100644
--- a/protos/perfetto/trace/sys_stats/sys_stats.proto
+++ b/protos/perfetto/trace/sys_stats/sys_stats.proto
@@ -179,4 +179,7 @@
     repeated CpuIdleStateEntry cpuidle_state_entry = 2;
   }
   repeated CpuIdleState cpuidle_state = 16;
-}
\ No newline at end of file
+
+  // Read GPU frequency info on Intel/AMD devices.
+  repeated uint64 gpufreq_mhz = 17;
+}
diff --git a/protos/perfetto/trace/system_info.proto b/protos/perfetto/trace/system_info.proto
index 63fe2fc..0048a2c 100644
--- a/protos/perfetto/trace/system_info.proto
+++ b/protos/perfetto/trace/system_info.proto
@@ -25,16 +25,35 @@
   optional string machine = 4;
 }
 
+// Next id: 15;
 message SystemInfo {
   optional Utsname utsname = 1;
   optional string android_build_fingerprint = 2;
 
+  // The manufacturer of the product/hardware.
+  // Source : "ro.product.manufacturer"
+  // Introduced after Android W in Nov 2024 and is not supported on older
+  // versions.
+  optional string android_device_manufacturer = 14;
+
   // The SoC model from which trace is collected
   optional string android_soc_model = 9;
 
+  // The guest SoC model from which trace is collected in case of VMs
+  optional string android_guest_soc_model = 13;
+
   // The hardware reversion from android device
   optional string android_hardware_revision = 10;
 
+  // The storage component from android_device. This field has been introduced
+  // after Android W in Aug 2024 and is not supported on older versions.
+  optional string android_storage_model = 11;
+
+  // The RAM component information from android device. This field has been
+  // introduced after Android W in Aug 2024 and is not supported on older
+  // versions.
+  optional string android_ram_model = 12;
+
   // The version of traced (the same returned by `traced --version`).
   // This is a human readable string with and its format varies depending on
   // the build system and the repo (standalone vs AOSP).
diff --git a/protos/perfetto/trace/system_info/cpu_info.proto b/protos/perfetto/trace/system_info/cpu_info.proto
index 5e04af2..762761a 100644
--- a/protos/perfetto/trace/system_info/cpu_info.proto
+++ b/protos/perfetto/trace/system_info/cpu_info.proto
@@ -19,6 +19,23 @@
 
 // Information about CPUs from procfs and sysfs.
 message CpuInfo {
+  message ArmCpuIdentifier {
+    // Implementer code
+    optional uint32 implementer = 1;
+
+    // Architecture code
+    optional uint32 architecture = 2;
+
+    // CPU variant
+    optional uint32 variant = 3;
+
+    // CPU part
+    optional uint32 part = 4;
+
+    // CPU revision
+    optional uint32 revision = 5;
+  }
+
   // Information about a single CPU.
   message Cpu {
     // Value of "Processor" field from /proc/cpuinfo for this CPU.
@@ -33,6 +50,11 @@
     // Cpu capacity from /sys/devices/system/cpu/cpuX/cpu_capacity where X is
     // the index of this CPU.
     optional uint32 capacity = 3;
+
+    // Code to identify the CPU
+    oneof identifier {
+      ArmCpuIdentifier arm_identifier = 4;
+    }
   }
 
   // Describes available CPUs, one entry per CPU.
diff --git a/protos/perfetto/trace/track_event/BUILD.gn b/protos/perfetto/trace/track_event/BUILD.gn
index c101c46..a7467d7 100644
--- a/protos/perfetto/trace/track_event/BUILD.gn
+++ b/protos/perfetto/trace/track_event/BUILD.gn
@@ -44,11 +44,6 @@
     "track_descriptor.proto",
     "track_event.proto",
   ]
-}
-
-perfetto_proto_library("@TYPE@") {
-  proto_generators = [ "descriptor" ]
   generate_descriptor = "track_event.descriptor"
-  sources = [ "track_event.proto" ]
-  deps = [ ":source_set" ]
+  descriptor_root_source = "track_event.proto"
 }
diff --git a/protos/perfetto/trace/track_event/track_descriptor.proto b/protos/perfetto/trace/track_event/track_descriptor.proto
index 76890f2..2f76c87 100644
--- a/protos/perfetto/trace/track_event/track_descriptor.proto
+++ b/protos/perfetto/trace/track_event/track_descriptor.proto
@@ -37,7 +37,7 @@
 // |TrackEvent::track_uuid|. It is possible but not necessary to emit a
 // TrackDescriptor for this implicit track.
 //
-// Next id: 11.
+// Next id: 14.
 message TrackDescriptor {
   // Unique ID that identifies this track. This ID is global to the whole trace.
   // Producers should ensure that it is unlikely to clash with IDs emitted by
@@ -62,6 +62,9 @@
     // This field is only set by the SDK when perfetto::StaticString is
     // provided.
     string static_name = 10;
+    // Equivalent to name, used just to mark that the data is coming from
+    // android.os.Trace.
+    string atrace_name = 13;
   }
 
   // Associate the track with a process, making it the process-global track.
@@ -96,4 +99,33 @@
   // system events use nanoseconds. It results in broken event nesting when
   // track events and system events share a track.
   optional bool disallow_merging_with_system_tracks = 9;
+
+  // Specifies how the UI should display child tracks of this track (i.e. tracks
+  // where `parent_uuid` is specified to this track `uuid`). Note that this
+  // value is simply a *hint* to the UI: the UI is not guarnateed to respect
+  // this if it has a good reason not to do so.
+  enum ChildTracksOrdering {
+    // The default ordering, with no bearing on how the UI will visualise the
+    // tracks.
+    UNKNOWN = 0;
+
+    // Order tracks by `name` or `static_name` depending on which one has been
+    // specified.
+    LEXICOGRAPHIC = 1;
+
+    // Order tracks by the first `ts` event in a track.
+    CHRONOLOGICAL = 2;
+
+    // Order tracks by `sibling_order_rank` of child tracks. Child tracks with
+    // the lower values will be shown before tracks with higher values. Tracks
+    // with no value will be treated as having 0 rank.
+    EXPLICIT = 3;
+  }
+  optional ChildTracksOrdering child_ordering = 11;
+
+  // An opaque value which allows specifying how two sibling tracks should be
+  // ordered relative to each other: tracks with lower ranks will appear before
+  // tracks with higher ranks. An unspecified rank will be treated as a rank of
+  // 0.
+  optional int32 sibling_order_rank = 12;
 }
diff --git a/protos/perfetto/trace_processor/BUILD.gn b/protos/perfetto/trace_processor/BUILD.gn
index f514f42..1d25669 100644
--- a/protos/perfetto/trace_processor/BUILD.gn
+++ b/protos/perfetto/trace_processor/BUILD.gn
@@ -20,7 +20,6 @@
     "cpp",
     "lite",
     "zero",
-    "source_set",
   ]
   deps = [ "../common:@TYPE@" ]  # needed for descriptor.proto.
   sources = []
@@ -29,16 +28,14 @@
   }
 }
 
-perfetto_proto_library("stack_descriptor") {
-  proto_generators = [ "descriptor" ]
-  generate_descriptor = "stack.descriptor"
+perfetto_proto_library("stack_@TYPE@") {
+  proto_generators = []
   sources = [ "stack.proto" ]
+  generate_descriptor = "stack.descriptor"
+  descriptor_root_source = "stack.proto"
 }
 
 perfetto_proto_library("metrics_impl_@TYPE@") {
-  proto_generators = [
-    "zero",
-    "source_set",
-  ]
+  proto_generators = [ "zero" ]
   sources = [ "metrics_impl.proto" ]
 }
diff --git a/protos/perfetto/trace_processor/trace_processor.proto b/protos/perfetto/trace_processor/trace_processor.proto
index e7a6cd5..bfb2a1f 100644
--- a/protos/perfetto/trace_processor/trace_processor.proto
+++ b/protos/perfetto/trace_processor/trace_processor.proto
@@ -59,7 +59,9 @@
   // 11. Removal of experimental module from stdlib.
   // 12. Changed UI to be more aggresive about version matching.
   //     Added version_code.
-  TRACE_PROCESSOR_CURRENT_API_VERSION = 12;
+  // 13. Added TPM_REGISTER_SQL_MODULE method.
+  // 14. Added parsing mode option to RESET method.
+  TRACE_PROCESSOR_CURRENT_API_VERSION = 14;
 }
 
 // At lowest level, the wire-format of the RPC protocol is a linear sequence of
@@ -87,13 +89,15 @@
   optional string fatal_error = 5;
 
   enum TraceProcessorMethod {
+    reserved 4, 12;
+    reserved "TPM_QUERY_RAW_DEPRECATED";
+    reserved "TPM_REGISTER_SQL_MODULE";
+
     TPM_UNSPECIFIED = 0;
     TPM_APPEND_TRACE_DATA = 1;
     TPM_FINALIZE_TRACE_DATA = 2;
     TPM_QUERY_STREAMING = 3;
     // Previously: TPM_QUERY_RAW_DEPRECATED
-    reserved 4;
-    reserved "TPM_QUERY_RAW_DEPRECATED";
     TPM_COMPUTE_METRIC = 5;
     TPM_GET_METRIC_DESCRIPTORS = 6;
     TPM_RESTORE_INITIAL_TABLES = 7;
@@ -101,6 +105,7 @@
     TPM_DISABLE_AND_READ_METATRACE = 9;
     TPM_GET_STATUS = 10;
     TPM_RESET_TRACE_PROCESSOR = 11;
+    TPM_REGISTER_SQL_PACKAGE = 13;
   }
 
   oneof type {
@@ -132,6 +137,8 @@
     EnableMetatraceArgs enable_metatrace_args = 106;
     // For TPM_RESET_TRACE_PROCESSOR.
     ResetTraceProcessorArgs reset_trace_processor_args = 107;
+    // For TPM_REGISTER_SQL_PACKAGE.
+    RegisterSqlPackageArgs register_sql_package_args = 108;
 
     // TraceProcessorMethod response args.
     // For TPM_APPEND_TRACE_DATA.
@@ -146,6 +153,8 @@
     DisableAndReadMetatraceResult metatrace = 209;
     // For TPM_GET_STATUS.
     StatusResult status = 210;
+    // For TPM_REGISTER_SQL_PACKAGE.
+    RegisterSqlPackageResult register_sql_package_result = 211;
   }
 
   // Previously: RawQueryArgs for TPM_QUERY_RAW_DEPRECATED
@@ -322,9 +331,29 @@
     NO_DROP = 0;
     TRACK_EVENT_RANGE_OF_INTEREST = 1;
   }
+  enum ParsingMode {
+    DEFAULT = 0;
+    TOKENIZE_ONLY = 1;
+    TOKENIZE_AND_SORT = 2;
+  }
   // Mirror of the corresponding perfetto::trace_processor::Config fields.
   optional DropTrackEventDataBefore drop_track_event_data_before = 1;
   optional bool ingest_ftrace_in_raw_table = 2;
   optional bool analyze_trace_proto_content = 3;
   optional bool ftrace_drop_until_all_cpus_valid = 4;
+  optional ParsingMode parsing_mode = 5;
 }
+
+message RegisterSqlPackageArgs {
+  message Module {
+    optional string name = 1;
+    optional string sql = 2;
+  }
+  optional string package_name = 1;
+  repeated Module modules = 2;
+  optional bool allow_override = 3;
+}
+
+message RegisterSqlPackageResult {
+  optional string error = 1;
+}
\ No newline at end of file
diff --git a/protos/third_party/CHROMIUM_OWNERS b/protos/third_party/CHROMIUM_OWNERS
index 0fa67ac..09fc76f 100644
--- a/protos/third_party/CHROMIUM_OWNERS
+++ b/protos/third_party/CHROMIUM_OWNERS
@@ -2,7 +2,6 @@
 # The current sheriffs are listed at:
 # https://oncall.corp.google.com/chrometto-sheriff
 
-agarwaltushar@google.com
 altimin@google.com
 carlscab@google.com
 ddrone@google.com
diff --git a/protos/third_party/chromium/BUILD.gn b/protos/third_party/chromium/BUILD.gn
index b0f4f77..c6e47ad 100644
--- a/protos/third_party/chromium/BUILD.gn
+++ b/protos/third_party/chromium/BUILD.gn
@@ -5,13 +5,9 @@
 perfetto_proto_library("@TYPE@") {
   sources = chrome_track_event_sources
   public_deps = [ "../../perfetto/trace/track_event:@TYPE@" ]
-}
 
-perfetto_proto_library("@TYPE@") {
-  proto_generators = [ "descriptor" ]
-  sources = chrome_track_event_sources
   generate_descriptor = "chrome_track_event.descriptor"
-  deps = [ ":source_set" ]
+  descriptor_root_source = "chrome_track_event.proto"
 
   # When rolled into Chrome, extension descriptor is going to be linked into
   # binary, therefore increasing its size. Including imports means that the
diff --git a/protos/third_party/chromium/chrome_track_event.proto b/protos/third_party/chromium/chrome_track_event.proto
index eab0690..7cf29f6 100644
--- a/protos/third_party/chromium/chrome_track_event.proto
+++ b/protos/third_party/chromium/chrome_track_event.proto
@@ -768,9 +768,10 @@
     TASK_TYPE_LOW_PRIORITY_SCRIPT_EXECUTION = 81;
     TASK_TYPE_STORAGE = 82;
     TASK_TYPE_NETWORKING_UNFREEZABLE_RENDER_BLOCKING_LOADING = 83;
-    TASK_TYPE_MAIN_THREAD_TASK_QUEUE_V8_LOW_PRIORITY = 84;
+    TASK_TYPE_MAIN_THREAD_TASK_QUEUE_V8_USER_VISIBLE = 84;
     TASK_TYPE_CLIPBOARD = 85;
     TASK_TYPE_MACHINE_LEARNING = 86;
+    TASK_TYPE_MAIN_THREAD_TASK_QUEUE_V8_BEST_EFFORT = 87;
   }
 
   enum FrameType {
@@ -978,7 +979,8 @@
     UI_BEFORE_UNLOAD_BROWSER_RESPONSE_TQ = 54;
     IO_BEFORE_UNLOAD_BROWSER_RESPONSE_TQ = 55;
 
-    V8_LOW_PRIORITY_TQ = 56;
+    V8_USER_VISIBLE_TQ = 56;
+    V8_BEST_EFFORT_TQ = 57;
   }
 
   optional Priority priority = 1;
@@ -1391,14 +1393,16 @@
   enum StepName {
     STEP_UNKNOWN = 0;
     STEP_DID_NOT_PRODUCE_FRAME = 1;
+    STEP_DID_NOT_PRODUCE_COMPOSITOR_FRAME = 22;
     STEP_GENERATE_COMPOSITOR_FRAME = 2;
     STEP_GENERATE_RENDER_PASS = 3;
     STEP_ISSUE_BEGIN_FRAME = 4;
     STEP_RECEIVE_COMPOSITOR_FRAME = 5;
     STEP_RECEIVE_BEGIN_FRAME = 6;
     STEP_RECEIVE_BEGIN_FRAME_DISCARD = 7;
-    STEP_SEND_BEGIN_MAIN_FRAME = 8;
+    STEP_SEND_BEGIN_MAIN_FRAME = 8 [deprecated = true];
     STEP_SUBMIT_COMPOSITOR_FRAME = 9;
+    STEP_DRAW_AND_SWAP = 21;
     STEP_SURFACE_AGGREGATION = 10;
     STEP_SEND_BUFFER_SWAP = 11;
     STEP_BUFFER_SWAP_POST_SUBMIT = 12;
@@ -1448,13 +1452,47 @@
   }
   optional StepName step = 1;
   optional FrameSinkId frame_sink_id = 2;
+
+  // Id used to link `ChromeGraphicsPipeline`s corresponding to work done by a
+  // Viz client to produce a frame. This corresponds to
+  // `viz::BeginFrameAck.trace_id`. Covers steps:
+  // * STEP_ISSUE_BEGIN_FRAME
+  // * STEP_RECEIVE_BEGIN_FRAME
+  // * STEP_GENERATE_RENDER_PASS
+  // * STEP_GENERATE_COMPOSITOR_FRAME
+  // * STEP_SUBMIT_COMPOSITOR_FRAME
+  // * STEP_RECEIVE_COMPOSITOR_FRAME
+  // * STEP_RECEIVE_BEGIN_FRAME_DISCARD
+  // * STEP_DID_NOT_PRODUCE_FRAME
+  // * STEP_DID_NOT_PRODUCE_COMPOSITOR_FRAME
+  optional int64 surface_frame_trace_id = 10;
+
+  // Id used to link `ChromeGraphicsPipeline`s corresponding to work done
+  // on creating and presenting one frame *after* surface aggregation. Covers
+  // steps:
+  // * STEP_DRAW_AND_SWAP
+  // * STEP_SURFACE_AGGREGATION
+  // * STEP_SEND_BUFFER_SWAP
+  // * STEP_BUFFER_SWAP_POST_SUBMIT
+  // * STEP_FINISH_BUFFER_SWAP
+  // * STEP_SWAP_BUFFERS_ACK
   optional int64 display_trace_id = 3;
+
+  // `viz::BeginFrameAck.trace_id`s for frames aggregated at this step.
+  // Used with `STEP_SURFACE_AGGREGATION` only. This can be "joined" with
+  // `surface_frame_trace_id`.
+  repeated int64 aggregated_surface_frame_trace_ids = 8;
+
   optional LocalSurfaceId local_surface_id = 4;
   optional int64 frame_sequence = 5;
   optional FrameSkippedReason frame_skipped_reason = 6;
 
   // Optional variable that can be set together with STEP_BACKEND*.
   optional int64 backend_frame_id = 7;
+
+  // List of LatencyInfo.trace_ids associated with this frame, which
+  // should allow us to match input to frame production.
+  repeated int64 latency_ids = 9;
 };
 
 message LibunwindstackUnwinder {
@@ -1521,6 +1559,7 @@
 message StartUp {
   // This enum must be kept up to date with LaunchCauseMetrics.LaunchCause.
   enum LaunchCauseType {
+    UNINITIALIZED = -1;
     OTHER = 0;
     CUSTOM_TAB = 1;
     TWA = 2;
@@ -1540,7 +1579,7 @@
     HOME_SCREEN_SHORTCUT = 16;
     SHARE_INTENT = 17;
     NFC = 18;
-    AUTH_VIEW = 19;
+    AUTH_TAB = 19;
   }
 
   optional int64 activity_id = 1;
@@ -1922,13 +1961,316 @@
     GOOGLE_ADS_LIBRARIES = 14;
     FUNDING_CHOICES = 15;
     ELEMENTOR = 16;
+    SLIDER_REVOLUTION = 17;
   }
   optional ThirdPartyTechnology third_party_technology = 10;
 }
 
+message BeginFrameId {
+  optional uint64 source_id = 1;
+  optional uint64 sequence_number = 2;
+}
+
+message MainFramePipeline {
+  enum Step {
+    UNKNOWN = 0;
+    SEND_BEGIN_MAIN_FRAME = 1;
+    BEGIN_MAIN_FRAME = 2;
+    ABORTED_ON_MAIN = 3;
+    COMMIT_ON_MAIN = 4;
+    READY_TO_COMMIT_ON_IMPL = 5;
+    COMMIT_ON_IMPL = 6;
+    COMMIT_COMPLETE = 7;
+    READY_TO_ACTIVATE = 8;
+    ACTIVATE = 9;
+    DRAW = 10;
+    UPDATE_DISPLAY_TREE = 11;
+  }
+
+  // Id connecting the steps of the pipeline together.
+  optional uint64 main_frame_id = 1;
+  optional Step step = 2;
+
+  // Id for the BeginFrame, which triggered this main frame.
+  // Set for the SEND_BEGIN_MAIN_FRAME step.
+  optional BeginFrameId begin_frame_id = 3;
+
+  // If the step is ABORTED_ON_MAIN, this field will contain the reason.
+  enum AbortedOnMainReason {
+    ABORTED_MAIN_REASON_UNKNOWN = 0;
+    NOT_VISIBLE = 1;
+    DEFERRED_UPDATE = 2;
+    DEFERRED_COMMIT_ABORTED = 3;
+    NO_UPDATE = 4;
+  }
+  optional AbortedOnMainReason aborted_on_main_reason = 4;
+
+  // Id of the last begin frame id issued by renderer compositor by the time
+  // this main frame is drawn for the first time.
+  // The difference between `begin_frame_id` and
+  // `last_begin_frame_id_during_first_draw` can be used to reason about the
+  // main frame latency. Set for the first DRAW step.
+  optional BeginFrameId last_begin_frame_id_during_first_draw = 5;
+}
+
+message CurrentTask {
+  // t1 - t0, where t1 is the start timestamp of this slice and t0 is the start
+  // timestamp of the task containing this slice.
+  optional uint64 event_offset_from_task_start_time_us = 1;
+
+  // Timestamp in microseconds of the start of the task containing this slice.
+  optional uint64 task_start_time_us = 2;
+}
+
+message ChromeLatencyInfo2 {
+  optional int64 trace_id = 1;
+
+  // NEXT ID: 15
+  // All step are optional but the enum is ordered (not by number) below in the
+  // order we expect them to appear if they are emitted in trace in a blocking
+  // fashion.
+  enum Step {
+    STEP_UNSPECIFIED = 0;
+    // Emitted on the browser main thread.
+    STEP_SEND_INPUT_EVENT_UI = 3;
+    // Happens on the renderer's compositor.
+    STEP_HANDLE_INPUT_EVENT_IMPL = 5;
+    STEP_RESAMPLE_SCROLL_EVENTS = 14;
+    STEP_DID_HANDLE_INPUT_AND_OVERSCROLL = 8;
+    // Occurs on the Renderer's main thread.
+    STEP_HANDLE_INPUT_EVENT_MAIN = 4;
+    STEP_MAIN_THREAD_SCROLL_UPDATE = 2;
+    STEP_HANDLE_INPUT_EVENT_MAIN_COMMIT = 1;
+    // Could be emitted on both the renderer's main OR compositor.
+    STEP_HANDLED_INPUT_EVENT_MAIN_OR_IMPL = 9;
+    // Optionally sometimes HANDLED_INPUT_EVENT_MAIN_OR_IMPL will proxy to the
+    // renderer's compositor and this will be emitted.
+    STEP_HANDLED_INPUT_EVENT_IMPL = 10;
+    // Occurs on Browser Main.
+    STEP_TOUCH_EVENT_HANDLED = 12;
+    // Occurs on Browser Main.
+    STEP_GESTURE_EVENT_HANDLED = 13;
+    // Renderer's compositor.
+    STEP_SWAP_BUFFERS = 6 [deprecated = true];
+    // Happens on the VizCompositor in the GPU process.
+    STEP_DRAW_AND_SWAP = 7 [deprecated = true];
+    // Happens on the GPU main thread after the swap has completed.
+    STEP_FINISHED_SWAP_BUFFERS = 11 [deprecated = true];
+    // See above for NEXT ID, enum steps are not ordered by tag number.
+  };
+
+  optional Step step = 2;
+  optional int32 frame_tree_node_id = 3;
+
+  // This enum is a copy of LatencyComponentType enum in Chrome, located in
+  // ui/latency/latency_info.h, modulo added UNKNOWN value per protobuf
+  // practices.
+  enum LatencyComponentType {
+    COMPONENT_UNSPECIFIED = 0;
+    COMPONENT_INPUT_EVENT_LATENCY_BEGIN_RWH = 1;
+    COMPONENT_INPUT_EVENT_LATENCY_SCROLL_UPDATE_ORIGINAL = 2;
+    COMPONENT_INPUT_EVENT_LATENCY_FIRST_SCROLL_UPDATE_ORIGINAL = 3;
+    COMPONENT_INPUT_EVENT_LATENCY_ORIGINAL = 4;
+    COMPONENT_INPUT_EVENT_LATENCY_UI = 5;
+    COMPONENT_INPUT_EVENT_LATENCY_RENDERER_MAIN = 6;
+    COMPONENT_INPUT_EVENT_LATENCY_RENDERING_SCHEDULED_MAIN = 7;
+    COMPONENT_INPUT_EVENT_LATENCY_RENDERING_SCHEDULED_IMPL = 8;
+    COMPONENT_INPUT_EVENT_LATENCY_SCROLL_UPDATE_LAST_EVENT = 9;
+    COMPONENT_INPUT_EVENT_LATENCY_ACK_RWH = 10;
+    COMPONENT_INPUT_EVENT_LATENCY_RENDERER_SWAP = 11;
+    COMPONENT_DISPLAY_COMPOSITOR_RECEIVED_FRAME = 12;
+    COMPONENT_INPUT_EVENT_GPU_SWAP_BUFFER = 13;
+    COMPONENT_INPUT_EVENT_LATENCY_FRAME_SWAP = 14;
+  }
+
+  message ComponentInfo {
+    optional LatencyComponentType component_type = 1;
+
+    // Microsecond timestamp in CLOCK_MONOTONIC domain
+    optional uint64 time_us = 2;
+  };
+
+  repeated ComponentInfo component_info = 4;
+  optional bool is_coalesced = 5;
+  optional int64 gesture_scroll_id = 6;
+  optional int64 touch_id = 7;
+
+  // Must be kept in sync with blink::mojom::EventType.
+  // All values are incremented by one for consistency with other proto enums.
+  enum InputType {
+    UNDEFINED_EVENT = 0;
+    MOUSE_DOWN_EVENT = 1;
+    MOUSE_UP_EVENT = 2;
+    MOUSE_MOVE_EVENT = 3;
+    MOUSE_ENTER_EVENT = 4;
+    MOUSE_LEAVE_EVENT = 5;
+    CONTEXT_MENU_EVENT = 6;
+
+    MOUSE_WHEEL_EVENT = 7;
+
+    RAW_KEY_DOWN_EVENT = 8;
+    KEY_DOWN_EVENT = 9;
+    KEY_UP_EVENT = 10;
+    CHAR_EVENT = 11;
+
+    GESTURE_SCROLL_BEGIN_EVENT = 12;
+    GESTURE_SCROLL_END_EVENT = 13;
+    GESTURE_SCROLL_UPDATE_EVENT = 14;
+    GESTURE_FLING_START_EVENT = 15;
+    GESTURE_FLING_CANCEL_EVENT = 16;
+    GESTURE_PINCH_BEGIN_EVENT = 17;
+    GESTURE_PINCH_END_EVENT = 18;
+    GESTURE_PINCH_UPDATE_EVENT = 19;
+
+    GESTURE_BEGIN_EVENT = 20;
+
+    GESTURE_TAP_DOWN_EVENT = 21;
+    GESTURE_SHOW_PRESS_EVENT = 22;
+    GESTURE_TAP_EVENT = 23;
+    GESTURE_TAP_CANCEL_EVENT = 24;
+    GESTURE_SHORT_PRESS_EVENT = 25;
+    GESTURE_LONG_PRESS_EVENT = 26;
+    GESTURE_LONG_TAP_EVENT = 27;
+    GESTURE_TWO_FINGER_TAP_EVENT = 28;
+    GESTURE_TAP_UNCONFIRMED_EVENT = 29;
+
+    GESTURE_DOUBLE_TAP_EVENT = 30;
+    GESTURE_END_EVENT = 31;
+
+    TOUCH_START_EVENT = 32;
+    TOUCH_MOVE_EVENT = 33;
+    TOUCH_END_EVENT = 34;
+    TOUCH_CANCEL_EVENT = 35;
+    TOUCH_SCROLL_STARTED_EVENT = 36;
+
+    POINTER_DOWN_EVENT = 37;
+    POINTER_UP_EVENT = 38;
+    POINTER_MOVE_EVENT = 39;
+    POINTER_RAW_UPDATE_EVENT = 40;
+    POINTER_CANCEL_EVENT = 41;
+    POINTER_CAUSED_UA_ACTION_EVENT = 42;
+  }
+
+  // The type of input corresponding to this `ChromeLatencyInfo`.
+  optional InputType input_type = 8;
+
+  // Intended to mirror `blink::mojom::InputEventResultState`.
+  enum InputResultState {
+    UNKNOWN = 0;
+    CONSUMED = 1;
+    NOT_CONSUMED = 2;
+    NO_CONSUMER_EXISTS = 3;
+    IGNORED = 4;
+    SET_NON_BLOCKING = 5;
+    SET_NON_BLOCKING_DUE_TO_FLING = 6;
+  }
+
+  // If applicable, the result of handling the input corresponding to this
+  // `ChromeLatencyInfo`.
+  optional InputResultState input_result_state = 9;
+
+  // `trace_id` values corresponding to inputs that were coalesced/combined into
+  // the input for this `ChromeLatencyInfo`.
+  repeated int64 coalesced_trace_ids = 10;
+}
+
+message EventTiming {
+  optional bool cancelable = 1;
+  optional string frame = 2;
+  optional uint32 interaction_id = 3;
+  optional uint32 interaction_offset = 4;
+  optional int64 node_id = 5;
+  optional int64 key_code = 6;
+  optional int32 pointer_id = 7;
+  optional uint64 fallback_time_us = 8;
+  enum EventType {
+    UNDEFINED = 0;
+    AUX_CLICK_EVENT = 1;
+    CLICK_EVENT = 2;
+    CONTEXT_MENU_EVENT = 3;
+    DOUBLE_CLICK_EVENT = 4;
+    MOUSE_DOWN_EVENT = 5;
+    MOUSE_ENTER_EVENT = 6;
+    MOUSE_LEAVE_EVENT = 7;
+    MOUSE_OUT_EVENT = 9;
+    MOUSE_OVER_EVENT = 10;
+    MOUSE_UP_EVENT = 11;
+    POINTER_OVER_EVENT = 12;
+    POINTER_ENTER_EVENT = 13;
+    POINTER_DOWN_EVENT = 14;
+    POINTER_UP_EVENT = 15;
+    POINTER_CANCEL_EVENT = 16;
+    POINTER_OUT_EVENT = 17;
+    POINTER_LEAVE_EVENT = 18;
+    GOT_POINTER_CAPTURE_EVENT = 19;
+    LOST_POINTER_CAPTURE_EVENT = 20;
+    TOUCH_START_EVENT = 21;
+    TOUCH_END_EVENT = 22;
+    TOUCH_CANCEL_EVENT = 23;
+    KEY_DOWN_EVENT = 24;
+    KEY_PRESS_EVENT = 25;
+    KEY_UP_EVENT = 26;
+    BEFORE_INPUT_EVENT = 27;
+    INPUT_EVENT = 28;
+    COMPOSITION_START_EVENT = 29;
+    COMPOSITION_UPDATE_EVENT = 30;
+    COMPOSITION_END_EVENT = 31;
+    DRAG_START_EVENT = 32;
+    DRAG_END_EVENT = 33;
+    DRAG_ENTER_EVENT = 34;
+    DRAG_LEAVE_EVENT = 35;
+    DRAG_OVER_EVENT = 36;
+    DROP_EVENT = 37;
+  }
+  optional EventType type = 9;
+}
+
+// Frame information provided by Android's Choreographer.
+// See
+// https://developer.android.com/ndk/reference/group/choreographer#achoreographerframecallbackdata.
+// Next ID: 4
+message AndroidChoreographerFrameCallbackData {
+  // The time in microseconds at which the frame started being rendered.
+  // See
+  // https://developer.android.com/ndk/reference/group/choreographer#achoreographerframecallbackdata_getframetimenanos.
+  optional int64 frame_time_us = 1;
+
+  message FrameTimeline {
+    // The token used by the platform to identify the frame timeline.
+    // See
+    // https://developer.android.com/ndk/reference/group/choreographer#achoreographerframecallbackdata_getframetimelinevsyncid.
+    optional int64 vsync_id = 1;
+
+    // The difference in microseconds between:
+    // (A) the time at which the frame needs to be ready by in order to be
+    //     presented on time and
+    // (B) the time at which the frame started being rendered (frame_time_us).
+    // See
+    // https://developer.android.com/ndk/reference/group/choreographer#achoreographerframecallbackdata_getframetimelinedeadlinenanos.
+    optional int64 latch_delta_us = 2;
+
+    // The difference in microseconds between:
+    // (A) the time at which the frame is expected to be presented and
+    // (B) the time at which the frame started being rendered (frame_time_us).
+    // See
+    // https://developer.android.com/ndk/reference/group/choreographer#achoreographerframecallbackdata_getframetimelineexpectedpresentationtimenanos.
+    optional int64 present_delta_us = 3;
+  }
+
+  // Possible frame timelines.
+  // See
+  // https://developer.android.com/ndk/reference/group/choreographer#achoreographerframecallbackdata_getframetimelineslength.
+  repeated FrameTimeline frame_timeline = 2;
+
+  // The index (into frame_timeline) of the platform-preferred frame timeline.
+  // See
+  // https://developer.android.com/ndk/reference/group/choreographer#achoreographerframecallbackdata_getpreferredframetimelineindex.
+  optional int64 preferred_frame_timeline_index = 3;
+}
+
 message ChromeTrackEvent {
   // Extension range for Chrome: 1000-1999
-  // Next ID: 1067
+  // Next ID: 1072
   extend TrackEvent {
     optional ChromeAppState chrome_app_state = 1000;
 
@@ -2070,5 +2412,16 @@
         1065;
 
     optional ScrollMetrics scroll_metrics = 1066;
+
+    optional MainFramePipeline main_frame_pipeline = 1067;
+
+    optional ChromeLatencyInfo2 chrome_latency_info = 1068;
+
+    optional EventTiming event_timing = 1069;
+
+    optional AndroidChoreographerFrameCallbackData
+        android_choreographer_frame_callback_data = 1070;
+
+    optional CurrentTask current_task = 1071;
   }
 }
diff --git a/protos/third_party/pprof/BUILD.gn b/protos/third_party/pprof/BUILD.gn
index f38894c..9b8daa2 100644
--- a/protos/third_party/pprof/BUILD.gn
+++ b/protos/third_party/pprof/BUILD.gn
@@ -18,13 +18,8 @@
   proto_generators = [
     "zero",
     "cpp",
-    "source_set",
   ]
   sources = [ "profile.proto" ]
-}
-
-perfetto_proto_library("profile_descriptor") {
-  proto_generators = [ "descriptor" ]
   generate_descriptor = "profile.descriptor"
-  sources = [ "profile.proto" ]
+  descriptor_root_source = "profile.proto"
 }
diff --git a/protos/third_party/statsd/BUILD.gn b/protos/third_party/statsd/BUILD.gn
index 5558d7b..5a1889e 100644
--- a/protos/third_party/statsd/BUILD.gn
+++ b/protos/third_party/statsd/BUILD.gn
@@ -15,10 +15,7 @@
 import("../../../gn/proto_library.gni")
 
 perfetto_proto_library("config_@TYPE@") {
-  proto_generators = [
-    "zero",
-    "source_set",
-  ]
+  proto_generators = [ "zero" ]
   sources = [
     "shell_config.proto",
     "shell_data.proto",
diff --git a/python/BUILD b/python/BUILD
index 1a97e31..cb9d863 100644
--- a/python/BUILD
+++ b/python/BUILD
@@ -79,6 +79,16 @@
     python_version = "PY3",
 )
 
+# GN target: //python:sql_processing
+perfetto_py_library(
+    name = "sql_processing",
+    srcs = [
+        "generators/sql_processing/docs_extractor.py",
+        "generators/sql_processing/docs_parse.py",
+        "generators/sql_processing/utils.py",
+    ],
+)
+
 # GN target: //python:common
 perfetto_py_library(
     name = "common",
diff --git a/python/BUILD.gn b/python/BUILD.gn
index 8608525..e38f216 100644
--- a/python/BUILD.gn
+++ b/python/BUILD.gn
@@ -124,3 +124,11 @@
     "../gn:tp_vendor_py",
   ]
 }
+
+perfetto_py_library("sql_processing") {
+  sources = [
+    "generators/sql_processing/docs_extractor.py",
+    "generators/sql_processing/docs_parse.py",
+    "generators/sql_processing/utils.py",
+  ]
+}
diff --git a/python/generators/diff_tests/runner.py b/python/generators/diff_tests/runner.py
index a6c49f0..49bd278 100644
--- a/python/generators/diff_tests/runner.py
+++ b/python/generators/diff_tests/runner.py
@@ -417,15 +417,12 @@
           os.path.join(metrics_protos_path, 'webview',
                        'all_webview_metrics.descriptor')
       ]
-    result_str = ""
-
     result, run_str = self.__run(metrics_descriptor_paths,
                                  extension_descriptor_paths, keep_input, rebase)
-    result_str += run_str
     if not result:
-      return self.test.name, result_str, None
+      return self.test.name, run_str, None
 
-    return self.test.name, result_str, result
+    return self.test.name, run_str, result
 
 
 # Fetches and executes all diff viable tests.
@@ -435,12 +432,15 @@
   trace_processor_path: str
   trace_descriptor_path: str
   test_runners: List[TestCaseRunner]
+  quiet: bool
 
   def __init__(self, name_filter: str, trace_processor_path: str,
                trace_descriptor: str, no_colors: bool,
-               override_sql_module_paths: List[str], test_dir: str):
+               override_sql_module_paths: List[str], test_dir: str,
+               quiet: bool):
     self.tests = read_all_tests(name_filter, test_dir)
     self.trace_processor_path = trace_processor_path
+    self.quiet = quiet
 
     out_path = os.path.dirname(self.trace_processor_path)
     self.trace_descriptor_path = get_trace_descriptor_path(
@@ -461,6 +461,7 @@
     failures = []
     rebased = []
     test_run_start = datetime.datetime.now()
+    completed_tests = 0
 
     with concurrent.futures.ProcessPoolExecutor() as e:
       fut = [
@@ -471,7 +472,16 @@
       ]
       for res in concurrent.futures.as_completed(fut):
         test_name, res_str, result = res.result()
-        sys.stderr.write(res_str)
+
+        if self.quiet:
+          completed_tests += 1
+          sys.stderr.write(f"\rRan {completed_tests} tests")
+          if not result.passed:
+            sys.stderr.write(f"\r")
+            sys.stderr.write(res_str)
+        else:
+          sys.stderr.write(res_str)
+
         if not result or not result.passed:
           if rebase:
             rebased.append(test_name)
@@ -480,4 +490,6 @@
           perf_results.append(result.perf_result)
     test_time_ms = int(
         (datetime.datetime.now() - test_run_start).total_seconds() * 1000)
+    if self.quiet:
+      sys.stderr.write(f"\r")
     return TestResults(failures, perf_results, rebased, test_time_ms)
diff --git a/python/generators/sql_processing/docs_extractor.py b/python/generators/sql_processing/docs_extractor.py
index 99571f0..6da238c 100644
--- a/python/generators/sql_processing/docs_extractor.py
+++ b/python/generators/sql_processing/docs_extractor.py
@@ -30,18 +30,12 @@
   sql: str
 
   @dataclass
-  class Annotation:
-    key: str
-    value: str
-
-  @dataclass
   class Extract:
     """Extracted documentation for a single view/table/function."""
     obj_kind: ObjKind
     obj_match: Match
 
     description: str
-    annotations: List['DocsExtractor.Annotation']
 
   def __init__(self, path: str, module_name: str, sql: str):
     self.path = path
@@ -57,6 +51,7 @@
     extracted += self._extract_for_kind(ObjKind.function)
     extracted += self._extract_for_kind(ObjKind.table_function)
     extracted += self._extract_for_kind(ObjKind.macro)
+    extracted += self._extract_for_kind(ObjKind.include)
     return extracted
 
   def _extract_for_kind(self, kind: ObjKind) -> List[Extract]:
@@ -71,27 +66,13 @@
 
   def _extract_from_comment(self, kind: ObjKind, match: Match,
                             comment_lines: List[str]) -> Optional[Extract]:
-    extract = DocsExtractor.Extract(kind, match, '', [])
+    extract = DocsExtractor.Extract(kind, match, '')
     for line in comment_lines:
       assert line.startswith('--')
 
       # Remove the comment.
       comment_stripped = line.lstrip('--')
       stripped = comment_stripped.lstrip()
+      extract.description += comment_stripped + "\n"
 
-      # Check if the line is an annotation.
-      if not stripped.startswith('@'):
-        # We are not in annotation: if we haven't seen an annotation yet, we
-        # must be still be parsing the description. Just add to that
-        if not extract.annotations:
-          extract.description += comment_stripped + "\n"
-          continue
-
-        # Otherwise, add to the latest annotation.
-        extract.annotations[-1].value += " " + stripped
-        continue
-
-      # This line is an annotation: find its name and add a new entry
-      annotation, rest = stripped.split(' ', 1)
-      extract.annotations.append(DocsExtractor.Annotation(annotation, rest))
     return extract
diff --git a/python/generators/sql_processing/docs_parse.py b/python/generators/sql_processing/docs_parse.py
index a0ec31c..b01933e 100644
--- a/python/generators/sql_processing/docs_parse.py
+++ b/python/generators/sql_processing/docs_parse.py
@@ -25,10 +25,8 @@
 from python.generators.sql_processing.utils import ALLOWED_PREFIXES
 from python.generators.sql_processing.utils import OBJECT_NAME_ALLOWLIST
 
-from python.generators.sql_processing.utils import COLUMN_ANNOTATION_PATTERN
 from python.generators.sql_processing.utils import ANY_PATTERN
 from python.generators.sql_processing.utils import ARG_DEFINITION_PATTERN
-from python.generators.sql_processing.utils import ARG_ANNOTATION_PATTERN
 
 
 def _is_internal(name: str) -> bool:
@@ -102,88 +100,23 @@
       self._error('Description of the table/view/function/macro is missing')
     return desc.strip()
 
-  def _validate_only_contains_annotations(self,
-                                          ans: List[DocsExtractor.Annotation],
-                                          ans_types: Set[str]):
-    used_ans_types = set(a.key for a in ans)
-    for type in used_ans_types.difference(ans_types):
-      self._error(f'Unknown documentation annotation {type}')
-
-  def _parse_columns(self, ans: List[DocsExtractor.Annotation],
-                     schema: Optional[str]) -> Dict[str, Arg]:
-    column_annotations = {}
-    for t in ans:
-      if t.key != '@column':
-        continue
-      m = re.match(COLUMN_ANNOTATION_PATTERN, t.value)
-      if not m:
-        self._error(f'@column annotation value {t.value} does not match '
-                    f'pattern {COLUMN_ANNOTATION_PATTERN}')
-        continue
-      column_annotations[m.group(1)] = Arg(None, m.group(2).strip())
-
-    if not schema:
-      # If we don't have schema, we have to accept annotations as the source of
-      # truth.
-      return column_annotations
-
-    columns = self._parse_args_definition(schema)
-
+  def _parse_columns(self, schema: str) -> Dict[str, Arg]:
+    columns = self._parse_args_definition(schema) if schema else {}
     for column in columns:
-      inline_comment = columns[column].description
-      if not inline_comment and column not in column_annotations:
+      if not columns[column].description:
         self._error(f'Column "{column}" is missing a description. Please add a '
                     'comment in front of the column definition')
         continue
 
-      if column not in column_annotations:
-        continue
-      annotation = column_annotations[column].description
-      if inline_comment and annotation:
-        self._error(f'Column "{column}" is documented twice. Please remove the '
-                    '@column annotation')
-      if not inline_comment and annotation:
-        # Absorb old-style annotations.
-        columns[column] = Arg(columns[column].type, annotation)
-
-    # Check that the annotations match existing columns.
-    for annotation in column_annotations:
-      if annotation not in columns:
-        self._error(f'Column "{annotation}" is documented but does not exist '
-                    'in table definition')
     return columns
 
-  def _parse_args(self, ans: List[DocsExtractor.Annotation],
-                  sql_args_str: str) -> Dict[str, Arg]:
+  def _parse_args(self, sql_args_str: str) -> Dict[str, Arg]:
     args = self._parse_args_definition(sql_args_str)
 
-    arg_annotations = {}
-    for an in ans:
-      if an.key != '@arg':
-        continue
-      m = re.match(ARG_ANNOTATION_PATTERN, an.value)
-      if m is None:
-        self._error(f'Expected arg documentation "{an.value}" to match pattern '
-                    f'{ARG_ANNOTATION_PATTERN}')
-        continue
-      arg_annotations[m.group(1)] = Arg(m.group(2), m.group(3).strip())
-
     for arg in args:
-      if not args[arg].description and arg not in arg_annotations:
+      if not args[arg].description:
         self._error(f'Arg "{arg}" is missing a description. '
                     'Please add a comment in front of the arg definition.')
-      if args[arg].description and arg in arg_annotations:
-        self._error(f'Arg "{arg}" is documented twice. '
-                    'Please remove the @arg annotation')
-      if not args[arg].description and arg in arg_annotations:
-        # Absorb old-style annotations.
-        # TODO(b/307926059): Remove it once stdlib is migrated.
-        args[arg] = Arg(args[arg].type, arg_annotations[arg].description)
-
-    for arg in arg_annotations:
-      if arg not in args:
-        self._error(
-            f'Arg "{arg}" is documented but not found in function definition.')
     return args
 
   # Parse function argument definition list or a table schema, e.g.
@@ -243,22 +176,33 @@
           f'{type} "{self.name}": CREATE OR REPLACE is not allowed in stdlib '
           f'as standard library modules can only included once. Please just '
           f'use CREATE instead.')
+      return
+
     if _is_internal(self.name):
       return None
 
-    is_perfetto_table_or_view = (
-        perfetto_or_virtual and perfetto_or_virtual.lower() == 'perfetto')
-    if not schema and is_perfetto_table_or_view:
+    if not schema and self.name.lower() != "window":
       self._error(
           f'{type} "{self.name}": schema is missing for a non-internal stdlib'
           f' perfetto table or view')
+      return
 
-    self._validate_only_contains_annotations(doc.annotations, {'@column'})
+    if type.lower() == "table" and not perfetto_or_virtual:
+      self._error(
+          f'{type} "{self.name}": Can only expose CREATE PERFETTO tables')
+      return
+
+    is_virtual_table = type.lower() == "table" and perfetto_or_virtual.lower(
+    ) == "virtual"
+    if is_virtual_table and self.name.lower() != "window":
+      self._error(f'{type} "{self.name}": Virtual tables cannot be exposed.')
+      return
+
     return TableOrView(
         name=self._parse_name(),
         type=type,
         desc=self._parse_desc_not_empty(doc.description),
-        cols=self._parse_columns(doc.annotations, schema),
+        cols=self._parse_columns(schema),
     )
 
 
@@ -309,7 +253,7 @@
     return Function(
         name=name,
         desc=self._parse_desc_not_empty(doc.description),
-        args=self._parse_args(doc.annotations, args),
+        args=self._parse_args(args),
         return_type=ret_type,
         return_desc=ret_desc,
     )
@@ -342,13 +286,12 @@
           f'Function "{self.name}": CREATE OR REPLACE is not allowed in stdlib '
           f'as standard library modules can only included once. Please just '
           f'use CREATE instead.')
+      return
 
     # Ignore internal functions.
     if _is_internal(self.name):
       return None
 
-    self._validate_only_contains_annotations(doc.annotations,
-                                             {'@arg', '@column'})
     name = self._parse_name()
 
     if not _is_snake_case(name):
@@ -358,8 +301,8 @@
     return TableFunction(
         name=name,
         desc=self._parse_desc_not_empty(doc.description),
-        cols=self._parse_columns(doc.annotations, columns),
-        args=self._parse_args(doc.annotations, args),
+        cols=self._parse_columns(columns),
+        args=self._parse_args(args),
     )
 
 
@@ -398,7 +341,6 @@
     if _is_internal(self.name):
       return None
 
-    self._validate_only_contains_annotations(doc.annotations, set())
     name = self._parse_name()
 
     if not _is_snake_case(name):
@@ -410,77 +352,126 @@
         desc=self._parse_desc_not_empty(doc.description),
         return_desc=parse_comment(return_desc),
         return_type=return_type,
-        args=self._parse_args(doc.annotations, args),
+        args=self._parse_args(args),
     )
 
 
-class ParsedFile:
-  """Data class containing all of the docmentation of single SQL file"""
+class Include:
+  package: str
+  module: str
+  module_as_list: List[str]
+
+  def __init__(self, package: str, module: str, module_as_list: List[str]):
+    self.package = package
+    self.module = module
+    self.module_as_list = module_as_list
+
+
+class IncludeParser(AbstractDocParser):
+  """Parses the includes of module."""
+
+  def __init__(self, path: str, module: str):
+    super().__init__(path, module)
+
+  def parse(self, doc: DocsExtractor.Extract) -> Optional[Include]:
+    self.module = list(doc.obj_match)[0]
+    module_as_list = self.module.split('.')
+
+    return Include(
+        package=module_as_list[0],
+        module=self.module,
+        module_as_list=module_as_list,
+    )
+
+
+class ParsedModule:
+  """Data class containing all of the documentation of single SQL file"""
+  package_name: str = ""
+  module_as_list: List[str]
+  module: str
   errors: List[str] = []
   table_views: List[TableOrView] = []
   functions: List[Function] = []
   table_functions: List[TableFunction] = []
   macros: List[Macro] = []
+  includes: List[Include]
 
-  def __init__(self, errors: List[str], table_views: List[TableOrView],
+  def __init__(self, package_name: str, module_as_list: List[str],
+               errors: List[str], table_views: List[TableOrView],
                functions: List[Function], table_functions: List[TableFunction],
-               macros: List[Macro]):
+               macros: List[Macro], includes: List[Include]):
+    self.package_name = package_name
+    self.module_as_list = module_as_list
+    self.module = ".".join(module_as_list)
     self.errors = errors
     self.table_views = table_views
     self.functions = functions
     self.table_functions = table_functions
     self.macros = macros
+    self.includes = includes
 
 
-def parse_file(path: str, sql: str) -> Optional[ParsedFile]:
+def parse_file(path: str, sql: str) -> Optional[ParsedModule]:
   """Reads the provided SQL and, if possible, generates a dictionary with data
     from documentation together with errors from validation of the schema."""
   if sys.platform.startswith('win'):
     path = path.replace('\\', '/')
 
-  # Get module name
-  module_name = path.split('/stdlib/')[-1].split('/')[0]
+  module_as_list: List[str] = path.split('/stdlib/')[-1].split(".sql")[0].split(
+      '/')
 
-  # Disable support for `deprecated` module
-  if module_name == "deprecated":
+  # Get package name
+  package_name = module_as_list[0]
+
+  # Disable support for `deprecated` package
+  if package_name == "deprecated":
     return
 
   # Extract all the docs from the SQL.
-  extractor = DocsExtractor(path, module_name, sql)
+  extractor = DocsExtractor(path, package_name, sql)
   docs = extractor.extract()
   if extractor.errors:
-    return ParsedFile(extractor.errors, [], [], [], [])
+    return ParsedModule(package_name, module_as_list, extractor.errors, [], [],
+                        [], [], [])
 
   # Parse the extracted docs.
-  errors = []
-  table_views = []
-  functions = []
-  table_functions = []
-  macros = []
+  errors: List[str] = []
+  table_views: List[TableOrView] = []
+  functions: List[Function] = []
+  table_functions: List[TableFunction] = []
+  macros: List[Macro] = []
+  includes: List[Include] = []
   for doc in docs:
     if doc.obj_kind == ObjKind.table_view:
-      parser = TableViewDocParser(path, module_name)
+      parser = TableViewDocParser(path, package_name)
       res = parser.parse(doc)
       if res:
         table_views.append(res)
       errors += parser.errors
     if doc.obj_kind == ObjKind.function:
-      parser = FunctionDocParser(path, module_name)
+      parser = FunctionDocParser(path, package_name)
       res = parser.parse(doc)
       if res:
         functions.append(res)
       errors += parser.errors
     if doc.obj_kind == ObjKind.table_function:
-      parser = TableFunctionDocParser(path, module_name)
+      parser = TableFunctionDocParser(path, package_name)
       res = parser.parse(doc)
       if res:
         table_functions.append(res)
       errors += parser.errors
     if doc.obj_kind == ObjKind.macro:
-      parser = MacroDocParser(path, module_name)
+      parser = MacroDocParser(path, package_name)
       res = parser.parse(doc)
       if res:
         macros.append(res)
       errors += parser.errors
+    if doc.obj_kind == ObjKind.include:
+      parser = IncludeParser(path, package_name)
+      res = parser.parse(doc)
+      if res:
+        includes.append(res)
+      errors += parser.errors
 
-  return ParsedFile(errors, table_views, functions, table_functions, macros)
+  return ParsedModule(package_name, module_as_list, errors, table_views,
+                      functions, table_functions, macros, includes)
diff --git a/python/generators/sql_processing/utils.py b/python/generators/sql_processing/utils.py
index d8408d7..edb6b95 100644
--- a/python/generators/sql_processing/utils.py
+++ b/python/generators/sql_processing/utils.py
@@ -49,8 +49,8 @@
 
 CREATE_VIEW_AS_PATTERN = update_pattern(fr'^CREATE VIEW ({NAME}) AS')
 
-DROP_TABLE_VIEW_PATTERN = update_pattern(fr'^DROP (TABLE|VIEW) IF EXISTS '
-                                         fr'({NAME});$')
+DROP_TABLE_VIEW_PATTERN = update_pattern(
+    fr'^DROP (VIEW|TABLE|INDEX) (?:IF EXISTS)? ({NAME});$')
 
 INCLUDE_ALL_PATTERN = update_pattern(
     fr'^INCLUDE PERFETTO MODULE [a-zA-Z0-9_\.]*\*;')
@@ -80,12 +80,10 @@
     fr"({COMMENTS})"
     fr" RETURNS ({TYPE})")
 
-COLUMN_ANNOTATION_PATTERN = update_pattern(fr'^ ({NAME}) ({ANY_WORDS})')
+INCLUDE_PATTERN = update_pattern(fr'^INCLUDE PERFETTO MODULE ([A-Za-z_.*]*);$')
 
 NAME_AND_TYPE_PATTERN = update_pattern(fr' ({NAME})\s+({TYPE}) ')
 
-ARG_ANNOTATION_PATTERN = fr'\s*{NAME_AND_TYPE_PATTERN}\s+({ANY_WORDS})'
-
 ARG_DEFINITION_PATTERN = update_pattern(ARG_PATTERN)
 
 FUNCTION_RETURN_PATTERN = update_pattern(fr'^ ({TYPE})\s+({ANY_WORDS})')
@@ -98,13 +96,15 @@
   function = 'function'
   table_function = 'table_function'
   macro = 'macro'
+  include = 'include'
 
 
 PATTERN_BY_KIND = {
     ObjKind.table_view: CREATE_TABLE_VIEW_PATTERN,
     ObjKind.function: CREATE_FUNCTION_PATTERN,
     ObjKind.table_function: CREATE_TABLE_FUNCTION_PATTERN,
-    ObjKind.macro: CREATE_MACRO_PATTERN
+    ObjKind.macro: CREATE_MACRO_PATTERN,
+    ObjKind.include: INCLUDE_PATTERN
 }
 
 ALLOWED_PREFIXES = {
@@ -113,15 +113,14 @@
     'chrome/util': ['cr'],
     'intervals': ['interval'],
     'graphs': ['graph'],
-    'slices': ['slice'],
-    'linux': ['cpu', 'memory']
+    'slices': ['slice', 'thread_slice', 'process_slice'],
+    'linux': ['cpu', 'memory'],
+    'stacks': ['cpu_profiling'],
 }
 
 # Allows for nonstandard object names.
 OBJECT_NAME_ALLOWLIST = {
     'graphs/partition.sql': ['tree_structural_partition_by_group'],
-    'slices/with_context.sql': ['process_slice', 'thread_slice'],
-    'slices/cpu_time.sql': ['thread_slice_cpu_time']
 }
 
 
@@ -156,7 +155,7 @@
 
 # Given SQL string check whether any of the words is used, and create error
 # string if needed.
-def check_banned_words(sql: str, path: str) -> List[str]:
+def check_banned_words(sql: str) -> List[str]:
   lines = [l.strip() for l in sql.split('\n')]
   errors = []
 
@@ -167,60 +166,62 @@
 
     if 'like' in line.casefold():
       errors.append(
-          'LIKE is banned in trace processor metrics. Prefer GLOB instead.\n'
-          f'Offending file: {path}\n')
+          'LIKE is banned in trace processor metrics. Prefer GLOB instead.\n')
       continue
 
     if 'create_function' in line.casefold():
       errors.append('CREATE_FUNCTION is deprecated in trace processor. '
-                    'Use CREATE PERFETTO FUNCTION instead.\n'
-                    f'Offending file: {path}')
+                    'Use CREATE PERFETTO FUNCTION instead.')
 
     if 'create_view_function' in line.casefold():
-      errors.append(
-          'CREATE_VIEW_FUNCTION is deprecated in trace processor. '
-          'Use CREATE PERFETTO FUNCTION $name RETURNS TABLE instead.\n'
-          f'Offending file: {path}')
+      errors.append('CREATE_VIEW_FUNCTION is deprecated in trace processor. '
+                    'Use CREATE PERFETTO FUNCTION $name RETURNS TABLE instead.')
 
     if 'import(' in line.casefold():
       errors.append('SELECT IMPORT is deprecated in trace processor. '
-                    'Use INCLUDE PERFETTO MODULE instead.\n'
-                    f'Offending file: {path}')
+                    'Use INCLUDE PERFETTO MODULE instead.')
 
   return errors
 
 
 # Given SQL string check whether there is (not allowlisted) usage of
 # CREATE TABLE {name} AS.
-def check_banned_create_table_as(sql: str, filename: str,
-                                 stdlib_path: str) -> List[str]:
+def check_banned_create_table_as(sql: str) -> List[str]:
   errors = []
   for _, matches in match_pattern(CREATE_TABLE_AS_PATTERN, sql).items():
     name = matches[0]
-    if name != "trace_bounds":
+    if name != "_trace_bounds":
       errors.append(
           f"Table '{name}' uses CREATE TABLE which is deprecated "
-          "and this table is not allowlisted. Use CREATE PERFETTO TABLE.\n"
-          f"Offending file: {filename}\n")
+          "and this table is not allowlisted. Use CREATE PERFETTO TABLE.")
   return errors
 
 
 # Given SQL string check whether there is usage of CREATE VIEW {name} AS.
-def check_banned_create_view_as(sql: str, filename: str) -> List[str]:
+def check_banned_create_view_as(sql: str) -> List[str]:
   errors = []
   for _, matches in match_pattern(CREATE_VIEW_AS_PATTERN, sql).items():
     name = matches[0]
     errors.append(f"CREATE VIEW '{name}' is deprecated. "
-                  "Use CREATE PERFETTO VIEW instead.\n"
-                  f"Offending file: {filename}\n")
+                  "Use CREATE PERFETTO VIEW instead.")
+  return errors
+
+
+# Given SQL string check whether there is usage of DROP TABLE/VIEW/MACRO/INDEX.
+def check_banned_drop(sql: str) -> List[str]:
+  errors = []
+  for _, matches in match_pattern(DROP_TABLE_VIEW_PATTERN, sql).items():
+    sql_type = matches[0]
+    name = matches[2]
+    errors.append(f"Dropping object {sql_type} '{name}' is banned.")
   return errors
 
 
 # Given SQL string check whether there is usage of CREATE VIEW {name} AS.
-def check_banned_include_all(sql: str, filename: str) -> List[str]:
+def check_banned_include_all(sql: str) -> List[str]:
   errors = []
   for _, matches in match_pattern(INCLUDE_ALL_PATTERN, sql).items():
     errors.append(
-        f"INCLUDE PERFETTO MODULE with wildcards is not allowed in stdlib. "
-        f"Import specific modules instead. Offending file: {filename}")
+        "INCLUDE PERFETTO MODULE with wildcards is not allowed in stdlib. "
+        "Import specific modules instead.")
   return errors
diff --git a/python/perfetto/batch_trace_processor/api.py b/python/perfetto/batch_trace_processor/api.py
index f6f353a..20de918 100644
--- a/python/perfetto/batch_trace_processor/api.py
+++ b/python/perfetto/batch_trace_processor/api.py
@@ -24,6 +24,7 @@
 import pandas as pd
 
 from perfetto.batch_trace_processor.platform import PlatformDelegate
+from perfetto.common.exceptions import PerfettoException
 from perfetto.trace_processor.api import PLATFORM_DELEGATE as TP_PLATFORM_DELEGATE
 from perfetto.trace_processor.api import TraceProcessor
 from perfetto.trace_processor.api import TraceProcessorException
@@ -41,6 +42,7 @@
 TraceListReference = registry.TraceListReference
 Metadata = Dict[str, str]
 
+MAX_LOAD_WORKERS = 32
 
 # Enum encoding how errors while loading/querying traces in BatchTraceProcessor
 # should be handled.
@@ -61,7 +63,7 @@
 class BatchTraceProcessorConfig:
   tp_config: TraceProcessorConfig
   load_failure_handling: FailureHandling
-  query_failure_handling: FailureHandling
+  execute_failure_handling: FailureHandling
 
   def __init__(
       self,
@@ -179,12 +181,14 @@
     query_executor = self.platform_delegate.create_query_executor(
         len(resolved)) or cf.ThreadPoolExecutor(
             max_workers=multiprocessing.cpu_count())
-    load_exectuor = self.platform_delegate.create_load_executor(
-        len(resolved)) or query_executor
+    # Loading trace involves FS access, so it makes sense to limit parallelism
+    max_load_workers = min(multiprocessing.cpu_count(), MAX_LOAD_WORKERS)
+    load_executor = self.platform_delegate.create_load_executor(
+        len(resolved)) or cf.ThreadPoolExecutor(max_workers=max_load_workers)
 
     self.query_executor = query_executor
     self.tps_and_metadata = [
-        x for x in load_exectuor.map(self._create_tp, resolved) if x is not None
+        x for x in load_executor.map(self._create_tp, resolved) if x is not None
     ]
 
   def metric(self, metrics: List[str]):
@@ -367,7 +371,7 @@
     try:
       return TraceProcessor(
           trace=trace.generator, config=self.config.tp_config), trace.metadata
-    except TraceProcessorException as ex:
+    except Exception as ex:
       if self.config.load_failure_handling == FailureHandling.RAISE_EXCEPTION:
         raise ex
       self._stats.load_failures += 1
diff --git a/python/perfetto/bigtrace/api.py b/python/perfetto/bigtrace/api.py
index d9cbbe7..3dc1fa5 100644
--- a/python/perfetto/bigtrace/api.py
+++ b/python/perfetto/bigtrace/api.py
@@ -23,12 +23,18 @@
 from perfetto.common.query_result_iterator import QueryResultIterator
 from perfetto.common.exceptions import PerfettoException
 
+# C++ INT_MAX which is the maximum gRPC message size
+MAX_MESSAGE_SIZE = 2147483647
+
+
 class Bigtrace:
 
   def __init__(self,
                orchestrator_address="127.0.0.1:5051",
                wait_for_ready_for_testing=False):
-    channel = grpc.insecure_channel(orchestrator_address)
+    options = [('grpc.max_receive_message_length', MAX_MESSAGE_SIZE),
+               ('grpc.max_message_length', MAX_MESSAGE_SIZE)]
+    channel = grpc.insecure_channel(orchestrator_address, options=options)
     self.stub = BigtraceOrchestratorStub(channel)
     self.wait_for_ready_for_testing = wait_for_ready_for_testing
 
@@ -52,7 +58,7 @@
           repeated_batches.extend(result.batch)
         iterator = QueryResultIterator(column_names, repeated_batches)
         df = iterator.as_pandas_dataframe()
-        # TODO(ivankc) Investigate whether this is the
+        # TODO(b/366409021) Investigate whether this is the
         # best place to insert these addresses for performance
         df.insert(0, '_trace_address', response.trace)
         tables.append(df)
diff --git a/python/perfetto/bigtrace_clickhouse/bigtrace_output_schema.py b/python/perfetto/bigtrace_clickhouse/bigtrace_output_schema.py
new file mode 100755
index 0000000..9af6a29
--- /dev/null
+++ b/python/perfetto/bigtrace_clickhouse/bigtrace_output_schema.py
@@ -0,0 +1,46 @@
+#!/usr/bin/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.
+
+# Executable script used by Clickhouse to create the output schema
+# for the TVF
+
+import sys
+from perfetto.trace_processor import TraceProcessor, TraceProcessorConfig
+
+
+def main():
+  for input in sys.stdin:
+    sql_query = input.rstrip("\n")
+
+    try:
+      config = TraceProcessorConfig(
+          bin_path="/var/lib/clickhouse/user_scripts/trace_processor_shell")
+      tp = TraceProcessor(config=config)
+      qr_it = tp.query(sql_query)
+      qr_df = qr_it.as_pandas_dataframe()
+      columns = ", ".join([
+          f"{x} Tuple(`int64_value` Nullable(Int64),"
+          "`string_value` Nullable(String),"
+          "`double_value` Nullable(Float64))" for x in qr_df.columns
+      ])
+    except Exception as e:
+      columns = str(e)
+
+    print(columns + '\n', end='')
+    sys.stdout.flush()
+
+
+if __name__ == "__main__":
+  main()
diff --git a/python/perfetto/bigtrace_clickhouse/bigtrace_query.py b/python/perfetto/bigtrace_clickhouse/bigtrace_query.py
new file mode 100755
index 0000000..0f38884
--- /dev/null
+++ b/python/perfetto/bigtrace_clickhouse/bigtrace_query.py
@@ -0,0 +1,107 @@
+#!/usr/bin/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.
+
+# Executable script used by Clickhouse to make gRPC calls to the Orchestrator
+# from a TVF
+
+import grpc
+import sys
+import os
+
+from protos.perfetto.bigtrace.orchestrator_pb2 import BigtraceQueryArgs
+from protos.perfetto.bigtrace.orchestrator_pb2_grpc import BigtraceOrchestratorStub
+from query_result_iterator import QueryResultIterator
+
+USING_MANUAL_OUTPUT_SCHEMA = True
+
+
+# TODO(lalitm) Look into using this alongside the generated
+# output schema from bigtrace_output_schema.py to improve UX
+# when Variants are not experimental and a concept similar
+# to macros can be used to simplify boilerplate
+def generate_for_udf_output_schema(row):
+  # This is required to allow the use of an inner UDF which creates
+  # a generic output schema where each column is of the type
+  # Tuple(int64_value, string_value, double_value) and values are
+  # accessed by type e.g. column_name.string_value
+  data = []
+  for value in row.__repr__().values():
+    data_tuple = "(NULL,NULL,NULL)"
+    data_type = type(value).__name__ if type(value) else ""
+    data_value = str(value)
+
+    if (data_type == "int"):
+      data_tuple = f"({data_value},NULL,NULL)"
+    elif (data_type == "str"):
+      data_tuple = f"(NULL,'{data_value}',NULL)"
+    elif (data_type == "float"):
+      data_tuple = f"(NULL,NULL,{data_value})"
+
+    data.append(data_tuple)
+  # Convert the list to a tab separated format for Clickhouse to ingest
+  data_str = '\t'.join(data)
+  print(data_str + '\n', end='')
+
+
+def generate_for_manual_output_schema(row):
+  data = [(str(value) if value is not None else "")
+          for value in row.__repr__().values()]
+  # Convert the list to a tab separated format for Clickhouse to ingest
+  data_str = '\t'.join(data)
+  print(data_str + '\n', end='')
+
+
+def main():
+  orchestrator_address = os.environ.get("BIGTRACE_ORCHESTRATOR_ADDRESS")
+
+  for input in sys.stdin:
+    # TODO(lalitm) Investigate why this is required for clickhouse
+    # to return rows
+    input = input.replace('\\n', '')
+    # Clickhouse input is specified as tab separated
+    traces, sql_query = input.strip("\n").split("\t")
+    # Convert the string representation of list of traces given by
+    # Clickhouse into a Python list
+    trace_list = [x[1:-1] for x in traces[1:-1].split(',')]
+
+    channel = grpc.insecure_channel(orchestrator_address)
+    stub = BigtraceOrchestratorStub(channel)
+    args = BigtraceQueryArgs(traces=trace_list, sql_query=sql_query)
+
+    responses = stub.Query(args, wait_for_ready=False)
+    for response in responses:
+      repeated_batches = []
+      results = response.result
+      column_names = results[0].column_names
+      for result in results:
+        repeated_batches.extend(result.batch)
+      qr_it = QueryResultIterator(column_names, repeated_batches)
+
+      for row in qr_it:
+        if USING_MANUAL_OUTPUT_SCHEMA:
+          # Output values directly from the row to be typed by
+          # the user in a manually entered output schema in
+          # the Bigtrace query
+          generate_for_manual_output_schema(row)
+        else:
+          # Convert values to Tuple(int64_value, string_value, double_value)
+          # format as automatically generated by bigtrace_output_schema UDF
+          generate_for_udf_output_schema(row)
+
+    sys.stdout.flush()
+
+
+if __name__ == "__main__":
+  main()
diff --git a/python/perfetto/bigtrace_clickhouse/query_result_iterator.py b/python/perfetto/bigtrace_clickhouse/query_result_iterator.py
new file mode 100644
index 0000000..8fdb56f
--- /dev/null
+++ b/python/perfetto/bigtrace_clickhouse/query_result_iterator.py
@@ -0,0 +1,165 @@
+# 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.
+
+
+class PerfettoException(Exception):
+
+  def __init__(self, message):
+    super().__init__(message)
+
+
+# Provides a Python interface to operate on the contents of QueryResult protos
+class QueryResultIterator:
+  # Values of these constants correspond to the QueryResponse message at
+  # protos/perfetto/trace_processor/trace_processor.proto
+  QUERY_CELL_INVALID_FIELD_ID = 0
+  QUERY_CELL_NULL_FIELD_ID = 1
+  QUERY_CELL_VARINT_FIELD_ID = 2
+  QUERY_CELL_FLOAT64_FIELD_ID = 3
+  QUERY_CELL_STRING_FIELD_ID = 4
+  QUERY_CELL_BLOB_FIELD_ID = 5
+
+  # This is the class returned to the user and contains one row of the
+  # resultant query. Each column name is stored as an attribute of this
+  # class, with the value corresponding to the column name and row in
+  # the query results table.
+  class Row(object):
+    # Required for pytype to correctly infer attributes from Row objects
+    _HAS_DYNAMIC_ATTRIBUTES = True
+
+    def __str__(self):
+      return str(self.__dict__)
+
+    def __repr__(self):
+      return self.__dict__
+
+  def __init__(self, column_names, batches):
+    self.__column_names = list(column_names)
+    self.__column_count = 0
+    self.__count = 0
+    self.__cells = []
+    self.__data_lists = [[], [], [], [], [], []]
+    self.__data_lists_index = [0, 0, 0, 0, 0, 0]
+    self.__current_index = 0
+
+    # Iterate over all the batches and collect their
+    # contents into lists based on the type of the batch
+    batch_index = 0
+    while True:
+      # It's possible on some occasions that there are non UTF-8 characters
+      # in the string_cells field. If this is the case, string_cells is
+      # a bytestring which needs to be decoded (but passing ignore so that
+      # we don't fail in decoding).
+      strings_batch_str = batches[batch_index].string_cells
+      try:
+        strings_batch_str = strings_batch_str.decode('utf-8', 'ignore')
+      except AttributeError:
+        # AttributeError can occur when |strings_batch_str| is an str which
+        # happens when everything in it is UTF-8 (protobuf automatically
+        # does the conversion if it can).
+        pass
+
+      # Null-terminated strings in a batch are concatenated
+      # into a single large byte array, so we split on the
+      # null-terminator to get the individual strings
+      strings_batch = strings_batch_str.split('\0')[:-1]
+      self.__data_lists[QueryResultIterator.QUERY_CELL_STRING_FIELD_ID].extend(
+          strings_batch)
+      self.__data_lists[QueryResultIterator.QUERY_CELL_VARINT_FIELD_ID].extend(
+          batches[batch_index].varint_cells)
+      self.__data_lists[QueryResultIterator.QUERY_CELL_FLOAT64_FIELD_ID].extend(
+          batches[batch_index].float64_cells)
+      self.__data_lists[QueryResultIterator.QUERY_CELL_BLOB_FIELD_ID].extend(
+          batches[batch_index].blob_cells)
+      self.__cells.extend(batches[batch_index].cells)
+
+      if batches[batch_index].is_last_batch:
+        break
+
+      batch_index += 1
+
+    # If there are no rows in the query result, don't bother updating the
+    # counts to avoid dealing with / 0 errors.
+    if len(self.__cells) == 0:
+      return
+
+    # The count we collected so far was a count of all individual columns
+    # in the query result, so we divide by the number of columns in a row
+    # to get the number of rows
+    self.__column_count = len(self.__column_names)
+    self.__count = int(len(self.__cells) / self.__column_count)
+
+    # Data integrity check - see that we have the expected amount of cells
+    # for the number of rows that we need to return
+    if len(self.__cells) % self.__column_count != 0:
+      raise PerfettoException("Cell count " + str(len(self.__cells)) +
+                              " is not a multiple of column count " +
+                              str(len(self.__column_names)))
+
+  # To use the query result as a populated Pandas dataframe, this
+  # function must be called directly after calling query inside
+  # TraceProcessor / Bigtrace.
+  def as_pandas_dataframe(self):
+    try:
+      import pandas as pd
+
+      # Populate the dataframe with the query results
+      rows = []
+      for i in range(0, self.__count):
+        row = []
+        base_cell_index = i * self.__column_count
+        for num in range(len(self.__column_names)):
+          col_type = self.__cells[base_cell_index + num]
+          if col_type == QueryResultIterator.QUERY_CELL_INVALID_FIELD_ID:
+            raise PerfettoException('Invalid cell type')
+
+          if col_type == QueryResultIterator.QUERY_CELL_NULL_FIELD_ID:
+            row.append(None)
+          else:
+            col_index = self.__data_lists_index[col_type]
+            self.__data_lists_index[col_type] += 1
+            row.append(self.__data_lists[col_type][col_index])
+        rows.append(row)
+
+      df = pd.DataFrame(rows, columns=self.__column_names)
+      return df.astype(object).where(df.notnull(), None).reset_index(drop=True)
+
+    except ModuleNotFoundError:
+      raise PerfettoException(
+          'Python dependencies missing. Please pip3 install pandas numpy')
+
+  def __len__(self):
+    return self.__count
+
+  def __iter__(self):
+    return self
+
+  def __next__(self):
+    if self.__current_index == self.__count:
+      raise StopIteration
+    result = QueryResultIterator.Row()
+    base_cell_index = self.__current_index * self.__column_count
+    for num, column_name in enumerate(self.__column_names):
+      col_type = self.__cells[base_cell_index + num]
+      if col_type == QueryResultIterator.QUERY_CELL_INVALID_FIELD_ID:
+        raise PerfettoException('Invalid cell type')
+      if col_type != QueryResultIterator.QUERY_CELL_NULL_FIELD_ID:
+        col_index = self.__data_lists_index[col_type]
+        self.__data_lists_index[col_type] += 1
+        setattr(result, column_name, self.__data_lists[col_type][col_index])
+      else:
+        setattr(result, column_name, None)
+
+    self.__current_index += 1
+    return result
diff --git a/python/perfetto/prebuilts/manifests/trace_processor_shell.py b/python/perfetto/prebuilts/manifests/trace_processor_shell.py
index 2becf1a..1bebad7 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 v47.0
+# This file has been generated by: tools/roll-prebuilts v48.1
 TRACE_PROCESSOR_SHELL_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        10209056,
+        9949656,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/trace_processor_shell',
     'sha256':
-        '203c4c7a3621ee7c60a3d558613216427aa0f7245dc34fbe27e03cbcaf15cbd7',
+        'e9dcf95aaa02f8c00a724f0ff34ba3a454c717beb9900cf9fd97ab142b362452',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -19,11 +19,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9518360,
+        9223224,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/trace_processor_shell',
     'sha256':
-        '02130db81f477e795f0fb33e5183eed6d9350057346d730fe30aac5a6443d9c1',
+        '9a0541a0f52f95bfcb8dc88d94bc4494c660d95eefc40fc946ab43d995051ff7',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        10363488,
+        10142800,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/trace_processor_shell',
     'sha256':
-        '832425c3c7934904d1e0ec1721beb51423de7dbcf399a899973f2b6b464603fa',
+        '18c8730b52f8ee1d9e202031527435b6b2e3149fbd9b1046b2e77d18f06aa337',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        7682608,
+        7329432,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/trace_processor_shell',
     'sha256':
-        '0d5e41279051326311b178c73289d6027493bdd8627f537e538aa39a6f74af81',
+        '0558040998666576e1063d6d626b8aa9e354f18d73d225240f043b3c9236befa',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9949744,
+        9703384,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/trace_processor_shell',
     'sha256':
-        '2a9e5f6ee3d9a0d6007fc5503a9358629d7b3881233ee6fbe157edaa0f5a3b1b',
+        'eeb95cc54358df08375ffae4862c043a6737902179ce8e0408984004c32cf93c',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -75,55 +75,55 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        7716332,
+        7367412,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/trace_processor_shell',
     'sha256':
-        'a3a1f49448e72c368748cb6ec0cb1f63ba4fe5598ff08118053dac68916b9433'
+        'd29b1e6aee52ceff24c072f56c7be7795d0fa29f3596e2633fafa60782384718'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9861544,
+        9598784,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/trace_processor_shell',
     'sha256':
-        '49c9f94802986b9cada8ffab9ec911f21a416966a7c0b2acc3e467f03892ec56'
+        '06e80c562c0043cca9225ade3c961a081bcc7435660117d5a6db26b815d0b9ca'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        10805720,
+        10625488,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x86/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/trace_processor_shell',
     'sha256':
-        'b188a7d95533a26b9eadcc5233d9fcc8552f94c0c7224a7f39f5e6eaebd7e981'
+        '2a576fb397da14d0dabcfa97f5eeec15b4dc55df009308f75a5fdf9de8a9b0dd'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        10150016,
+        9915664,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/trace_processor_shell',
     'sha256':
-        'ff83eb7f53fc91d42c8756bb752fd70ed1f03b40a9daba99a6843c391bc8ff66'
+        'a30be9f09b53110394e87af4d6b41ae24cd74d9a3f97ac1cc4d6ae2057ac6977'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'trace_processor_shell.exe',
     'file_size':
-        10187264,
+        9922560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/windows-amd64/trace_processor_shell.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/trace_processor_shell.exe',
     'sha256':
-        'f9b39c21a99f412697b4bf59f7046f80482c9f07dc3507c2d448dda02915aa14',
+        'd41639844a6c36dbaa195d91e9c356f2172d924c70a1bfed5432c407f857f009',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/python/perfetto/prebuilts/manifests/tracebox.py b/python/perfetto/prebuilts/manifests/tracebox.py
index ec6a7a2..0745b63 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 v47.0
+# This file has been generated by: tools/roll-prebuilts v48.1
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1597456,
+        1613864,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/tracebox',
     'sha256':
-        '1e4b56533ad59e8131473ae6d4204356288a7b7a92241e303ab9865842d36c1d',
+        'dfb1a3affe905d2e7d1f82bc4dda46b1fda6db054d60ae87c3215dd529b77fee',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -19,11 +19,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1475640,
+        1492184,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/tracebox',
     'sha256':
-        '8eae02034fa45581bd7262d1e3095616cc4f9a06a1bc0345cb5cae1277d8b4e4',
+        '4a492a629dd1f13f3146c4b8267c0b163afba8cef1d49e0c00c48bb727496066',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2351336,
+        2380040,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/tracebox',
     'sha256':
-        '0a533702f1ddf80998aaf3e95ce2ee8b154bfcf010c87bb740be6d04ac2e7380',
+        'd70b284e8c28858fd539ae61ca59764d7f9fd6232073c304926e892fe75e692a',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1433188,
+        1450708,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/tracebox',
     'sha256':
-        'd346f0ef77211230dd1f61284badb8edf4736852d446b36bb3d3e52a195934e4',
+        '178fa6a1a9bc80f72d81938d40fe201c25c595ffaff7e030d59c2af09dfcc06c',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2245088,
+        2269816,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/tracebox',
     'sha256':
-        '7899b352ead70894a0cce25cd47db81229804daa168c9b18760003ae2068d3b0',
+        '42c64f9807756aaa08a2bfa13e9e4828c193a6b90ba1329408873c3ebf5adf3f',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -75,42 +75,42 @@
     'file_name':
         'tracebox',
     'file_size':
-        1323304,
+        1333336,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/tracebox',
     'sha256':
-        '727bfbab060aeaf8e97bdef45f318d28c9e7452f91a7135311aff81f72a02fe7'
+        '93a78d2c42e3c00f117e2f155326383f69c891281ed693a39d87b8cb54ca4e19'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        2101880,
+        2115984,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/tracebox',
     'sha256':
-        'ca9f2bbcc6fda0f8b2915e7c6b3d113a0a0ec256da14edcdb3ae4ffe69b4f2cb'
+        '508248a9e47ab605fd742efb700391d7267b68b586199a93e13e6ca14b72fe3d'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        2282928,
+        2302960,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/tracebox',
     'sha256':
-        'ffddf5dcdbe72419a610e7218908a96352b1a6b4fa27cd333aeab34f80a47fc1'
+        '63d20a69c4e0c291329d7917e640fa0d4f146c344e79988e87393b1431d594b1'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'tracebox',
     'file_size':
-        2131400,
+        2147880,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/tracebox',
     'sha256':
-        'defba9ba1730c2583da87326096448cd7445271254392cd8f250e2fde0b54456'
+        'c0ea1d5fd6d020e4c2b45d4d45cdd0c44ae63cd755d69260a3e5d2bacd3cbd6a'
 }]
diff --git a/python/perfetto/prebuilts/manifests/traceconv.py b/python/perfetto/prebuilts/manifests/traceconv.py
index cacc549..abe58a0 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 v47.0
+# This file has been generated by: tools/roll-prebuilts v48.1
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        9481408,
+        9041560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/traceconv',
     'sha256':
-        'b6819bb922438d585e816646c4d60e43bfa823d5f3f499bd8efcaccd26a9009c',
+        'cec2da5cb771a4812d0b2d15604d5023954d28e0af12e87313da2ab70d26b970',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -19,11 +19,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8852520,
+        8375512,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/traceconv',
     'sha256':
-        '9e9eac795578f6ed76128127d8a81c1ce5620b3d47f2c5e23c1f7f2ebbff9bea',
+        '64e200a58ea9c9f366e1071dd274d0023d1fd14043f75dbba3fe0cc138ff5fc7',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        9538432,
+        9134136,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/traceconv',
     'sha256':
-        '01881a82050f36b8db427c741ce236360bb86548e6a6c9445b2477c6150de05b',
+        '87b87e1778367c1e3b99fc77439a28b4911125d2751f9909fd1b51f6bd60b6f4',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7286504,
+        6753020,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/traceconv',
     'sha256':
-        'bc700f945c78c4a65a60bdb499c6c59e671c5173f45f42976fd0661396b72c16',
+        '804c4e13aca5798731056952d9cb0c6ee58795c03477c69514ccd39703060812',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        9168408,
+        8740064,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/traceconv',
     'sha256':
-        'd0192785a56c5088811a175ab083bb599aab5fd594a3248057380038f0b2b5c2',
+        '0d781886531d11e1d573a1ec5e06376ef139bb479eec38c16c8735821c35b895',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -75,55 +75,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        7322024,
+        6792280,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/traceconv',
     'sha256':
-        '77583ed2eefe75796a3fe2a7149d6e234c643d4f83690f732367442bbb259812'
+        '7d91e4133184a3722a25488edd3692c5a195148eba56621014311d3f85d3fc15'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        9123224,
+        8677992,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/traceconv',
     'sha256':
-        'cd93333b3d56f41949066688308a23fdd3fbbf367b03aceff711d8102e696702'
+        'c03c4a901ed23f1e20a12c98ce4556353a62bddcd260fb4d797cd29ff6c49a05'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        9868752,
+        9503704,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/traceconv',
     'sha256':
-        '9b2921ba776ad99bf2ef0d28ff6919dea5ed726a3c61a81159b9d99b39dd7b6d'
+        '704e58a7249de56aadec64d4c0d83bab0821d2c4fd77114a9b71705ff4224539'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        9382824,
+        8964488,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/traceconv',
     'sha256':
-        'c1fdbcb3c2cd15838468c3cc0ed8131a073f220c133384139aa57c4c05b2d34b'
+        'e4f07836fc2a5fb7cd997a9acc4183af7a06997d1e73aac71021af5114b921bc'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        9209856,
+        8763904,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/traceconv.exe',
     'sha256':
-        'ddef23109550784b6069b57bbac1ee627a1ab09086fdd72950965e866cfba536',
+        '084670ac28ed59a9642782a30e051735c1b7474b8cd569b9bc94c305af68290e',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/python/perfetto/trace_processor/api.py b/python/perfetto/trace_processor/api.py
index ebb5640..085ba5a 100644
--- a/python/perfetto/trace_processor/api.py
+++ b/python/perfetto/trace_processor/api.py
@@ -46,6 +46,8 @@
   ingest_ftrace_in_raw: bool
   enable_dev_features: bool
   resolver_registry: Optional[ResolverRegistry]
+  load_timeout: int
+  extra_flags: Optional[List[str]]
 
   def __init__(self,
                bin_path: Optional[str] = None,
@@ -53,13 +55,17 @@
                verbose: bool = False,
                ingest_ftrace_in_raw: bool = False,
                enable_dev_features=False,
-               resolver_registry: Optional[ResolverRegistry] = None):
+               resolver_registry: Optional[ResolverRegistry] = None,
+               load_timeout: int = 2,
+               extra_flags: Optional[List[str]] = None):
     self.bin_path = bin_path
     self.unique_port = unique_port
     self.verbose = verbose
     self.ingest_ftrace_in_raw = ingest_ftrace_in_raw
     self.enable_dev_features = enable_dev_features
     self.resolver_registry = resolver_registry
+    self.load_timeout = load_timeout
+    self.extra_flags = extra_flags
 
 
 class TraceProcessor:
@@ -187,7 +193,9 @@
                                       self.config.verbose,
                                       self.config.ingest_ftrace_in_raw,
                                       self.config.enable_dev_features,
-                                      self.platform_delegate)
+                                      self.platform_delegate,
+                                      self.config.load_timeout,
+                                      self.config.extra_flags)
     return TraceProcessorHttp(url, protos=self.protos)
 
   def _parse_trace(self, trace: TraceReference):
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 00bd98a..ed15b47 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/python/perfetto/trace_processor/protos.py b/python/perfetto/trace_processor/protos.py
index ea13297..b2ac388 100644
--- a/python/perfetto/trace_processor/protos.py
+++ b/python/perfetto/trace_processor/protos.py
@@ -43,9 +43,7 @@
 
     def create_message_factory(message_type):
       message_desc = self.descriptor_pool.FindMessageTypeByName(message_type)
-      if hasattr(message_factory, 'GetMessageClass'):
-        return message_factory.GetMessageClass(message_desc)
-      return message_factory.MessageFactory().GetPrototype(message_desc)
+      return message_factory.GetMessageClass(message_desc)
 
     # Create proto messages to correctly communicate with the RPC API by sending
     # and receiving data as protos
diff --git a/python/perfetto/trace_processor/shell.py b/python/perfetto/trace_processor/shell.py
index 40cf964..2211912 100644
--- a/python/perfetto/trace_processor/shell.py
+++ b/python/perfetto/trace_processor/shell.py
@@ -16,18 +16,26 @@
 import os
 import subprocess
 import sys
+import tempfile
 import time
+from typing import List, Optional
 from urllib import request, error
 
+from perfetto.common.exceptions import PerfettoException
 from perfetto.trace_processor.platform import PlatformDelegate
 
 # Default port that trace_processor_shell runs on
 TP_PORT = 9001
 
 
-def load_shell(bin_path: str, unique_port: bool, verbose: bool,
-               ingest_ftrace_in_raw: bool, enable_dev_features: bool,
-               platform_delegate: PlatformDelegate):
+def load_shell(bin_path: str,
+               unique_port: bool,
+               verbose: bool,
+               ingest_ftrace_in_raw: bool,
+               enable_dev_features: bool,
+               platform_delegate: PlatformDelegate,
+               load_timeout: int = 2,
+               extra_flags: Optional[List[str]] = None):
   addr, port = platform_delegate.get_bind_addr(
       port=0 if unique_port else TP_PORT)
   url = f'{addr}:{str(port)}'
@@ -45,27 +53,34 @@
   if enable_dev_features:
     args.append('--dev')
 
+  if extra_flags:
+    args.extend(extra_flags)
+
+  temp_stdout = tempfile.TemporaryFile()
+  temp_stderr = tempfile.TemporaryFile()
   p = subprocess.Popen(
       tp_exec + args,
       stdin=subprocess.DEVNULL,
-      stdout=subprocess.DEVNULL,
-      stderr=None if verbose else subprocess.DEVNULL)
+      stdout=temp_stdout,
+      stderr=None if verbose else temp_stderr)
 
   success = False
-  for _ in range(3):
+  for _ in range(load_timeout + 1):
     try:
       if p.poll() is None:
         _ = request.urlretrieve(f'http://{url}/status')
         success = True
       break
-    except error.URLError:
+    except (error.URLError, ConnectionError):
       time.sleep(1)
 
   if not success:
-    raise Exception(
-        "Trace processor failed to start. Try rerunning with "
-        "verbose=True in TraceProcessorConfig for more detailed "
-        "information and file a bug at https://goto.google.com/perfetto-bug "
-        "or https://github.com/google/perfetto/issues if necessary.")
+    p.kill()
+    temp_stdout.seek(0)
+    stdout = temp_stdout.read().decode("utf-8")
+    temp_stderr.seek(0)
+    stderr = temp_stderr.read().decode("utf-8")
+    raise PerfettoException("Trace processor failed to start.\n"
+                            f"stdout: {stdout}\nstderr: {stderr}\n")
 
   return url, p
diff --git a/python/perfetto/trace_processor/trace_processor.descriptor b/python/perfetto/trace_processor/trace_processor.descriptor
index bb8e8b9..976fb9b 100644
--- a/python/perfetto/trace_processor/trace_processor.descriptor
+++ b/python/perfetto/trace_processor/trace_processor.descriptor
Binary files differ
diff --git a/python/setup.py b/python/setup.py
index a484eda..6bb7364 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -3,12 +3,15 @@
 setup(
     name='perfetto',
     packages=[
-        'perfetto', 'perfetto.batch_trace_processor',
-        'perfetto.trace_processor', 'perfetto.trace_uri_resolver'
+        'perfetto',
+        'perfetto.batch_trace_processor',
+        'perfetto.common',
+        'perfetto.trace_processor',
+        'perfetto.trace_uri_resolver',
     ],
     package_data={'perfetto.trace_processor': ['*.descriptor']},
     include_package_data=True,
-    version='0.7.0',
+    version='0.11.0',
     license='apache-2.0',
     description='Python API for Perfetto\'s Trace Processor',
     author='Perfetto',
diff --git a/python/test/api_integrationtest.py b/python/test/api_integrationtest.py
index 9e6e1bd..cb6e9fe 100644
--- a/python/test/api_integrationtest.py
+++ b/python/test/api_integrationtest.py
@@ -15,6 +15,7 @@
 
 import io
 import os
+import tempfile
 import unittest
 from typing import Optional
 
@@ -133,6 +134,17 @@
     with self.assertRaises(TraceProcessorException):
       _ = create_tp(trace=f)
 
+  def test_runtime_error(self):
+    # We emulate a situation when TP returns an error by passing the --version
+    # flag. This makes TP output version information and exit, instead of
+    # starting an http server.
+    config = TraceProcessorConfig(
+        bin_path=os.environ["SHELL_PATH"], extra_flags=["--version"])
+    with self.assertRaisesRegex(
+        TraceProcessorException,
+        expected_regex='.*Trace Processor RPC API version:.*'):
+      TraceProcessor(trace=io.BytesIO(b''), config=config)
+
   def test_trace_path(self):
     # Get path to trace_processor_shell and construct TraceProcessor
     tp = create_tp(trace=example_android_trace_path())
@@ -280,3 +292,18 @@
     with self.assertRaisesRegex(
         TraceProcessorException, expected_regex='.*source.*generator.*'):
       _ = btp.query('select * from sl')
+
+  def test_extra_flags(self):
+    with tempfile.TemporaryDirectory() as temp_dir:
+      test_module_dir = os.path.join(temp_dir, 'ext')
+      os.makedirs(test_module_dir)
+      test_module = os.path.join(test_module_dir, 'module.sql')
+      with open(test_module, 'w') as f:
+        f.write('CREATE TABLE test_table AS SELECT 123 AS test_value\n')
+      config = TraceProcessorConfig(
+          bin_path=os.environ["SHELL_PATH"],
+          extra_flags=['--add-sql-module', test_module_dir])
+      with TraceProcessor(trace=io.BytesIO(b''), config=config) as tp:
+        qr_iterator = tp.query(
+            'SELECT IMPORT("ext.module"); SELECT test_value FROM test_table')
+        self.assertEqual(next(qr_iterator).test_value, 123)
diff --git a/python/test/bigtrace_api_integrationtest.py b/python/test/bigtrace_api_integrationtest.py
index 73bd852..779de6e 100644
--- a/python/test/bigtrace_api_integrationtest.py
+++ b/python/test/bigtrace_api_integrationtest.py
@@ -49,36 +49,72 @@
     self.orchestrator.wait()
     del self.client
 
-  def test_valid_traces(self):
+  def test_simple_valid_request(self):
     result = self.client.query([
-        f"{self.root_dir}/test/data/api24_startup_cold.perfetto-trace",
-        f"{self.root_dir}/test/data/api24_startup_hot.perfetto-trace"
+        f"/local/{self.root_dir}/test/data/api24_startup_cold.perfetto-trace",
+        f"/local/{self.root_dir}/test/data/api24_startup_hot.perfetto-trace"
     ], "SELECT count(1) as count FROM slice LIMIT 5")
 
     self.assertEqual(
-        result.loc[
-            result['_trace_address'] ==
-            f"{self.root_dir}/test/data/api24_startup_cold.perfetto-trace",
-            'count'].iloc[0], 9726)
+        result.loc[result['_trace_address'] ==
+                   f"/local/{self.root_dir}/test/data/"
+                   "api24_startup_cold.perfetto-trace", 'count'].iloc[0], 9726)
     self.assertEqual(
-        result.loc[
-            result['_trace_address'] ==
-            f"{self.root_dir}/test/data/api24_startup_hot.perfetto-trace",
-            'count'].iloc[0], 5726)
+        result.loc[result['_trace_address'] ==
+                   f"/local/{self.root_dir}/test/data/"
+                   "api24_startup_hot.perfetto-trace", 'count'].iloc[0], 5726)
 
-  def test_empty_traces(self):
+  def test_include_perfetto_module_query(self):
+    traces = [
+        f"/local/{self.root_dir}/test/data/android_startup_real.perfetto_trace"
+    ]
+    result = self.client.query(
+        traces, "INCLUDE PERFETTO MODULE android.binder; "
+        "SELECT client_process FROM android_binder_txns")
+    self.assertEqual(len(result), 15874)
+    self.assertEqual(len(result.columns), 2)
+
+  def test_empty_trace_list(self):
     with self.assertRaises(PerfettoException):
       result = self.client.query([], "SELECT count(1) FROM slice LIMIT 5")
 
   def test_empty_sql_string(self):
     with self.assertRaises(PerfettoException):
       result = self.client.query([
-          f"{self.root_dir}/test/data/api24_startup_cold.perfetto-trace",
-          f"{self.root_dir}/test/data/api24_startup_hot.perfetto-trace"
+          f"/local/{self.root_dir}/test/data/api24_startup_cold.perfetto-trace",
+          f"/local/{self.root_dir}/test/data/api24_startup_hot.perfetto-trace"
       ], "")
 
-  def test_message_limit_exceeded(self):
+  def test_empty_trace_string(self):
+    with self.assertRaises(PerfettoException):
+      result = self.client.query([""], "SELECT count(1) FROM slice LIMIT 5")
+
+  def test_prefix_present_no_trace_path(self):
+    with self.assertRaises(PerfettoException):
+      result = self.client.query(["/local"],
+                                 "SELECT count(1) FROM slice LIMIT 5")
+
+  def test_invalid_prefix_format(self):
+    with self.assertRaises(PerfettoException):
+      result = self.client.query([
+          f"??{self.root_dir}/test/data/api24_startup_cold.perfetto-trace",
+      ], "")
+
+  def test_invalid_prefix_name(self):
+    with self.assertRaises(PerfettoException):
+      result = self.client.query([
+          f"/badprefix/{self.root_dir}/test/data/"
+          "api24_startup_cold.perfetto-trace"
+      ], "SELECT count(1) FROM slice LIMIT 5"),
+
+  def test_no_prefix(self):
     with self.assertRaises(PerfettoException):
       result = self.client.query(
-          [f"{self.root_dir}/test/data/long_task_tracking_trace"],
-          "SELECT * FROM slice")
+          [f"/{self.root_dir}/test/data/api24_startup_cold.perfetto-trace"],
+          "SELECT count(1) FROM slice LIMIT 5")
+
+  def test_unauthenticated_gcs(self):
+    with self.assertRaises(PerfettoException):
+      result = self.client.query(
+          [f"/gcs/trace_bucket_example/o/api24_startup_cold.perfetto-trace"],
+          "SELECT count(1) FROM slice LIMIT 5")
diff --git a/python/test/bigtrace_gcs_manualtest.py b/python/test/bigtrace_gcs_manualtest.py
new file mode 100644
index 0000000..8567dda
--- /dev/null
+++ b/python/test/bigtrace_gcs_manualtest.py
@@ -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 os
+import perfetto.bigtrace.api
+import subprocess
+import unittest
+
+from perfetto.common.exceptions import PerfettoException
+
+# To run this test you must setup the a GCS bucket and a GKE cluster setup with
+# Bigtrace running.
+# This should be executed within the same VPC to allow for connection to the
+# service.
+
+# This should be replaced with the name of the trace bucket you have deployed
+# on.
+TRACE_BUCKET_NAME = "trace_example_bucket"
+# This should be loaded in the top level of the bucket.
+TRACE_PATH = "android_startup_real.perfetto_trace"
+# This should be replaced with the address of the Orchestrator service for the
+# Bigtrace service.
+ORCHESTRATOR_ADDRESS = "127.0.0.1:5052"
+# This can be changed if testing on a different trace.
+QUERY_RESULT_COUNT = 339338
+
+
+class BigtraceGcsTest(unittest.TestCase):
+
+  def setUpClass(self):
+    self.client = perfetto.bigtrace.api.Bigtrace(
+        wait_for_ready_for_testing=True)
+
+  def test_valid_trace(self):
+    traces = [f"/gcs/{TRACE_BUCKET_NAME}/o/{TRACE_PATH}"]
+    result = self.client.query(traces, "SELECT count(1) as count FROM slice")
+    self.assertEqual(result['count'].iloc[0], QUERY_RESULT_COUNT)
+
+  def test_invalid_trace(self):
+    with self.assertRaises(PerfettoException):
+      traces = [f"/gcs/{TRACE_BUCKET_NAME}/o/badpath"]
+      result = self.client.query(traces, "SELECT count(1) as count FROM slice")
+
+  def test_invalid_bucket(self):
+    with self.assertRaises(PerfettoException):
+      traces = [f"/gcs//o/{TRACE_PATH}"]
+      result = self.client.query(traces, "SELECT count(1) as count FROM slice")
diff --git a/python/test/stdlib_unittest.py b/python/test/stdlib_unittest.py
index 34725cc..9114e65 100644
--- a/python/test/stdlib_unittest.py
+++ b/python/test/stdlib_unittest.py
@@ -25,93 +25,6 @@
 
 class TestStdlib(unittest.TestCase):
 
-  def test_valid_table(self):
-    res = parse_file(
-        'foo/bar.sql', f'''
--- First line.
--- Second line.
--- @column slice_id           Id of slice.
--- @column slice_name         Name of slice.
-CREATE TABLE foo_table AS
-SELECT 1;
-    '''.strip())
-    self.assertListEqual(res.errors, [])
-
-    table = res.table_views[0]
-    self.assertEqual(table.name, 'foo_table')
-    self.assertEqual(table.desc, 'First line.\n Second line.')
-    self.assertEqual(table.type, 'TABLE')
-    self.assertEqual(
-        table.cols, {
-            'slice_id': Arg(None, 'Id of slice.'),
-            'slice_name': Arg(None, 'Name of slice.'),
-        })
-
-  def test_valid_function(self):
-    res = parse_file(
-        'foo/bar.sql', f'''
--- First line.
--- Second line.
--- @arg utid INT              Utid of thread.
--- @arg name STRING           String name.
-CREATE PERFETTO FUNCTION foo_fn(utid INT, name STRING)
--- Exists.
-RETURNS BOOL
-AS
-SELECT 1;
-    '''.strip())
-    self.assertListEqual(res.errors, [])
-
-    fn = res.functions[0]
-    self.assertEqual(fn.name, 'foo_fn')
-    self.assertEqual(fn.desc, 'First line.\n Second line.')
-    self.assertEqual(
-        fn.args, {
-            'utid': Arg('INT', 'Utid of thread.'),
-            'name': Arg('STRING', 'String name.'),
-        })
-    self.assertEqual(fn.return_type, 'BOOL')
-    self.assertEqual(fn.return_desc, 'Exists.')
-
-  def test_valid_table_function(self):
-    res = parse_file(
-        'foo/bar.sql', f'''
--- Table comment.
--- @arg utid INT              Utid of thread.
--- @arg name STRING           String name.
--- @column slice_id           Id of slice.
--- @column slice_name         Name of slice.
-CREATE PERFETTO FUNCTION foo_view_fn(utid INT, name STRING)
-RETURNS TABLE(slice_id INT, slice_name STRING)
-AS SELECT 1;
-    '''.strip())
-    self.assertListEqual(res.errors, [])
-
-    fn = res.table_functions[0]
-    self.assertEqual(fn.name, 'foo_view_fn')
-    self.assertEqual(fn.desc, 'Table comment.')
-    self.assertEqual(
-        fn.args, {
-            'utid': Arg('INT', 'Utid of thread.'),
-            'name': Arg('STRING', 'String name.'),
-        })
-    self.assertEqual(
-        fn.cols, {
-            'slice_id': Arg('INT', 'Id of slice.'),
-            'slice_name': Arg('STRING', 'Name of slice.'),
-        })
-
-  def test_missing_module_name(self):
-    res = parse_file(
-        'foo/bar.sql', f'''
--- Comment
--- @column slice_id           Id of slice.
-CREATE TABLE bar_table AS
-SELECT 1;
-    '''.strip())
-    # Expecting an error: function prefix (bar) not matching module name (foo).
-    self.assertEqual(len(res.errors), 1)
-
   # Checks that custom prefixes (cr for chrome/util) are allowed.
   def test_custom_module_prefix(self):
     res = parse_file(
@@ -185,79 +98,6 @@
     # (allowed: foo).
     self.assertEqual(len(res.errors), 1)
 
-  def test_common_does_not_include_module_name(self):
-    res = parse_file(
-        'common/bar.sql', f'''
--- Comment.
--- @column slice_id           Id of slice.
-CREATE TABLE common_table AS
-SELECT 1;
-    '''.strip())
-    # Expecting an error: functions in common/ should not have a module prefix.
-    self.assertEqual(len(res.errors), 1)
-
-  def test_cols_typo(self):
-    res = parse_file(
-        'foo/bar.sql', f'''
--- Comment.
---
--- @column slice_id2          Foo.
--- @column slice_name         Bar.
-CREATE TABLE bar_table AS
-SELECT 1;
-    '''.strip())
-    # Expecting an error: column slice_id2 not found in the table.
-    self.assertEqual(len(res.errors), 1)
-
-  def test_cols_no_desc(self):
-    res = parse_file(
-        'foo/bar.sql', f'''
--- Comment.
---
--- @column slice_id
--- @column slice_name         Bar.
-CREATE TABLE bar_table AS
-SELECT 1;
-    '''.strip())
-    # Expecting an error: column slice_id is missing a description.
-    self.assertEqual(len(res.errors), 1)
-
-  def test_args_typo(self):
-    res = parse_file(
-        'foo/bar.sql', f'''
--- Comment.
---
--- @arg utid2 INT             Uint.
--- @arg name STRING           String name.
-CREATE PERFETTO FUNCTION foo_fn(utid INT, name STRING)
--- Exists.
-RETURNS BOOL
-AS
-SELECT 1;
-    '''.strip())
-    # Expecting 2 errors:
-    # - arg utid2 not found in the function (should be utid);
-    # - utid not documented.
-    self.assertEqual(len(res.errors), 2)
-
-  def test_args_no_desc(self):
-    res = parse_file(
-        'foo/bar.sql', f'''
--- Comment.
---
--- @arg utid INT
--- @arg name STRING           String name.
-CREATE PERFETTO FUNCTION foo_fn(utid INT, name STRING)
--- Exists.
-RETURNS BOOL
-AS
-SELECT 1;
-    '''.strip())
-    # Expecting 2 errors:
-    # - arg utid is missing a description;
-    # - arg utid is not documented.
-    self.assertEqual(len(res.errors), 2)
-
   def test_ret_no_desc(self):
     res = parse_file(
         'foo/bar.sql', f'''
@@ -295,35 +135,6 @@
     self.assertEqual(fn.desc,
                      'This\n is\n\n a\n      very\n\n long\n\n description.')
 
-  def test_multiline_arg_desc(self):
-    res = parse_file(
-        'foo/bar.sql', f'''
--- Comment.
---
--- @arg utid INT              Uint
--- spread
---
--- across lines.
--- @arg name STRING            String name
---                             which spans across multiple lines
--- inconsistently.
-CREATE PERFETTO FUNCTION foo_fn(utid INT, name STRING)
--- Exists.
-RETURNS BOOL
-AS
-SELECT 1;
-    '''.strip())
-
-    fn = res.functions[0]
-    self.assertEqual(
-        fn.args, {
-            'utid':
-                Arg('INT', 'Uint spread  across lines.'),
-            'name':
-                Arg(
-                    'STRING', 'String name which spans across multiple lines '
-                    'inconsistently.'),
-        })
 
   def test_function_name_style(self):
     res = parse_file(
diff --git a/python/tools/check_imports.py b/python/tools/check_imports.py
index 7f732a8..37c1a95 100755
--- a/python/tools/check_imports.py
+++ b/python/tools/check_imports.py
@@ -14,17 +14,6 @@
 # limitations under the License.
 """
 Enforce import rules for https://ui.perfetto.dev.
-Directory structure encodes ideas about the expected dependency graph
-of the code in those directories. Both in a fuzzy sense: we expect code
-withing a directory to have high cohesion within the directory and low
-coupling (aka fewer imports) outside of the directory - but also
-concrete rules:
-- "base should not depend on the fronted"
-- "plugins should only directly depend on the public API"
-- "we should not have circular dependencies"
-
-Without enforcement exceptions to this rule quickly slip in. This
-script allows such rules to be enforced at presubmit time.
 """
 
 import sys
@@ -32,324 +21,133 @@
 import re
 import collections
 import argparse
+import fnmatch
 
 ROOT_DIR = os.path.dirname(
     os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 UI_SRC_DIR = os.path.join(ROOT_DIR, 'ui', 'src')
 
-# Current plan for the dependency tree of the UI code (2023-09-21)
-# black = current
-# red = planning to remove
-# green = planning to add
-PLAN_DOT = """
-digraph g {
-    mithril [shape=rectangle, label="mithril"];
-    protos [shape=rectangle, label="//protos/perfetto"];
+NODE_MODULES = '%node_modules%'  # placeholder to depend on any node module.
 
-    _gen [shape=ellipse, label="/gen"];
-    _base [shape=ellipse, label="/base"];
-    _core [shape=ellipse, label="/core"];
-    _engine [shape=ellipse, label="/engine"];
+# The format of this array is: (src) -> (dst).
+# If src or dst are arrays, the semantic is the cartesian product, e.g.:
+# [a,b] -> [c,d] is equivalent to allowing a>c, a>d, b>c, b>d.
+DEPS_ALLOWLIST = [
+    # Everything can depend on base/, protos and NPM packages.
+    ('*', ['/base/*', '/protos/index', '/gen/perfetto_version', NODE_MODULES]),
 
-    _frontend [shape=ellipse, label="/frontend" color=red];
-    _common [shape=ellipse, label="/common" color=red];
-    _controller [shape=ellipse, label="/controller" color=red];
-    _tracks [shape=ellipse, label="/tracks" color=red];
+    # Integration tests can depend on everything.
+    ('/test/*', '*'),
 
-    _widgets [shape=ellipse, label="/widgets"];
-
-    _public [shape=ellipse, label="/public"];
-    _plugins [shape=ellipse, label="/plugins"];
-    _chrome_extension [shape=ellipse, label="/chrome_extension"];
-    _trace_processor [shape=ellipse, label="/trace_processor" color="green"];
-    _protos [shape=ellipse, label="/protos"];
-    engine_worker_bundle [shape=cds, label="Engine worker bundle"];
-    frontend_bundle [shape=cds, label="Frontend bundle"];
-
-    engine_worker_bundle -> _engine;
-    frontend_bundle -> _core [color=green];
-    frontend_bundle -> _frontend [color=red];
-
-    _core -> _public;
-    _plugins -> _public;
-
-    _widgets -> _base;
-    _core -> _base;
-    _core -> _widgets;
-
-
-    _widgets -> mithril;
-    _plugins -> mithril;
-    _core -> mithril
-
-    _plugins -> _widgets;
-
-    _core -> _chrome_extension;
-
-    _frontend -> _widgets [color=red];
-    _common -> _core [color=red];
-    _frontend -> _core [color=red];
-    _controller -> _core [color=red];
-
-    _frontend -> _controller [color=red];
-    _frontend -> _common [color=red];
-    _controller -> _frontend  [color=red];
-    _controller -> _common [color=red];
-    _common -> _controller [color=red];
-    _common -> _frontend [color=red];
-    _tracks -> _frontend  [color=red];
-    _tracks -> _controller  [color=red];
-    _common -> _chrome_extension [color=red];
-
-    _core -> _trace_processor [color=green];
-
-    _engine -> _trace_processor [color=green];
-    _engine -> _common [color=red];
-    _engine -> _base;
-
-    _gen -> protos;
-    _core -> _gen [color=red];
-
-    _core -> _protos;
-    _protos -> _gen;
-    _trace_processor -> _protos [color=green];
-
-    _trace_processor -> _public [color=green];
-
-    npm_trace_processor [shape=cds, label="npm trace_processor" color="green"];
-    npm_trace_processor -> engine_worker_bundle [color="green"];
-    npm_trace_processor -> _trace_processor [color="green"];
-}
-"""
-
-
-class Failure(object):
-
-  def __init__(self, path, rule):
-    self.path = path
-    self.rule = rule
-
-  def __str__(self):
-    nice_path = ["ui/src" + name + ".ts" for name in self.path]
-    return ''.join([
-        'Forbidden dependency path:\n\n ',
-        '\n    -> '.join(nice_path),
-        '\n',
-        '\n',
-        str(self.rule),
-        '\n',
-    ])
-
-
-class AllowList(object):
-
-  def __init__(self, allowed, dst, reasoning):
-    self.allowed = allowed
-    self.dst = dst
-    self.reasoning = reasoning
-
-  def check(self, graph):
-    for node, edges in graph.items():
-      for edge in edges:
-        if re.match(self.dst, edge):
-          if not any(re.match(a, node) for a in self.allowed):
-            yield Failure([node, edge], self)
-
-  def __str__(self):
-    return f'Only items in the allowlist ({self.allowed}) may directly depend on "{self.dst}" ' + self.reasoning
-
-
-class NoDirectDep(object):
-
-  def __init__(self, src, dst, reasoning):
-    self.src = src
-    self.dst = dst
-    self.reasoning = reasoning
-
-  def check(self, graph):
-    for node, edges in graph.items():
-      if re.match(self.src, node):
-        for edge in edges:
-          if re.match(self.dst, edge):
-            yield Failure([node, edge], self)
-
-  def __str__(self):
-    return f'"{self.src}" may not directly depend on "{self.dst}" ' + self.reasoning
-
-
-class NoDep(object):
-
-  def __init__(self, src, dst, reasoning):
-    self.src = src
-    self.dst = dst
-    self.reasoning = reasoning
-
-  def check(self, graph):
-    for node in graph:
-      if re.match(self.src, node):
-        for connected, path in bfs(graph, node):
-          if re.match(self.dst, connected):
-            yield Failure(path, self)
-
-  def __str__(self):
-    return f'"{self.src}" may not depend on "{self.dst}" ' + self.reasoning
-
-
-class NoCircularDeps(object):
-
-  def __init__(self):
-    pass
-
-  def check(self, graph):
-    for node in graph:
-      for child in graph[node]:
-        for reached, path in dfs(graph, child):
-          if reached == node:
-            yield Failure([node] + path, self)
-
-  def __str__(self):
-    return f'circular dependencies can cause complex issues'
-
-
-# We have three kinds of rules:
-# NoDirectDep(a, b) = files matching regex 'a' cannot *directly* import
-#   files matching regex 'b' - but they may indirectly depend on them.
-# NoDep(a, b) = as above but 'a' may not even transitively import 'b'.
-# NoCircularDeps = forbid introduction of circular dependencies
-RULES = [
-    AllowList(
-        ['/protos/index'],
-        r'/gen/protos',
-        'protos should be re-exported from /protos/index without the nesting.',
-    ),
-    NoDirectDep(
-        r'/plugins/.*',
-        r'/core/.*',
-        'instead plugins should depend on the API exposed at ui/src/public.',
-    ),
-    NoDirectDep(
-        r"/frontend/.*",
-        r"/core_plugins/.*",
-        "core code should not depend on plugins.",
-    ),
-    NoDirectDep(
-        r"/core/.*",
-        r"/core_plugins/.*",
-        "core code should not depend on plugins.",
-    ),
-    NoDirectDep(
-        r"/base/.*",
-        r"/core_plugins/.*",
-        "core code should not depend on plugins.",
-    ),
-    #NoDirectDep(
-    #    r'/tracks/.*',
-    #    r'/core/.*',
-    #    'instead tracks should depend on the API exposed at ui/src/public.',
-    #),
-    NoDep(
-        r'/core/.*',
-        r'/plugins/.*',
-        'otherwise the plugins are no longer optional.',
-    ),
-    NoDep(
-        r'/core/.*',
-        r'/frontend/.*',
-        'trying to reduce the dependency mess as we refactor into core',
-    ),
-    NoDep(
-        r'/core/.*',
-        r'/common/.*',
-        'trying to reduce the dependency mess as we refactor into core',
-    ),
-    NoDep(
-        r'/core/.*',
-        r'/controller/.*',
-        'trying to reduce the dependency mess as we refactor into core',
-    ),
-    NoDep(
-        r'/base/.*',
-        r'/core/.*',
-        'core should depend on base not the other way round',
-    ),
-    NoDep(
-        r'/base/.*',
-        r'/common/.*',
-        'common should depend on base not the other way round',
-    ),
-    NoDep(
-        r'/common/.*',
-        r'/chrome_extension/.*',
-        'chrome_extension must be a leaf',
+    # Dependencies allowed for internal UI code.
+    (
+        [
+            '/frontend/*',
+            '/core/*',
+            '/common/*',
+        ],
+        [
+            '/frontend/*',
+            '/core/*',
+            '/common/*',
+            '/public/*',
+            '/trace_processor/*',
+            '/widgets/*',
+            '/protos/*',
+            '/gen/perfetto_version',
+        ],
     ),
 
-    # Widgets
-    NoDep(
-        r'/widgets/.*',
-        r'/frontend/.*',
-        'widgets should only depend on base',
-    ),
-    NoDep(
-        r'/widgets/.*',
-        r'/core/.*',
-        'widgets should only depend on base',
-    ),
-    NoDep(
-        r'/widgets/.*',
-        r'/plugins/.*',
-        'widgets should only depend on base',
-    ),
-    NoDep(
-        r'/widgets/.*',
-        r'/common/.*',
-        'widgets should only depend on base',
+    # /public (interfaces + lib) can depend only on a restricted surface.
+    ('/public/*', ['/base/*', '/trace_processor/*']),
+
+    # /public/lib can also depend on the plublic interface and widgets.
+    ('/public/lib/*', ['/public/*', '/frontend/widgets/*', '/widgets/*']),
+
+    # /plugins (and core_plugins) can depend only on a restricted surface.
+    (
+        '/*plugins/*',
+        [
+            '/base/*',
+            '/public/*',
+            '/trace_processor/*',
+            '/widgets/*',
+            '/frontend/widgets/*',
+        ],
     ),
 
-    # Bigtrace
-    NoDep(
-        r'/bigtrace/.*',
-        r'/frontend/.*',
-        'bigtrace should not depend on frontend',
-    ),
-    NoDep(
-        r'/bigtrace/.*',
-        r'/common/.*',
-        'bigtrace should not depend on common',
-    ),
-    NoDep(
-        r'/bigtrace/.*',
-        r'/engine/.*',
-        'bigtrace should not depend on engine',
-    ),
-    NoDep(
-        r'/bigtrace/.*',
-        r'/trace_processor/.*',
-        'bigtrace should not depend on trace_processor',
-    ),
-    NoDep(
-        r'/bigtrace/.*',
-        r'/traceconv/.*',
-        'bigtrace should not depend on traceconv',
-    ),
-    NoDep(
-        r'/bigtrace/.*',
-        r'/tracks/.*',
-        'bigtrace should not depend on tracks',
-    ),
-    NoDep(
-        r'/bigtrace/.*',
-        r'/controller/.*',
-        'bigtrace should not depend on controller',
+    # Extra dependencies allowed for core_plugins only.
+    # TODO(priniano): remove this entry to figure out what it takes to move the
+    # remaining /core_plugins to /plugins and get rid of core_plugins.
+    (
+        ['/core_plugins/*'],
+        ['/core/*', '/frontend/*', '/common/actions'],
     ),
 
-    # Fails at the moment as we have several circular dependencies. One
-    # example:
-    # ui/src/frontend/cookie_consent.ts
-    #    -> ui/src/frontend/globals.ts
-    #    -> ui/src/frontend/router.ts
-    #    -> ui/src/frontend/pages.ts
-    #    -> ui/src/frontend/cookie_consent.ts
-    #NoCircularDeps(),
+    # Miscl legitimate deps.
+    ('/frontend/index', ['/gen/*']),
+    ('/traceconv/index', '/gen/traceconv'),
+    ('/engine/wasm_bridge', '/gen/trace_processor'),
+    ('/trace_processor/sql_utils/*', '/trace_processor/*'),
+    ('/protos/index', '/gen/protos'),
+
+    # ------ Technical debt that needs cleaning up below this point ------
+
+    # TODO(primiano): this dependency for BaseSliceTrack & co needs to be moved
+    # to /public/lib or something similar.
+    ('/*plugins/*', '/frontend/*track'),
+
+    # TODO(primiano): clean up generic_slice_details_tab.
+    ('/*plugins/*', '/frontend/generic_slice_details_tab'),
+
+    # TODO(primiano): these dependencies require a discussion with stevegolton@.
+    # unclear if they should be moved to public/lib/* or be part of the
+    # {Base/Named/Slice}Track overhaul.
+    ('/*plugins/*', [
+        '/frontend/slice_layout',
+        '/frontend/slice_args',
+        '/frontend/checkerboard',
+        '/common/track_helper',
+        '/common/track_data',
+    ]),
+
+    # TODO(primiano): clean up dependencies on feature flags.
+    (['/public/lib/colorizer'], '/core/feature_flags'),
+
+    # TODO(primiano): Record page-related technical debt.
+    ('/plugins/dev.perfetto.RecordTrace/*', '/frontend/globals'),
+    ('/chrome_extension/chrome_tracing_controller',
+     '/plugins/dev.perfetto.RecordTrace/*'),
+
+    # TODO(primiano): query-table tech debt.
+    (
+        '/public/lib/query_table/query_table',
+        ['/frontend/*', '/core/app_impl', '/core/router'],
+    ),
+
+    # TODO(primiano): tracks tech debt.
+    ('/public/lib/tracks/*', [
+        '/frontend/base_counter_track',
+        '/frontend/slice_args',
+        '/frontend/tracks/custom_sql_table_slice_track',
+        '/frontend/tracks/generic_slice_details_tab',
+    ]),
+
+    # TODO(primiano): controller-related tech debt.
+    ('/frontend/index', '/controller/*'),
+    ('/controller/*', ['/base/*', '/core/*', '/common/*']),
+
+    # TODO(primiano): check this with stevegolton@. Unclear if widgets should
+    # be allowed to depend on trace_processor.
+    ('/widgets/vega_view', '/trace_processor/*'),
+
+    # Bigtrace deps.
+    ('/bigtrace/*', ['/base/*', '/widgets/*', '/trace_processor/*']),
+
+    # TODO(primiano): misc tech debt.
+    ('/public/lib/extensions', '/frontend/*'),
+    ('/bigtrace/index', ['/core/live_reload', '/core/raf_scheduler']),
+    ('/plugins/dev.perfetto.HeapProfile/*', '/frontend/trace_converter'),
 ]
 
 
@@ -376,22 +174,78 @@
   return s[:-len(suffix)] if s.endswith(suffix) else s
 
 
+def normalize_path(path):
+  return remove_suffix(remove_prefix(path, UI_SRC_DIR), '.ts')
+
+
+def find_plugin_declared_deps(path):
+  """Returns the set of deps declared by the plugin (if any)
+
+  It scans the plugin/index.ts file, and resolves the declared dependencies,
+  working out the path of the plugin we depend on (by looking at the imports).
+  Returns a tuple of the form (src_plugin_path, set{dst_plugin_path})
+  Where:
+    src_plugin_path: is the normalized path of the input (e.g. /plugins/foo)
+    dst_path: is the normalized path of the declared dependency.
+  """
+  src = normalize_path(path)
+  src_plugin = get_plugin_path(src)
+  if src_plugin is None or src != src_plugin + '/index':
+    # If the file is not a plugin, or is not the plugin index.ts, bail out.
+    return
+  # First extract all the default-imports in the file. Usually there is one for
+  # each imported plugin, of the form:
+  # import ThreadPlugin from '../plugins/dev.perfetto.Thread'
+  import_map = {}  # 'ThreadPlugin' -> '/plugins/dev.perfetto.Thread'
+  for (src, target, default_import) in find_imports(path):
+    target_plugin = get_plugin_path(target)
+    if default_import is not None or target_plugin is not None:
+      import_map[default_import] = target_plugin
+
+  # Now extract the declared dependencies for the plugin. This looks for the
+  # statement 'static readonly dependencies = [ThreadPlugin]'. It can be broken
+  # down over multiple lines, so we approach this in two steps. First we find
+  # everything within the square brackets; then we remove spaces and \n and
+  # tokenize on commas
+  with open(path) as f:
+    s = f.read()
+  DEP_REGEX = r'^\s*static readonly dependencies\s*=\s*\[([^\]]*)\]'
+  all_deps = re.findall(DEP_REGEX, s, flags=re.MULTILINE)
+  if len(all_deps) == 0:
+    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(',')
+  for imported_as in declared_deps:
+    resolved_dep = import_map.get(imported_as)
+    if resolved_dep is None:
+      raise Exception('Could not resolve import %s in %s' % (imported_as, src))
+    yield (src_plugin, resolved_dep)
+
+
 def find_imports(path):
-  src = path
-  src = remove_prefix(src, UI_SRC_DIR)
-  src = remove_suffix(src, '.ts')
+  src = normalize_path(path)
   directory, _ = os.path.split(src)
   with open(path) as f:
     s = f.read()
-    for m in re.finditer("^import[^']*'([^']*)';", s, flags=re.MULTILINE):
-      raw_target = m[1]
-      if raw_target.startswith('.'):
-        target = os.path.normpath(os.path.join(directory, raw_target))
-        if is_dir(UI_SRC_DIR + target):
-          target = os.path.join(target, 'index')
-      else:
-        target = raw_target
-      yield (src, target)
+  for m in re.finditer(
+      "^import\s+([^;]+)\s+from\s+'([^']+)';$", s, flags=re.MULTILINE):
+    # Flatten multi-line imports into one line, removing spaces. The resulting
+    # import line can look like:
+    # '{foo,bar,baz}' in most cases
+    # 'DefaultImportName' when doing import DefaultImportName from '...'
+    # 'DefaultImportName,{foo,bar,bar}' when doing a mixture of the above.
+    imports = re.sub('\s', '', m[1])
+    default_import = (re.findall('^\w+', imports) + [None])[0]
+
+    # Normalize the imported file
+    target = m[2]
+    if target.startswith('.'):
+      target = os.path.normpath(os.path.join(directory, target))
+      if is_dir(UI_SRC_DIR + target):
+        target = os.path.join(target, 'index')
+
+    yield (src, target, default_import)
 
 
 def path_to_id(path):
@@ -406,42 +260,6 @@
   return not path.startswith('/')
 
 
-def bfs(graph, src):
-  seen = set()
-  queue = [(src, [])]
-
-  while queue:
-    node, path = queue.pop(0)
-    if node in seen:
-      continue
-
-    seen.add(node)
-
-    path = path[:]
-    path.append(node)
-
-    yield node, path
-    queue.extend([(child, path) for child in graph[node]])
-
-
-def dfs(graph, src):
-  seen = set()
-  queue = [(src, [])]
-
-  while queue:
-    node, path = queue.pop()
-    if node in seen:
-      continue
-
-    seen.add(node)
-
-    path = path[:]
-    path.append(node)
-
-    yield node, path
-    queue.extend([(child, path) for child in graph[node]])
-
-
 def write_dot(graph, f):
   print('digraph g {', file=f)
   for node, edges in graph.items():
@@ -455,19 +273,95 @@
   print('}', file=f)
 
 
-def do_check(options, graph):
-  for rule in RULES:
-    for failure in rule.check(graph):
-      print(failure)
-      return 1
-  return 0
+def get_plugin_path(path):
+  m = re.match('^(/(?:core_)?plugins/([^/]+))/.*', path)
+  return m.group(1) if m is not None else None
+
+
+def flatten_rules(rules):
+  flat_deps = []
+  for rule_src, rule_dst in rules:
+    src_list = rule_src if isinstance(rule_src, list) else [rule_src]
+    dst_list = rule_dst if isinstance(rule_dst, list) else [rule_dst]
+    for src in src_list:
+      for dst in dst_list:
+        flat_deps.append((src, dst))
+  return flat_deps
+
+
+def get_node_modules(graph):
+  """Infers the dependencies onto NPM packages (node_modules)
+
+  An import is guessed to be a node module if doesn't contain any . or .. in the
+  path, and optionally starts with @.
+  """
+  node_modules = set()
+  for _, imports in graph.items():
+    for dst in imports:
+      if re.match(r'^[@a-z][a-z0-9-_/]+$', dst):
+        node_modules.add(dst)
+  return node_modules
+
+
+def check_one_import(src, dst, allowlist, plugin_declared_deps, node_modules):
+  # Translate node_module deps into the wildcard '%node_modules%' so it can be
+  # treated as a single entity.
+  if dst in node_modules:
+    dst = NODE_MODULES
+
+  # Always allow imports from the same directory or its own subdirectories.
+  src_dir = '/'.join(src.split('/')[:-1])
+  dst_dir = '/'.join(dst.split('/')[:-1])
+  if dst_dir.startswith(src_dir):
+    return True
+
+  # Match against the (flattened) allowlist.
+  for rule_src, rule_dst in allowlist:
+    if fnmatch.fnmatch(src, rule_src) and fnmatch.fnmatch(dst, rule_dst):
+      return True
+
+  # Check inter-plugin deps.
+  src_plugin = get_plugin_path(src)
+  dst_plugin = get_plugin_path(dst)
+  extra_err = ''
+  if src_plugin is not None and dst_plugin is not None:
+    if src_plugin == dst_plugin:
+      # Allow a plugin to depends on arbitrary subdirectories of itself.
+      return True
+    # Check if there is a dependency declared by plugins, via
+    # static readonly dependencies = [DstPlugin]
+    declared_deps = plugin_declared_deps.get(src_plugin, set())
+    extra_err = '(plugin deps: %s)' % ','.join(declared_deps)
+    if dst_plugin in declared_deps:
+      return True
+  print('Import not allowed %s -> %s %s' % (src, dst, extra_err))
+  return False
+
+
+def do_check(_options, graph):
+  result = 0
+  rules = flatten_rules(DEPS_ALLOWLIST)
+  node_modules = get_node_modules(graph)
+
+  # Build a map of depencies declared between plugin. The maps looks like:
+  # 'Foo' -> {'Bar', 'Baz'}  # Foo declares a dependency on Bar and Baz
+  plugin_declared_deps = collections.defaultdict(set)
+  for path in all_source_files():
+    for src_plugin, dst_plugin in find_plugin_declared_deps(path):
+      plugin_declared_deps[src_plugin].add(dst_plugin)
+
+  for src, imports in graph.items():
+    for dst in imports:
+      if not check_one_import(src, dst, rules, plugin_declared_deps,
+                              node_modules):
+        result = 1
+  return result
 
 
 def do_desc(options, graph):
   print('Rules:')
-  for rule in RULES:
-    print("  - ", end='')
-    print(rule)
+  for rule in flatten_rules(DEPS_ALLOWLIST):
+    print(' - %s' % rule)
 
 
 def do_print(options, graph):
@@ -483,13 +377,12 @@
       return path
     return os.path.dirname(path)
 
-  if options.simplify:
-    new_graph = collections.defaultdict(set)
-    for node, edges in graph.items():
-      for edge in edges:
-        new_graph[simplify(edge)]
-        new_graph[simplify(node)].add(simplify(edge))
-    graph = new_graph
+  new_graph = collections.defaultdict(set)
+  for node, edges in graph.items():
+    for edge in edges:
+      new_graph[simplify(edge)]
+      new_graph[simplify(node)].add(simplify(edge))
+  graph = new_graph
 
   if options.ignore_external:
     new_graph = collections.defaultdict(set)
@@ -507,11 +400,6 @@
   return 0
 
 
-def do_plan_dot(options, _):
-  print(PLAN_DOT, file=sys.stdout)
-  return 0
-
-
 def main():
   parser = argparse.ArgumentParser(description=__doc__)
   parser.set_defaults(func=do_check)
@@ -529,29 +417,21 @@
 
   dot_command = subparsers.add_parser(
       'dot',
-      help='Output dependency graph in dot format suitble for use in graphviz (e.g. ./tools/check_imports dot | dot -Tpng -ograph.png)'
-  )
+      help='Output dependency graph in dot format suitble for use in ' +
+      'graphviz (e.g. ./tools/check_imports dot | dot -Tpng -ograph.png)')
   dot_command.set_defaults(func=do_dot)
   dot_command.add_argument(
-      '--simplify',
-      action='store_true',
-      help='Show directories rather than files',
-  )
-  dot_command.add_argument(
       '--ignore-external',
       action='store_true',
       help='Don\'t show external dependencies',
   )
 
-  plan_dot_command = subparsers.add_parser(
-      'plan-dot',
-      help='Output planned dependency graph in dot format suitble for use in graphviz (e.g. ./tools/check_imports plan-dot | dot -Tpng -ograph.png)'
-  )
-  plan_dot_command.set_defaults(func=do_plan_dot)
-
+  # This is a general import graph of the form /plugins/foo/index -> /base/hash
   graph = collections.defaultdict(set)
+
+  # Build the dep graph
   for path in all_source_files():
-    for src, target in find_imports(path):
+    for src, target, _ in find_imports(path):
       graph[src].add(target)
       graph[target]
 
diff --git a/python/tools/check_ratchet.py b/python/tools/check_ratchet.py
index 317508d..d53b0c3 100755
--- a/python/tools/check_ratchet.py
+++ b/python/tools/check_ratchet.py
@@ -36,8 +36,8 @@
 
 from dataclasses import dataclass
 
-EXPECTED_ANY_COUNT = 61
-EXPECTED_RUN_METRIC_COUNT = 5
+EXPECTED_ANY_COUNT = 52
+EXPECTED_RUN_METRIC_COUNT = 4
 
 ROOT_DIR = os.path.dirname(
     os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
diff --git a/python/tools/record_android_trace.py b/python/tools/record_android_trace.py
index 2d4a189..13d0c45 100755
--- a/python/tools/record_android_trace.py
+++ b/python/tools/record_android_trace.py
@@ -21,6 +21,7 @@
 import os
 import re
 import shutil
+import signal
 import socketserver
 import subprocess
 import sys
@@ -212,6 +213,18 @@
   return args
 
 
+class SignalException(Exception):
+  pass
+
+
+def signal_handler(sig, frame):
+  raise SignalException('Received signal ' + str(sig))
+
+
+signal.signal(signal.SIGINT, signal_handler)
+signal.signal(signal.SIGTERM, signal_handler)
+
+
 def start_trace(args, print_log=True):
   perfetto_cmd = 'perfetto'
   device_dir = '/data/misc/perfetto-traces/'
@@ -373,7 +386,7 @@
         prt('Too many unrecoverable ADB failures, bailing out', ANSI.RED)
         sys.exit(1)
       time.sleep(2)
-    except KeyboardInterrupt:
+    except (KeyboardInterrupt, SignalException):
       sig = 'TERM' if ctrl_c_count == 0 else 'KILL'
       ctrl_c_count += 1
       if print_log:
diff --git a/src/android_internal/health_hal.cc b/src/android_internal/health_hal.cc
index 7533cb3..5c76965 100644
--- a/src/android_internal/health_hal.cc
+++ b/src/android_internal/health_hal.cc
@@ -98,7 +98,8 @@
       g_svc.hidl->getHealthInfo(
           [&res, value](Result hal_res, const auto& hal_health_info) {
             res = hal_res;
-            *value = hal_health_info.legacy.batteryVoltage;
+            // batteryVoltage is in mV, convert to uV.
+            *value = hal_health_info.legacy.batteryVoltage * 1000;
           });
       break;
   }  // switch(counter)
@@ -136,7 +137,8 @@
     case BatteryCounter::kVoltage:
       ::aidl::android::hardware::health::HealthInfo health_info;
       status = g_svc.aidl->getHealthInfo(&health_info);
-      value32 = health_info.batteryVoltageMillivolts;
+      // Convert from mV to uV.
+      value32 = health_info.batteryVoltageMillivolts * 1000;
       break;
   }  // switch(counter)
 
diff --git a/src/android_stats/perfetto_atoms.h b/src/android_stats/perfetto_atoms.h
index 3a3308e..dbf6c8a 100644
--- a/src/android_stats/perfetto_atoms.h
+++ b/src/android_stats/perfetto_atoms.h
@@ -27,13 +27,14 @@
   // Checkpoints inside perfetto_cmd before tracing is finished.
   kTraceBegin = 1,
   kBackgroundTraceBegin = 2,
-  kCloneTraceBegin = 55,
-  kCloneTriggerTraceBegin = 56,
+  kCmdCloneTraceBegin = 55,
+  kCmdCloneTriggerTraceBegin = 56,
   kOnConnect = 3,
+  kCmdOnSessionClone = 58,
+  kCmdOnTriggerSessionClone = 59,
 
   // Guardrails inside perfetto_cmd before tracing is finished.
   kOnTimeout = 16,
-  kCmdUserBuildTracingNotAllowed = 43,
 
   // Checkpoints inside traced.
   kTracedEnableTracing = 37,
@@ -111,6 +112,10 @@
   // Contained status of guardrail state initialization and upload limit in
   // perfetto_cmd. Removed as perfetto no longer manages stateful guardrails
   // reserved 44, 45, 46;
+
+  // Contained the guardrail for user build tracing. Removed as this guardrail
+  // causes more problem than it solves these days.
+  // reserved 43;
 };
 
 // This must match the values of the PerfettoTrigger::TriggerType enum in:
@@ -118,17 +123,15 @@
 enum PerfettoTriggerAtom {
   kUndefined = 0,
 
-  kCmdTrigger = 1,
-  kCmdTriggerFail = 2,
-
-  kTriggerPerfettoTrigger = 3,
-  kTriggerPerfettoTriggerFail = 4,
-
   kTracedLimitProbability = 5,
   kTracedLimitMaxPer24h = 6,
 
-  kProbesProducerTrigger = 7,
-  kProbesProducerTriggerFail = 8,
+  kTracedTrigger = 9,
+
+  // Contained events of logging triggers through perfetto_cmd, probes and
+  // trigger_perfetto.
+  // Removed in W (Oct 2024) and replaced by |kTracedTrigger|.
+  // reserved 1, 2, 3, 4, 7, 8
 };
 
 }  // namespace perfetto
diff --git a/src/base/BUILD.gn b/src/base/BUILD.gn
index d8d00a7..ace0ac6 100644
--- a/src/base/BUILD.gn
+++ b/src/base/BUILD.gn
@@ -82,6 +82,15 @@
   }
 }
 
+perfetto_component("clock_snapshots") {
+  deps = [ "../../gn:default_deps" ]
+  public_deps = [
+    "../../include/perfetto/ext/base",
+    "../../protos/perfetto/common:zero",
+  ]
+  sources = [ "clock_snapshots.cc" ]
+}
+
 # This target needs to be named as such because it's exposed directly in Bazel
 # and Android.bp.
 perfetto_component("perfetto_base_default_platform") {
diff --git a/src/base/clock_snapshots.cc b/src/base/clock_snapshots.cc
new file mode 100644
index 0000000..4959e7a
--- /dev/null
+++ b/src/base/clock_snapshots.cc
@@ -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.
+ */
+
+#include "perfetto/ext/base/clock_snapshots.h"
+
+#include "perfetto/base/build_config.h"
+#include "perfetto/base/time.h"
+#include "protos/perfetto/common/builtin_clock.pbzero.h"
+
+namespace perfetto::base {
+
+ClockSnapshotVector CaptureClockSnapshots() {
+  ClockSnapshotVector snapshot_data;
+#if !PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE) && \
+    !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) &&   \
+    !PERFETTO_BUILDFLAG(PERFETTO_OS_NACL)
+  struct {
+    clockid_t id;
+    protos::pbzero::BuiltinClock type;
+    struct timespec ts;
+  } clocks[] = {
+      {CLOCK_BOOTTIME, protos::pbzero::BUILTIN_CLOCK_BOOTTIME, {0, 0}},
+      {CLOCK_REALTIME_COARSE,
+       protos::pbzero::BUILTIN_CLOCK_REALTIME_COARSE,
+       {0, 0}},
+      {CLOCK_MONOTONIC_COARSE,
+       protos::pbzero::BUILTIN_CLOCK_MONOTONIC_COARSE,
+       {0, 0}},
+      {CLOCK_REALTIME, protos::pbzero::BUILTIN_CLOCK_REALTIME, {0, 0}},
+      {CLOCK_MONOTONIC, protos::pbzero::BUILTIN_CLOCK_MONOTONIC, {0, 0}},
+      {CLOCK_MONOTONIC_RAW,
+       protos::pbzero::BUILTIN_CLOCK_MONOTONIC_RAW,
+       {0, 0}},
+  };
+  // First snapshot all the clocks as atomically as we can.
+  for (auto& clock : clocks) {
+    if (clock_gettime(clock.id, &clock.ts) == -1)
+      PERFETTO_DLOG("clock_gettime failed for clock %d", clock.id);
+  }
+  for (auto& clock : clocks) {
+    snapshot_data.push_back(ClockReading(
+        static_cast<uint32_t>(clock.type),
+        static_cast<uint64_t>(base::FromPosixTimespec(clock.ts).count())));
+  }
+#else  // OS_APPLE || OS_WIN && OS_NACL
+  auto wall_time_ns = static_cast<uint64_t>(base::GetWallTimeNs().count());
+  // The default trace clock is boot time, so we always need to emit a path to
+  // it. However since we don't actually have a boot time source on these
+  // platforms, pretend that wall time equals boot time.
+  snapshot_data.push_back(
+      ClockReading(protos::pbzero::BUILTIN_CLOCK_BOOTTIME, wall_time_ns));
+  snapshot_data.push_back(
+      ClockReading(protos::pbzero::BUILTIN_CLOCK_MONOTONIC, wall_time_ns));
+#endif
+
+#if PERFETTO_BUILDFLAG(PERFETTO_ARCH_CPU_X86_64)
+  // X86-specific but OS-independent TSC clocksource
+  snapshot_data.push_back(
+      ClockReading(protos::pbzero::BUILTIN_CLOCK_TSC, base::Rdtsc()));
+#endif  // PERFETTO_BUILDFLAG(PERFETTO_ARCH_CPU_X86_64)
+
+  return snapshot_data;
+}
+
+}  // namespace perfetto
diff --git a/src/base/status.cc b/src/base/status.cc
index 58e32d1..edf2f90 100644
--- a/src/base/status.cc
+++ b/src/base/status.cc
@@ -16,20 +16,53 @@
 
 #include "perfetto/base/status.h"
 
-#include <stdarg.h>
 #include <algorithm>
+#include <cstdarg>
+#include <cstdio>
+#include <string>
+#include <utility>
 
-namespace perfetto {
-namespace base {
+namespace perfetto::base {
 
 Status ErrStatus(const char* format, ...) {
-  char buffer[1024];
-  va_list ap;
-  va_start(ap, format);
-  vsnprintf(buffer, sizeof(buffer), format, ap);
-  va_end(ap);
-  Status status(buffer);
-  return status;
+  std::string buf;
+  buf.resize(1024);
+  for (;;) {
+    va_list ap;
+    va_start(ap, format);
+    int N = vsnprintf(buf.data(), buf.size() - 1, format, ap);
+    va_end(ap);
+
+    if (N <= 0) {
+      buf = "[printf format error]";
+      break;
+    }
+
+    auto sN = static_cast<size_t>(N);
+    if (sN > buf.size() - 1) {
+      // Indicates that the string was truncated and sN is the "number of
+      // non-null bytes which would be needed to fit the result". This is the
+      // C99 standard behaviour in the case of truncation. In that case, resize
+      // the buffer to match the returned value (with + 1 for the null
+      // terminator) and try again.
+      buf.resize(sN + 1);
+      continue;
+    }
+    if (sN == buf.size() - 1) {
+      // Indicates that the string was likely truncated and sN is just the
+      // number of bytes written into the string. This is the behaviour of
+      // non-standard compilers (MSVC) etc. In that case, just double the
+      // storage and try again.
+      buf.resize(sN * 2);
+      continue;
+    }
+
+    // Otherwise, indicates the string was written successfully: we need to
+    // resize to match the number of non-null bytes and return.
+    buf.resize(sN);
+    break;
+  }
+  return Status(std::move(buf));
 }
 
 std::optional<std::string_view> Status::GetPayload(
@@ -70,5 +103,4 @@
   return erased;
 }
 
-}  // namespace base
-}  // namespace perfetto
+}  // namespace perfetto::base
diff --git a/src/base/status_unittest.cc b/src/base/status_unittest.cc
index df42b31..3978379 100644
--- a/src/base/status_unittest.cc
+++ b/src/base/status_unittest.cc
@@ -15,11 +15,17 @@
  */
 
 #include "perfetto/base/status.h"
+#include <string>
 
 #include "test/gtest_and_gmock.h"
 
-namespace perfetto {
-namespace base {
+namespace perfetto::base {
+
+TEST(StatusTest, HugeError) {
+  std::string x(4096, 'x');
+  base::Status status = base::ErrStatus("%s", x.c_str());
+  ASSERT_EQ(status.message(), x);
+}
 
 TEST(StatusTest, GetMissingPayload) {
   base::Status status = base::ErrStatus("Error");
@@ -63,5 +69,4 @@
   ASSERT_EQ(status.GetPayload("test.foo.com/bar2"), "2");
 }
 
-}  // namespace base
-}  // namespace perfetto
+}  // namespace perfetto::base
diff --git a/src/base/string_utils.cc b/src/base/string_utils.cc
index e8845d1..c93a8f1 100644
--- a/src/base/string_utils.cc
+++ b/src/base/string_utils.cc
@@ -257,7 +257,7 @@
 #endif // PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
 
 size_t SprintfTrunc(char* dst, size_t dst_size, const char* fmt, ...) {
-  if (PERFETTO_UNLIKELY(dst_size) == 0)
+  if (PERFETTO_UNLIKELY(dst_size == 0))
     return 0;
 
   va_list args;
@@ -265,7 +265,7 @@
   int src_size = vsnprintf(dst, dst_size, fmt, args);
   va_end(args);
 
-  if (PERFETTO_UNLIKELY(src_size) <= 0) {
+  if (PERFETTO_UNLIKELY(src_size <= 0)) {
     dst[0] = '\0';
     return 0;
   }
diff --git a/src/base/task_runner_unittest.cc b/src/base/task_runner_unittest.cc
index 810ebc6..060cf43 100644
--- a/src/base/task_runner_unittest.cc
+++ b/src/base/task_runner_unittest.cc
@@ -25,7 +25,6 @@
 #include "perfetto/ext/base/pipe.h"
 #include "perfetto/ext/base/scoped_file.h"
 #include "perfetto/ext/base/utils.h"
-#include "src/base/test/gtest_test_suite.h"
 #include "test/gtest_and_gmock.h"
 
 namespace perfetto {
diff --git a/src/base/test/gtest_test_suite.h b/src/base/test/gtest_test_suite.h
deleted file mode 100644
index 4d1d550..0000000
--- a/src/base/test/gtest_test_suite.h
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 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
- *
- *      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_BASE_TEST_GTEST_TEST_SUITE_H_
-#define SRC_BASE_TEST_GTEST_TEST_SUITE_H_
-
-#include "test/gtest_and_gmock.h"
-
-// Define newer TEST_SUITE googletest APIs as aliases of the older APIs where
-// necessary. This makes it possible to migrate Perfetto to the newer APIs and
-// still use older googletest versions where necessary.
-//
-// TODO(costan): Remove this header after googletest is rolled in Android.
-
-#if !defined(INSTANTIATE_TEST_SUITE_P)
-#define INSTANTIATE_TEST_SUITE_P(...) INSTANTIATE_TEST_CASE_P(__VA_ARGS__)
-#endif
-
-#if !defined(INSTANTIATE_TYPED_TEST_SUITE_P)
-#define INSTANTIATE_TYPED_TEST_SUITE_P(...) \
-  INSTANTIATE_TYPED_TEST_CASE_P(__VA_ARGS__)
-#endif
-
-#if !defined(REGISTER_TEST_SUITE_P)
-#define REGISTER_TEST_SUITE_P(...) REGISTER_TEST_CASE_P(__VA_ARGS__)
-#endif
-
-#if !defined(TYPED_TEST_SUITE)
-#define TYPED_TEST_SUITE(...) TYPED_TEST_CASE(__VA_ARGS__)
-#endif
-
-#if !defined(TYPED_TEST_SUITE_P)
-#define TYPED_TEST_SUITE_P(...) TYPED_TEST_CASE_P(__VA_ARGS__)
-#endif
-
-#endif  // SRC_BASE_TEST_GTEST_TEST_SUITE_H_
diff --git a/src/base/test/test_task_runner.cc b/src/base/test/test_task_runner.cc
index 3576996..51d6d52 100644
--- a/src/base/test/test_task_runner.cc
+++ b/src/base/test/test_task_runner.cc
@@ -88,6 +88,14 @@
   };
 }
 
+void TestTaskRunner::AdvanceTimeAndRunUntilIdle(uint32_t ms) {
+  PERFETTO_DCHECK_THREAD(thread_checker_);
+  task_runner_.PostDelayedTask(std::bind(&TestTaskRunner::QuitIfIdle, this),
+                               ms);
+  task_runner_.AdvanceTimeForTesting(ms);
+  task_runner_.Run();
+}
+
 // TaskRunner implementation.
 void TestTaskRunner::PostTask(std::function<void()> closure) {
   task_runner_.PostTask(std::move(closure));
diff --git a/src/base/test/test_task_runner.h b/src/base/test/test_task_runner.h
index a7c5784..142970d 100644
--- a/src/base/test/test_task_runner.h
+++ b/src/base/test/test_task_runner.h
@@ -42,6 +42,10 @@
   void RunUntilCheckpoint(const std::string& checkpoint,
                           uint32_t timeout_ms = 5000);
 
+  // Pretends (for the purposes of running delayed tasks) that time advanced by
+  // `ms`. Run until then.
+  void AdvanceTimeAndRunUntilIdle(uint32_t ms);
+
   // TaskRunner implementation.
   void PostTask(std::function<void()> closure) override;
   void PostDelayedTask(std::function<void()>, uint32_t delay_ms) override;
diff --git a/src/base/threading/thread_pool.cc b/src/base/threading/thread_pool.cc
index 30843b3..fe3e69d 100644
--- a/src/base/threading/thread_pool.cc
+++ b/src/base/threading/thread_pool.cc
@@ -47,7 +47,10 @@
   thread_waiter_.notify_one();
 }
 
-void ThreadPool::RunThreadLoop() {
+void ThreadPool::RunThreadLoop() PERFETTO_NO_THREAD_SAFETY_ANALYSIS {
+  // 'std::unique_lock' lock doesn't work well with thread annotations
+  // (see https://github.com/llvm/llvm-project/issues/63239),
+  // so we suppress thread safety static analysis for this method.
   for (;;) {
     std::function<void()> fn;
     {
@@ -57,8 +60,10 @@
       }
       if (pending_tasks_.empty()) {
         thread_waiting_count_++;
-        thread_waiter_.wait(
-            guard, [this]() { return quit_ || !pending_tasks_.empty(); });
+        thread_waiter_.wait(guard,
+                            [this]() PERFETTO_EXCLUSIVE_LOCKS_REQUIRED(mutex_) {
+                              return quit_ || !pending_tasks_.empty();
+                            });
         thread_waiting_count_--;
         continue;
       }
diff --git a/src/base/unix_task_runner.cc b/src/base/unix_task_runner.cc
index ba2828f..3e35c03 100644
--- a/src/base/unix_task_runner.cc
+++ b/src/base/unix_task_runner.cc
@@ -53,7 +53,10 @@
 void UnixTaskRunner::Run() {
   PERFETTO_DCHECK_THREAD(thread_checker_);
   created_thread_id_.store(GetThreadId(), std::memory_order_relaxed);
-  quit_ = false;
+  {
+    std::lock_guard<std::mutex> lock(lock_);
+    quit_ = false;
+  }
   for (;;) {
     int poll_timeout_ms;
     {
@@ -108,6 +111,11 @@
   return immediate_tasks_.empty();
 }
 
+void UnixTaskRunner::AdvanceTimeForTesting(uint32_t ms) {
+  std::lock_guard<std::mutex> lock(lock_);
+  advanced_time_for_testing_ += TimeMillis(ms);
+}
+
 void UnixTaskRunner::UpdateWatchTasksLocked() {
   PERFETTO_DCHECK_THREAD(thread_checker_);
 #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
@@ -142,7 +150,7 @@
     }
     if (!delayed_tasks_.empty()) {
       auto it = delayed_tasks_.begin();
-      if (now >= it->first) {
+      if (now + advanced_time_for_testing_ >= it->first) {
         delayed_task = std::move(it->second);
         delayed_tasks_.erase(it);
       }
@@ -242,7 +250,8 @@
   if (!immediate_tasks_.empty())
     return 0;
   if (!delayed_tasks_.empty()) {
-    TimeMillis diff = delayed_tasks_.begin()->first - GetWallTimeMs();
+    TimeMillis diff = delayed_tasks_.begin()->first - GetWallTimeMs() -
+                      advanced_time_for_testing_;
     return std::max(0, static_cast<int>(diff.count()));
   }
   return -1;
@@ -264,7 +273,8 @@
   TimeMillis runtime = GetWallTimeMs() + TimeMillis(delay_ms);
   {
     std::lock_guard<std::mutex> lock(lock_);
-    delayed_tasks_.insert(std::make_pair(runtime, std::move(task)));
+    delayed_tasks_.insert(
+        std::make_pair(runtime + advanced_time_for_testing_, std::move(task)));
   }
   WakeUp();
 }
diff --git a/src/base/utils.cc b/src/base/utils.cc
index 0d9318c..419c7db 100644
--- a/src/base/utils.cc
+++ b/src/base/utils.cc
@@ -40,7 +40,6 @@
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
     PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#include <linux/prctl.h>
 #include <sys/prctl.h>
 
 #ifndef PR_GET_TAGGED_ADDR_CTRL
diff --git a/src/base/waitable_event.cc b/src/base/waitable_event.cc
index 67fb2e0..ac3d31f 100644
--- a/src/base/waitable_event.cc
+++ b/src/base/waitable_event.cc
@@ -22,14 +22,20 @@
 WaitableEvent::WaitableEvent() = default;
 WaitableEvent::~WaitableEvent() = default;
 
-void WaitableEvent::Wait(uint64_t notifications) {
+void WaitableEvent::Wait(uint64_t notifications)
+    PERFETTO_NO_THREAD_SAFETY_ANALYSIS {
+  // 'std::unique_lock' lock doesn't work well with thread annotations
+  // (see https://github.com/llvm/llvm-project/issues/63239),
+  // so we suppress thread safety static analysis for this method.
   std::unique_lock<std::mutex> lock(mutex_);
-  return event_.wait(lock, [&] { return notifications_ >= notifications; });
+  return event_.wait(lock, [&]() PERFETTO_EXCLUSIVE_LOCKS_REQUIRED(mutex_) {
+    return notifications_ >= notifications;
+  });
 }
 
 void WaitableEvent::Notify() {
-  std::unique_lock<std::mutex> lock(mutex_);
-  notifications_++;
+  std::lock_guard<std::mutex> lock(mutex_);
+  ++notifications_;
   event_.notify_all();
 }
 
diff --git a/src/base/watchdog_posix.cc b/src/base/watchdog_posix.cc
index 00934f0d..a647086 100644
--- a/src/base/watchdog_posix.cc
+++ b/src/base/watchdog_posix.cc
@@ -259,15 +259,16 @@
     // Check if any of the timers expired.
     int tid_to_kill = 0;
     WatchdogCrashReason crash_reason{};
-    std::unique_lock<std::mutex> guard(mutex_);
-    for (const auto& timer : timers_) {
-      if (now >= timer.deadline) {
-        tid_to_kill = timer.thread_id;
-        crash_reason = timer.crash_reason;
-        break;
+    {
+      std::lock_guard<std::mutex> guard(mutex_);
+      for (const auto& timer : timers_) {
+        if (now >= timer.deadline) {
+          tid_to_kill = timer.thread_id;
+          crash_reason = timer.crash_reason;
+          break;
+        }
       }
     }
-    guard.unlock();
 
     if (tid_to_kill)
       SerializeLogsAndKillThread(tid_to_kill, crash_reason);
@@ -282,15 +283,16 @@
         static_cast<uint64_t>(stat.rss_pages) * base::GetSysPageSize();
 
     bool threshold_exceeded = false;
-    guard.lock();
-    if (CheckMemory_Locked(rss_bytes) && !IsSyncMemoryTaggingEnabled()) {
-      threshold_exceeded = true;
-      crash_reason = WatchdogCrashReason::kMemGuardrail;
-    } else if (CheckCpu_Locked(cpu_time)) {
-      threshold_exceeded = true;
-      crash_reason = WatchdogCrashReason::kCpuGuardrail;
+    {
+      std::lock_guard<std::mutex> guard(mutex_);
+      if (CheckMemory_Locked(rss_bytes) && !IsSyncMemoryTaggingEnabled()) {
+        threshold_exceeded = true;
+        crash_reason = WatchdogCrashReason::kMemGuardrail;
+      } else if (CheckCpu_Locked(cpu_time)) {
+        threshold_exceeded = true;
+        crash_reason = WatchdogCrashReason::kCpuGuardrail;
+      }
     }
-    guard.unlock();
 
     if (threshold_exceeded)
       SerializeLogsAndKillThread(getpid(), crash_reason);
diff --git a/src/bigtrace/orchestrator/BUILD.gn b/src/bigtrace/orchestrator/BUILD.gn
index 8a0aff6..16c34db 100644
--- a/src/bigtrace/orchestrator/BUILD.gn
+++ b/src/bigtrace/orchestrator/BUILD.gn
@@ -24,6 +24,10 @@
       "orchestrator_impl.cc",
       "orchestrator_impl.h",
       "orchestrator_main.cc",
+      "resizable_task_pool.cc",
+      "resizable_task_pool.h",
+      "trace_address_pool.cc",
+      "trace_address_pool.h",
     ]
     deps = [
       "../../../gn:default_deps",
diff --git a/src/bigtrace/orchestrator/orchestrator_impl.cc b/src/bigtrace/orchestrator/orchestrator_impl.cc
index 21392bd..f9542db 100644
--- a/src/bigtrace/orchestrator/orchestrator_impl.cc
+++ b/src/bigtrace/orchestrator/orchestrator_impl.cc
@@ -15,115 +15,204 @@
  */
 
 #include <chrono>
+#include <memory>
 #include <mutex>
 #include <thread>
 
+#include <grpcpp/client_context.h>
+#include <grpcpp/support/status.h>
+
 #include "perfetto/base/logging.h"
+#include "perfetto/base/time.h"
+#include "perfetto/ext/base/utils.h"
+#include "protos/perfetto/bigtrace/orchestrator.pb.h"
 #include "src/bigtrace/orchestrator/orchestrator_impl.h"
+#include "src/bigtrace/orchestrator/resizable_task_pool.h"
+#include "src/bigtrace/orchestrator/trace_address_pool.h"
 
 namespace perfetto::bigtrace {
-
 namespace {
-const uint32_t kBufferPushDelay = 100;
+const uint32_t kBufferPushDelayMicroseconds = 100;
+
+grpc::Status ExecuteQueryOnTrace(
+    std::string sql_query,
+    std::string trace,
+    grpc::Status& query_status,
+    std::mutex& worker_lock,
+    std::vector<protos::BigtraceQueryResponse>& response_buffer,
+    std::unique_ptr<protos::BigtraceWorker::Stub>& stub,
+    ThreadWithContext* contextual_thread) {
+  protos::BigtraceQueryTraceArgs trace_args;
+  protos::BigtraceQueryTraceResponse trace_response;
+
+  trace_args.set_sql_query(sql_query);
+  trace_args.set_trace(trace);
+  grpc::Status status = stub->QueryTrace(
+      contextual_thread->client_context.get(), trace_args, &trace_response);
+
+  if (!status.ok()) {
+    {
+      std::lock_guard<std::mutex> status_guard(worker_lock);
+      // We check and only update the query status if it was not already errored
+      // to avoid unnecessary updates.
+      if (query_status.ok()) {
+        query_status = status;
+      }
+    }
+
+    return status;
+  }
+
+  protos::BigtraceQueryResponse response;
+  response.set_trace(trace_response.trace());
+  for (const protos::QueryResult& query_result : trace_response.result()) {
+    response.add_result()->CopyFrom(query_result);
+    if (query_result.has_error()) {
+      // TODO(b/366410502) Add a mode of operation where some traces are allowed
+      // to be dropped and a corresponding message is displayed to the user
+      // alongside partial results
+      std::lock_guard<std::mutex> status_guard(worker_lock);
+      query_status = grpc::Status(grpc::StatusCode::INTERNAL,
+                                  "[" + trace + "]: " + query_result.error());
+      break;
+    }
+  }
+  std::lock_guard<std::mutex> buffer_guard(worker_lock);
+  response_buffer.emplace_back(std::move(response));
+
+  return grpc::Status::OK;
 }
 
+void ThreadRunLoop(ThreadWithContext* contextual_thread,
+                   TraceAddressPool& address_pool,
+                   std::string sql_query,
+                   grpc::Status& query_status,
+                   std::mutex& worker_lock,
+                   std::vector<protos::BigtraceQueryResponse>& response_buffer,
+                   std::unique_ptr<protos::BigtraceWorker::Stub>& stub) {
+  for (;;) {
+    auto maybe_trace_address = address_pool.Pop();
+    if (!maybe_trace_address) {
+      return;
+    }
+
+    // The ordering of this context swap followed by the check on thread
+    // cancellation is essential and should not be changed to avoid a race where
+    // a request to cancel a thread is sent, followed by a context swap, causing
+    // the cancel to not be caught and the execution of the loop body to
+    // continue.
+    contextual_thread->client_context = std::make_unique<grpc::ClientContext>();
+
+    if (contextual_thread->IsCancelled()) {
+      address_pool.MarkCancelled(std::move(*maybe_trace_address));
+      return;
+    }
+
+    grpc::Status status = ExecuteQueryOnTrace(
+        sql_query, *maybe_trace_address, query_status, worker_lock,
+        response_buffer, stub, contextual_thread);
+
+    if (!status.ok()) {
+      if (status.error_code() == grpc::StatusCode::CANCELLED) {
+        address_pool.MarkCancelled(std::move(*maybe_trace_address));
+      }
+      return;
+    }
+  }
+}
+
+}  // namespace
+
 OrchestratorImpl::OrchestratorImpl(
     std::unique_ptr<protos::BigtraceWorker::Stub> stub,
-    uint32_t pool_size)
-    : stub_(std::move(stub)),
-      pool_(std::make_unique<base::ThreadPool>(pool_size)),
-      semaphore_(pool_size) {}
+    uint32_t max_query_concurrency)
+    : stub_(std::move(stub)), max_query_concurrency_(max_query_concurrency) {}
 
 grpc::Status OrchestratorImpl::Query(
     grpc::ServerContext*,
     const protos::BigtraceQueryArgs* args,
     grpc::ServerWriter<protos::BigtraceQueryResponse>* writer) {
   grpc::Status query_status;
-  std::mutex status_lock;
+  std::mutex worker_lock;
   const std::string& sql_query = args->sql_query();
+  std::vector<std::string> traces(args->traces().begin(), args->traces().end());
 
   std::vector<protos::BigtraceQueryResponse> response_buffer;
   uint64_t trace_count = static_cast<uint64_t>(args->traces_size());
 
-  std::thread push_response_buffer_thread([&]() {
-    uint64_t pushed_response_count = 0;
-    for (;;) {
-      {
-        std::lock_guard<std::mutex> status_guard(status_lock);
-        if (pushed_response_count == trace_count || !query_status.ok()) {
-          break;
-        }
-      }
-      std::this_thread::sleep_for(std::chrono::milliseconds(kBufferPushDelay));
-      if (response_buffer.empty()) {
-        continue;
-      }
-      std::vector<protos::BigtraceQueryResponse> buffer;
-      {
-        std::lock_guard<std::mutex> buffer_guard(buffer_lock_);
-        buffer = std::move(response_buffer);
-        response_buffer.clear();
-      }
-      for (protos::BigtraceQueryResponse& response : buffer) {
-        writer->Write(std::move(response));
-      }
-      pushed_response_count += buffer.size();
-    }
+  TraceAddressPool address_pool(std::move(traces));
+
+  // Update the query count on start and end ensuring that the query count is
+  // always decremented whenever the function is exited.
+  {
+    std::lock_guard<std::mutex> lk(query_count_mutex_);
+    query_count_++;
+  }
+  auto query_count_decrement = base::OnScopeExit([&]() {
+    std::lock_guard<std::mutex> lk(query_count_mutex_);
+    query_count_--;
   });
 
-  for (const std::string& trace : args->traces()) {
+  ResizableTaskPool task_pool([&](ThreadWithContext* new_contextual_thread) {
+    ThreadRunLoop(new_contextual_thread, address_pool, sql_query, query_status,
+                  worker_lock, response_buffer, stub_);
+  });
+
+  uint64_t pushed_response_count = 0;
+  uint32_t last_query_count = 0;
+  uint32_t current_query_count = 0;
+
+  for (;;) {
     {
-      std::lock_guard<std::mutex> status_guard(status_lock);
-      if (!query_status.ok()) {
+      std::lock_guard<std::mutex> lk(query_count_mutex_);
+      current_query_count = query_count_;
+    }
+
+    PERFETTO_CHECK(current_query_count != 0);
+
+    // Update the number of threads to the lower of {the remaining number of
+    // traces} and the {maximum concurrency divided by the number of active
+    // queries}. This ensures that at most |max_query_concurrency_| calls to the
+    // backend are outstanding at any one point.
+    if (last_query_count != current_query_count) {
+      auto new_size =
+          std::min(std::max<uint32_t>(address_pool.RemainingCount(), 1u),
+                   max_query_concurrency_ / current_query_count);
+      task_pool.Resize(new_size);
+      last_query_count = current_query_count;
+    }
+
+    // Exit the loop when either all responses have been successfully completed
+    // or if there is an error.
+    {
+      std::lock_guard<std::mutex> status_guard(worker_lock);
+      if (pushed_response_count == trace_count || !query_status.ok()) {
         break;
       }
     }
-    semaphore_.Acquire();
-    pool_->PostTask([&]() {
-      grpc::ClientContext client_context;
-      protos::BigtraceQueryTraceArgs trace_args;
-      protos::BigtraceQueryTraceResponse trace_response;
 
-      trace_args.set_sql_query(sql_query);
-      trace_args.set_trace(trace);
-      grpc::Status status =
-          stub_->QueryTrace(&client_context, trace_args, &trace_response);
-      if (!status.ok()) {
-        PERFETTO_ELOG("QueryTrace returned an error status %s",
-                      status.error_message().c_str());
-        {
-          std::lock_guard<std::mutex> status_guard(status_lock);
-          query_status = status;
-        }
-      } else {
-        protos::BigtraceQueryResponse response;
-        response.set_trace(trace_response.trace());
-        for (const protos::QueryResult& query_result :
-             trace_response.result()) {
-          response.add_result()->CopyFrom(query_result);
-        }
-        std::lock_guard<std::mutex> buffer_guard(buffer_lock_);
-        response_buffer.emplace_back(std::move(response));
-      }
-      semaphore_.Release();
-    });
+    // A buffer is used to periodically make writes to the client instead of
+    // writing every individual response in order to reduce contention on the
+    // writer.
+    base::SleepMicroseconds(kBufferPushDelayMicroseconds);
+    if (response_buffer.empty()) {
+      continue;
+    }
+    std::vector<protos::BigtraceQueryResponse> buffer;
+    {
+      std::lock_guard<std::mutex> buffer_guard(worker_lock);
+      buffer = std::move(response_buffer);
+      response_buffer.clear();
+    }
+    for (protos::BigtraceQueryResponse& response : buffer) {
+      writer->Write(std::move(response));
+    }
+    pushed_response_count += buffer.size();
   }
-  push_response_buffer_thread.join();
+
+  task_pool.JoinAll();
+
   return query_status;
 }
 
-void OrchestratorImpl::Semaphore::Acquire() {
-  std::unique_lock<std::mutex> lk(mutex_);
-  while (!count_) {
-    cv_.wait(lk);
-  }
-  --count_;
-}
-
-void OrchestratorImpl::Semaphore::Release() {
-  std::lock_guard<std::mutex> lk(mutex_);
-  ++count_;
-  cv_.notify_one();
-}
-
 }  // namespace perfetto::bigtrace
diff --git a/src/bigtrace/orchestrator/orchestrator_impl.h b/src/bigtrace/orchestrator/orchestrator_impl.h
index b989c68..f74251e 100644
--- a/src/bigtrace/orchestrator/orchestrator_impl.h
+++ b/src/bigtrace/orchestrator/orchestrator_impl.h
@@ -14,42 +14,38 @@
  * limitations under the License.
  */
 
+#ifndef SRC_BIGTRACE_ORCHESTRATOR_ORCHESTRATOR_IMPL_H_
+#define SRC_BIGTRACE_ORCHESTRATOR_ORCHESTRATOR_IMPL_H_
+
+#include <grpcpp/client_context.h>
+#include <memory>
+#include <mutex>
+#include <optional>
 #include "perfetto/ext/base/threading/thread_pool.h"
 #include "protos/perfetto/bigtrace/orchestrator.grpc.pb.h"
 #include "protos/perfetto/bigtrace/worker.grpc.pb.h"
 
-#ifndef SRC_BIGTRACE_ORCHESTRATOR_ORCHESTRATOR_IMPL_H_
-#define SRC_BIGTRACE_ORCHESTRATOR_ORCHESTRATOR_IMPL_H_
-
 namespace perfetto::bigtrace {
+namespace {
+const uint64_t kDefaultMaxQueryConcurrency = 8;
+}  // namespace
 
 class OrchestratorImpl final : public protos::BigtraceOrchestrator::Service {
  public:
   explicit OrchestratorImpl(std::unique_ptr<protos::BigtraceWorker::Stub> stub,
-                            uint32_t pool_size);
+                            uint32_t max_query_concurrency);
+
   grpc::Status Query(
       grpc::ServerContext*,
       const protos::BigtraceQueryArgs* args,
       grpc::ServerWriter<protos::BigtraceQueryResponse>* writer) override;
 
  private:
-  class Semaphore {
-   public:
-    explicit Semaphore(uint32_t count) : count_(count) {}
-    void Acquire();
-    void Release();
-
-   private:
-    std::mutex mutex_;
-    std::condition_variable cv_;
-    uint32_t count_;
-  };
   std::unique_ptr<protos::BigtraceWorker::Stub> stub_;
   std::unique_ptr<base::ThreadPool> pool_;
-  std::mutex buffer_lock_;
-  // Used to interleave requests to the Orchestrator to distribute jobs more
-  // fairly
-  Semaphore semaphore_;
+  uint32_t max_query_concurrency_ = kDefaultMaxQueryConcurrency;
+  uint32_t query_count_ = 0;
+  std::mutex query_count_mutex_;
 };
 
 }  // namespace perfetto::bigtrace
diff --git a/src/bigtrace/orchestrator/orchestrator_main.cc b/src/bigtrace/orchestrator/orchestrator_main.cc
index 7e12a27..8870965 100644
--- a/src/bigtrace/orchestrator/orchestrator_main.cc
+++ b/src/bigtrace/orchestrator/orchestrator_main.cc
@@ -15,6 +15,7 @@
  */
 
 #include <chrono>
+#include <limits>
 #include <memory>
 #include <mutex>
 
@@ -70,9 +71,8 @@
                                          -w -p -n EXCLUSIVELY)
  -r --name_resolution_scheme SCHEME     Specify the name resolution
                                         scheme for gRPC (e.g. ipv4:, dns://)
- -t -thread_pool_size POOL_SIZE         Specify the size of the thread pool
-                                        which determines number of concurrent
-                                        gRPCs from the Orchestrator
+ -t -max_query_concurrency              Specify the number of concurrent
+  MAX_QUERY_CONCURRENCY                 queries/gRPCs from the Orchestrator
                                         )",
                 argv[0]);
 }
@@ -156,7 +156,6 @@
   std::string worker_address_list = options->worker_address_list;
   uint64_t worker_count = options->worker_count;
 
-  // TODO(ivankc) Replace with DNS resolver
   std::string target_address = options->name_resolution_scheme.empty()
                                    ? "ipv4:"
                                    : options->name_resolution_scheme;
@@ -183,6 +182,7 @@
   }
   grpc::ChannelArguments args;
   args.SetLoadBalancingPolicyName("round_robin");
+  args.SetMaxReceiveMessageSize(std::numeric_limits<int32_t>::max());
   auto channel = grpc::CreateCustomChannel(
       target_address, grpc::InsecureChannelCredentials(), args);
   auto stub = protos::BigtraceWorker::NewStub(channel);
@@ -190,6 +190,8 @@
 
   // Setup the Orchestrator Server
   grpc::ServerBuilder builder;
+  builder.SetMaxReceiveMessageSize(std::numeric_limits<int32_t>::max());
+  builder.SetMaxMessageSize(std::numeric_limits<int32_t>::max());
   builder.AddListeningPort(server_socket, grpc::InsecureServerCredentials());
   builder.RegisterService(service.get());
   std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
diff --git a/src/bigtrace/orchestrator/resizable_task_pool.cc b/src/bigtrace/orchestrator/resizable_task_pool.cc
new file mode 100644
index 0000000..8d0aca6
--- /dev/null
+++ b/src/bigtrace/orchestrator/resizable_task_pool.cc
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/bigtrace/orchestrator/resizable_task_pool.h"
+
+namespace perfetto::bigtrace {
+
+ResizableTaskPool::ResizableTaskPool(std::function<void(ThreadWithContext*)> fn)
+    : fn_(std::move(fn)) {}
+
+// Resizes the number of threads in the task pool to |new_size|
+//
+// This works by performing one of two possible actions:
+// 1) When the number of threads is reduced, the excess are cancelled and joined
+// 2) When the number of threads is increased, new threads are created and
+// started
+void ResizableTaskPool::Resize(uint32_t new_size) {
+  if (size_t old_size = contextual_threads_.size(); new_size < old_size) {
+    for (size_t i = new_size; i < old_size; ++i) {
+      contextual_threads_[i]->Cancel();
+    }
+    for (size_t i = new_size; i < old_size; ++i) {
+      contextual_threads_[i]->thread.join();
+    }
+    contextual_threads_.resize(new_size);
+  } else {
+    contextual_threads_.resize(new_size);
+    for (size_t i = old_size; i < new_size; ++i) {
+      contextual_threads_[i] = std::make_unique<ThreadWithContext>(fn_);
+    }
+  }
+}
+
+// Joins all threads in the task pool
+void ResizableTaskPool::JoinAll() {
+  for (auto& contextual_thread : contextual_threads_) {
+    contextual_thread->thread.join();
+  }
+}
+
+}  // namespace perfetto::bigtrace
diff --git a/src/bigtrace/orchestrator/resizable_task_pool.h b/src/bigtrace/orchestrator/resizable_task_pool.h
new file mode 100644
index 0000000..32b1516
--- /dev/null
+++ b/src/bigtrace/orchestrator/resizable_task_pool.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_BIGTRACE_ORCHESTRATOR_RESIZABLE_TASK_POOL_H_
+#define SRC_BIGTRACE_ORCHESTRATOR_RESIZABLE_TASK_POOL_H_
+
+#include <functional>
+#include <mutex>
+#include <thread>
+
+#include <grpcpp/client_context.h>
+
+namespace perfetto::bigtrace {
+
+// This struct maps a thread to a context in order to allow for the cancellation
+// of the thread's current gRPC call through ClientContext's TryCancel
+struct ThreadWithContext {
+  explicit ThreadWithContext(std::function<void(ThreadWithContext*)> fn)
+      : thread(fn, this) {}
+
+  // Cancels the gRPC call through ClientContext as well as signalling a stop to
+  // the thread
+  void Cancel() {
+    client_context->TryCancel();
+    std::lock_guard<std::mutex> lk(mutex);
+    is_thread_cancelled = true;
+  }
+
+  // Returns whether the thread has been cancelled
+  bool IsCancelled() {
+    std::lock_guard<std::mutex> lk(mutex);
+    return is_thread_cancelled;
+  }
+
+  std::mutex mutex;
+  std::unique_ptr<grpc::ClientContext> client_context;
+  std::thread thread;
+  bool is_thread_cancelled = false;
+};
+
+// This pool manages a set of running tasks for a given query, and provides the
+// ability to resize in order to fairly distribute an equal number of workers
+// for each user through preemption
+class ResizableTaskPool {
+ public:
+  explicit ResizableTaskPool(std::function<void(ThreadWithContext*)> fn);
+  void Resize(uint32_t new_size);
+  void JoinAll();
+
+ private:
+  std::function<void(ThreadWithContext*)> fn_;
+  std::vector<std::unique_ptr<ThreadWithContext>> contextual_threads_;
+};
+}  // namespace perfetto::bigtrace
+
+#endif  // SRC_BIGTRACE_ORCHESTRATOR_RESIZABLE_TASK_POOL_H_
diff --git a/src/bigtrace/orchestrator/trace_address_pool.cc b/src/bigtrace/orchestrator/trace_address_pool.cc
new file mode 100644
index 0000000..33ee311
--- /dev/null
+++ b/src/bigtrace/orchestrator/trace_address_pool.cc
@@ -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.
+ */
+
+#include "src/bigtrace/orchestrator/trace_address_pool.h"
+#include "perfetto/base/logging.h"
+
+namespace perfetto::bigtrace {
+
+TraceAddressPool::TraceAddressPool(
+    const std::vector<std::string>& trace_addresses)
+    : trace_addresses_(trace_addresses) {}
+
+// Pops a trace address from the pool, blocking if necessary
+//
+// Returns a nullopt if the pool is empty
+std::optional<std::string> TraceAddressPool::Pop() {
+  std::lock_guard<std::mutex> trace_addresses_guard(trace_addresses_lock_);
+  if (trace_addresses_.size() == 0) {
+    return std::nullopt;
+  }
+  std::string trace_address = trace_addresses_.back();
+  trace_addresses_.pop_back();
+  running_queries_++;
+  return trace_address;
+}
+
+// Marks a trace address as cancelled
+//
+// Returns cancelled trace addresses to the pool for future calls to |Pop|
+void TraceAddressPool::MarkCancelled(std::string trace_address) {
+  std::lock_guard<std::mutex> guard(trace_addresses_lock_);
+  PERFETTO_CHECK(running_queries_-- > 0);
+  trace_addresses_.push_back(std::move(trace_address));
+}
+
+// Returns the number of remaining trace addresses which require processing
+uint32_t TraceAddressPool::RemainingCount() {
+  std::lock_guard<std::mutex> guard(trace_addresses_lock_);
+  return static_cast<uint32_t>(trace_addresses_.size()) + running_queries_;
+}
+
+}  // namespace perfetto::bigtrace
diff --git a/src/bigtrace/orchestrator/trace_address_pool.h b/src/bigtrace/orchestrator/trace_address_pool.h
new file mode 100644
index 0000000..d175ef7
--- /dev/null
+++ b/src/bigtrace/orchestrator/trace_address_pool.h
@@ -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.
+ */
+
+#ifndef SRC_BIGTRACE_ORCHESTRATOR_TRACE_ADDRESS_POOL_H_
+#define SRC_BIGTRACE_ORCHESTRATOR_TRACE_ADDRESS_POOL_H_
+
+#include <mutex>
+#include <optional>
+#include <vector>
+
+namespace perfetto::bigtrace {
+
+// This pool contains all trace addresses of a given query and facilitates a
+// thread safe way of popping traces and returning them to the pool if the query
+// is cancelled
+class TraceAddressPool {
+ public:
+  explicit TraceAddressPool(const std::vector<std::string>& trace_addresses);
+  std::optional<std::string> Pop();
+  void MarkCancelled(std::string trace_address);
+  uint32_t RemainingCount();
+
+ private:
+  std::vector<std::string> trace_addresses_;
+  std::mutex trace_addresses_lock_;
+  uint32_t running_queries_ = 0;
+};
+
+}  // namespace perfetto::bigtrace
+
+#endif  // SRC_BIGTRACE_ORCHESTRATOR_TRACE_ADDRESS_POOL_H_
diff --git a/src/bigtrace/worker/BUILD.gn b/src/bigtrace/worker/BUILD.gn
index 2f91816..0c23c4e 100644
--- a/src/bigtrace/worker/BUILD.gn
+++ b/src/bigtrace/worker/BUILD.gn
@@ -26,14 +26,17 @@
       "worker_main.cc",
     ]
     deps = [
+      "../../../gn:cpp_httplib",
       "../../../gn:default_deps",
       "../../../gn:grpc",
+      "../../../gn:jsoncpp",
       "../../../include/perfetto/ext/trace_processor/rpc:query_result_serializer",
       "../../../protos/perfetto/bigtrace:worker_grpc",
       "../../../protos/perfetto/bigtrace:worker_lite",
       "../../../src/trace_processor",
       "../../../src/trace_processor/rpc:rpc",
       "../../base",
+      "repository_policies:repository_policies",
     ]
   }
 }
diff --git a/src/bigtrace/worker/repository_policies/BUILD.gn b/src/bigtrace/worker/repository_policies/BUILD.gn
new file mode 100644
index 0000000..3b496dc
--- /dev/null
+++ b/src/bigtrace/worker/repository_policies/BUILD.gn
@@ -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("../../../../gn/perfetto.gni")
+import("../../../../gn/test.gni")
+
+assert(enable_perfetto_trace_processor &&
+       enable_perfetto_trace_processor_sqlite && enable_perfetto_grpc)
+
+source_set("repository_policies") {
+  sources = [
+    "gcs_trace_processor_loader.cc",
+    "gcs_trace_processor_loader.h",
+    "local_trace_processor_loader.cc",
+    "local_trace_processor_loader.h",
+    "trace_processor_loader.cc",
+    "trace_processor_loader.h",
+  ]
+  deps = [
+    "../../../../gn:cpp_httplib",
+    "../../../../gn:default_deps",
+    "../../../../gn:grpc",
+    "../../../../gn:jsoncpp",
+    "../../../../src/trace_processor",
+    "../../../../src/trace_processor/rpc:rpc",
+    "../../../../src/trace_processor/util:util",
+    "../../../base",
+  ]
+}
diff --git a/src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.cc b/src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.cc
new file mode 100644
index 0000000..91649b8
--- /dev/null
+++ b/src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.cc
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+#define CPPHTTPLIB_NO_EXCEPTIONS
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include <httplib.h>
+#include <json/json.h>
+
+#include "perfetto/base/status.h"
+#include "src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto::bigtrace {
+
+namespace {
+
+constexpr char kAuthDomain[] = "http://metadata.google.internal";
+constexpr char kAuthPath[] =
+    "/computeMetadata/v1/instance/service-accounts/default/token";
+constexpr char kGcsDomain[] = "https://storage.googleapis.com";
+constexpr char kGcsBucketPath[] = "/download/storage/v1/b/";
+constexpr char kGcsParams[] = "?alt=media";
+
+}  // namespace
+
+base::StatusOr<std::unique_ptr<trace_processor::TraceProcessor>>
+GcsTraceProcessorLoader::LoadTraceProcessor(const std::string& path) {
+  trace_processor::Config config;
+  std::unique_ptr<trace_processor::TraceProcessor> tp =
+      trace_processor::TraceProcessor::CreateInstance(config);
+
+  // Retrieve access token to use in GET request to GCS
+  httplib::Headers auth_headers{{"Metadata-Flavor", "Google"}};
+  httplib::Client auth_client(kAuthDomain);
+
+  httplib::Result auth_response = auth_client.Get(kAuthPath, auth_headers);
+  std::string json_string = auth_response->body;
+
+  if (auth_response->status != httplib::StatusCode::OK_200) {
+    return base::ErrStatus("Failed to get access token: %s",
+                           auth_response->body.c_str());
+  }
+
+  // Parse access token from response
+  Json::Value json_value;
+  Json::Reader json_reader;
+  bool parsed_successfully = json_reader.parse(json_string, json_value);
+  if (!parsed_successfully) {
+    return base::ErrStatus("Failed to parse GCS access token");
+  }
+  std::string access_token = json_value["access_token"].asString();
+
+  // Download trace from GCS
+  std::string gcs_path = kGcsBucketPath + path + kGcsParams;
+  httplib::Headers gcs_headers{{"Authorization", "Bearer " + access_token}};
+
+  httplib::Client gcs_client(kGcsDomain);
+  base::Status response_status;
+
+  httplib::Result trace_response = gcs_client.Get(
+      gcs_path, gcs_headers,
+      [&](const httplib::Response& response) {
+        if (httplib::StatusCode::OK_200 != response.status) {
+          response_status = base::ErrStatus("Failed to download trace: %s",
+                                            response.reason.c_str());
+          return false;
+        }
+        return true;
+      },
+      [&](const char* data, size_t data_length) {
+        std::unique_ptr<uint8_t[]> buf(new uint8_t[data_length]);
+        memcpy(buf.get(), data, data_length);
+        auto status = tp->Parse(std::move(buf), data_length);
+        return true;
+      });
+
+  tp->NotifyEndOfFile();
+
+  RETURN_IF_ERROR(response_status);
+
+  return tp;
+}
+}  // namespace perfetto::bigtrace
diff --git a/src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.h b/src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.h
new file mode 100644
index 0000000..223edaf
--- /dev/null
+++ b/src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.h
@@ -0,0 +1,33 @@
+/*
+ * 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_BIGTRACE_WORKER_REPOSITORY_POLICIES_GCS_TRACE_PROCESSOR_LOADER_H_
+#define SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_GCS_TRACE_PROCESSOR_LOADER_H_
+
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/trace_processor.h"
+#include "src/bigtrace/worker/repository_policies/trace_processor_loader.h"
+
+namespace perfetto::bigtrace {
+
+class GcsTraceProcessorLoader : public TraceProcessorLoader {
+ public:
+  base::StatusOr<std::unique_ptr<trace_processor::TraceProcessor>>
+  LoadTraceProcessor(const std::string& path) override;
+};
+
+}  // namespace perfetto::bigtrace
+
+#endif  // SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_GCS_TRACE_PROCESSOR_LOADER_H_
diff --git a/src/bigtrace/worker/repository_policies/local_trace_processor_loader.cc b/src/bigtrace/worker/repository_policies/local_trace_processor_loader.cc
new file mode 100644
index 0000000..536405a
--- /dev/null
+++ b/src/bigtrace/worker/repository_policies/local_trace_processor_loader.cc
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "src/bigtrace/worker/repository_policies/local_trace_processor_loader.h"
+#include "perfetto/trace_processor/read_trace.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto::bigtrace {
+
+base::StatusOr<std::unique_ptr<trace_processor::TraceProcessor>>
+LocalTraceProcessorLoader::LoadTraceProcessor(const std::string& path) {
+  trace_processor::Config config;
+  std::unique_ptr<trace_processor::TraceProcessor> tp =
+      trace_processor::TraceProcessor::CreateInstance(config);
+
+  RETURN_IF_ERROR(trace_processor::ReadTrace(tp.get(), path.c_str()));
+
+  return tp;
+}
+
+}  // namespace perfetto::bigtrace
diff --git a/src/bigtrace/worker/repository_policies/local_trace_processor_loader.h b/src/bigtrace/worker/repository_policies/local_trace_processor_loader.h
new file mode 100644
index 0000000..0f60f89
--- /dev/null
+++ b/src/bigtrace/worker/repository_policies/local_trace_processor_loader.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_LOCAL_TRACE_PROCESSOR_LOADER_H_
+#define SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_LOCAL_TRACE_PROCESSOR_LOADER_H_
+
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/trace_processor.h"
+#include "src/bigtrace/worker/repository_policies/trace_processor_loader.h"
+
+namespace perfetto::bigtrace {
+
+class LocalTraceProcessorLoader : public TraceProcessorLoader {
+ public:
+  base::StatusOr<std::unique_ptr<trace_processor::TraceProcessor>>
+  LoadTraceProcessor(const std::string& path) override;
+};
+
+}  // namespace perfetto::bigtrace
+
+#endif  // SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_LOCAL_TRACE_PROCESSOR_LOADER_H_
diff --git a/src/bigtrace/worker/repository_policies/trace_processor_loader.cc b/src/bigtrace/worker/repository_policies/trace_processor_loader.cc
new file mode 100644
index 0000000..d55bec7
--- /dev/null
+++ b/src/bigtrace/worker/repository_policies/trace_processor_loader.cc
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "src/bigtrace/worker/repository_policies/trace_processor_loader.h"
+
+namespace perfetto::bigtrace {
+
+TraceProcessorLoader::~TraceProcessorLoader() = default;
+
+}
diff --git a/src/bigtrace/worker/repository_policies/trace_processor_loader.h b/src/bigtrace/worker/repository_policies/trace_processor_loader.h
new file mode 100644
index 0000000..b1a6522
--- /dev/null
+++ b/src/bigtrace/worker/repository_policies/trace_processor_loader.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_TRACE_PROCESSOR_LOADER_H_
+#define SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_TRACE_PROCESSOR_LOADER_H_
+
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/trace_processor.h"
+
+namespace perfetto::bigtrace {
+
+// This interface is designed to facilitate interaction with multiple file
+// systems/object stores e.g. GCS, S3 or the local filesystem by allowing
+// implementation of classes which retrieve a trace using the specific store's
+// interface and returns a TraceProcessor instance containing the loaded trace
+// to the Worker
+class TraceProcessorLoader {
+ public:
+  virtual ~TraceProcessorLoader();
+  // Virtual method to load a trace from a given filesystem/object store and
+  // returns a TraceProcessor instance with the loaded trace
+  virtual base::StatusOr<std::unique_ptr<trace_processor::TraceProcessor>>
+  LoadTraceProcessor(const std::string& path) = 0;
+};
+
+}  // namespace perfetto::bigtrace
+
+#endif  // SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_TRACE_PROCESSOR_LOADER_H_
diff --git a/src/bigtrace/worker/worker_impl.cc b/src/bigtrace/worker/worker_impl.cc
index 53f8606..7c09984 100644
--- a/src/bigtrace/worker/worker_impl.cc
+++ b/src/bigtrace/worker/worker_impl.cc
@@ -14,41 +14,104 @@
  * limitations under the License.
  */
 
-#include "src/bigtrace/worker/worker_impl.h"
+#include <mutex>
+#include <thread>
+
+#include <grpcpp/server_context.h>
+#include <grpcpp/support/status.h>
+
+#include "perfetto/base/time.h"
 #include "perfetto/ext/trace_processor/rpc/query_result_serializer.h"
-#include "perfetto/trace_processor/read_trace.h"
 #include "perfetto/trace_processor/trace_processor.h"
+#include "src/bigtrace/worker/worker_impl.h"
 
 namespace perfetto::bigtrace {
 
 grpc::Status WorkerImpl::QueryTrace(
-    grpc::ServerContext*,
+    grpc::ServerContext* server_context,
     const protos::BigtraceQueryTraceArgs* args,
     protos::BigtraceQueryTraceResponse* response) {
-  trace_processor::Config config;
-  std::unique_ptr<trace_processor::TraceProcessor> tp =
-      trace_processor::TraceProcessor::CreateInstance(config);
+  std::mutex mutex;
+  bool is_thread_done = false;
 
-  base::Status status =
-      trace_processor::ReadTrace(tp.get(), args->trace().c_str());
-  if (!status.ok()) {
-    const std::string& error_message = status.c_message();
+  std::string args_trace = args->trace();
+
+  if (args_trace.empty()) {
+    return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
+                        "Empty trace name is not valid");
+  }
+
+  if (args_trace[0] != '/') {
+    return grpc::Status(
+        grpc::StatusCode::INVALID_ARGUMENT,
+        "Trace path must contain and begin with / for the prefix");
+  }
+
+  std::string prefix = args_trace.substr(0, args_trace.find("/", 1));
+  if (registry_.find(prefix) == registry_.end()) {
+    return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
+                        "Path prefix does not exist in registry");
+  }
+
+  if (prefix.length() == args_trace.length()) {
+    return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
+                        "Empty path is invalid");
+  }
+
+  std::string path = args_trace.substr(prefix.length() + 1);
+
+  base::StatusOr<std::unique_ptr<trace_processor::TraceProcessor>> tp_or =
+      registry_[prefix]->LoadTraceProcessor(path);
+
+  if (!tp_or.ok()) {
+    const std::string& error_message = tp_or.status().message();
     return grpc::Status(grpc::StatusCode::INTERNAL, error_message);
   }
-  auto iter = tp->ExecuteQuery(args->sql_query());
-  trace_processor::QueryResultSerializer serializer =
-      trace_processor::QueryResultSerializer(std::move(iter));
 
-  std::vector<uint8_t> serialized;
-  for (bool has_more = true; has_more;) {
-    serialized.clear();
-    has_more = serializer.Serialize(&serialized);
-    response->add_result()->ParseFromArray(serialized.data(),
-                                           static_cast<int>(serialized.size()));
+  std::unique_ptr<trace_processor::TraceProcessor> tp = std::move(*tp_or);
+  std::optional<trace_processor::Iterator> iterator;
+
+  std::thread execute_query_thread([&]() {
+    iterator = tp->ExecuteQuery(args->sql_query());
+    std::lock_guard<std::mutex> lk(mutex);
+    is_thread_done = true;
+  });
+
+  for (;;) {
+    if (server_context->IsCancelled()) {
+      // If the thread is cancelled, we need to propagate the information to the
+      // trace processor thread and we do this by attempting to interrupt the
+      // trace processor every 10ms until the trace processor thread returns.
+      //
+      // A loop is necessary here because, due to scheduling delay, it is
+      // possible we are cancelled before trace processor even started running.
+      // InterruptQuery is ignored if it happens before entering TraceProcessor
+      // which can cause the query to not be interrupted at all.
+      while (!execute_query_thread.joinable()) {
+        base::SleepMicroseconds(10000);
+        tp->InterruptQuery();
+      }
+      execute_query_thread.join();
+      return grpc::Status::CANCELLED;
+    }
+
+    std::lock_guard<std::mutex> lk(mutex);
+    if (is_thread_done) {
+      execute_query_thread.join();
+      trace_processor::QueryResultSerializer serializer =
+          trace_processor::QueryResultSerializer(*std::move(iterator));
+
+      std::vector<uint8_t> serialized;
+      for (bool has_more = true; has_more;) {
+        serialized.clear();
+        has_more = serializer.Serialize(&serialized);
+        response->add_result()->ParseFromArray(
+            serialized.data(), static_cast<int>(serialized.size()));
+      }
+      response->set_trace(args->trace());
+      return grpc::Status::OK;
+    }
   }
-  response->set_trace(args->trace());
-
-  return grpc::Status::OK;
 }
 
 }  // namespace perfetto::bigtrace
diff --git a/src/bigtrace/worker/worker_impl.h b/src/bigtrace/worker/worker_impl.h
index b5a9664..946715d 100644
--- a/src/bigtrace/worker/worker_impl.h
+++ b/src/bigtrace/worker/worker_impl.h
@@ -13,20 +13,31 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-#include "protos/perfetto/bigtrace/worker.grpc.pb.h"
-#include "protos/perfetto/bigtrace/worker.pb.h"
-
 #ifndef SRC_BIGTRACE_WORKER_WORKER_IMPL_H_
 #define SRC_BIGTRACE_WORKER_WORKER_IMPL_H_
 
+#include <unordered_map>
+
+#include "protos/perfetto/bigtrace/worker.grpc.pb.h"
+#include "protos/perfetto/bigtrace/worker.pb.h"
+#include "src/bigtrace/worker/repository_policies/trace_processor_loader.h"
+
 namespace perfetto::bigtrace {
 
 class WorkerImpl final : public protos::BigtraceWorker::Service {
  public:
+  explicit WorkerImpl(
+      std::unordered_map<std::string, std::unique_ptr<TraceProcessorLoader>>
+          registry)
+      : registry_(std::move(registry)) {}
   grpc::Status QueryTrace(
       grpc::ServerContext*,
       const protos::BigtraceQueryTraceArgs* args,
       protos::BigtraceQueryTraceResponse* response) override;
+
+ private:
+  std::unordered_map<std::string, std::unique_ptr<TraceProcessorLoader>>
+      registry_;
 };
 
 }  // namespace perfetto::bigtrace
diff --git a/src/bigtrace/worker/worker_main.cc b/src/bigtrace/worker/worker_main.cc
index b985f69..c61cfc8 100644
--- a/src/bigtrace/worker/worker_main.cc
+++ b/src/bigtrace/worker/worker_main.cc
@@ -21,6 +21,9 @@
 
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/getopt.h"
+#include "src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.h"
+#include "src/bigtrace/worker/repository_policies/local_trace_processor_loader.h"
+#include "src/bigtrace/worker/repository_policies/trace_processor_loader.h"
 #include "src/bigtrace/worker/worker_impl.h"
 
 namespace perfetto::bigtrace {
@@ -53,7 +56,13 @@
   CommandLineOptions options = ParseCommandLineOptions(argc, argv);
   std::string socket =
       options.socket.empty() ? "127.0.0.1:5052" : options.socket;
-  auto service = std::make_unique<WorkerImpl>();
+
+  std::unordered_map<std::string, std::unique_ptr<TraceProcessorLoader>>
+      registry;
+  registry["/gcs"] = std::make_unique<GcsTraceProcessorLoader>();
+  registry["/local"] = std::make_unique<LocalTraceProcessorLoader>();
+
+  auto service = std::make_unique<WorkerImpl>(std::move(registry));
   grpc::ServerBuilder builder;
   builder.RegisterService(service.get());
   builder.AddListeningPort(socket, grpc::InsecureServerCredentials());
diff --git a/src/perfetto_cmd/BUILD.gn b/src/perfetto_cmd/BUILD.gn
index 3f90a57..4cfe0ea 100644
--- a/src/perfetto_cmd/BUILD.gn
+++ b/src/perfetto_cmd/BUILD.gn
@@ -71,8 +71,6 @@
     "packet_writer.h",
     "perfetto_cmd.cc",
     "perfetto_cmd.h",
-    "rate_limiter.cc",
-    "rate_limiter.h",
   ]
   if (is_android) {
     deps += [ "../android_internal:lazy_library_loader" ]
@@ -136,10 +134,7 @@
 }
 
 perfetto_proto_library("protos_@TYPE@") {
-  proto_generators = [
-    "cpp",
-    "source_set",
-  ]
+  proto_generators = [ "cpp" ]
   sources = [ "perfetto_cmd_state.proto" ]
   proto_path = perfetto_root_path
 }
@@ -163,6 +158,5 @@
     "config_unittest.cc",
     "packet_writer_unittest.cc",
     "pbtxt_to_pb_unittest.cc",
-    "rate_limiter_unittest.cc",
   ]
 }
diff --git a/src/perfetto_cmd/perfetto_cmd.cc b/src/perfetto_cmd/perfetto_cmd.cc
index 726b266..d3a12d4 100644
--- a/src/perfetto_cmd/perfetto_cmd.cc
+++ b/src/perfetto_cmd/perfetto_cmd.cc
@@ -78,7 +78,6 @@
 #include "src/perfetto_cmd/config.h"
 #include "src/perfetto_cmd/packet_writer.h"
 #include "src/perfetto_cmd/pbtxt_to_pb.h"
-#include "src/perfetto_cmd/rate_limiter.h"
 #include "src/perfetto_cmd/trigger_producer.h"
 
 #include "protos/perfetto/common/ftrace_descriptor.gen.h"
@@ -159,30 +158,6 @@
   return true;
 }
 
-bool IsUserBuild() {
-#if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
-  std::string build_type = base::GetAndroidProp("ro.build.type");
-  if (build_type.empty()) {
-    PERFETTO_ELOG("Unable to read ro.build.type: assuming user build");
-    return true;
-  }
-  return build_type == "user";
-#else
-  return false;
-#endif  // PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
-}
-
-std::optional<PerfettoStatsdAtom> ConvertRateLimiterResponseToAtom(
-    RateLimiter::ShouldTraceResponse resp) {
-  switch (resp) {
-    case RateLimiter::kNotAllowedOnUserBuild:
-      return PerfettoStatsdAtom::kCmdUserBuildTracingNotAllowed;
-    case RateLimiter::kOkToTrace:
-      return std::nullopt;
-  }
-  PERFETTO_FATAL("For GCC");
-}
-
 void ArgsAppend(std::string* str, const std::string& arg) {
   str->append(arg);
   str->append("\0", 1);
@@ -219,6 +194,9 @@
                              received, non-zero otherwise (error or timeout).
   --clone TSID             : Creates a read-only clone of an existing tracing
                              session, identified by its ID (see --query).
+  --clone-by-name NAME     : Creates a read-only clone of an existing tracing
+                             session, identified by its unique_session_name in
+                             the config.
   --clone-for-bugreport    : Can only be used with --clone. It disables the
                              trace_filter on the cloned session.
   --config         -c      : /path/to/trace/config/file or - for stdin
@@ -279,6 +257,7 @@
     OPT_BUGREPORT,
     OPT_BUGREPORT_ALL,
     OPT_CLONE,
+    OPT_CLONE_BY_NAME,
     OPT_CLONE_SKIP_FILTER,
     OPT_CONFIG_ID,
     OPT_CONFIG_UID,
@@ -319,6 +298,7 @@
       {"detach", required_argument, nullptr, OPT_DETACH},
       {"attach", required_argument, nullptr, OPT_ATTACH},
       {"clone", required_argument, nullptr, OPT_CLONE},
+      {"clone-by-name", required_argument, nullptr, OPT_CLONE_BY_NAME},
       {"clone-for-bugreport", no_argument, nullptr, OPT_CLONE_SKIP_FILTER},
       {"is_detached", required_argument, nullptr, OPT_IS_DETACHED},
       {"stop", no_argument, nullptr, OPT_STOP},
@@ -334,7 +314,6 @@
   std::string trace_config_raw;
   bool parse_as_pbtxt = false;
   TraceConfig::StatsdMetadata statsd_metadata;
-  limiter_.reset(new RateLimiter());
 
   ConfigOptions config_options;
   bool has_config_options = false;
@@ -386,7 +365,15 @@
         trace_config_raw = snapshot_config_;
       } else {
         if (!base::ReadFile(optarg, &trace_config_raw)) {
+#if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
+          PERFETTO_PLOG(
+              "Could not open %s. If this is a permission denied error, try "
+              "placing the config in /data/misc/perfetto-configs: Perfetto "
+              "should always be able to access this directory.",
+              optarg);
+#else
           PERFETTO_PLOG("Could not open %s", optarg);
+#endif
           return 1;
         }
       }
@@ -414,6 +401,11 @@
       continue;
     }
 
+    if (option == OPT_CLONE_BY_NAME) {
+      clone_name_ = optarg;
+      continue;
+    }
+
     if (option == OPT_CLONE_SKIP_FILTER) {
       clone_for_bugreport_ = true;
       continue;
@@ -598,8 +590,13 @@
     return 1;
   }
 
-  if (clone_for_bugreport_ && !clone_tsid_) {
-    PERFETTO_ELOG("--clone-for-bugreport requires --clone");
+  if (clone_tsid_ && !clone_name_.empty()) {
+    PERFETTO_ELOG("--clone and --clone-by-name are mutually exclusive");
+    return 1;
+  }
+
+  if (clone_for_bugreport_ && !is_clone()) {
+    PERFETTO_ELOG("--clone-for-bugreport requires --clone or --clone-by-name");
     return 1;
   }
 
@@ -638,7 +635,7 @@
     }
     parsed = CreateConfigFromOptions(config_options, trace_config_.get());
   } else {
-    if (trace_config_raw.empty() && !clone_tsid_) {
+    if (trace_config_raw.empty() && !is_clone()) {
       PERFETTO_ELOG("The TraceConfig is empty");
       return 1;
     }
@@ -662,7 +659,7 @@
   if (parsed) {
     *trace_config_->mutable_statsd_metadata() = std::move(statsd_metadata);
     trace_config_raw.clear();
-  } else if (will_trace_or_trigger && !clone_tsid_) {
+  } else if (will_trace_or_trigger && !is_clone()) {
     PERFETTO_ELOG("The trace config is invalid, bailing out.");
     if (cfg_could_be_txt) {
       PERFETTO_ELOG(
@@ -946,8 +943,6 @@
   // connect as a consumer or run the trace. So bail out after processing all
   // the options.
   if (!triggers_to_activate_.empty()) {
-    LogTriggerEvents(PerfettoTriggerAtom::kCmdTrigger, triggers_to_activate_);
-
     bool finished_with_success = false;
     auto weak_this = weak_factory_.GetWeakPtr();
     TriggerProducer producer(
@@ -960,10 +955,6 @@
         },
         &triggers_to_activate_);
     task_runner_.Run();
-    if (!finished_with_success) {
-      LogTriggerEvents(PerfettoTriggerAtom::kCmdTriggerFail,
-                       triggers_to_activate_);
-    }
     return finished_with_success ? 0 : 1;
   }  // if (triggers_to_activate_)
 
@@ -974,11 +965,6 @@
     return 1;  // We can legitimately get here if the service disconnects.
   }
 
-  RateLimiter::Args args{};
-  args.is_user_build = IsUserBuild();
-  args.is_uploading = save_to_incidentd_ || report_to_android_framework_;
-  args.allow_user_build_tracing = trace_config_->allow_user_build_tracing();
-
   if (!trace_config_->unique_session_name().empty())
     base::MaybeSetThreadName("p-" + trace_config_->unique_session_name());
 
@@ -1002,11 +988,11 @@
     std::this_thread::sleep_for(std::chrono::milliseconds(dist(minstd)));
   }
 
-  if (clone_tsid_) {
+  if (is_clone()) {
     if (snapshot_trigger_name_.empty()) {
-      LogUploadEvent(PerfettoStatsdAtom::kCloneTraceBegin);
+      LogUploadEvent(PerfettoStatsdAtom::kCmdCloneTraceBegin);
     } else {
-      LogUploadEvent(PerfettoStatsdAtom::kCloneTriggerTraceBegin,
+      LogUploadEvent(PerfettoStatsdAtom::kCmdCloneTriggerTraceBegin,
                      snapshot_trigger_name_);
     }
   } else if (trace_config_->trigger_config().trigger_timeout_ms() == 0) {
@@ -1015,12 +1001,6 @@
     LogUploadEvent(PerfettoStatsdAtom::kBackgroundTraceBegin);
   }
 
-  auto err_atom = ConvertRateLimiterResponseToAtom(limiter_->ShouldTrace(args));
-  if (err_atom) {
-    LogUploadEvent(err_atom.value());
-    return 1;
-  }
-
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
   if (!background_ && !is_detach() && !upload_flag_ &&
       triggers_to_activate_.empty() && !isatty(STDIN_FILENO) &&
@@ -1085,13 +1065,18 @@
     return;
   }
 
-  if (clone_tsid_.has_value()) {
+  if (is_clone()) {
     task_runner_.PostDelayedTask(std::bind(&PerfettoCmd::OnTimeout, this),
                                  kCloneTimeoutMs);
     ConsumerEndpoint::CloneSessionArgs args;
     args.skip_trace_filter = clone_for_bugreport_;
     args.for_bugreport = clone_for_bugreport_;
-    consumer_endpoint_->CloneSession(*clone_tsid_, std::move(args));
+    if (clone_tsid_.has_value()) {
+      args.tsid = *clone_tsid_;
+    } else if (!clone_name_.empty()) {
+      args.unique_session_name = clone_name_;
+    }
+    consumer_endpoint_->CloneSession(std::move(args));
     return;
   }
 
@@ -1123,7 +1108,7 @@
   // Failsafe mechanism to avoid waiting indefinitely if the service hangs.
   // Note: when using prefer_suspend_clock_for_duration the actual duration
   // might be < expected_duration_ms_ measured in in wall time. But this is fine
-  // because the resulting timeout will be conservative (it will be accurate
+  // because the resulting timeout will be conservative (it will be accut
   // if the device never suspends, and will be more lax if it does).
   if (expected_duration_ms_) {
     uint32_t trace_timeout = expected_duration_ms_ + 60000 +
@@ -1372,12 +1357,17 @@
 }
 
 void PerfettoCmd::OnSessionCloned(const OnSessionClonedArgs& args) {
-  PERFETTO_DLOG("Cloned tracing session %" PRIu64 ", success=%d",
-                clone_tsid_.value_or(0), args.success);
+  PERFETTO_DLOG("Cloned tracing session %" PRIu64 ", name=\"%s\", success=%d",
+                clone_tsid_.value_or(0), clone_name_.c_str(), args.success);
   std::string full_error;
   if (!args.success) {
-    full_error = "Failed to clone tracing session " +
-                 std::to_string(clone_tsid_.value_or(0)) + ": " + args.error;
+    std::string name;
+    if (clone_tsid_.has_value()) {
+      name = std::to_string(*clone_tsid_);
+    } else {
+      name = "\"" + clone_name_ + "\"";
+    }
+    full_error = "Failed to clone tracing session " + name + ": " + args.error;
   }
 
   // This is used with --save-all-for-bugreport, to pause all cloning threads
@@ -1392,6 +1382,14 @@
   // Kick off the readback and file finalization (as if we started tracing and
   // reached the duration_ms timeout).
   uuid_ = args.uuid.ToString();
+
+  // Log the new UUID with the clone tag.
+  if (snapshot_trigger_name_.empty()) {
+    LogUploadEvent(PerfettoStatsdAtom::kCmdOnSessionClone);
+  } else {
+    LogUploadEvent(PerfettoStatsdAtom::kCmdOnTriggerSessionClone,
+                   snapshot_trigger_name_);
+  }
   ReadbackTraceDataAndQuit(full_error);
 }
 
diff --git a/src/perfetto_cmd/perfetto_cmd.h b/src/perfetto_cmd/perfetto_cmd.h
index 160a701..17bb751 100644
--- a/src/perfetto_cmd/perfetto_cmd.h
+++ b/src/perfetto_cmd/perfetto_cmd.h
@@ -42,8 +42,6 @@
 
 namespace perfetto {
 
-class RateLimiter;
-
 // Directory for local state and temporary files. This is automatically
 // created by the system by setting setprop persist.traced.enable=1.
 extern const char* kStateDir;
@@ -93,6 +91,9 @@
   void OnTimeout();
   bool is_detach() const { return !detach_key_.empty(); }
   bool is_attach() const { return !attach_key_.empty(); }
+  bool is_clone() const {
+    return clone_tsid_.has_value() || !clone_name_.empty();
+  }
 
   // Once we call ReadBuffers we expect one or more calls to OnTraceData
   // with the last call having |has_more| set to false. However we should
@@ -145,7 +146,6 @@
 
   base::UnixTaskRunner task_runner_;
 
-  std::unique_ptr<RateLimiter> limiter_;
   std::unique_ptr<perfetto::TracingService::ConsumerEndpoint>
       consumer_endpoint_;
   std::unique_ptr<TraceConfig> trace_config_;
@@ -177,6 +177,7 @@
   bool connected_ = false;
   std::string uuid_;
   std::optional<TracingSessionID> clone_tsid_{};
+  std::string clone_name_;
   bool clone_for_bugreport_ = false;
   std::function<void()> on_session_cloned_;
 
diff --git a/src/perfetto_cmd/rate_limiter.cc b/src/perfetto_cmd/rate_limiter.cc
deleted file mode 100644
index 8b80718..0000000
--- a/src/perfetto_cmd/rate_limiter.cc
+++ /dev/null
@@ -1,44 +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.
- */
-
-#include "src/perfetto_cmd/rate_limiter.h"
-
-#include "perfetto/base/logging.h"
-#include "src/perfetto_cmd/perfetto_cmd.h"
-
-namespace perfetto {
-
-RateLimiter::RateLimiter() = default;
-RateLimiter::~RateLimiter() = default;
-
-RateLimiter::ShouldTraceResponse RateLimiter::ShouldTrace(const Args& args) {
-  // Not uploading?
-  // -> We can just trace.
-  if (!args.is_uploading)
-    return ShouldTraceResponse::kOkToTrace;
-
-  // If we're tracing a user build we should only trace if the override in
-  // the config is set:
-  if (args.is_user_build && !args.allow_user_build_tracing) {
-    PERFETTO_ELOG(
-        "Guardrail: allow_user_build_tracing must be set to trace on user "
-        "builds");
-    return ShouldTraceResponse::kNotAllowedOnUserBuild;
-  }
-  return ShouldTraceResponse::kOkToTrace;
-}
-
-}  // namespace perfetto
diff --git a/src/perfetto_cmd/rate_limiter.h b/src/perfetto_cmd/rate_limiter.h
deleted file mode 100644
index f82211e..0000000
--- a/src/perfetto_cmd/rate_limiter.h
+++ /dev/null
@@ -1,42 +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.
- */
-
-#ifndef SRC_PERFETTO_CMD_RATE_LIMITER_H_
-#define SRC_PERFETTO_CMD_RATE_LIMITER_H_
-
-namespace perfetto {
-
-class RateLimiter {
- public:
-  struct Args {
-    bool is_user_build = false;
-    bool is_uploading = false;
-    bool allow_user_build_tracing = false;
-  };
-  enum ShouldTraceResponse {
-    kOkToTrace,
-    kNotAllowedOnUserBuild,
-  };
-
-  RateLimiter();
-  virtual ~RateLimiter();
-
-  ShouldTraceResponse ShouldTrace(const Args& args);
-};
-
-}  // namespace perfetto
-
-#endif  // SRC_PERFETTO_CMD_RATE_LIMITER_H_
diff --git a/src/perfetto_cmd/rate_limiter_unittest.cc b/src/perfetto_cmd/rate_limiter_unittest.cc
deleted file mode 100644
index 59045b0..0000000
--- a/src/perfetto_cmd/rate_limiter_unittest.cc
+++ /dev/null
@@ -1,55 +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.
- */
-
-#include "src/perfetto_cmd/rate_limiter.h"
-
-#include "test/gtest_and_gmock.h"
-
-using testing::_;
-using testing::Contains;
-using testing::Invoke;
-using testing::NiceMock;
-using testing::Return;
-using testing::StrictMock;
-
-namespace perfetto {
-namespace {
-
-TEST(RateLimiterTest, CantTraceOnUser) {
-  RateLimiter limiter;
-  RateLimiter::Args args;
-
-  args.is_user_build = true;
-  args.allow_user_build_tracing = false;
-  args.is_uploading = true;
-
-  ASSERT_EQ(limiter.ShouldTrace(args), RateLimiter::kNotAllowedOnUserBuild);
-}
-
-TEST(RateLimiterTest, CanTraceOnUser) {
-  RateLimiter limiter;
-  RateLimiter::Args args;
-
-  args.is_user_build = false;
-  args.allow_user_build_tracing = false;
-  args.is_uploading = true;
-
-  ASSERT_EQ(limiter.ShouldTrace(args), RateLimiter::kOkToTrace);
-}
-
-}  // namespace
-
-}  // namespace perfetto
diff --git a/src/perfetto_cmd/trigger_perfetto.cc b/src/perfetto_cmd/trigger_perfetto.cc
index 3b7f891..c1711d4 100644
--- a/src/perfetto_cmd/trigger_perfetto.cc
+++ b/src/perfetto_cmd/trigger_perfetto.cc
@@ -86,9 +86,6 @@
     return PrintUsage(argv[0]);
   }
 
-  android_stats::MaybeLogTriggerEvents(
-      PerfettoTriggerAtom::kTriggerPerfettoTrigger, triggers_to_activate);
-
   bool finished_with_success = false;
   base::UnixTaskRunner task_runner;
   TriggerProducer producer(
@@ -101,8 +98,6 @@
   task_runner.Run();
 
   if (!finished_with_success) {
-    android_stats::MaybeLogTriggerEvents(
-        PerfettoTriggerAtom::kTriggerPerfettoTriggerFail, triggers_to_activate);
     return 1;
   }
   return 0;
diff --git a/src/profiling/memory/BUILD.gn b/src/profiling/memory/BUILD.gn
index 254d82d..af081f4 100644
--- a/src/profiling/memory/BUILD.gn
+++ b/src/profiling/memory/BUILD.gn
@@ -354,6 +354,7 @@
     "../../../protos/perfetto/config/profiling:cpp",
     "../../../protos/perfetto/trace/interned_data:cpp",
     "../../../protos/perfetto/trace/profiling:cpp",
+    "../../../test:integrationtest_initializer",
     "../../../test:test_helper",
     "../../base",
     "../../base:test_support",
diff --git a/src/profiling/memory/heapprofd_end_to_end_test.cc b/src/profiling/memory/heapprofd_end_to_end_test.cc
index 90180a4..6541243 100644
--- a/src/profiling/memory/heapprofd_end_to_end_test.cc
+++ b/src/profiling/memory/heapprofd_end_to_end_test.cc
@@ -41,6 +41,7 @@
 #include "src/base/test/test_task_runner.h"
 #include "src/profiling/memory/heapprofd_producer.h"
 #include "test/gtest_and_gmock.h"
+#include "test/integrationtest_initializer.h"
 #include "test/test_helper.h"
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
@@ -53,6 +54,7 @@
 #include "protos/perfetto/trace/profiling/profile_packet.gen.h"
 
 namespace perfetto {
+
 namespace profiling {
 namespace {
 
@@ -277,7 +279,7 @@
   return child;
 }
 
-void __attribute__((constructor(1024))) RunContinuousMalloc() {
+void RunContinuousMalloc() {
   const char* a0 = getenv("HEAPPROFD_TESTING_RUN_MALLOC_ARG0");
   const char* a1 = getenv("HEAPPROFD_TESTING_RUN_MALLOC_ARG1");
   const char* a2 = getenv("HEAPPROFD_TESTING_RUN_MALLOC_ARG2");
@@ -296,7 +298,7 @@
   exit(0);
 }
 
-void __attribute__((constructor(1024))) RunAccurateMalloc() {
+void PERFETTO_NO_INLINE RunAccurateMalloc() {
   const char* a0 = getenv("HEAPPROFD_TESTING_RUN_ACCURATE_MALLOC");
   if (a0 == nullptr)
     return;
@@ -377,7 +379,7 @@
   }
 }
 
-void __attribute__((constructor(1024))) RunAccurateSample() {
+void RunAccurateSample() {
   const char* a0 = getenv("HEAPPROFD_TESTING_RUN_ACCURATE_SAMPLE");
   if (a0 == nullptr)
     return;
@@ -416,14 +418,14 @@
   }
 }
 
-void __attribute__((constructor(1024))) RunAccurateMallocWithVfork() {
+void RunAccurateMallocWithVfork() {
   const char* a0 = getenv("HEAPPROFD_TESTING_RUN_ACCURATE_MALLOC_WITH_VFORK");
   if (a0 == nullptr)
     return;
   RunAccurateMallocWithVforkCommon();
 }
 
-void __attribute__((constructor(1024))) RunAccurateMallocWithVforkThread() {
+void RunAccurateMallocWithVforkThread() {
   const char* a0 =
       getenv("HEAPPROFD_TESTING_RUN_ACCURATE_MALLOC_WITH_VFORK_THREAD");
   if (a0 == nullptr)
@@ -432,7 +434,7 @@
   th.join();
 }
 
-void __attribute__((constructor(1024))) RunReInit() {
+void RunReInit() {
   const char* a0 = getenv("HEAPPROFD_TESTING_RUN_REINIT_ARG0");
   if (a0 == nullptr)
     return;
@@ -467,7 +469,7 @@
   PERFETTO_FATAL("Should be unreachable");
 }
 
-void __attribute__((constructor(1024))) RunCustomLifetime() {
+void RunCustomLifetime() {
   const char* a0 = getenv("HEAPPROFD_TESTING_RUN_LIFETIME_ARG0");
   const char* a1 = getenv("HEAPPROFD_TESTING_RUN_LIFETIME_ARG1");
   if (a0 == nullptr)
@@ -525,6 +527,32 @@
   }
 }
 
+void MainInitializer() {
+  // *** TRICKY ***
+  //
+  // The tests want to launch another binary and attach heapprofd to it.
+  // Carrying another binary is difficult, so another approach is taken:
+  // * The test execute its own binary with special environment variables.
+  // * If these environment variables are detected, instead of running the
+  //   gtest tests, the binary just need to do some allocation an exit.
+
+  // This is run before all the gtest tests are executed.
+  //
+  // If one of these function recognizes the environment variable, it will do
+  // its job and exit().
+  RunContinuousMalloc();
+  RunAccurateMalloc();
+  RunAccurateMallocWithVfork();
+  RunAccurateMallocWithVforkThread();
+  RunReInit();
+  RunCustomLifetime();
+  RunAccurateSample();
+}
+
+int PERFETTO_UNUSED initializer =
+    integration_tests::RegisterHeapprofdEndToEndTestInitializer(
+        MainInitializer);
+
 class TraceProcessorTestHelper : public TestHelper {
  public:
   explicit TraceProcessorTestHelper(base::TestTaskRunner* task_runner)
diff --git a/src/profiling/memory/heapprofd_producer.cc b/src/profiling/memory/heapprofd_producer.cc
index 7490115..e2b152b 100644
--- a/src/profiling/memory/heapprofd_producer.cc
+++ b/src/profiling/memory/heapprofd_producer.cc
@@ -385,8 +385,7 @@
   }
 
   if (data_sources_.find(id) != data_sources_.end()) {
-    PERFETTO_DFATAL_OR_ELOG(
-        "Received duplicated data source instance id: %" PRIu64, id);
+    PERFETTO_ELOG("Received duplicated data source instance id: %" PRIu64, id);
     return;
   }
 
@@ -519,16 +518,15 @@
     // This is expected in child heapprofd, where we reject uninteresting data
     // sources in SetupDataSource.
     if (mode_ == HeapprofdMode::kCentral) {
-      PERFETTO_DFATAL_OR_ELOG(
-          "Received invalid data source instance to start: %" PRIu64, id);
+      PERFETTO_ELOG("Received invalid data source instance to start: %" PRIu64,
+                    id);
     }
     return;
   }
 
   DataSource& data_source = it->second;
   if (data_source.started) {
-    PERFETTO_DFATAL_OR_ELOG(
-        "Trying to start already started data-source: %" PRIu64, id);
+    PERFETTO_ELOG("Trying to start already started data-source: %" PRIu64, id);
     return;
   }
   const HeapprofdConfig& heapprofd_config = data_source.config;
@@ -568,8 +566,7 @@
   if (it == data_sources_.end()) {
     endpoint_->NotifyDataSourceStopped(id);
     if (mode_ == HeapprofdMode::kCentral)
-      PERFETTO_DFATAL_OR_ELOG(
-          "Trying to stop non existing data source: %" PRIu64, id);
+      PERFETTO_ELOG("Trying to stop non existing data source: %" PRIu64, id);
     return;
   }
 
@@ -773,8 +770,7 @@
   for (size_t i = 0; i < num_ids; ++i) {
     auto it = data_sources_.find(ids[i]);
     if (it == data_sources_.end()) {
-      PERFETTO_DFATAL_OR_ELOG("Trying to flush unknown data-source %" PRIu64,
-                              ids[i]);
+      PERFETTO_ELOG("Trying to flush unknown data-source %" PRIu64, ids[i]);
       flush_in_progress--;
       continue;
     }
@@ -801,8 +797,7 @@
 void HeapprofdProducer::FinishDataSourceFlush(FlushRequestID flush_id) {
   auto it = flushes_in_progress_.find(flush_id);
   if (it == flushes_in_progress_.end()) {
-    PERFETTO_DFATAL_OR_ELOG("FinishDataSourceFlush id invalid: %" PRIu64,
-                            flush_id);
+    PERFETTO_ELOG("FinishDataSourceFlush id invalid: %" PRIu64, flush_id);
     return;
   }
   size_t& flush_in_progress = it->second;
@@ -815,7 +810,7 @@
 void HeapprofdProducer::SocketDelegate::OnDisconnect(base::UnixSocket* self) {
   auto it = producer_->pending_processes_.find(self->peer_pid_linux());
   if (it == producer_->pending_processes_.end()) {
-    PERFETTO_DFATAL_OR_ELOG("Unexpected disconnect.");
+    PERFETTO_ELOG("Unexpected disconnect.");
     return;
   }
 
@@ -838,7 +833,7 @@
     base::UnixSocket* self) {
   auto it = producer_->pending_processes_.find(self->peer_pid_linux());
   if (it == producer_->pending_processes_.end()) {
-    PERFETTO_DFATAL_OR_ELOG("Unexpected data.");
+    PERFETTO_ELOG("Unexpected data.");
     return;
   }
 
@@ -910,8 +905,7 @@
         .PostHandoffSocket(std::move(handoff_data));
     producer_->pending_processes_.erase(it);
   } else if (fds[kHandshakeMaps] || fds[kHandshakeMem]) {
-    PERFETTO_DFATAL_OR_ELOG("%d: Received partial FDs.",
-                            self->peer_pid_linux());
+    PERFETTO_ELOG("%d: Received partial FDs.", self->peer_pid_linux());
     producer_->pending_processes_.erase(it);
   } else {
     PERFETTO_ELOG("%d: Received no FDs.", self->peer_pid_linux());
@@ -976,7 +970,7 @@
 
   pid_t peer_pid = new_connection->peer_pid_linux();
   if (peer_pid != process.pid) {
-    PERFETTO_DFATAL_OR_ELOG("Invalid PID connected.");
+    PERFETTO_ELOG("Invalid PID connected.");
     return;
   }
 
diff --git a/src/profiling/perf/BUILD.gn b/src/profiling/perf/BUILD.gn
index d77cb84..6465bff 100644
--- a/src/profiling/perf/BUILD.gn
+++ b/src/profiling/perf/BUILD.gn
@@ -32,6 +32,7 @@
     ":producer",
     "../../../gn:default_deps",
     "../../../src/base",
+    "../../../src/base:version",
     "../../../src/tracing/ipc/producer",
   ]
   sources = [
@@ -101,6 +102,8 @@
     "../common:unwind_support",
   ]
   sources = [
+    "frame_pointer_unwinder.cc",
+    "frame_pointer_unwinder.h",
     "unwind_queue.h",
     "unwinding.cc",
     "unwinding.h",
@@ -147,6 +150,7 @@
   ]
   sources = [
     "event_config_unittest.cc",
+    "frame_pointer_unwinder_unittest.cc",
     "perf_producer_unittest.cc",
     "unwind_queue_unittest.cc",
   ]
diff --git a/src/profiling/perf/common_types.h b/src/profiling/perf/common_types.h
index b201995..32fa298 100644
--- a/src/profiling/perf/common_types.h
+++ b/src/profiling/perf/common_types.h
@@ -39,10 +39,11 @@
   pid_t tid = 0;
   uint64_t timestamp = 0;
   uint64_t timebase_count = 0;
+  std::vector<uint64_t> follower_counts;
 };
 
 // A parsed perf sample record (PERF_RECORD_SAMPLE from the kernel buffer).
-// Self-contained, used as as input to the callstack unwinding.
+// Self-contained, used as input to the callstack unwinding.
 struct ParsedSample {
   // move-only
   ParsedSample() = default;
diff --git a/src/profiling/perf/event_config.cc b/src/profiling/perf/event_config.cc
index 79df2ce..94d563c 100644
--- a/src/profiling/perf/event_config.cc
+++ b/src/profiling/perf/event_config.cc
@@ -241,6 +241,52 @@
   }
 }
 
+// Build a singular event from an event description provided by either
+// a PerfEvents::Timebase or a FollowerEvent materialized by the
+// polymorphic parameter event_desc.
+template <typename T>
+std::optional<PerfCounter> MakePerfCounter(
+    EventConfig::tracepoint_id_fn_t& tracepoint_id_lookup,
+    const std::string& name,
+    const T& event_desc) {
+  if (event_desc.has_counter()) {
+    auto maybe_counter = ToPerfCounter(name, event_desc.counter());
+    if (!maybe_counter)
+      return std::nullopt;
+    return *maybe_counter;
+  } else if (event_desc.has_tracepoint()) {
+    const auto& tracepoint_pb = event_desc.tracepoint();
+    std::optional<uint32_t> maybe_id =
+        ParseTracepointAndResolveId(tracepoint_pb, tracepoint_id_lookup);
+    if (!maybe_id)
+      return std::nullopt;
+    return PerfCounter::Tracepoint(name, tracepoint_pb.name(),
+                                   tracepoint_pb.filter(), *maybe_id);
+  } else if (event_desc.has_raw_event()) {
+    const auto& raw = event_desc.raw_event();
+    return PerfCounter::RawEvent(name, raw.type(), raw.config(), raw.config1(),
+                                 raw.config2());
+  } else {
+    return PerfCounter::BuiltinCounter(
+        name, protos::gen::PerfEvents::PerfEvents::SW_CPU_CLOCK,
+        PERF_TYPE_SOFTWARE, PERF_COUNT_SW_CPU_CLOCK);
+  }
+}
+
+bool IsSupportedUnwindMode(
+    protos::gen::PerfEventConfig::UnwindMode unwind_mode) {
+  using protos::gen::PerfEventConfig;
+  switch (static_cast<int>(unwind_mode)) {  // cast to pacify -Wswitch-enum
+    case PerfEventConfig::UNWIND_UNKNOWN:
+    case PerfEventConfig::UNWIND_SKIP:
+    case PerfEventConfig::UNWIND_DWARF:
+    case PerfEventConfig::UNWIND_FRAME_POINTER:
+      return true;
+    default:
+      return false;
+  }
+}
+
 }  // namespace
 
 // static
@@ -314,63 +360,43 @@
   PERFETTO_DCHECK(sampling_period && !sampling_frequency ||
                   !sampling_period && sampling_frequency);
 
-  // Timebase event. Default: CPU timer.
+  // Leader event. Default: CPU timer.
   PerfCounter timebase_event;
   std::string timebase_name = pb_config.timebase().name();
-  if (pb_config.timebase().has_counter()) {
-    auto maybe_counter =
-        ToPerfCounter(timebase_name, pb_config.timebase().counter());
-    if (!maybe_counter)
+
+  // Build timebase.
+  auto maybe_perf_counter = MakePerfCounter(tracepoint_id_lookup, timebase_name,
+                                            pb_config.timebase());
+  if (!maybe_perf_counter) {
+    return std::nullopt;
+  }
+  timebase_event = std::move(*maybe_perf_counter);
+
+  // Build the followers.
+  std::vector<PerfCounter> followers;
+  for (const auto& event : pb_config.followers()) {
+    const auto& name = event.name();
+    auto maybe_follower_counter =
+        MakePerfCounter(tracepoint_id_lookup, name, event);
+    if (!maybe_follower_counter) {
       return std::nullopt;
-    timebase_event = *maybe_counter;
-
-  } else if (pb_config.timebase().has_tracepoint()) {
-    const auto& tracepoint_pb = pb_config.timebase().tracepoint();
-    std::optional<uint32_t> maybe_id =
-        ParseTracepointAndResolveId(tracepoint_pb, tracepoint_id_lookup);
-    if (!maybe_id)
-      return std::nullopt;
-    timebase_event = PerfCounter::Tracepoint(
-        timebase_name, tracepoint_pb.name(), tracepoint_pb.filter(), *maybe_id);
-
-  } else if (pb_config.timebase().has_raw_event()) {
-    const auto& raw = pb_config.timebase().raw_event();
-    timebase_event = PerfCounter::RawEvent(
-        timebase_name, raw.type(), raw.config(), raw.config1(), raw.config2());
-
-  } else {
-    timebase_event = PerfCounter::BuiltinCounter(
-        timebase_name, protos::gen::PerfEvents::PerfEvents::SW_CPU_CLOCK,
-        PERF_TYPE_SOFTWARE, PERF_COUNT_SW_CPU_CLOCK);
+    }
+    followers.push_back(std::move(*maybe_follower_counter));
   }
 
   // Callstack sampling.
-  bool user_frames = false;
   bool kernel_frames = false;
+  // Disable user_frames by default.
+  auto unwind_mode = protos::gen::PerfEventConfig::UNWIND_SKIP;
+
   TargetFilter target_filter;
   bool legacy_config = pb_config.all_cpus();  // all_cpus was mandatory before
   if (pb_config.has_callstack_sampling() || legacy_config) {
-    user_frames = true;
-
     // Userspace callstacks.
-    using protos::gen::PerfEventConfig;
-    switch (static_cast<int>(pb_config.callstack_sampling().user_frames())) {
-      case PerfEventConfig::UNWIND_UNKNOWN:
-        // default to true, both for backwards compatibility and because it's
-        // almost always what the user wants.
-        user_frames = true;
-        break;
-      case PerfEventConfig::UNWIND_SKIP:
-        user_frames = false;
-        break;
-      case PerfEventConfig::UNWIND_DWARF:
-        user_frames = true;
-        break;
-      default:
-        // enum value from the future that we don't yet know, refuse the config
-        // TODO(rsavitski): double-check that both pbzero and ::gen propagate
-        // unknown enum values.
-        return std::nullopt;
+    unwind_mode = pb_config.callstack_sampling().user_frames();
+    if (!IsSupportedUnwindMode(unwind_mode)) {
+      // enum value from the future that we don't yet know, refuse the config
+      return std::nullopt;
     }
 
     // Process scoping. Sharding parameter is supplied from outside as it is
@@ -456,7 +482,7 @@
   pe.clockid = ToClockId(pb_config.timebase().timestamp_clock());
   pe.use_clockid = true;
 
-  if (user_frames) {
+  if (IsUserFramesEnabled(unwind_mode)) {
     pe.sample_type |= PERF_SAMPLE_STACK_USER | PERF_SAMPLE_REGS_USER;
     // PERF_SAMPLE_STACK_USER:
     // Needs to be < ((u16)(~0u)), and have bottom 8 bits clear.
@@ -473,19 +499,65 @@
     pe.exclude_callchain_user = true;
   }
 
+  // Build the events associated with the timebase event (pe).
+  // The timebase event drives the capture with its frequency or period
+  // parameter. When linux captures the timebase event it also reads and report
+  // the values of associated events.
+  std::vector<perf_event_attr> pe_followers;
+  if (!followers.empty()) {
+    pe.read_format = PERF_FORMAT_GROUP;
+    pe_followers.reserve(followers.size());
+  }
+
+  for (const auto& e : followers) {
+    perf_event_attr pe_follower = {};
+    pe_follower.size = sizeof(perf_event_attr);
+    pe_follower.disabled = 0;  // activated when the timebase is activated
+    pe_follower.type = e.attr_type;
+    pe_follower.config = e.attr_config;
+    pe_follower.config1 = e.attr_config1;
+    pe_follower.config2 = e.attr_config2;
+    pe_follower.sample_type =
+        PERF_SAMPLE_TID | PERF_SAMPLE_TIME | PERF_SAMPLE_READ;
+    pe_follower.freq = 0;
+    pe_follower.sample_period = 0;
+    pe_follower.clockid = ToClockId(pb_config.timebase().timestamp_clock());
+    pe_follower.use_clockid = true;
+
+    pe_followers.push_back(pe_follower);
+  }
+
   return EventConfig(
-      raw_ds_config, pe, timebase_event, user_frames, kernel_frames,
-      std::move(target_filter), ring_buffer_pages.value(), read_tick_period_ms,
-      samples_per_tick_limit, remote_descriptor_timeout_ms,
-      pb_config.unwind_state_clear_period_ms(), max_enqueued_footprint_bytes,
-      pb_config.target_installed_by());
+      raw_ds_config, pe, std::move(pe_followers), timebase_event, followers,
+      kernel_frames, unwind_mode, std::move(target_filter),
+      ring_buffer_pages.value(), read_tick_period_ms, samples_per_tick_limit,
+      remote_descriptor_timeout_ms, pb_config.unwind_state_clear_period_ms(),
+      max_enqueued_footprint_bytes, pb_config.target_installed_by());
+}
+
+// static
+bool EventConfig::IsUserFramesEnabled(
+    const protos::gen::PerfEventConfig::UnwindMode unwind_mode) {
+  using protos::gen::PerfEventConfig;
+  switch (unwind_mode) {
+    case PerfEventConfig::UNWIND_UNKNOWN:
+    // default to true, both for backwards compatibility and because it's
+    // almost always what the user wants.
+    case PerfEventConfig::UNWIND_DWARF:
+    case PerfEventConfig::UNWIND_FRAME_POINTER:
+      return true;
+    case PerfEventConfig::UNWIND_SKIP:
+      return false;
+  }
 }
 
 EventConfig::EventConfig(const DataSourceConfig& raw_ds_config,
-                         const perf_event_attr& pe,
+                         const perf_event_attr& pe_timebase,
+                         std::vector<perf_event_attr> pe_followers,
                          const PerfCounter& timebase_event,
-                         bool user_frames,
+                         std::vector<PerfCounter> follower_events,
                          bool kernel_frames,
+                         protos::gen::PerfEventConfig::UnwindMode unwind_mode,
                          TargetFilter target_filter,
                          uint32_t ring_buffer_pages,
                          uint32_t read_tick_period_ms,
@@ -494,10 +566,12 @@
                          uint32_t unwind_state_clear_period_ms,
                          uint64_t max_enqueued_footprint_bytes,
                          std::vector<std::string> target_installed_by)
-    : perf_event_attr_(pe),
+    : perf_event_attr_(pe_timebase),
+      perf_event_followers_(std::move(pe_followers)),
       timebase_event_(timebase_event),
-      user_frames_(user_frames),
+      follower_events_(std::move(follower_events)),
       kernel_frames_(kernel_frames),
+      unwind_mode_(unwind_mode),
       target_filter_(std::move(target_filter)),
       ring_buffer_pages_(ring_buffer_pages),
       read_tick_period_ms_(read_tick_period_ms),
diff --git a/src/profiling/perf/event_config.h b/src/profiling/perf/event_config.h
index dd1a7e3..a87429d 100644
--- a/src/profiling/perf/event_config.h
+++ b/src/profiling/perf/event_config.h
@@ -31,6 +31,7 @@
 #include "perfetto/tracing/core/data_source_config.h"
 
 #include "protos/perfetto/common/perf_events.gen.h"
+#include "protos/perfetto/config/profiling/perf_event_config.gen.h"
 
 namespace perfetto {
 namespace protos {
@@ -136,25 +137,41 @@
   uint64_t max_enqueued_footprint_bytes() const {
     return max_enqueued_footprint_bytes_;
   }
-  bool sample_callstacks() const { return user_frames_ || kernel_frames_; }
-  bool user_frames() const { return user_frames_; }
+  bool sample_callstacks() const { return user_frames() || kernel_frames_; }
+  bool user_frames() const { return IsUserFramesEnabled(unwind_mode_); }
   bool kernel_frames() const { return kernel_frames_; }
+  protos::gen::PerfEventConfig::UnwindMode unwind_mode() const {
+    return unwind_mode_;
+  }
   const TargetFilter& filter() const { return target_filter_; }
   perf_event_attr* perf_attr() const {
     return const_cast<perf_event_attr*>(&perf_event_attr_);
   }
+  const std::vector<perf_event_attr>& perf_attr_followers() const {
+    return perf_event_followers_;
+  }
   const PerfCounter& timebase_event() const { return timebase_event_; }
+
+  const std::vector<PerfCounter>& follower_events() const {
+    return follower_events_;
+  }
+
   const std::vector<std::string>& target_installed_by() const {
     return target_installed_by_;
   }
   const DataSourceConfig& raw_ds_config() const { return raw_ds_config_; }
 
  private:
+  static bool IsUserFramesEnabled(
+      const protos::gen::PerfEventConfig::UnwindMode unwind_mode);
+
   EventConfig(const DataSourceConfig& raw_ds_config,
-              const perf_event_attr& pe,
+              const perf_event_attr& pe_timebase,
+              std::vector<perf_event_attr> pe_followers,
               const PerfCounter& timebase_event,
-              bool user_frames,
+              std::vector<PerfCounter> follower_events,
               bool kernel_frames,
+              protos::gen::PerfEventConfig::UnwindMode unwind_mode,
               TargetFilter target_filter,
               uint32_t ring_buffer_pages,
               uint32_t read_tick_period_ms,
@@ -164,20 +181,25 @@
               uint64_t max_enqueued_footprint_bytes,
               std::vector<std::string> target_installed_by);
 
-  // Parameter struct for the leader (timebase) perf_event_open syscall.
+  // Parameter struct for the timebase perf_event_open syscall.
   perf_event_attr perf_event_attr_ = {};
 
-  // Leader event, which is already described by |perf_event_attr_|. But this
+  std::vector<perf_event_attr> perf_event_followers_ = {};
+
+  // Timebase event, which is already described by |perf_event_attr_|. But this
   // additionally carries a tracepoint filter if that needs to be set via an
   // ioctl after creating the event.
   const PerfCounter timebase_event_;
 
-  // If true, include userspace frames in sampled callstacks.
-  const bool user_frames_;
+  // Timebase event, which are already described by |perf_event_followers_|.
+  std::vector<PerfCounter> follower_events_;
 
   // If true, include kernel frames in sampled callstacks.
   const bool kernel_frames_;
 
+  // Userspace unwinding mode.
+  const protos::gen::PerfEventConfig::UnwindMode unwind_mode_;
+
   // Parsed allow/deny-list for filtering samples.
   const TargetFilter target_filter_;
 
diff --git a/src/profiling/perf/event_config_unittest.cc b/src/profiling/perf/event_config_unittest.cc
index 35ec628..9494440 100644
--- a/src/profiling/perf/event_config_unittest.cc
+++ b/src/profiling/perf/event_config_unittest.cc
@@ -368,6 +368,66 @@
   }
 }
 
+TEST(EventConfigTest, GroupMultipleType) {
+  protos::gen::PerfEventConfig cfg;
+  {
+    // timebase:
+    auto* mutable_timebase = cfg.mutable_timebase();
+    mutable_timebase->set_period(500);
+    mutable_timebase->set_counter(protos::gen::PerfEvents::HW_CPU_CYCLES);
+    mutable_timebase->set_name("timebase");
+
+    // raw follower:
+    auto* raw_follower = cfg.add_followers();
+    raw_follower->set_name("raw");
+    auto* raw_event = raw_follower->mutable_raw_event();
+    raw_event->set_type(8);
+    raw_event->set_config(8);
+
+    // HW counter follower:
+    auto* counter_follower = cfg.add_followers();
+    counter_follower->set_name("counter");
+    counter_follower->set_counter(
+        protos::gen::PerfEvents::HW_BRANCH_INSTRUCTIONS);
+
+    // tracepoint follower:
+    auto* tracepoint_follower = cfg.add_followers();
+    tracepoint_follower->set_name("tracepoint");
+    auto* tracepoint_event = tracepoint_follower->mutable_tracepoint();
+    tracepoint_event->set_name("sched:sched_switch");
+  }
+
+  auto id_lookup = [](const std::string& group, const std::string& name) {
+    return (group == "sched" && name == "sched_switch") ? 42 : 0;
+  };
+  std::optional<EventConfig> event_config = CreateEventConfig(cfg, id_lookup);
+
+  ASSERT_TRUE(event_config.has_value());
+  EXPECT_EQ(event_config->perf_attr()->type, PERF_TYPE_HARDWARE);
+  EXPECT_EQ(event_config->perf_attr()->config, PERF_COUNT_HW_CPU_CYCLES);
+  EXPECT_EQ(event_config->perf_attr()->sample_type &
+                (PERF_SAMPLE_STACK_USER | PERF_SAMPLE_REGS_USER),
+            0u);
+  EXPECT_EQ(event_config->perf_attr()->read_format, PERF_FORMAT_GROUP);
+
+  ASSERT_EQ(event_config->perf_attr_followers().size(), 3u);
+
+  const auto& raw_event = event_config->perf_attr_followers().at(0);
+  EXPECT_EQ(raw_event.type, 8u);
+  EXPECT_EQ(raw_event.config, 8u);
+  EXPECT_TRUE(raw_event.sample_type & PERF_SAMPLE_READ);
+
+  const auto& hw_counter = event_config->perf_attr_followers().at(1);
+  EXPECT_EQ(hw_counter.type, PERF_TYPE_HARDWARE);
+  EXPECT_EQ(hw_counter.config, PERF_COUNT_HW_BRANCH_INSTRUCTIONS);
+  EXPECT_TRUE(hw_counter.sample_type & PERF_SAMPLE_READ);
+
+  const auto& tracepoint = event_config->perf_attr_followers().at(2);
+  EXPECT_EQ(tracepoint.type, PERF_TYPE_TRACEPOINT);
+  EXPECT_EQ(tracepoint.config, 42u);
+  EXPECT_TRUE(tracepoint.sample_type & PERF_SAMPLE_READ);
+}
+
 }  // namespace
 }  // namespace profiling
 }  // namespace perfetto
diff --git a/src/profiling/perf/event_reader.cc b/src/profiling/perf/event_reader.cc
index cb606f6..02c3fe7 100644
--- a/src/profiling/perf/event_reader.cc
+++ b/src/profiling/perf/event_reader.cc
@@ -208,10 +208,12 @@
 EventReader::EventReader(uint32_t cpu,
                          perf_event_attr event_attr,
                          base::ScopedFile perf_fd,
+                         std::vector<base::ScopedFile> followers_fds,
                          PerfRingBuffer ring_buffer)
     : cpu_(cpu),
       event_attr_(event_attr),
       perf_fd_(std::move(perf_fd)),
+      follower_fds_(std::move(followers_fds)),
       ring_buffer_(std::move(ring_buffer)) {}
 
 EventReader& EventReader::operator=(EventReader&& other) noexcept {
@@ -226,21 +228,46 @@
 std::optional<EventReader> EventReader::ConfigureEvents(
     uint32_t cpu,
     const EventConfig& event_cfg) {
-  auto leader_fd = PerfEventOpen(cpu, event_cfg.perf_attr());
-  if (!leader_fd) {
+  auto timebase_fd = PerfEventOpen(cpu, event_cfg.perf_attr());
+  if (!timebase_fd) {
     PERFETTO_PLOG("Failed perf_event_open");
     return std::nullopt;
   }
-  if (!MaybeApplyTracepointFilter(leader_fd.get(), event_cfg.timebase_event()))
+
+  // Open followers.
+  std::vector<base::ScopedFile> follower_fds;
+  for (auto follower_attr : event_cfg.perf_attr_followers()) {
+    auto follower_fd = PerfEventOpen(cpu, &follower_attr, timebase_fd.get());
+    if (!follower_fd) {
+      PERFETTO_PLOG("Failed perf_event_open (follower)");
+      return std::nullopt;
+    }
+    follower_fds.push_back(std::move(follower_fd));
+  }
+
+  // Apply the tracepoint to the timebase.
+  if (!MaybeApplyTracepointFilter(timebase_fd.get(),
+                                  event_cfg.timebase_event()))
     return std::nullopt;
 
-  auto ring_buffer =
-      PerfRingBuffer::Allocate(leader_fd.get(), event_cfg.ring_buffer_pages());
+  // Apply the tracepoint to the followers.
+  if (follower_fds.size() != event_cfg.follower_events().size()) {
+    return std::nullopt;
+  }
+
+  for (size_t i = 0; i < follower_fds.size(); ++i) {
+    if (!MaybeApplyTracepointFilter(follower_fds[i].get(),
+                                    event_cfg.follower_events()[i]))
+      return std::nullopt;
+  }
+
+  auto ring_buffer = PerfRingBuffer::Allocate(timebase_fd.get(),
+                                              event_cfg.ring_buffer_pages());
   if (!ring_buffer.has_value()) {
     return std::nullopt;
   }
-  return EventReader(cpu, *event_cfg.perf_attr(), std::move(leader_fd),
-                     std::move(ring_buffer.value()));
+  return EventReader(cpu, *event_cfg.perf_attr(), std::move(timebase_fd),
+                     std::move(follower_fds), std::move(ring_buffer.value()));
 }
 
 std::optional<ParsedSample> EventReader::ReadUntilSample(
@@ -325,7 +352,23 @@
   }
 
   if (event_attr_.sample_type & PERF_SAMPLE_READ) {
-    parse_pos = ReadValue(&sample.common.timebase_count, parse_pos);
+    if (event_attr_.read_format & PERF_FORMAT_GROUP) {
+      // When PERF_FORMAT_GROUP is specified, the record starts with the number
+      // of events it contains followed by the events. The event list always
+      // starts with the value of the timebase.
+      // In a ParsedSample, the value of the timebase goes into timebase_count
+      // and the value of the followers events goes into follower_counts.
+      uint64_t nr = 0;
+      parse_pos = ReadValue(&nr, parse_pos);
+      PERFETTO_CHECK(nr != 0);
+      parse_pos = ReadValue(&sample.common.timebase_count, parse_pos);
+      sample.common.follower_counts.resize(nr - 1);
+      for (size_t i = 0; i < nr - 1; ++i) {
+        parse_pos = ReadValue(&sample.common.follower_counts[i], parse_pos);
+      }
+    } else {
+      parse_pos = ReadValue(&sample.common.timebase_count, parse_pos);
+    }
   }
 
   if (event_attr_.sample_type & PERF_SAMPLE_CALLCHAIN) {
diff --git a/src/profiling/perf/event_reader.h b/src/profiling/perf/event_reader.h
index 11a63bb..5bc11c8 100644
--- a/src/profiling/perf/event_reader.h
+++ b/src/profiling/perf/event_reader.h
@@ -99,6 +99,7 @@
   EventReader(uint32_t cpu,
               perf_event_attr event_attr,
               base::ScopedFile perf_fd,
+              std::vector<base::ScopedFile> followers_fds,
               PerfRingBuffer ring_buffer);
 
   ParsedSample ParseSampleRecord(uint32_t cpu, const char* record_start);
@@ -107,6 +108,7 @@
   const uint32_t cpu_;
   const perf_event_attr event_attr_;
   base::ScopedFile perf_fd_;
+  std::vector<base::ScopedFile> follower_fds_;
   PerfRingBuffer ring_buffer_;
 };
 
diff --git a/src/profiling/perf/frame_pointer_unwinder.cc b/src/profiling/perf/frame_pointer_unwinder.cc
new file mode 100644
index 0000000..6841dc6
--- /dev/null
+++ b/src/profiling/perf/frame_pointer_unwinder.cc
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "src/profiling/perf/frame_pointer_unwinder.h"
+
+#include <cinttypes>
+
+#include "perfetto/base/logging.h"
+
+namespace perfetto {
+namespace profiling {
+
+void FramePointerUnwinder::Unwind() {
+  if (!IsArchSupported()) {
+    PERFETTO_ELOG("Unsupported architecture: %d", arch_);
+    last_error_.code = unwindstack::ErrorCode::ERROR_UNSUPPORTED;
+    return;
+  }
+
+  if (maps_ == nullptr || maps_->Total() == 0) {
+    PERFETTO_ELOG("No maps provided");
+    last_error_.code = unwindstack::ErrorCode::ERROR_INVALID_MAP;
+    return;
+  }
+
+  PERFETTO_DCHECK(stack_size_ > 0u);
+
+  frames_.reserve(max_frames_);
+  ClearErrors();
+  TryUnwind();
+}
+
+void FramePointerUnwinder::TryUnwind() {
+  uint64_t fp = 0;
+  switch (arch_) {
+    case unwindstack::ARCH_ARM64:
+      fp = reinterpret_cast<uint64_t*>(
+          regs_->RawData())[unwindstack::Arm64Reg::ARM64_REG_R29];
+      break;
+    case unwindstack::ARCH_X86_64:
+      fp = reinterpret_cast<uint64_t*>(
+          regs_->RawData())[unwindstack::X86_64Reg::X86_64_REG_RBP];
+      break;
+    case unwindstack::ARCH_RISCV64:
+      fp = reinterpret_cast<uint64_t*>(
+          regs_->RawData())[unwindstack::Riscv64Reg::RISCV64_REG_S0];
+      break;
+    case unwindstack::ARCH_UNKNOWN:
+    case unwindstack::ARCH_ARM:
+    case unwindstack::ARCH_X86:
+        // not supported
+        ;
+  }
+  uint64_t sp = regs_->sp();
+  uint64_t pc = regs_->pc();
+  for (size_t i = 0; i < max_frames_; i++) {
+    if (!IsFrameValid(fp, sp))
+      return;
+
+    // retrive the map info and elf info
+    std::shared_ptr<unwindstack::MapInfo> map_info = maps_->Find(pc);
+    if (map_info == nullptr) {
+      last_error_.code = unwindstack::ErrorCode::ERROR_INVALID_MAP;
+      return;
+    }
+
+    unwindstack::FrameData frame;
+    frame.num = i;
+    frame.rel_pc = pc;
+    frame.pc = pc;
+    frame.map_info = map_info;
+    unwindstack::Elf* elf = map_info->GetElf(process_memory_, arch_);
+    if (elf != nullptr) {
+      uint64_t relative_pc = elf->GetRelPc(pc, map_info.get());
+      uint64_t pc_adjustment = GetPcAdjustment(relative_pc, elf, arch_);
+      frame.rel_pc = relative_pc - pc_adjustment;
+      frame.pc = pc - pc_adjustment;
+      if (!resolve_names_ ||
+          !elf->GetFunctionName(frame.rel_pc, &frame.function_name,
+                                &frame.function_offset)) {
+        frame.function_name = "";
+        frame.function_offset = 0;
+      }
+    }
+    frames_.push_back(frame);
+    // move to the next frame
+    fp = DecodeFrame(fp, &pc, &sp);
+  }
+}
+
+uint64_t FramePointerUnwinder::DecodeFrame(uint64_t fp,
+                                           uint64_t* next_pc,
+                                           uint64_t* next_sp) {
+  uint64_t next_fp;
+  if (!process_memory_->ReadFully(static_cast<uint64_t>(fp), &next_fp,
+                                  sizeof(next_fp)))
+    return 0;
+
+  uint64_t pc;
+  if (!process_memory_->ReadFully(static_cast<uint64_t>(fp + sizeof(uint64_t)),
+                                  &pc, sizeof(pc)))
+    return 0;
+
+  // Ensure there's not a stack overflow.
+  if (__builtin_add_overflow(fp, sizeof(uint64_t) * 2, next_sp))
+    return 0;
+
+  *next_pc = static_cast<uint64_t>(pc);
+  return next_fp;
+}
+
+bool FramePointerUnwinder::IsFrameValid(uint64_t fp, uint64_t sp) {
+  uint64_t align_mask = 0;
+  switch (arch_) {
+    case unwindstack::ARCH_ARM64:
+      align_mask = 0x1;
+      break;
+    case unwindstack::ARCH_X86_64:
+      align_mask = 0xf;
+      break;
+    case unwindstack::ARCH_RISCV64:
+      align_mask = 0x7;
+      break;
+    case unwindstack::ARCH_UNKNOWN:
+    case unwindstack::ARCH_ARM:
+    case unwindstack::ARCH_X86:
+        // not supported
+        ;
+  }
+
+  if (fp == 0 || fp <= sp)
+    return false;
+
+  // Ensure there's space on the stack to read two values: the caller's
+  // frame pointer and the return address.
+  uint64_t result;
+  if (__builtin_add_overflow(fp, sizeof(uint64_t) * 2, &result))
+    return false;
+
+  return result <= stack_end_ && (fp & align_mask) == 0;
+}
+
+}  // namespace profiling
+}  // namespace perfetto
diff --git a/src/profiling/perf/frame_pointer_unwinder.h b/src/profiling/perf/frame_pointer_unwinder.h
new file mode 100644
index 0000000..14534d9
--- /dev/null
+++ b/src/profiling/perf/frame_pointer_unwinder.h
@@ -0,0 +1,103 @@
+/*
+ * 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_PROFILING_PERF_FRAME_POINTER_UNWINDER_H_
+#define SRC_PROFILING_PERF_FRAME_POINTER_UNWINDER_H_
+
+#include <stdint.h>
+#include <memory>
+#include <vector>
+
+#include <unwindstack/Error.h>
+#include <unwindstack/MachineArm64.h>
+#include <unwindstack/MachineRiscv64.h>
+#include <unwindstack/MachineX86_64.h>
+#include <unwindstack/Unwinder.h>
+
+namespace perfetto {
+namespace profiling {
+
+class FramePointerUnwinder {
+ public:
+  FramePointerUnwinder(size_t max_frames,
+                       unwindstack::Maps* maps,
+                       unwindstack::Regs* regs,
+                       std::shared_ptr<unwindstack::Memory> process_memory,
+                       size_t stack_size)
+      : max_frames_(max_frames),
+        maps_(maps),
+        regs_(regs),
+        process_memory_(process_memory),
+        stack_size_(stack_size),
+        arch_(regs->Arch()) {
+    stack_end_ = regs->sp() + stack_size;
+  }
+
+  FramePointerUnwinder(const FramePointerUnwinder&) = delete;
+  FramePointerUnwinder& operator=(const FramePointerUnwinder&) = delete;
+
+  void Unwind();
+
+  // Disabling the resolving of names results in the function name being
+  // set to an empty string and the function offset being set to zero.
+  void SetResolveNames(bool resolve) { resolve_names_ = resolve; }
+
+  unwindstack::ErrorCode LastErrorCode() const { return last_error_.code; }
+  uint64_t warnings() const { return warnings_; }
+
+  std::vector<unwindstack::FrameData> ConsumeFrames() {
+    std::vector<unwindstack::FrameData> frames = std::move(frames_);
+    frames_.clear();
+    return frames;
+  }
+
+  bool IsArchSupported() const {
+    return arch_ == unwindstack::ARCH_ARM64 ||
+           arch_ == unwindstack::ARCH_X86_64;
+  }
+
+  void ClearErrors() {
+    warnings_ = unwindstack::WARNING_NONE;
+    last_error_.code = unwindstack::ERROR_NONE;
+    last_error_.address = 0;
+  }
+
+ protected:
+  const size_t max_frames_;
+  unwindstack::Maps* maps_;
+  unwindstack::Regs* regs_;
+  std::vector<unwindstack::FrameData> frames_;
+  std::shared_ptr<unwindstack::Memory> process_memory_;
+  const size_t stack_size_;
+  unwindstack::ArchEnum arch_ = unwindstack::ARCH_UNKNOWN;
+  bool resolve_names_ = false;
+  size_t stack_end_;
+
+  unwindstack::ErrorData last_error_;
+  uint64_t warnings_ = 0;
+
+ private:
+  void TryUnwind();
+  // Given a frame pointer, returns the frame pointer of the calling stack
+  // frame, places the return address of the calling stack frame into
+  // `ret_addr` and stack pointer into `sp`.
+  uint64_t DecodeFrame(uint64_t fp, uint64_t* ret_addr, uint64_t* sp);
+  bool IsFrameValid(uint64_t fp, uint64_t sp);
+};
+
+}  // namespace profiling
+}  // namespace perfetto
+
+#endif  // SRC_PROFILING_PERF_FRAME_POINTER_UNWINDER_H_
diff --git a/src/profiling/perf/frame_pointer_unwinder_unittest.cc b/src/profiling/perf/frame_pointer_unwinder_unittest.cc
new file mode 100644
index 0000000..c494fd7
--- /dev/null
+++ b/src/profiling/perf/frame_pointer_unwinder_unittest.cc
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/profiling/perf/frame_pointer_unwinder.h"
+
+#include <sys/mman.h>
+#include <unwindstack/Unwinder.h>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/scoped_file.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace profiling {
+namespace {
+
+class RegsFake : public unwindstack::Regs {
+ public:
+  RegsFake(uint16_t total_regs)
+      : unwindstack::Regs(
+            total_regs,
+            unwindstack::Regs::Location(unwindstack::Regs::LOCATION_UNKNOWN,
+                                        0)) {
+    fake_data_ = std::make_unique<uint64_t[]>(total_regs);
+  }
+  ~RegsFake() override = default;
+
+  unwindstack::ArchEnum Arch() override { return fake_arch_; }
+  void* RawData() override { return fake_data_.get(); }
+  uint64_t pc() override { return fake_pc_; }
+  uint64_t sp() override { return fake_sp_; }
+  void set_pc(uint64_t pc) override { fake_pc_ = pc; }
+  void set_sp(uint64_t sp) override { fake_sp_ = sp; }
+
+  void set_fp(uint64_t fp) {
+    switch (fake_arch_) {
+      case unwindstack::ARCH_ARM64:
+        fake_data_[unwindstack::Arm64Reg::ARM64_REG_R29] = fp;
+        break;
+      case unwindstack::ARCH_X86_64:
+        fake_data_[unwindstack::X86_64Reg::X86_64_REG_RBP] = fp;
+        break;
+      case unwindstack::ARCH_RISCV64:
+        fake_data_[unwindstack::Riscv64Reg::RISCV64_REG_S0] = fp;
+        break;
+      case unwindstack::ARCH_UNKNOWN:
+      case unwindstack::ARCH_ARM:
+      case unwindstack::ARCH_X86:
+          // not supported
+          ;
+    }
+  }
+
+  bool SetPcFromReturnAddress(unwindstack::Memory*) override { return false; }
+
+  void IterateRegisters(std::function<void(const char*, uint64_t)>) override {}
+
+  bool StepIfSignalHandler(uint64_t,
+                           unwindstack::Elf*,
+                           unwindstack::Memory*) override {
+    return false;
+  }
+
+  void FakeSetArch(unwindstack::ArchEnum arch) { fake_arch_ = arch; }
+
+  Regs* Clone() override { return nullptr; }
+
+ private:
+  unwindstack::ArchEnum fake_arch_ = unwindstack::ARCH_UNKNOWN;
+  uint64_t fake_pc_ = 0;
+  uint64_t fake_sp_ = 0;
+  std::unique_ptr<uint64_t[]> fake_data_;
+};
+
+class MemoryFake : public unwindstack::Memory {
+ public:
+  MemoryFake() = default;
+  ~MemoryFake() override = default;
+
+  size_t Read(uint64_t addr, void* memory, size_t size) override {
+    uint8_t* dst = reinterpret_cast<uint8_t*>(memory);
+    for (size_t i = 0; i < size; i++, addr++) {
+      auto value = data_.find(addr);
+      if (value == data_.end()) {
+        return i;
+      }
+      dst[i] = value->second;
+    }
+    return size;
+  }
+
+  void SetMemory(uint64_t addr, const void* memory, size_t length) {
+    const uint8_t* src = reinterpret_cast<const uint8_t*>(memory);
+    for (size_t i = 0; i < length; i++, addr++) {
+      auto value = data_.find(addr);
+      if (value != data_.end()) {
+        value->second = src[i];
+      } else {
+        data_.insert({addr, src[i]});
+      }
+    }
+  }
+
+  void SetData8(uint64_t addr, uint8_t value) {
+    SetMemory(addr, &value, sizeof(value));
+  }
+
+  void SetData16(uint64_t addr, uint16_t value) {
+    SetMemory(addr, &value, sizeof(value));
+  }
+
+  void SetData32(uint64_t addr, uint32_t value) {
+    SetMemory(addr, &value, sizeof(value));
+  }
+
+  void SetData64(uint64_t addr, uint64_t value) {
+    SetMemory(addr, &value, sizeof(value));
+  }
+
+  void SetMemory(uint64_t addr, std::vector<uint8_t> values) {
+    SetMemory(addr, values.data(), values.size());
+  }
+
+  void SetMemory(uint64_t addr, std::string string) {
+    SetMemory(addr, string.c_str(), string.size() + 1);
+  }
+
+  void Clear() override { data_.clear(); }
+
+ private:
+  std::unordered_map<uint64_t, uint8_t> data_;
+};
+
+constexpr static uint64_t kMaxFrames = 64;
+constexpr static uint64_t kStackSize = 0xFFFFFFF;
+
+class FramePointerUnwinderTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    memory_fake_ = new MemoryFake;
+    maps_.reset(new unwindstack::Maps);
+    regs_fake_ = std::make_unique<RegsFake>(64);
+    regs_fake_->FakeSetArch(unwindstack::ARCH_X86_64);
+    process_memory_.reset(memory_fake_);
+
+    unwinder_ = std::make_unique<FramePointerUnwinder>(
+        kMaxFrames, maps_.get(), regs_fake_.get(), process_memory_, kStackSize);
+  }
+
+  MemoryFake* memory_fake_;
+  std::unique_ptr<unwindstack::Maps> maps_;
+  std::unique_ptr<RegsFake> regs_fake_;
+  std::shared_ptr<unwindstack::Memory> process_memory_;
+
+  std::unique_ptr<FramePointerUnwinder> unwinder_;
+};
+
+TEST_F(FramePointerUnwinderTest, UnwindUnsupportedArch) {
+  regs_fake_->FakeSetArch(unwindstack::ARCH_UNKNOWN);
+  unwinder_.reset(new FramePointerUnwinder(
+      kMaxFrames, maps_.get(), regs_fake_.get(), process_memory_, kStackSize));
+  unwinder_->Unwind();
+  EXPECT_EQ(unwinder_->LastErrorCode(),
+            unwindstack::ErrorCode::ERROR_UNSUPPORTED);
+
+  regs_fake_->FakeSetArch(unwindstack::ARCH_X86);
+  unwinder_.reset(new FramePointerUnwinder(
+      kMaxFrames, maps_.get(), regs_fake_.get(), process_memory_, kStackSize));
+  unwinder_->Unwind();
+  EXPECT_EQ(unwinder_->LastErrorCode(),
+            unwindstack::ErrorCode::ERROR_UNSUPPORTED);
+
+  regs_fake_->FakeSetArch(unwindstack::ARCH_ARM);
+  unwinder_.reset(new FramePointerUnwinder(
+      kMaxFrames, maps_.get(), regs_fake_.get(), process_memory_, kStackSize));
+  unwinder_->Unwind();
+  EXPECT_EQ(unwinder_->LastErrorCode(),
+            unwindstack::ErrorCode::ERROR_UNSUPPORTED);
+}
+
+TEST_F(FramePointerUnwinderTest, UnwindInvalidMaps) {
+  // Set up a valid stack frame
+  regs_fake_->set_pc(0x1000);
+  regs_fake_->set_sp(0x2000);
+  memory_fake_->SetData64(0x2000, 0x3000);
+  memory_fake_->SetData64(0x2008, 0x2000);
+  unwinder_->Unwind();
+  EXPECT_EQ(unwinder_->LastErrorCode(),
+            unwindstack::ErrorCode::ERROR_INVALID_MAP);
+  EXPECT_EQ(unwinder_->ConsumeFrames().size(), 0UL);
+}
+
+TEST_F(FramePointerUnwinderTest, UnwindValidStack) {
+  regs_fake_->set_pc(0x1900);
+  regs_fake_->set_sp(0x1800);
+  regs_fake_->set_fp(0x2000);
+
+  memory_fake_->SetData64(0x2000, 0x2200);  // mock next_fp
+  memory_fake_->SetData64(0x2000 + sizeof(uint64_t),
+                          0x2100);  // mock return_address(next_pc)
+
+  memory_fake_->SetData64(0x2200, 0);
+
+  maps_->Add(0x1000, 0x12000, 0, PROT_READ | PROT_WRITE, "libmock.so");
+
+  unwinder_.reset(new FramePointerUnwinder(
+      kMaxFrames, maps_.get(), regs_fake_.get(), process_memory_, kStackSize));
+  unwinder_->Unwind();
+  EXPECT_EQ(unwinder_->LastErrorCode(), unwindstack::ErrorCode::ERROR_NONE);
+  EXPECT_EQ(unwinder_->ConsumeFrames().size(), 2UL);
+}
+
+}  // namespace
+}  // namespace profiling
+}  // namespace perfetto
diff --git a/src/profiling/perf/perf_producer.cc b/src/profiling/perf/perf_producer.cc
index 109b580..86d7b15 100644
--- a/src/profiling/perf/perf_producer.cc
+++ b/src/profiling/perf/perf_producer.cc
@@ -163,7 +163,7 @@
     timebase_pb->set_period(perf_attr->sample_period);
   }
 
-  // event:
+  // timebase event:
   const PerfCounter& timebase = event_config.timebase_event();
   switch (timebase.event_type()) {
     case PerfCounter::Type::kBuiltinCounter: {
@@ -192,6 +192,34 @@
     timebase_pb->set_name(timebase.name);
   }
 
+  // follower events:
+  for (const auto& e : event_config.follower_events()) {
+    auto* followers_pb = perf_defaults->add_followers();
+    followers_pb->set_name(e.name);
+
+    switch (e.event_type()) {
+      case PerfCounter::Type::kBuiltinCounter: {
+        followers_pb->set_counter(
+            static_cast<protos::pbzero::PerfEvents::Counter>(e.counter));
+        break;
+      }
+      case PerfCounter::Type::kTracepoint: {
+        auto* tracepoint_pb = followers_pb->set_tracepoint();
+        tracepoint_pb->set_name(e.tracepoint_name);
+        tracepoint_pb->set_filter(e.tracepoint_filter);
+        break;
+      }
+      case PerfCounter::Type::kRawEvent: {
+        auto* raw_pb = followers_pb->set_raw_event();
+        raw_pb->set_type(e.attr_type);
+        raw_pb->set_config(e.attr_config);
+        raw_pb->set_config1(e.attr_config1);
+        raw_pb->set_config2(e.attr_config2);
+        break;
+      }
+    }
+  }
+
   // Not setting timebase.timestamp_clock since the field that matters during
   // parsing is the root timestamp_clock_id set above.
 
@@ -472,8 +500,12 @@
 
   // Inform unwinder of the new data source instance, and optionally start a
   // periodic task to clear its cached state.
-  unwinding_worker_->PostStartDataSource(ds_id,
-                                         ds.event_config.kernel_frames());
+  auto unwind_mode = (ds.event_config.unwind_mode() ==
+                      protos::gen::PerfEventConfig::UNWIND_FRAME_POINTER)
+                         ? Unwinder::UnwindMode::kFramePointer
+                         : Unwinder::UnwindMode::kUnwindStack;
+  unwinding_worker_->PostStartDataSource(ds_id, ds.event_config.kernel_frames(),
+                                         unwind_mode);
   if (ds.event_config.unwind_state_clear_period_ms()) {
     unwinding_worker_->PostClearCachedStatePeriodic(
         ds_id, ds.event_config.unwind_state_clear_period_ms());
@@ -947,6 +979,11 @@
   perf_sample->set_tid(static_cast<uint32_t>(sample.common.tid));
   perf_sample->set_cpu_mode(ToCpuModeEnum(sample.common.cpu_mode));
   perf_sample->set_timebase_count(sample.common.timebase_count);
+
+  for (size_t i = 0; i < sample.common.follower_counts.size(); ++i) {
+    perf_sample->add_follower_counts(sample.common.follower_counts[i]);
+  }
+
   perf_sample->set_callstack_iid(callstack_iid);
   if (sample.unwind_error != unwindstack::ERROR_NONE) {
     perf_sample->set_unwind_error(ToProtoEnum(sample.unwind_error));
@@ -1016,6 +1053,10 @@
   perf_sample->set_cpu_mode(ToCpuModeEnum(sample.common.cpu_mode));
   perf_sample->set_timebase_count(sample.common.timebase_count);
 
+  for (size_t i = 0; i < sample.common.follower_counts.size(); ++i) {
+    perf_sample->add_follower_counts(sample.common.follower_counts[i]);
+  }
+
   using PerfSample = protos::pbzero::PerfSample;
   switch (reason) {
     case SampleSkipReason::kReadStage:
diff --git a/src/profiling/perf/traced_perf.cc b/src/profiling/perf/traced_perf.cc
index 7664053..f08d866 100644
--- a/src/profiling/perf/traced_perf.cc
+++ b/src/profiling/perf/traced_perf.cc
@@ -15,8 +15,14 @@
  */
 
 #include "src/profiling/perf/traced_perf.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "perfetto/ext/base/getopt.h"
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/unix_task_runner.h"
+#include "perfetto/ext/base/version.h"
 #include "perfetto/tracing/default_socket.h"
 #include "src/profiling/perf/perf_producer.h"
 #include "src/profiling/perf/proc_descriptors.h"
@@ -41,7 +47,40 @@
 }  // namespace
 
 // TODO(rsavitski): watchdog.
-int TracedPerfMain(int, char**) {
+int TracedPerfMain(int argc, char** argv) {
+  enum LongOption {
+    OPT_BACKGROUND = 1000,
+    OPT_VERSION,
+  };
+
+  bool background = false;
+
+  static const option long_options[] = {
+      {"background", no_argument, nullptr, OPT_BACKGROUND},
+      {"version", no_argument, nullptr, OPT_VERSION},
+      {nullptr, 0, nullptr, 0}};
+
+  for (;;) {
+    int option = getopt_long(argc, argv, "", long_options, nullptr);
+    if (option == -1)
+      break;
+    switch (option) {
+      case OPT_BACKGROUND:
+        background = true;
+        break;
+      case OPT_VERSION:
+        printf("%s\n", base::GetVersionString());
+        return 0;
+      default:
+        fprintf(stderr, "Usage: %s [--background] [--version]\n", argv[0]);
+        return 1;
+    }
+  }
+
+  if (background) {
+    base::Daemonize([] { return 0; });
+  }
+
   base::UnixTaskRunner task_runner;
 
 // TODO(rsavitski): support standalone --root or similar on android.
diff --git a/src/profiling/perf/unwinding.cc b/src/profiling/perf/unwinding.cc
index 69700a9..7ead30f 100644
--- a/src/profiling/perf/unwinding.cc
+++ b/src/profiling/perf/unwinding.cc
@@ -25,6 +25,7 @@
 #include "perfetto/ext/base/no_destructor.h"
 #include "perfetto/ext/base/thread_utils.h"
 #include "perfetto/ext/base/utils.h"
+#include "src/profiling/perf/frame_pointer_unwinder.h"
 
 namespace {
 constexpr size_t kUnwindingMaxFrames = 1000;
@@ -43,18 +44,23 @@
 }
 
 void Unwinder::PostStartDataSource(DataSourceInstanceID ds_id,
-                                   bool kernel_frames) {
+                                   bool kernel_frames,
+                                   UnwindMode unwind_mode) {
   // No need for a weak pointer as the associated task runner quits (stops
   // running tasks) strictly before the Unwinder's destruction.
-  task_runner_->PostTask(
-      [this, ds_id, kernel_frames] { StartDataSource(ds_id, kernel_frames); });
+  task_runner_->PostTask([this, ds_id, kernel_frames, unwind_mode] {
+    StartDataSource(ds_id, kernel_frames, unwind_mode);
+  });
 }
 
-void Unwinder::StartDataSource(DataSourceInstanceID ds_id, bool kernel_frames) {
+void Unwinder::StartDataSource(DataSourceInstanceID ds_id,
+                               bool kernel_frames,
+                               UnwindMode unwind_mode) {
   PERFETTO_DCHECK_THREAD(thread_checker_);
   PERFETTO_DLOG("Unwinder::StartDataSource(%zu)", static_cast<size_t>(ds_id));
 
-  auto it_and_inserted = data_sources_.emplace(ds_id, DataSourceState{});
+  auto it_and_inserted =
+      data_sources_.emplace(ds_id, DataSourceState{unwind_mode});
   PERFETTO_DCHECK(it_and_inserted.second);
 
   if (kernel_frames) {
@@ -259,6 +265,28 @@
       continue;
     }
 
+    // b/324757089: we are not precisely tracking process lifetimes, so the
+    // sample might be for a different process that reused the pid since the
+    // start of the session. Normally this is both infrequent and not a problem
+    // since the unwinding will fail due to invalidated procfs descriptors.
+    // However we need this explicit skip for the specific case of a kernel
+    // thread reusing a userspace pid, as the unwinding doesn't expect absent
+    // userspace state for a thought-to-be-userspace process.
+    // TODO(rsavitski): start tracking process exits more accurately, either
+    // via PERF_RECORD_EXIT records or by checking the validity of the procfs
+    // descriptors.
+    if (PERFETTO_UNLIKELY(!entry.sample.regs &&
+                          proc_state.status ==
+                              ProcessState::Status::kFdsResolved)) {
+      PERFETTO_DLOG(
+          "Unwinder discarding sample for pid [%d]: uspace->kthread pid reuse",
+          static_cast<int>(pid));
+
+      PERFETTO_CHECK(sampled_stack_bytes == 0);
+      entry = UnwindEntry::Invalid();
+      continue;
+    }
+
     // Sample ready - process it.
     if (proc_state.status == ProcessState::Status::kFdsResolved ||
         proc_state.status == ProcessState::Status::kNoUserspace) {
@@ -275,8 +303,9 @@
           (proc_state.unwind_state.has_value()
                ? &proc_state.unwind_state.value()
                : nullptr);
-      CompletedSample unwound_sample = UnwindSample(
-          entry.sample, opt_user_state, proc_state.attempted_unwinding);
+      CompletedSample unwound_sample =
+          UnwindSample(entry.sample, opt_user_state,
+                       proc_state.attempted_unwinding, ds.unwind_mode);
       proc_state.attempted_unwinding = true;
 
       PERFETTO_METATRACE_COUNTER(TAG_PRODUCER, PROFILER_UNWIND_CURRENT_PID, 0);
@@ -312,7 +341,8 @@
 
 CompletedSample Unwinder::UnwindSample(const ParsedSample& sample,
                                        UnwindingMetadata* opt_user_state,
-                                       bool pid_unwound_before) {
+                                       bool pid_unwound_before,
+                                       UnwindMode unwind_mode) {
   PERFETTO_DCHECK_THREAD(thread_checker_);
 
   CompletedSample ret;
@@ -353,7 +383,7 @@
     UnwindResult& operator=(UnwindResult&&) = default;
   };
   auto attempt_unwind = [&sample, unwind_state, pid_unwound_before,
-                         &overlay_memory]() -> UnwindResult {
+                         &overlay_memory, unwind_mode]() -> UnwindResult {
     metatrace::ScopedEvent m(metatrace::TAG_PRODUCER,
                              pid_unwound_before
                                  ? metatrace::PROFILER_UNWIND_ATTEMPT
@@ -362,16 +392,29 @@
     // Unwindstack clobbers registers, so make a copy in case of retries.
     auto regs_copy = std::unique_ptr<unwindstack::Regs>{sample.regs->Clone()};
 
-    unwindstack::Unwinder unwinder(kUnwindingMaxFrames, &unwind_state->fd_maps,
-                                   regs_copy.get(), overlay_memory);
+    switch (unwind_mode) {
+      case UnwindMode::kFramePointer: {
+        FramePointerUnwinder unwinder(kUnwindingMaxFrames,
+                                      &unwind_state->fd_maps, regs_copy.get(),
+                                      overlay_memory, sample.stack.size());
+        unwinder.Unwind();
+        return {unwinder.LastErrorCode(), unwinder.warnings(),
+                unwinder.ConsumeFrames()};
+      }
+      case UnwindMode::kUnwindStack: {
+        unwindstack::Unwinder unwinder(kUnwindingMaxFrames,
+                                       &unwind_state->fd_maps, regs_copy.get(),
+                                       overlay_memory);
 #if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
-    unwinder.SetJitDebug(unwind_state->GetJitDebug(regs_copy->Arch()));
-    unwinder.SetDexFiles(unwind_state->GetDexFiles(regs_copy->Arch()));
+        unwinder.SetJitDebug(unwind_state->GetJitDebug(regs_copy->Arch()));
+        unwinder.SetDexFiles(unwind_state->GetDexFiles(regs_copy->Arch()));
 #endif
-    unwinder.Unwind(/*initial_map_names_to_skip=*/nullptr,
-                    /*map_suffixes_to_ignore=*/nullptr);
-    return {unwinder.LastErrorCode(), unwinder.warnings(),
-            unwinder.ConsumeFrames()};
+        unwinder.Unwind(/*initial_map_names_to_skip=*/nullptr,
+                        /*map_suffixes_to_ignore=*/nullptr);
+        return {unwinder.LastErrorCode(), unwinder.warnings(),
+                unwinder.ConsumeFrames()};
+      }
+    }
   };
 
   // first unwind attempt
diff --git a/src/profiling/perf/unwinding.h b/src/profiling/perf/unwinding.h
index 8295fb8..13a7d6d 100644
--- a/src/profiling/perf/unwinding.h
+++ b/src/profiling/perf/unwinding.h
@@ -75,6 +75,8 @@
  public:
   friend class UnwinderHandle;
 
+  enum class UnwindMode { kUnwindStack, kFramePointer };
+
   // Callbacks from the unwinder to the primary producer thread.
   class Delegate {
    public:
@@ -89,7 +91,9 @@
 
   ~Unwinder() { PERFETTO_DCHECK_THREAD(thread_checker_); }
 
-  void PostStartDataSource(DataSourceInstanceID ds_id, bool kernel_frames);
+  void PostStartDataSource(DataSourceInstanceID ds_id,
+                           bool kernel_frames,
+                           UnwindMode unwind_mode);
   void PostAdoptProcDescriptors(DataSourceInstanceID ds_id,
                                 pid_t pid,
                                 base::ScopedFile maps_fd,
@@ -144,8 +148,11 @@
 
   struct DataSourceState {
     enum class Status { kActive, kShuttingDown };
+    explicit DataSourceState(UnwindMode _unwind_mode)
+        : unwind_mode(_unwind_mode) {}
 
     Status status = Status::kActive;
+    const UnwindMode unwind_mode;
     std::map<pid_t, ProcessState> process_states;
   };
 
@@ -163,7 +170,9 @@
 
   // Marks the data source as valid and active at the unwinding stage.
   // Initializes kernel address symbolization if needed.
-  void StartDataSource(DataSourceInstanceID ds_id, bool kernel_frames);
+  void StartDataSource(DataSourceInstanceID ds_id,
+                       bool kernel_frames,
+                       UnwindMode unwind_mode);
 
   void AdoptProcDescriptors(DataSourceInstanceID ds_id,
                             pid_t pid,
@@ -184,7 +193,8 @@
 
   CompletedSample UnwindSample(const ParsedSample& sample,
                                UnwindingMetadata* opt_user_state,
-                               bool pid_unwound_before);
+                               bool pid_unwound_before,
+                               UnwindMode unwind_mode);
 
   // Returns a list of symbolized kernel frames in the sample (if any).
   std::vector<unwindstack::FrameData> SymbolizeKernelCallchain(
diff --git a/src/profiling/symbolizer/local_symbolizer.cc b/src/profiling/symbolizer/local_symbolizer.cc
index c8ad70f..5271e82 100644
--- a/src/profiling/symbolizer/local_symbolizer.cc
+++ b/src/profiling/symbolizer/local_symbolizer.cc
@@ -116,7 +116,7 @@
 }
 
 template <typename E>
-std::optional<uint64_t> GetLoadBias(void* mem, size_t size) {
+std::optional<uint64_t> GetElfLoadBias(void* mem, size_t size) {
   const typename E::Ehdr* ehdr = static_cast<typename E::Ehdr*>(mem);
   if (!InRange(mem, size, ehdr, sizeof(typename E::Ehdr))) {
     PERFETTO_ELOG("Corrupted ELF.");
@@ -136,7 +136,7 @@
 }
 
 template <typename E>
-std::optional<std::string> GetBuildId(void* mem, size_t size) {
+std::optional<std::string> GetElfBuildId(void* mem, size_t size) {
   const typename E::Ehdr* ehdr = static_cast<typename E::Ehdr*>(mem);
   if (!InRange(mem, size, ehdr, sizeof(typename E::Ehdr))) {
     PERFETTO_ELOG("Corrupted ELF.");
@@ -202,13 +202,103 @@
           mem[EI_MAG2] == ELFMAG2 && mem[EI_MAG3] == ELFMAG3);
 }
 
-struct BuildIdAndLoadBias {
-  std::string build_id;
-  uint64_t load_bias;
+constexpr uint32_t kMachO64Magic = 0xfeedfacf;
+
+bool IsMachO64(const char* mem, size_t size) {
+  if (size < sizeof(kMachO64Magic))
+    return false;
+  return memcmp(mem, &kMachO64Magic, sizeof(kMachO64Magic)) == 0;
+}
+
+struct mach_header_64 {
+  uint32_t magic;      /* mach magic number identifier */
+  int32_t cputype;     /* cpu specifier */
+  int32_t cpusubtype;  /* machine specifier */
+  uint32_t filetype;   /* type of file */
+  uint32_t ncmds;      /* number of load commands */
+  uint32_t sizeofcmds; /* the size of all the load commands */
+  uint32_t flags;      /* flags */
+  uint32_t reserved;   /* reserved */
 };
 
-std::optional<BuildIdAndLoadBias> GetBuildIdAndLoadBias(const char* fname,
-                                                        size_t size) {
+struct load_command {
+  uint32_t cmd;     /* type of load command */
+  uint32_t cmdsize; /* total size of command in bytes */
+};
+
+struct segment_64_command {
+  uint32_t cmd;      /* LC_SEGMENT_64 */
+  uint32_t cmdsize;  /* includes sizeof section_64 structs */
+  char segname[16];  /* segment name */
+  uint64_t vmaddr;   /* memory address of this segment */
+  uint64_t vmsize;   /* memory size of this segment */
+  uint64_t fileoff;  /* file offset of this segment */
+  uint64_t filesize; /* amount to map from the file */
+  uint32_t maxprot;  /* maximum VM protection */
+  uint32_t initprot; /* initial VM protection */
+  uint32_t nsects;   /* number of sections in segment */
+  uint32_t flags;    /* flags */
+};
+
+struct BinaryInfo {
+  std::string build_id;
+  uint64_t load_bias;
+  BinaryType type;
+};
+
+std::optional<BinaryInfo> GetMachOBinaryInfo(char* mem, size_t size) {
+  if (size < sizeof(mach_header_64))
+    return {};
+
+  mach_header_64 header;
+  memcpy(&header, mem, sizeof(mach_header_64));
+
+  if (size < sizeof(mach_header_64) + header.sizeofcmds)
+    return {};
+
+  std::optional<std::string> build_id;
+  uint64_t load_bias = 0;
+
+  char* pcmd = mem + sizeof(mach_header_64);
+  char* pcmds_end = pcmd + header.sizeofcmds;
+  while (pcmd < pcmds_end) {
+    load_command cmd_header;
+    memcpy(&cmd_header, pcmd, sizeof(load_command));
+
+    constexpr uint32_t LC_SEGMENT_64 = 0x19;
+    constexpr uint32_t LC_UUID = 0x1b;
+
+    switch (cmd_header.cmd) {
+      case LC_UUID: {
+        build_id = std::string(pcmd + sizeof(load_command),
+                               cmd_header.cmdsize - sizeof(load_command));
+        break;
+      }
+      case LC_SEGMENT_64: {
+        segment_64_command seg_cmd;
+        memcpy(&seg_cmd, pcmd, sizeof(segment_64_command));
+        if (strcmp(seg_cmd.segname, "__TEXT") == 0) {
+          load_bias = seg_cmd.vmaddr;
+        }
+        break;
+      }
+      default:
+        break;
+    }
+
+    pcmd += cmd_header.cmdsize;
+  }
+
+  if (build_id) {
+    constexpr uint32_t MH_DSYM = 0xa;
+    BinaryType type = header.filetype == MH_DSYM ? BinaryType::kMachODsym
+                                                 : BinaryType::kMachO;
+    return BinaryInfo{*build_id, load_bias, type};
+  }
+  return {};
+}
+
+std::optional<BinaryInfo> GetBinaryInfo(const char* fname, size_t size) {
   static_assert(EI_CLASS > EI_MAG3, "mem[EI_MAG?] accesses are in range.");
   if (size <= EI_CLASS)
     return std::nullopt;
@@ -219,25 +309,26 @@
   }
   char* mem = static_cast<char*>(map.data());
 
-  if (!IsElf(mem, size))
-    return std::nullopt;
-
   std::optional<std::string> build_id;
   std::optional<uint64_t> load_bias;
-  switch (mem[EI_CLASS]) {
-    case ELFCLASS32:
-      build_id = GetBuildId<Elf32>(mem, size);
-      load_bias = GetLoadBias<Elf32>(mem, size);
-      break;
-    case ELFCLASS64:
-      build_id = GetBuildId<Elf64>(mem, size);
-      load_bias = GetLoadBias<Elf64>(mem, size);
-      break;
-    default:
-      return std::nullopt;
-  }
-  if (build_id && load_bias) {
-    return BuildIdAndLoadBias{*build_id, *load_bias};
+  if (IsElf(mem, size)) {
+    switch (mem[EI_CLASS]) {
+      case ELFCLASS32:
+        build_id = GetElfBuildId<Elf32>(mem, size);
+        load_bias = GetElfLoadBias<Elf32>(mem, size);
+        break;
+      case ELFCLASS64:
+        build_id = GetElfBuildId<Elf64>(mem, size);
+        load_bias = GetElfLoadBias<Elf64>(mem, size);
+        break;
+      default:
+        return std::nullopt;
+    }
+    if (build_id && load_bias) {
+      return BinaryInfo{*build_id, *load_bias, BinaryType::kElf};
+    }
+  } else if (IsMachO64(mem, size)) {
+    return GetMachOBinaryInfo(mem, size);
   }
   return std::nullopt;
 }
@@ -245,6 +336,7 @@
 std::map<std::string, FoundBinary> BuildIdIndex(std::vector<std::string> dirs) {
   std::map<std::string, FoundBinary> result;
   WalkDirectories(std::move(dirs), [&result](const char* fname, size_t size) {
+    static_assert(EI_MAG3 + 1 == sizeof(kMachO64Magic));
     char magic[EI_MAG3 + 1];
     // Scope file access. On windows OpenFile opens an exclusive lock.
     // This lock needs to be released before mapping the file.
@@ -259,16 +351,39 @@
         PERFETTO_PLOG("Failed to read %s", fname);
         return;
       }
-      if (!IsElf(magic, static_cast<size_t>(rd))) {
-        PERFETTO_DLOG("%s not an ELF.", fname);
+      if (!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;
       }
     }
-    std::optional<BuildIdAndLoadBias> build_id_and_load_bias =
-        GetBuildIdAndLoadBias(fname, size);
-    if (build_id_and_load_bias) {
-      result.emplace(build_id_and_load_bias->build_id,
-                     FoundBinary{fname, build_id_and_load_bias->load_bias});
+    std::optional<BinaryInfo> binary_info = GetBinaryInfo(fname, size);
+    if (!binary_info) {
+      PERFETTO_DLOG("Failed to extract build id from %s.", fname);
+      return;
+    }
+    auto it = result.emplace(
+        binary_info->build_id,
+        FoundBinary{fname, binary_info->load_bias, binary_info->type});
+
+    // If there was already an existing FoundBinary, the emplace wouldn't insert
+    // anything. But, for Mac binaries, we prefer dSYM files over the original
+    // binary, so make sure these overwrite the FoundBinary entry.
+    bool has_existing = it.second == false;
+    if (has_existing) {
+      if (it.first->second.type == BinaryType::kMachO &&
+          binary_info->type == BinaryType::kMachODsym) {
+        PERFETTO_LOG("Overwriting index entry for %s to %s.",
+                     base::ToHex(binary_info->build_id).c_str(), fname);
+        it.first->second =
+            FoundBinary{fname, binary_info->load_bias, binary_info->type};
+      } else {
+        PERFETTO_DLOG("Ignoring %s, index entry for %s already exists.", fname,
+                      base::ToHex(binary_info->build_id).c_str());
+      }
+    } else {
+      PERFETTO_LOG("Indexed: %s (%s)", fname,
+                   base::ToHex(binary_info->build_id).c_str());
     }
   });
   return result;
@@ -548,6 +663,13 @@
 
   std::optional<FoundBinary>& cache_entry = p.first->second;
 
+  // Try the absolute path first.
+  if (base::StartsWith(abspath, "/")) {
+    cache_entry = IsCorrectFile(abspath, build_id);
+    if (cache_entry)
+      return cache_entry;
+  }
+
   for (const std::string& root_str : roots_) {
     cache_entry = FindBinaryInRoot(root_str, abspath, build_id);
     if (cache_entry)
@@ -579,14 +701,14 @@
     return std::nullopt;
   }
 
-  std::optional<BuildIdAndLoadBias> build_id_and_load_bias =
-      GetBuildIdAndLoadBias(symbol_file.c_str(), size);
-  if (!build_id_and_load_bias)
+  std::optional<BinaryInfo> binary_info =
+      GetBinaryInfo(symbol_file.c_str(), size);
+  if (!binary_info)
     return std::nullopt;
-  if (build_id_and_load_bias->build_id != build_id) {
+  if (binary_info->build_id != build_id) {
     return std::nullopt;
   }
-  return FoundBinary{symbol_file, build_id_and_load_bias->load_bias};
+  return FoundBinary{symbol_file, binary_info->load_bias, binary_info->type};
 }
 
 std::optional<FoundBinary> LocalBinaryFinder::FindBinaryInRoot(
diff --git a/src/profiling/symbolizer/local_symbolizer.h b/src/profiling/symbolizer/local_symbolizer.h
index d7185ef..36a8718 100644
--- a/src/profiling/symbolizer/local_symbolizer.h
+++ b/src/profiling/symbolizer/local_symbolizer.h
@@ -33,10 +33,16 @@
 
 bool ParseLlvmSymbolizerJsonLine(const std::string& line,
                                  std::vector<SymbolizedFrame>* result);
+enum BinaryType {
+  kElf,
+  kMachO,
+  kMachODsym,
+};
 
 struct FoundBinary {
   std::string file_name;
   uint64_t load_bias;
+  BinaryType type;
 };
 
 class BinaryFinder {
diff --git a/src/protozero/BUILD.gn b/src/protozero/BUILD.gn
index d1285bb..ebdcc6f 100644
--- a/src/protozero/BUILD.gn
+++ b/src/protozero/BUILD.gn
@@ -104,6 +104,8 @@
     "test/example_proto/test_messages.proto",
     "test/example_proto/upper_import.proto",
   ]
+  generate_descriptor = "test_messages.descriptor"
+  descriptor_root_source = "test/example_proto/test_messages.proto"
   proto_path = perfetto_root_path
 }
 
@@ -117,13 +119,6 @@
   proto_path = perfetto_root_path
 }
 
-perfetto_proto_library("test_messages_descriptor") {
-  proto_generators = [ "descriptor" ]
-  generate_descriptor = "test_messages.descriptor"
-  sources = [ "test/example_proto/test_messages.proto" ]
-  deps = [ ":testing_messages_source_set" ]
-}
-
 perfetto_fuzzer_test("protozero_decoder_fuzzer") {
   sources = [ "proto_decoder_fuzzer.cc" ]
   deps = [
diff --git a/src/shared_lib/data_source.cc b/src/shared_lib/data_source.cc
index 583d166..668ebcf 100644
--- a/src/shared_lib/data_source.cc
+++ b/src/shared_lib/data_source.cc
@@ -401,8 +401,10 @@
 
 void PerfettoDsImplReleaseInstanceLocked(struct PerfettoDsImpl* ds_impl,
                                          PerfettoDsInstanceIndex idx) {
-  auto* internal_state = ds_impl->cpp_type.static_state()->TryGet(idx);
-  PERFETTO_CHECK(internal_state);
+  // The `valid_instances` bitmap might have changed since the lock has been
+  // taken, but the instance must still be alive (we were holding the lock on
+  // it).
+  auto* internal_state = ds_impl->cpp_type.static_state()->GetUnsafe(idx);
   internal_state->lock.unlock();
 }
 
diff --git a/src/shared_lib/test/api_integrationtest.cc b/src/shared_lib/test/api_integrationtest.cc
index 1b581bd..d5c7d45 100644
--- a/src/shared_lib/test/api_integrationtest.cc
+++ b/src/shared_lib/test/api_integrationtest.cc
@@ -66,6 +66,7 @@
 using ::testing::DoAll;
 using ::testing::ElementsAre;
 using ::testing::InSequence;
+using ::testing::IsNull;
 using ::testing::NiceMock;
 using ::testing::ResultOf;
 using ::testing::Return;
@@ -966,6 +967,134 @@
   PERFETTO_DS_TRACE(data_source_1, ctx) {}
 }
 
+TEST_F(SharedLibDataSourceTest, GetInstanceLockedSuccess) {
+  bool ignored = false;
+  void* const kInstancePtr = &ignored;
+  EXPECT_CALL(ds2_callbacks_, OnSetup(_, _, _, _, kDataSource2UserArg, _))
+      .WillOnce(Return(kInstancePtr));
+  TracingSession tracing_session =
+      TracingSession::Builder().set_data_source_name(kDataSourceName2).Build();
+
+  void* arg = nullptr;
+  PERFETTO_DS_TRACE(data_source_2, ctx) {
+    arg = PerfettoDsImplGetInstanceLocked(data_source_2.impl, ctx.impl.inst_id);
+    if (arg) {
+      PerfettoDsImplReleaseInstanceLocked(data_source_2.impl, ctx.impl.inst_id);
+    }
+  }
+
+  EXPECT_EQ(arg, kInstancePtr);
+}
+
+TEST_F(SharedLibDataSourceTest, GetInstanceLockedFailure) {
+  bool ignored = false;
+  void* const kInstancePtr = &ignored;
+  EXPECT_CALL(ds2_callbacks_, OnSetup(_, _, _, _, kDataSource2UserArg, _))
+      .WillOnce(Return(kInstancePtr));
+  TracingSession tracing_session =
+      TracingSession::Builder().set_data_source_name(kDataSourceName2).Build();
+
+  WaitableEvent inside_tracing;
+  WaitableEvent stopped;
+
+  std::thread t([&] {
+    PERFETTO_DS_TRACE(data_source_2, ctx) {
+      inside_tracing.Notify();
+      stopped.WaitForNotification();
+      void* arg =
+          PerfettoDsImplGetInstanceLocked(data_source_2.impl, ctx.impl.inst_id);
+      if (arg) {
+        PerfettoDsImplReleaseInstanceLocked(data_source_2.impl,
+                                            ctx.impl.inst_id);
+      }
+      EXPECT_THAT(arg, IsNull());
+    }
+  });
+
+  inside_tracing.WaitForNotification();
+  tracing_session.StopBlocking();
+  stopped.Notify();
+  t.join();
+}
+
+// Regression test for a `PerfettoDsImplReleaseInstanceLocked()`. Under very
+// specific circumstances, that depends on the implementation details of
+// `TracingMuxerImpl`, the following events can happen:
+// * `PerfettoDsImplGetInstanceLocked()` is called after
+//   `TracingMuxerImpl::StopDataSource_AsyncBeginImpl`, but before
+//   `TracingMuxerImpl::StopDataSource_AsyncEnd`.
+//   `PerfettoDsImplGetInstanceLocked()` succeeds and returns a valid instance.
+// * `TracingMuxerImpl::StopDataSource_AsyncEnd()` is called.
+//   `DataSourceStaticState::valid_instances` is reset.
+// * `PerfettoDsImplReleaseInstanceLocked()` is called.
+//
+// In this case `PerfettoDsImplReleaseInstanceLocked()` should work even though
+// the instance is not there in the valid_instances bitmap anymore.
+//
+// In order to reproduce the specific failure, the test makes assumptions about
+// the internal implementation (that valid_instance is changed outside of the
+// lock). If that were to change and the test would fail, the test should be
+// changed/deleted.
+TEST_F(SharedLibDataSourceTest, GetInstanceLockedStopBeforeRelease) {
+  bool ignored = false;
+  void* const kInstancePtr = &ignored;
+  EXPECT_CALL(ds2_callbacks_, OnSetup(_, _, _, _, kDataSource2UserArg, _))
+      .WillOnce(Return(kInstancePtr));
+  TracingSession tracing_session =
+      TracingSession::Builder().set_data_source_name(kDataSourceName2).Build();
+
+  WaitableEvent inside_tracing;
+  WaitableEvent stopping;
+  WaitableEvent locked;
+  WaitableEvent fully_stopped;
+
+  std::thread t([&] {
+    PERFETTO_DS_TRACE(data_source_2, ctx) {
+      inside_tracing.Notify();
+      stopping.WaitForNotification();
+      void* arg =
+          PerfettoDsImplGetInstanceLocked(data_source_2.impl, ctx.impl.inst_id);
+      EXPECT_EQ(arg, kInstancePtr);
+      locked.Notify();
+      fully_stopped.WaitForNotification();
+      if (arg) {
+        PerfettoDsImplReleaseInstanceLocked(data_source_2.impl,
+                                            ctx.impl.inst_id);
+      }
+    }
+  });
+
+  inside_tracing.WaitForNotification();
+
+  struct PerfettoDsAsyncStopper* stopper = nullptr;
+
+  EXPECT_CALL(ds2_callbacks_, OnStop(_, _, kDataSource2UserArg, _, _))
+      .WillOnce([&](struct PerfettoDsImpl*, PerfettoDsInstanceIndex, void*,
+                    void*, struct PerfettoDsOnStopArgs* args) {
+        stopper = PerfettoDsOnStopArgsPostpone(args);
+        stopping.Notify();
+      });
+
+  tracing_session.StopAsync();
+
+  locked.WaitForNotification();
+  PerfettoDsStopDone(stopper);
+  // Wait for PerfettoDsImplTraceIterateBegin to return a nullptr tracer. This
+  // means that the valid_instances bitmap has been reset.
+  for (;;) {
+    PerfettoDsImplTracerIterator iterator =
+        PerfettoDsImplTraceIterateBegin(data_source_2.impl);
+    if (iterator.tracer == nullptr) {
+      break;
+    }
+    PerfettoDsImplTraceIterateBreak(data_source_2.impl, &iterator);
+    std::this_thread::yield();
+  }
+  fully_stopped.Notify();
+  tracing_session.WaitForStopped();
+  t.join();
+}
+
 class SharedLibProducerTest : public testing::Test {
  protected:
   void SetUp() override {
diff --git a/src/shared_lib/test/utils.cc b/src/shared_lib/test/utils.cc
index 966c7f0..6aa8ccd 100644
--- a/src/shared_lib/test/utils.cc
+++ b/src/shared_lib/test/utils.cc
@@ -159,6 +159,10 @@
   stopped_->WaitForNotification();
 }
 
+void TracingSession::StopAsync() {
+  PerfettoTracingSessionStopAsync(session_);
+}
+
 void TracingSession::StopBlocking() {
   PerfettoTracingSessionStopBlocking(session_);
 }
diff --git a/src/shared_lib/test/utils.h b/src/shared_lib/test/utils.h
index f6cfddc..51514c6 100644
--- a/src/shared_lib/test/utils.h
+++ b/src/shared_lib/test/utils.h
@@ -99,7 +99,11 @@
   struct PerfettoTracingSessionImpl* session() const { return session_; }
 
   bool FlushBlocking(uint32_t timeout_ms);
+  // Waits for the tracing session to be stopped.
   void WaitForStopped();
+  // Asks the tracing session to stop. Doesn't wait for it to be stopped.
+  void StopAsync();
+  // Equivalent to StopAsync() + WaitForStopped().
   void StopBlocking();
   std::vector<uint8_t> ReadBlocking();
 
diff --git a/src/shared_lib/track_event.cc b/src/shared_lib/track_event.cc
index e3115ba..6714032 100644
--- a/src/shared_lib/track_event.cc
+++ b/src/shared_lib/track_event.cc
@@ -962,8 +962,10 @@
     return;
   }
 
-  const PerfettoTeRegisteredTrackImpl* registered_track = nullptr;
-  const PerfettoTeHlExtraNamedTrack* named_track = nullptr;
+  std::variant<std::monostate, const PerfettoTeRegisteredTrackImpl*,
+               const PerfettoTeHlExtraNamedTrack*,
+               const PerfettoTeHlExtraProtoTrack*>
+      track;
   std::optional<uint64_t> track_uuid;
 
   const struct PerfettoTeHlExtraTimestamp* custom_timestamp = nullptr;
@@ -979,12 +981,13 @@
       const auto& cast =
           reinterpret_cast<const struct PerfettoTeHlExtraRegisteredTrack&>(
               extra);
-      registered_track = cast.track;
-      named_track = nullptr;
+      track = cast.track;
     } else if (extra.type == PERFETTO_TE_HL_EXTRA_TYPE_NAMED_TRACK) {
-      registered_track = nullptr;
-      named_track =
+      track =
           &reinterpret_cast<const struct PerfettoTeHlExtraNamedTrack&>(extra);
+    } else if (extra.type == PERFETTO_TE_HL_EXTRA_TYPE_PROTO_TRACK) {
+      track =
+          &reinterpret_cast<const struct PerfettoTeHlExtraProtoTrack&>(extra);
     } else if (extra.type == PERFETTO_TE_HL_EXTRA_TYPE_TIMESTAMP) {
       custom_timestamp =
           &reinterpret_cast<const struct PerfettoTeHlExtraTimestamp&>(extra);
@@ -1032,7 +1035,9 @@
   ResetIncrementalStateIfRequired(ii->instance->trace_writer.get(), incr_state,
                                   track_event_tls, ts);
 
-  if (registered_track) {
+  if (std::holds_alternative<const PerfettoTeRegisteredTrackImpl*>(track)) {
+    auto* registered_track =
+        std::get<const PerfettoTeRegisteredTrackImpl*>(track);
     if (incr_state->seen_track_uuids.insert(registered_track->uuid).second) {
       auto packet = ii->instance->trace_writer->NewTracePacket();
       auto* track_descriptor = packet->set_track_descriptor();
@@ -1040,7 +1045,9 @@
                                             registered_track->descriptor_size);
     }
     track_uuid = registered_track->uuid;
-  } else if (named_track) {
+  } else if (std::holds_alternative<const PerfettoTeHlExtraNamedTrack*>(
+                 track)) {
+    auto* named_track = std::get<const PerfettoTeHlExtraNamedTrack*>(track);
     uint64_t uuid = named_track->parent_uuid;
     uuid ^= PerfettoFnv1a(named_track->name, strlen(named_track->name));
     uuid ^= named_track->id;
@@ -1054,6 +1061,17 @@
       track_descriptor->set_name(named_track->name);
     }
     track_uuid = uuid;
+  } else if (std::holds_alternative<const PerfettoTeHlExtraProtoTrack*>(
+                 track)) {
+    auto* counter_track = std::get<const PerfettoTeHlExtraProtoTrack*>(track);
+    uint64_t uuid = counter_track->uuid;
+    if (incr_state->seen_track_uuids.insert(uuid).second) {
+      auto packet = ii->instance->trace_writer->NewTracePacket();
+      auto* track_descriptor = packet->set_track_descriptor();
+      track_descriptor->set_uuid(uuid);
+      AppendHlProtoFields(track_descriptor, counter_track->fields);
+    }
+    track_uuid = uuid;
   }
 
   perfetto::TraceWriterBase* trace_writer = ii->instance->trace_writer.get();
diff --git a/src/tools/ftrace_proto_gen/event_list b/src/tools/ftrace_proto_gen/event_list
index 9d5553d..8c99f49 100644
--- a/src/tools/ftrace_proto_gen/event_list
+++ b/src/tools/ftrace_proto_gen/event_list
@@ -531,4 +531,9 @@
 kgsl/adreno_cmdbatch_queued
 kgsl/adreno_cmdbatch_submitted
 kgsl/adreno_cmdbatch_sync
-kgsl/adreno_cmdbatch_retired
\ No newline at end of file
+kgsl/adreno_cmdbatch_retired
+pixel_mm/pixel_mm_kswapd_wake
+pixel_mm/pixel_mm_kswapd_done
+sched/sched_wakeup_task_attr
+devfreq/devfreq_frequency
+cpm_trace/param_set_value_cpm
diff --git a/src/tools/ftrace_proto_gen/ftrace_proto_gen.cc b/src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
index a036016..3fd774c 100644
--- a/src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
+++ b/src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
@@ -169,6 +169,10 @@
       *fout << "    GenericFtraceEvent generic = " << i << ";\n";
       ++i;
     }
+    if (i == 542) {
+      *fout << "    KprobeEvent kprobe_event = " << i << ";\n";
+      ++i;
+    }
   }
   *fout << "  }\n";
   *fout << "}\n";
diff --git a/src/tools/ftrace_proto_gen/proto_gen_utils.cc b/src/tools/ftrace_proto_gen/proto_gen_utils.cc
index d734e88..ee4257b 100644
--- a/src/tools/ftrace_proto_gen/proto_gen_utils.cc
+++ b/src/tools/ftrace_proto_gen/proto_gen_utils.cc
@@ -170,6 +170,9 @@
   if (type == google::protobuf::FieldDescriptor::Type::TYPE_STRING)
     return String(is_repeated);
 
+  if (type == google::protobuf::FieldDescriptor::Type::TYPE_ENUM)
+    return Numeric(32, true, is_repeated);
+
   return Invalid();
 }
 
diff --git a/src/tools/proto_merger/proto_file.cc b/src/tools/proto_merger/proto_file.cc
index 8f595e0..0fdbef9 100644
--- a/src/tools/proto_merger/proto_file.cc
+++ b/src/tools/proto_merger/proto_file.cc
@@ -51,15 +51,6 @@
         "sint64",    // TYPE_SINT64
 };
 
-const char* const
-    kLabelToName[google::protobuf::FieldDescriptor::MAX_LABEL + 1] = {
-        "ERROR",  // 0 is reserved for errors
-
-        "optional",  // LABEL_OPTIONAL
-        "required",  // LABEL_REQUIRED
-        "repeated",  // LABEL_REPEATED
-};
-
 std::optional<std::string> MinimizeType(const std::string& a,
                                         const std::string& b) {
   auto a_pieces = base::SplitString(a, ".");
@@ -198,14 +189,20 @@
     const google::protobuf::Descriptor& parent,
     const google::protobuf::FieldDescriptor& desc) {
   auto field = InitFromDescriptor<ProtoFile::Field>(desc);
-  // Map fields have label repeated but are actually emitted without a label
-  // in practice.
-  field.label = desc.is_map() ? "" : kLabelToName[desc.label()];
+  field.is_repeated = desc.is_repeated();
   field.packageless_type = FieldTypeFromDescriptor(parent, desc, true);
   field.type = FieldTypeFromDescriptor(parent, desc, false);
   field.name = desc.name();
   field.number = desc.number();
   field.options = OptionsFromMessage(*desc.file()->pool(), desc.options());
+
+  // Protobuf editions: packed fields are no longer an option, but have the same
+  // syntax as far as writing the merged .proto file is concerned.
+  if (desc.is_packed()) {
+    field.options.push_back(
+        ProtoFile::Option{"features.repeated_field_encoding", "PACKED"});
+  }
+
   return field;
 }
 
diff --git a/src/tools/proto_merger/proto_file.h b/src/tools/proto_merger/proto_file.h
index 985af47..08a410b 100644
--- a/src/tools/proto_merger/proto_file.h
+++ b/src/tools/proto_merger/proto_file.h
@@ -49,7 +49,7 @@
     std::vector<Value> deleted_values;
   };
   struct Field : Member {
-    std::string label;
+    bool is_repeated;
     std::string packageless_type;
     std::string type;
     std::string name;
diff --git a/src/tools/proto_merger/proto_file_serializer.cc b/src/tools/proto_merger/proto_file_serializer.cc
index c94a101..7b6a884 100644
--- a/src/tools/proto_merger/proto_file_serializer.cc
+++ b/src/tools/proto_merger/proto_file_serializer.cc
@@ -120,12 +120,11 @@
   std::string output;
   output += SerializeLeadingComments(prefix, field);
 
-  std::string label;
-  if (write_label) {
-    label = field.label + " ";
-  }
-  output += prefix + label + field.type + " " + field.name + " = " +
-            std::to_string(field.number);
+  output += prefix;
+  if (write_label && field.is_repeated)
+    output += "repeated ";
+  output +=
+      field.type + " " + field.name + " = " + std::to_string(field.number);
 
   output += SerializeOptions(field.options);
   output += ";\n";
diff --git a/src/tools/proto_merger/proto_file_serializer.h b/src/tools/proto_merger/proto_file_serializer.h
index a2c4817..4f4a40e 100644
--- a/src/tools/proto_merger/proto_file_serializer.h
+++ b/src/tools/proto_merger/proto_file_serializer.h
@@ -31,13 +31,11 @@
 //       name: Baz
 //       fields: [
 //         Field {
-//           label: optional
 //           type: Foo
 //           name: foo
 //           number: 1
 //         }
 //         Field {
-//           label: optional
 //           type: Bar
 //           name: bar
 //           number: 2
@@ -50,8 +48,8 @@
 // will convert to:
 //
 // message Baz {
-//   optional Foo foo = 1;
-//   optonal Bar bar = 2;
+//   Foo foo = 1;
+//   Bar bar = 2;
 // }
 std::string ProtoFileToDotProto(const ProtoFile&);
 
diff --git a/src/tools/proto_merger/proto_merger.cc b/src/tools/proto_merger/proto_merger.cc
index ab55ab9..f052e76 100644
--- a/src/tools/proto_merger/proto_merger.cc
+++ b/src/tools/proto_merger/proto_merger.cc
@@ -202,7 +202,7 @@
   // Get the comments, label and the name from the source of truth.
   out.leading_comments = upstream.leading_comments;
   out.trailing_comments = upstream.trailing_comments;
-  out.label = upstream.label;
+  out.is_repeated = upstream.is_repeated;
   out.name = upstream.name;
 
   // Get everything else from the input.
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index 53e8a1c..5666a50 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -156,7 +156,6 @@
       "trace_processor_impl.cc",
       "trace_processor_impl.h",
     ]
-
     deps = [
       ":metatrace",
       ":storage_minimal",
@@ -166,22 +165,23 @@
       "../../protos/perfetto/trace/perfetto:zero",
       "../../protos/perfetto/trace_processor:zero",
       "../base",
+      "../base:clock_snapshots",
       "../protozero",
       "db",
       "importers/android_bugreport",
+      "importers/archive",
+      "importers/art_method",
       "importers/common",
       "importers/etw:full",
       "importers/ftrace:full",
       "importers/fuchsia:full",
-      "importers/gzip:full",
-      "importers/json:full",
       "importers/json:minimal",
       "importers/ninja",
       "importers/perf",
+      "importers/perf_text",
       "importers/proto:full",
       "importers/proto:minimal",
       "importers/systrace:full",
-      "importers/zip:full",
       "metrics",
       "perfetto_sql/engine",
       "perfetto_sql/intrinsics/functions",
@@ -204,6 +204,15 @@
       "../../gn:sqlite",  # iterator_impl.h includes sqlite3.h.
       "../../include/perfetto/trace_processor",
     ]
+    if (enable_perfetto_trace_processor_mac_instruments) {
+      deps += [ "importers/instruments" ]
+    }
+    if (enable_perfetto_trace_processor_json) {
+      deps += [
+        "importers/gecko",
+        "importers/json",
+      ]
+    }
   }
 
   executable("trace_processor_shell") {
@@ -219,6 +228,7 @@
       "../base:version",
       "metrics",
       "rpc:stdiod",
+      "sqlite",
       "util",
       "util:stdlib",
     ]
@@ -237,7 +247,7 @@
         "../../protos/perfetto/trace:descriptor",
         "../../protos/perfetto/trace:test_extensions_descriptor",
         "../../protos/perfetto/trace_processor:stack_descriptor",
-        "../../protos/third_party/pprof:profile_descriptor",
+        "../../protos/third_party/pprof:descriptor",
       ]
     }
   }
@@ -323,6 +333,9 @@
       "perfetto_sql/intrinsics/functions:unittests",
       "perfetto_sql/intrinsics/operators:unittests",
       "perfetto_sql/intrinsics/table_functions:unittests",
+      "perfetto_sql/parser:unittests",
+      "perfetto_sql/preprocessor:unittests",
+      "perfetto_sql/tokenizer:unittests",
       "sqlite:unittests",
     ]
   }
@@ -330,7 +343,7 @@
 
 perfetto_cc_proto_descriptor("gen_cc_test_messages_descriptor") {
   descriptor_name = "test_messages.descriptor"
-  descriptor_target = "../protozero:test_messages_descriptor"
+  descriptor_target = "../protozero:testing_messages_descriptor"
 }
 
 source_set("integrationtests") {
diff --git a/src/trace_processor/containers/row_map_unittest.cc b/src/trace_processor/containers/row_map_unittest.cc
index 28d5e0e..049a071 100644
--- a/src/trace_processor/containers/row_map_unittest.cc
+++ b/src/trace_processor/containers/row_map_unittest.cc
@@ -18,7 +18,6 @@
 
 #include <memory>
 
-#include "src/base/test/gtest_test_suite.h"
 #include "test/gtest_and_gmock.h"
 
 namespace perfetto {
diff --git a/src/trace_processor/db/column_storage.h b/src/trace_processor/db/column_storage.h
index 5974081..eae4bef 100644
--- a/src/trace_processor/db/column_storage.h
+++ b/src/trace_processor/db/column_storage.h
@@ -137,6 +137,10 @@
     data_.insert(data_.end(), count, val);
     valid_.Resize(valid_.size() + static_cast<uint32_t>(count), true);
   }
+  void Append(const std::vector<T>& vals) {
+    data_.insert(data_.end(), vals.begin(), vals.end());
+    valid_.Resize(valid_.size() + static_cast<uint32_t>(vals.size()), true);
+  }
   void Set(uint32_t idx, T val) {
     if (mode_ == Mode::kDense) {
       valid_.Set(idx);
diff --git a/src/trace_processor/db/runtime_table.cc b/src/trace_processor/db/runtime_table.cc
index 9019d00..4370499 100644
--- a/src/trace_processor/db/runtime_table.cc
+++ b/src/trace_processor/db/runtime_table.cc
@@ -274,27 +274,31 @@
 }
 
 base::Status RuntimeTable::Builder::AddIntegers(uint32_t idx,
-                                                int64_t res,
+                                                int64_t val,
                                                 uint32_t count) {
   auto* col = storage_[idx].get();
   if (auto* leading_nulls_ptr = std::get_if<uint32_t>(col)) {
     *col = Fill<NullIntStorage>(*leading_nulls_ptr, std::nullopt);
   }
   if (auto* doubles = std::get_if<NullDoubleStorage>(col)) {
-    if (!IsPerfectlyRepresentableAsDouble(res)) {
+    if (!IsPerfectlyRepresentableAsDouble(val)) {
       return base::ErrStatus("Column %s contains %" PRId64
                              " which cannot be represented as a double",
-                             col_names_[idx].c_str(), res);
+                             col_names_[idx].c_str(), val);
     }
-    doubles->AppendMultiple(static_cast<double>(res), count);
+    doubles->AppendMultiple(static_cast<double>(val), count);
     return base::OkStatus();
   }
-  auto* ints = std::get_if<NullIntStorage>(col);
+  if (auto* null_ints = std::get_if<NullIntStorage>(col)) {
+    null_ints->AppendMultiple(val, count);
+    return base::OkStatus();
+  }
+  auto* ints = std::get_if<IntStorage>(col);
   if (!ints) {
     return base::ErrStatus("Column %s does not have consistent types",
                            col_names_[idx].c_str());
   }
-  ints->AppendMultiple(res, count);
+  ints->AppendMultiple(val, count);
   return base::OkStatus();
 }
 
@@ -369,9 +373,28 @@
   std::get<IntStorage>(*storage_[idx]).Append(res);
 }
 
+void RuntimeTable::Builder::AddNullIntegersUnchecked(
+    uint32_t idx,
+    const std::vector<int64_t>& res) {
+  std::get<NullIntStorage>(*storage_[idx]).Append(res);
+}
+
+void RuntimeTable::Builder::AddNonNullDoublesUnchecked(
+    uint32_t idx,
+    const std::vector<double>& vals) {
+  std::get<DoubleStorage>(*storage_[idx]).Append(vals);
+}
+
+void RuntimeTable::Builder::AddNullDoublesUnchecked(
+    uint32_t idx,
+    const std::vector<double>& vals) {
+  std::get<NullDoubleStorage>(*storage_[idx]).Append(vals);
+}
+
 base::StatusOr<std::unique_ptr<RuntimeTable>> RuntimeTable::Builder::Build(
     uint32_t rows) && {
-  std::vector<RefPtr<column::StorageLayer>> storage_layers(col_names_.size() + 1);
+  std::vector<RefPtr<column::StorageLayer>> storage_layers(col_names_.size() +
+                                                           1);
   std::vector<RefPtr<column::OverlayLayer>> null_layers(col_names_.size() + 1);
 
   std::vector<ColumnLegacy> legacy_columns;
@@ -422,12 +445,24 @@
           i, col_names_[i].c_str(), std::get_if<IntStorage>(col),
           storage_layers, overlay_layers, legacy_columns, legacy_overlays);
 
-    } else if (auto* doubles = std::get_if<NullDoubleStorage>(col)) {
-      // The doubles column.
+    } else if (auto* doubles = std::get_if<DoubleStorage>(col)) {
+      // The `doubles` column for tables where column types was provided before.
       PERFETTO_CHECK(doubles->size() == rows);
-      if (doubles->non_null_size() == doubles->size()) {
+      bool is_sorted =
+          std::is_sorted(doubles->vector().begin(), doubles->vector().end());
+      uint32_t flags =
+          is_sorted ? ColumnLegacy::Flag::kNonNull | ColumnLegacy::Flag::kSorted
+                    : ColumnLegacy::Flag::kNonNull;
+      legacy_columns.emplace_back(col_names_[i].c_str(), doubles, flags, i, 0);
+      storage_layers[i].reset(new column::NumericStorage<double>(
+          &doubles->vector(), ColumnType::kDouble, is_sorted));
+
+    } else if (auto* null_doubles = std::get_if<NullDoubleStorage>(col)) {
+      // The doubles column.
+      PERFETTO_CHECK(null_doubles->size() == rows);
+      if (null_doubles->non_null_size() == null_doubles->size()) {
         // The column is not nullable.
-        *col = DoubleStorage::CreateFromAssertNonNull(std::move(*doubles));
+        *col = DoubleStorage::CreateFromAssertNonNull(std::move(*null_doubles));
 
         auto* non_null_doubles = std::get_if<DoubleStorage>(col);
         bool is_sorted = std::is_sorted(non_null_doubles->vector().begin(),
@@ -441,12 +476,12 @@
             &non_null_doubles->vector(), ColumnType::kDouble, is_sorted));
       } else {
         // The column is nullable.
-        legacy_columns.emplace_back(col_names_[i].c_str(), doubles,
+        legacy_columns.emplace_back(col_names_[i].c_str(), null_doubles,
                                     ColumnLegacy::Flag::kNoFlag, i, 0);
         storage_layers[i].reset(new column::NumericStorage<double>(
-            &doubles->non_null_vector(), ColumnType::kDouble, false));
+            &null_doubles->non_null_vector(), ColumnType::kDouble, false));
         null_layers[i].reset(
-            new column::NullOverlay(&doubles->non_null_bit_vector()));
+            new column::NullOverlay(&null_doubles->non_null_bit_vector()));
       }
 
     } else if (auto* strings = std::get_if<StringStorage>(col)) {
diff --git a/src/trace_processor/db/runtime_table.h b/src/trace_processor/db/runtime_table.h
index 3dd2a78..ed93250 100644
--- a/src/trace_processor/db/runtime_table.h
+++ b/src/trace_processor/db/runtime_table.h
@@ -80,8 +80,10 @@
     void AddNonNullIntegerUnchecked(uint32_t idx, int64_t res) {
       std::get<IntStorage>(*storage_[idx]).Append(res);
     }
-    void AddNonNullIntegersUnchecked(uint32_t idx,
-                                     const std::vector<int64_t>& res);
+    void AddNonNullIntegersUnchecked(uint32_t idx, const std::vector<int64_t>&);
+    void AddNullIntegersUnchecked(uint32_t idx, const std::vector<int64_t>&);
+    void AddNonNullDoublesUnchecked(uint32_t idx, const std::vector<double>&);
+    void AddNullDoublesUnchecked(uint32_t idx, const std::vector<double>&);
 
     base::StatusOr<std::unique_ptr<RuntimeTable>> Build(uint32_t rows) &&;
 
diff --git a/src/trace_processor/db/table.h b/src/trace_processor/db/table.h
index 1b6e848..e205259 100644
--- a/src/trace_processor/db/table.h
+++ b/src/trace_processor/db/table.h
@@ -160,7 +160,7 @@
   std::optional<OrderedIndices> GetIndex(
       const std::vector<uint32_t>& cols) const {
     for (const auto& idx : indexes_) {
-      if (cols.size() >= idx.columns.size()) {
+      if (cols.size() > idx.columns.size()) {
         continue;
       }
       if (std::equal(cols.begin(), cols.end(), idx.columns.begin())) {
diff --git a/src/trace_processor/export_json_unittest.cc b/src/trace_processor/export_json_unittest.cc
index 5a77efd..08c4148 100644
--- a/src/trace_processor/export_json_unittest.cc
+++ b/src/trace_processor/export_json_unittest.cc
@@ -44,6 +44,7 @@
 #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/track_tracker.h"
+#include "src/trace_processor/importers/common/tracks.h"
 #include "src/trace_processor/importers/proto/track_event_tracker.h"
 #include "src/trace_processor/storage/metadata.h"
 #include "src/trace_processor/storage/stats.h"
@@ -251,8 +252,8 @@
 }
 
 TEST_F(ExportJsonTest, SystemEventsIgnored) {
-  TrackId track = context_.track_tracker->CreateProcessAsyncTrack(
-      /*name=*/kNullStringId, /*upid=*/0, /*source=*/kNullStringId);
+  TrackId track =
+      context_.track_tracker->InternProcessTrack(tracks::unknown, 0);
   context_.args_tracker->Flush();  // Flush track args.
 
   // System events have no category.
@@ -768,8 +769,13 @@
   const char* kName = "name";
 
   // Global legacy track.
-  TrackId track =
-      context_.track_tracker->GetOrCreateLegacyChromeGlobalInstantTrack();
+  TrackId track = context_.track_tracker->InternGlobalTrack(
+      tracks::legacy_chrome_global_instants, TrackTracker::AutoName(),
+      [this](ArgsTracker::BoundInserter& inserter) {
+        inserter.AddArg(
+            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));
@@ -784,7 +790,9 @@
       {kTimestamp2, 0, track2, cat_id, name_id, 0, 0, 0});
 
   // Async event track.
-  track_event_tracker.ReserveDescriptorChildTrack(1234, 0, kNullStringId);
+  TrackEventTracker::DescriptorTrackReservation reservation;
+  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(
@@ -982,11 +990,11 @@
   StringId name3_id = context_.storage->InternString(base::StringView(kName3));
 
   constexpr int64_t kSourceId = 235;
-  TrackId track = context_.track_tracker->InternLegacyChromeAsyncTrack(
+  TrackId track = context_.track_tracker->LegacyInternLegacyChromeAsyncTrack(
       name_id, upid, kSourceId, /*trace_id_is_process_scoped=*/true,
       /*source_scope=*/kNullStringId);
   constexpr int64_t kSourceId2 = 236;
-  TrackId track2 = context_.track_tracker->InternLegacyChromeAsyncTrack(
+  TrackId track2 = context_.track_tracker->LegacyInternLegacyChromeAsyncTrack(
       name3_id, upid, kSourceId2, /*trace_id_is_process_scoped=*/true,
       /*source_scope=*/kNullStringId);
   context_.args_tracker->Flush();  // Flush track args.
@@ -1129,11 +1137,11 @@
   };
 
   constexpr int64_t kSourceId = 235;
-  TrackId track = context_.track_tracker->InternLegacyChromeAsyncTrack(
+  TrackId track = context_.track_tracker->LegacyInternLegacyChromeAsyncTrack(
       name_id, upid, kSourceId, /*trace_id_is_process_scoped=*/true,
       /*source_scope=*/kNullStringId);
   constexpr int64_t kSourceId2 = 236;
-  TrackId track2 = context_.track_tracker->InternLegacyChromeAsyncTrack(
+  TrackId track2 = context_.track_tracker->LegacyInternLegacyChromeAsyncTrack(
       name3_id, upid, kSourceId2, /*trace_id_is_process_scoped=*/true,
       /*source_scope=*/kNullStringId);
   context_.args_tracker->Flush();  // Flush track args.
@@ -1252,7 +1260,7 @@
   StringId name_id = context_.storage->InternString(base::StringView(kName));
 
   constexpr int64_t kSourceId = 235;
-  TrackId track = context_.track_tracker->InternLegacyChromeAsyncTrack(
+  TrackId track = context_.track_tracker->LegacyInternLegacyChromeAsyncTrack(
       name_id, upid, kSourceId, /*trace_id_is_process_scoped=*/true,
       /*source_scope=*/kNullStringId);
   context_.args_tracker->Flush();  // Flush track args.
@@ -1308,7 +1316,7 @@
   StringId name_id = context_.storage->InternString(base::StringView(kName));
 
   constexpr int64_t kSourceId = 235;
-  TrackId track = context_.track_tracker->InternLegacyChromeAsyncTrack(
+  TrackId track = context_.track_tracker->LegacyInternLegacyChromeAsyncTrack(
       name_id, upid, kSourceId, /*trace_id_is_process_scoped=*/true,
       /*source_scope=*/kNullStringId);
   context_.args_tracker->Flush();  // Flush track args.
@@ -1353,7 +1361,7 @@
   StringId name_id = context_.storage->InternString(base::StringView(kName));
 
   constexpr int64_t kSourceId = 235;
-  TrackId track = context_.track_tracker->InternLegacyChromeAsyncTrack(
+  TrackId track = context_.track_tracker->LegacyInternLegacyChromeAsyncTrack(
       name_id, upid, kSourceId, /*trace_id_is_process_scoped=*/true,
       /*source_scope=*/kNullStringId);
   context_.args_tracker->Flush();  // Flush track args.
@@ -1806,7 +1814,8 @@
   const char* kModuleDebugPath = "debugpath";
 
   UniquePid upid = context_.process_tracker->GetOrCreateProcess(kProcessID);
-  TrackId track = context_.track_tracker->InternProcessTrack(upid);
+  TrackId track =
+      context_.track_tracker->InternProcessTrack(tracks::track_event, upid);
   StringId level_of_detail_id =
       context_.storage->InternString(base::StringView(kLevelOfDetail));
   auto snapshot_id = context_.storage->mutable_memory_snapshot_table()
@@ -1816,7 +1825,7 @@
   StringId peak_resident_set_size_id =
       context_.storage->InternString("chrome.peak_resident_set_kb");
   TrackId peak_resident_set_size_counter =
-      context_.track_tracker->InternProcessCounterTrack(
+      context_.track_tracker->LegacyInternProcessCounterTrack(
           peak_resident_set_size_id, upid);
   context_.event_tracker->PushCounter(kTimestamp, kPeakResidentSetSize,
                                       peak_resident_set_size_counter);
@@ -1824,7 +1833,7 @@
   StringId private_footprint_bytes_id =
       context_.storage->InternString("chrome.private_footprint_kb");
   TrackId private_footprint_bytes_counter =
-      context_.track_tracker->InternProcessCounterTrack(
+      context_.track_tracker->LegacyInternProcessCounterTrack(
           private_footprint_bytes_id, upid);
   context_.event_tracker->PushCounter(kTimestamp, kPrivateFootprintBytes,
                                       private_footprint_bytes_counter);
@@ -1927,7 +1936,8 @@
 
   UniquePid os_upid =
       context_.process_tracker->GetOrCreateProcess(kOsProcessID);
-  TrackId track = context_.track_tracker->InternProcessTrack(os_upid);
+  TrackId track =
+      context_.track_tracker->InternProcessTrack(tracks::track_event, os_upid);
   StringId level_of_detail_id =
       context_.storage->InternString(base::StringView(kLevelOfDetail));
   auto snapshot_id = context_.storage->mutable_memory_snapshot_table()
diff --git a/src/trace_processor/forwarding_trace_parser.cc b/src/trace_processor/forwarding_trace_parser.cc
index 70bffd5..2bf7958 100644
--- a/src/trace_processor/forwarding_trace_parser.cc
+++ b/src/trace_processor/forwarding_trace_parser.cc
@@ -18,27 +18,30 @@
 
 #include <memory>
 #include <optional>
+#include <utility>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/basic_types.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/trace_file_tracker.h"
 #include "src/trace_processor/importers/proto/proto_trace_reader.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/storage/stats.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/trace_reader_registry.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/util/status_macros.h"
 #include "src/trace_processor/util/trace_type.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 namespace {
 
 TraceSorter::SortingMode ConvertSortingMode(SortingMode sorting_mode) {
   switch (sorting_mode) {
     case SortingMode::kDefaultHeuristics:
-    case SortingMode::kForceFlushPeriodWindowedSort:
       return TraceSorter::SortingMode::kDefault;
     case SortingMode::kForceFullSort:
       return TraceSorter::SortingMode::kFullSort;
@@ -57,13 +60,18 @@
       return std::nullopt;
 
     case kPerfDataTraceType:
+    case kInstrumentsXmlTraceType:
       return TraceSorter::SortingMode::kDefault;
 
     case kUnknownTraceType:
     case kJsonTraceType:
     case kFuchsiaTraceType:
     case kZipFile:
+    case kTarTraceType:
     case kAndroidLogcatTraceType:
+    case kGeckoTraceType:
+    case kArtMethodTraceType:
+    case kPerfTextTraceType:
       return TraceSorter::SortingMode::kFullSort;
 
     case kProtoTraceType:
@@ -80,10 +88,11 @@
 
 }  // namespace
 
-ForwardingTraceParser::ForwardingTraceParser(TraceProcessorContext* context)
-    : context_(context) {}
+ForwardingTraceParser::ForwardingTraceParser(TraceProcessorContext* context,
+                                             tables::TraceFileTable::Id id)
+    : context_(context), file_id_(id) {}
 
-ForwardingTraceParser::~ForwardingTraceParser() {}
+ForwardingTraceParser::~ForwardingTraceParser() = default;
 
 base::Status ForwardingTraceParser::Init(const TraceBlobView& blob) {
   PERFETTO_CHECK(!reader_);
@@ -98,13 +107,9 @@
     // The UI's error_dialog.ts uses it to make the dialog more graceful.
     return base::ErrStatus("Unknown trace type provided (ERR:fmt)");
   }
-
-  base::StatusOr<std::unique_ptr<ChunkedTraceReader>> reader_or =
-      context_->reader_registry->CreateTraceReader(trace_type_);
-  if (!reader_or.ok()) {
-    return reader_or.status();
-  }
-  reader_ = std::move(*reader_or);
+  context_->trace_file_tracker->StartParsing(file_id_, trace_type_);
+  ASSIGN_OR_RETURN(reader_,
+                   context_->reader_registry->CreateTraceReader(trace_type_));
 
   PERFETTO_DLOG("%s trace detected", TraceTypeToString(trace_type_));
   UpdateSorterForTraceType(trace_type_);
@@ -115,7 +120,6 @@
   if (trace_type_ == kProtoTraceType || trace_type_ == kSystraceTraceType) {
     context_->process_tracker->SetPidZeroIsUpidZeroIdleProcess();
   }
-
   return base::OkStatus();
 }
 
@@ -127,7 +131,26 @@
   }
 
   if (!context_->sorter) {
-    context_->sorter.reset(new TraceSorter(context_, *minimum_sorting_mode));
+    TraceSorter::EventHandling event_handling;
+    switch (context_->config.parsing_mode) {
+      case ParsingMode::kDefault:
+        event_handling = TraceSorter::EventHandling::kSortAndPush;
+        break;
+      case ParsingMode::kTokenizeOnly:
+        event_handling = TraceSorter::EventHandling::kDrop;
+        break;
+      case ParsingMode::kTokenizeAndSort:
+        event_handling = TraceSorter::EventHandling::kSortAndDrop;
+        break;
+    }
+    if (context_->config.enable_dev_features) {
+      auto it = context_->config.dev_flags.find("drop-after-sort");
+      if (it != context_->config.dev_flags.end() && it->second == "true") {
+        event_handling = TraceSorter::EventHandling::kSortAndDrop;
+      }
+    }
+    context_->sorter = std::make_shared<TraceSorter>(
+        context_, *minimum_sorting_mode, event_handling);
   }
 
   switch (context_->sorter->sorting_mode()) {
@@ -146,12 +169,18 @@
   if (!reader_) {
     RETURN_IF_ERROR(Init(blob));
   }
+  trace_size_ += blob.size();
   return reader_->Parse(std::move(blob));
 }
 
 base::Status ForwardingTraceParser::NotifyEndOfFile() {
-  return reader_ ? reader_->NotifyEndOfFile() : base::OkStatus();
+  if (reader_) {
+    RETURN_IF_ERROR(reader_->NotifyEndOfFile());
+  }
+  if (trace_type_ != kUnknownTraceType) {
+    context_->trace_file_tracker->DoneParsing(file_id_, trace_size_);
+  }
+  return base::OkStatus();
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/forwarding_trace_parser.h b/src/trace_processor/forwarding_trace_parser.h
index fa90279..1f4858a 100644
--- a/src/trace_processor/forwarding_trace_parser.h
+++ b/src/trace_processor/forwarding_trace_parser.h
@@ -22,6 +22,7 @@
 #include "perfetto/base/status.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/util/trace_type.h"
 
 namespace perfetto::trace_processor {
@@ -30,7 +31,8 @@
 
 class ForwardingTraceParser : public ChunkedTraceReader {
  public:
-  explicit ForwardingTraceParser(TraceProcessorContext*);
+  explicit ForwardingTraceParser(TraceProcessorContext*,
+                                 tables::TraceFileTable::Id);
   ~ForwardingTraceParser() override;
 
   // ChunkedTraceReader implementation
@@ -43,6 +45,8 @@
   base::Status Init(const TraceBlobView&);
   void UpdateSorterForTraceType(TraceType trace_type);
   TraceProcessorContext* const context_;
+  tables::TraceFileTable::Id file_id_;
+  size_t trace_size_ = 0;
   std::unique_ptr<ChunkedTraceReader> reader_;
   TraceType trace_type_ = kUnknownTraceType;
 };
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 abded5f..07c8434 100644
--- a/src/trace_processor/importers/android_bugreport/android_bugreport_reader.cc
+++ b/src/trace_processor/importers/android_bugreport/android_bugreport_reader.cc
@@ -23,9 +23,9 @@
 #include <string>
 #include <vector>
 
-#include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/string_view.h"
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
 #include "src/trace_processor/importers/android_bugreport/android_dumpstate_reader.h"
 #include "src/trace_processor/importers/android_bugreport/android_log_reader.h"
@@ -38,17 +38,23 @@
 
 namespace perfetto::trace_processor {
 namespace {
-const util::ZipFile* FindBugReportFile(
-    const std::vector<util::ZipFile>& zip_file_entries) {
-  for (const auto& zf : zip_file_entries) {
-    if (base::StartsWith(zf.name(), "bugreport-") &&
-        base::EndsWith(zf.name(), ".txt")) {
-      return &zf;
-    }
-  }
-  return nullptr;
+
+using ZipFileVector = std::vector<util::ZipFile>;
+
+bool IsBugReportFile(const util::ZipFile& zip) {
+  return base::StartsWith(zip.name(), "bugreport-") &&
+         base::EndsWith(zip.name(), ".txt");
 }
 
+bool IsLogFile(const util::ZipFile& file) {
+  return base::StartsWith(file.name(), "FS/data/misc/logd/logcat") &&
+         !base::EndsWith(file.name(), "logcat.id");
+}
+
+// Extracts the year field from the bugreport-xxx.txt file name.
+// This is because logcat events have only the month and day.
+// This is obviously bugged for cases of bugreports collected across new year
+// but we'll live with that.
 std::optional<int32_t> ExtractYearFromBugReportFilename(
     const std::string& filename) {
   // Typical name: "bugreport-product-TP1A.220623.001-2022-06-24-16-24-37.txt".
@@ -57,34 +63,76 @@
   return base::StringToInt32(year_str);
 }
 
+struct FindBugReportFileResult {
+  size_t file_index;
+  int32_t year;
+};
+
+std::optional<FindBugReportFileResult> FindBugReportFile(
+    const ZipFileVector& files) {
+  for (size_t i = 0; i < files.size(); ++i) {
+    if (!IsBugReportFile(files[i])) {
+      continue;
+    }
+    std::optional<int32_t> year =
+        ExtractYearFromBugReportFilename(files[i].name());
+    if (!year.has_value()) {
+      continue;
+    }
+
+    return FindBugReportFileResult{i, *year};
+  }
+
+  return std::nullopt;
+}
+
 }  // namespace
 
 // static
 bool AndroidBugreportReader::IsAndroidBugReport(
-    const std::vector<util::ZipFile>& zip_file_entries) {
-  if (const util::ZipFile* file = FindBugReportFile(zip_file_entries);
-      file != nullptr) {
-    return ExtractYearFromBugReportFilename(file->name()).has_value();
-  }
-
-  return false;
+    const std::vector<util::ZipFile>& files) {
+  return FindBugReportFile(files).has_value();
 }
 
 // static
-util::Status AndroidBugreportReader::Parse(
-    TraceProcessorContext* context,
-    std::vector<util::ZipFile> zip_file_entries) {
-  if (!IsAndroidBugReport(zip_file_entries)) {
+util::Status AndroidBugreportReader::Parse(TraceProcessorContext* context,
+                                           std::vector<util::ZipFile> files) {
+  auto res = FindBugReportFile(files);
+  if (!res.has_value()) {
     return base::ErrStatus("Not a bug report");
   }
-  return AndroidBugreportReader(context, std::move(zip_file_entries))
+
+  // Move the file to the end move it out of the list and pop the back.
+  std::swap(files[res->file_index], files.back());
+  auto id = context->trace_file_tracker->AddFile(files.back().name());
+  BugReportFile bug_report{id, res->year, std::move(files.back())};
+  files.pop_back();
+
+  std::set<LogFile> ordered_log_files;
+  for (size_t i = 0; i < files.size(); ++i) {
+    id = context->trace_file_tracker->AddFile(files[i].name());
+    // Set size in case we end up not parsing this file.
+    context->trace_file_tracker->SetSize(id, files[i].compressed_size());
+    if (!IsLogFile(files[i])) {
+      continue;
+    }
+
+    int64_t timestamp = files[i].GetDatetime();
+    ordered_log_files.insert(LogFile{id, timestamp, std::move(files[i])});
+  }
+
+  return AndroidBugreportReader(context, std::move(bug_report),
+                                std::move(ordered_log_files))
       .ParseImpl();
 }
 
 AndroidBugreportReader::AndroidBugreportReader(
     TraceProcessorContext* context,
-    std::vector<util::ZipFile> zip_file_entries)
-    : context_(context), zip_file_entries_(std::move(zip_file_entries)) {}
+    BugReportFile bug_report,
+    std::set<LogFile> ordered_log_files)
+    : context_(context),
+      bug_report_(std::move(bug_report)),
+      ordered_log_files_(std::move(ordered_log_files)) {}
 
 AndroidBugreportReader::~AndroidBugreportReader() = default;
 
@@ -94,10 +142,6 @@
   // 1970), but that is the state of affairs.
   context_->clock_tracker->SetTraceTimeClock(
       protos::pbzero::BUILTIN_CLOCK_REALTIME);
-  if (!DetectYearAndBrFilename()) {
-    context_->storage->IncrementStats(stats::android_br_parse_errors);
-    return base::ErrStatus("Zip file does not contain bugreport file.");
-  }
 
   ASSIGN_OR_RETURN(std::vector<TimestampedAndroidLogEvent> logcat_events,
                    ParsePersistentLogcat());
@@ -106,74 +150,41 @@
 
 base::Status AndroidBugreportReader::ParseDumpstateTxt(
     std::vector<TimestampedAndroidLogEvent> logcat_events) {
-  PERFETTO_CHECK(dumpstate_file_);
-  ScopedActiveTraceFile trace_file = context_->trace_file_tracker->StartNewFile(
-      dumpstate_file_->name(), kAndroidDumpstateTraceType,
-      dumpstate_file_->uncompressed_size());
-  AndroidDumpstateReader reader(context_, br_year_, std::move(logcat_events));
-  return dumpstate_file_->DecompressLines(
+  context_->trace_file_tracker->StartParsing(bug_report_.id,
+                                             kAndroidDumpstateTraceType);
+  AndroidDumpstateReader reader(context_, bug_report_.year,
+                                std::move(logcat_events));
+  base::Status status = bug_report_.file.DecompressLines(
       [&](const std::vector<base::StringView>& lines) {
         for (const base::StringView& line : lines) {
           reader.ParseLine(line);
         }
       });
+  context_->trace_file_tracker->DoneParsing(
+      bug_report_.id, bug_report_.file.uncompressed_size());
+  return status;
 }
 
 base::StatusOr<std::vector<TimestampedAndroidLogEvent>>
 AndroidBugreportReader::ParsePersistentLogcat() {
-  BufferingAndroidLogReader log_reader(context_, br_year_);
-
-  // Sort files to ease the job of the subsequent line-based sort. Unfortunately
-  // lines within each file are not 100% timestamp-ordered, due to things like
-  // kernel messages where log time != event time.
-  std::vector<std::pair<uint64_t, const util::ZipFile*>> log_files;
-  for (const util::ZipFile& zf : zip_file_entries_) {
-    if (base::StartsWith(zf.name(), "FS/data/misc/logd/logcat") &&
-        !base::EndsWith(zf.name(), "logcat.id")) {
-      log_files.push_back(std::make_pair(zf.GetDatetime(), &zf));
-    }
-  }
-
-  std::sort(log_files.begin(), log_files.end());
+  BufferingAndroidLogReader log_reader(context_, bug_report_.year);
 
   // Push all events into the AndroidLogParser. It will take care of string
   // interning into the pool. Appends entries into `log_events`.
-  for (const auto& log_file : log_files) {
-    ScopedActiveTraceFile trace_file =
-        context_->trace_file_tracker->StartNewFile(
-            log_file.second->name(), kAndroidLogcatTraceType,
-            log_file.second->uncompressed_size());
-    RETURN_IF_ERROR(log_file.second->DecompressLines(
+  for (const auto& log_file : ordered_log_files_) {
+    context_->trace_file_tracker->StartParsing(log_file.id,
+                                               kAndroidLogcatTraceType);
+    RETURN_IF_ERROR(log_file.file.DecompressLines(
         [&](const std::vector<base::StringView>& lines) {
           for (const auto& line : lines) {
             log_reader.ParseLine(line);
           }
         }));
+    context_->trace_file_tracker->DoneParsing(
+        log_file.id, log_file.file.uncompressed_size());
   }
 
   return std::move(log_reader).ConsumeBufferedEvents();
 }
 
-// Populates the `year_` field from the bugreport-xxx.txt file name.
-// This is because logcat events have only the month and day.
-// This is obviously bugged for cases of bugreports collected across new year
-// but we'll live with that.
-bool AndroidBugreportReader::DetectYearAndBrFilename() {
-  const util::ZipFile* br_file = FindBugReportFile(zip_file_entries_);
-  if (!br_file) {
-    PERFETTO_ELOG("Could not find bugreport-*.txt in the zip file");
-    return false;
-  }
-
-  std::optional<int32_t> year =
-      ExtractYearFromBugReportFilename(br_file->name());
-  if (!year.has_value()) {
-    PERFETTO_ELOG("Could not parse the year from %s", br_file->name().c_str());
-    return false;
-  }
-  br_year_ = *year;
-  dumpstate_file_ = br_file;
-  return true;
-}
-
 }  // namespace perfetto::trace_processor
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 ddcec9a..b36c45c 100644
--- a/src/trace_processor/importers/android_bugreport/android_bugreport_reader.h
+++ b/src/trace_processor/importers/android_bugreport/android_bugreport_reader.h
@@ -19,12 +19,14 @@
 
 #include <cstddef>
 #include <cstdint>
+#include <set>
 #include <vector>
 
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/status_or.h"
 #include "perfetto/trace_processor/status.h"
 #include "src/trace_processor/importers/android_bugreport/android_log_reader.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/util/zip_reader.h"
 
 namespace perfetto ::trace_processor {
@@ -44,22 +46,41 @@
                             std::vector<util::ZipFile> zip_file_entries);
 
  private:
+  struct BugReportFile {
+    tables::TraceFileTable::Id id;
+    int32_t year;
+    util::ZipFile file;
+  };
+  struct LogFile {
+    tables::TraceFileTable::Id id;
+    int64_t timestamp;
+    util::ZipFile file;
+    // Sort files to ease the job of the line-based sort. Unfortunately
+    // lines within each file are not 100% timestamp-ordered, due to things like
+    // kernel messages where log time != event time.
+    bool operator<(const LogFile& other) const {
+      return timestamp < other.timestamp;
+    }
+  };
+
+  static std::optional<BugReportFile> ExtractBugReportFile(
+      std::vector<util::ZipFile>& vector);
+
   AndroidBugreportReader(TraceProcessorContext* context,
-                         std::vector<util::ZipFile> zip_file_entries);
+                         BugReportFile bug_report,
+                         std::set<LogFile> ordered_log_files);
   ~AndroidBugreportReader();
   util::Status ParseImpl();
 
-  bool DetectYearAndBrFilename();
   base::StatusOr<std::vector<TimestampedAndroidLogEvent>>
   ParsePersistentLogcat();
   base::Status ParseDumpstateTxt(std::vector<TimestampedAndroidLogEvent>);
 
   TraceProcessorContext* const context_;
-  std::vector<util::ZipFile> zip_file_entries_;
-  int32_t br_year_ = 0;  // The year when the bugreport has been taken.
-  const util::ZipFile* dumpstate_file_ =
-      nullptr;  // The bugreport-xxx-2022-08-04....txt file
-  std::string build_fpr_;
+  BugReportFile bug_report_;
+  // Log files conveniently sorted by their file timestamp (see operator< in
+  // LogFile)
+  std::set<LogFile> ordered_log_files_;
 };
 
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/android_bugreport/android_log_event.cc b/src/trace_processor/importers/android_bugreport/android_log_event.cc
index fe1ce77..d9c7a5d 100644
--- a/src/trace_processor/importers/android_bugreport/android_log_event.cc
+++ b/src/trace_processor/importers/android_bugreport/android_log_event.cc
@@ -17,6 +17,7 @@
 #include "src/trace_processor/importers/android_bugreport/android_log_event.h"
 
 #include <algorithm>
+#include <cstddef>
 #include <cstdint>
 #include <optional>
 #include <vector>
@@ -35,8 +36,7 @@
 
   for (; ptr != end; ++ptr) {
     if (*ptr == '\n') {
-      lines.push_back(
-          base::StringView(line_start, static_cast<size_t>(ptr - line_start)));
+      lines.emplace_back(line_start, static_cast<size_t>(ptr - line_start));
       line_start = ptr + 1;
     }
   }
diff --git a/src/trace_processor/importers/archive/BUILD.gn b/src/trace_processor/importers/archive/BUILD.gn
new file mode 100644
index 0000000..4f092ad
--- /dev/null
+++ b/src/trace_processor/importers/archive/BUILD.gn
@@ -0,0 +1,45 @@
+# 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.
+
+source_set("archive") {
+  sources = [
+    "archive_entry.cc",
+    "archive_entry.h",
+    "gzip_trace_parser.cc",
+    "gzip_trace_parser.h",
+    "tar_trace_reader.cc",
+    "tar_trace_reader.h",
+    "zip_trace_reader.cc",
+    "zip_trace_reader.h",
+  ]
+  deps = [
+    "../..:storage_minimal",
+    "../../../../gn:default_deps",
+    "../../../../include/perfetto/base:base",
+    "../../../../include/perfetto/ext/base:base",
+    "../../../base",
+    "../../../trace_processor:storage_minimal",
+    "../../storage",
+    "../../tables:tables_python",
+    "../../types",
+    "../../util",
+    "../../util:gzip",
+    "../../util:trace_blob_view_reader",
+    "../../util:trace_type",
+    "../../util:zip_reader",
+    "../android_bugreport",
+    "../common",
+    "../proto:minimal",
+  ]
+}
diff --git a/src/trace_processor/importers/archive/archive_entry.cc b/src/trace_processor/importers/archive/archive_entry.cc
new file mode 100644
index 0000000..1d187df
--- /dev/null
+++ b/src/trace_processor/importers/archive/archive_entry.cc
@@ -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.
+ */
+
+#include "src/trace_processor/importers/archive/archive_entry.h"
+
+#include <tuple>
+
+namespace perfetto::trace_processor {
+
+bool ArchiveEntry::operator<(const ArchiveEntry& rhs) const {
+  // Traces with symbols should be the last ones to be read.
+  // TODO(carlscab): Proto traces with just ModuleSymbols packets should be an
+  // exception. We actually need those are the very end (once whe have all the
+  // Frames). Alternatively we could build a map address -> symbol during
+  // tokenization and use this during parsing to resolve symbols.
+  if (trace_type == kSymbolsTraceType) {
+    return false;
+  }
+  if (rhs.trace_type == kSymbolsTraceType) {
+    return true;
+  }
+
+  // Proto traces should always parsed first as they might contains clock sync
+  // data needed to correctly parse other traces.
+  if (rhs.trace_type == TraceType::kProtoTraceType) {
+    return false;
+  }
+  if (trace_type == TraceType::kProtoTraceType) {
+    return true;
+  }
+
+  if (rhs.trace_type == TraceType::kGzipTraceType) {
+    return false;
+  }
+  if (trace_type == TraceType::kGzipTraceType) {
+    return true;
+  }
+
+  return std::tie(name, index) < std::tie(rhs.name, rhs.index);
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/archive/archive_entry.h b/src/trace_processor/importers/archive/archive_entry.h
new file mode 100644
index 0000000..ae13cf7
--- /dev/null
+++ b/src/trace_processor/importers/archive/archive_entry.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_ARCHIVE_ARCHIVE_ENTRY_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ARCHIVE_ARCHIVE_ENTRY_H_
+
+#include <string>
+
+#include "src/trace_processor/util/trace_type.h"
+
+namespace perfetto::trace_processor {
+
+// Helper class to determine a proper tokenization. This class can be used as
+// a key of a std::map to automatically sort files before sending them in proper
+// order for tokenization.
+struct ArchiveEntry {
+  // File name. Used to break ties.
+  std::string name;
+  // Position. Used to break ties.
+  size_t index;
+  // Trace type. This is the main attribute traces are ordered by. Proto
+  // traces are always parsed first as they might contains clock sync
+  // data needed to correctly parse other traces.
+  TraceType trace_type;
+  // Comparator used to determine the order in which files in the ZIP will be
+  // read.
+  bool operator<(const ArchiveEntry& rhs) const;
+};
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_ARCHIVE_ARCHIVE_ENTRY_H_
diff --git a/src/trace_processor/importers/archive/gzip_trace_parser.cc b/src/trace_processor/importers/archive/gzip_trace_parser.cc
new file mode 100644
index 0000000..bc4074a
--- /dev/null
+++ b/src/trace_processor/importers/archive/gzip_trace_parser.cc
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 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
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/archive/gzip_trace_parser.h"
+
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/trace_processor/trace_blob.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/forwarding_trace_parser.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/importers/common/trace_file_tracker.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/gzip_utils.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto::trace_processor {
+
+namespace {
+
+using ResultCode = util::GzipDecompressor::ResultCode;
+
+}  // namespace
+
+GzipTraceParser::GzipTraceParser(TraceProcessorContext* context)
+    : context_(context) {}
+
+GzipTraceParser::GzipTraceParser(std::unique_ptr<ChunkedTraceReader> reader)
+    : context_(nullptr), inner_(std::move(reader)) {}
+
+GzipTraceParser::~GzipTraceParser() = default;
+
+base::Status GzipTraceParser::Parse(TraceBlobView blob) {
+  return ParseUnowned(blob.data(), blob.size());
+}
+
+base::Status GzipTraceParser::ParseUnowned(const uint8_t* data, size_t size) {
+  const uint8_t* start = data;
+  size_t len = size;
+
+  if (!inner_) {
+    PERFETTO_CHECK(context_);
+    inner_.reset(new ForwardingTraceParser(
+        context_, context_->trace_file_tracker->AddFile("")));
+  }
+
+  if (!first_chunk_parsed_) {
+    // .ctrace files begin with: "TRACE:\n" or "done. TRACE:\n" strip this if
+    // present.
+    base::StringView beginning(reinterpret_cast<const char*>(start), size);
+
+    static const char* kSystraceFileHeader = "TRACE:\n";
+    size_t offset = Find(kSystraceFileHeader, beginning);
+    if (offset != std::string::npos) {
+      start += strlen(kSystraceFileHeader) + offset;
+      len -= strlen(kSystraceFileHeader) + offset;
+    }
+    first_chunk_parsed_ = true;
+  }
+
+  // Our default uncompressed buffer size is 32MB as it allows for good
+  // throughput.
+  constexpr size_t kUncompressedBufferSize = 32ul * 1024 * 1024;
+  decompressor_.Feed(start, len);
+
+  for (;;) {
+    if (!buffer_) {
+      buffer_.reset(new uint8_t[kUncompressedBufferSize]);
+      bytes_written_ = 0;
+    }
+
+    auto result =
+        decompressor_.ExtractOutput(buffer_.get() + bytes_written_,
+                                    kUncompressedBufferSize - bytes_written_);
+    util::GzipDecompressor::ResultCode ret = result.ret;
+    if (ret == ResultCode::kError)
+      return base::ErrStatus("Failed to decompress trace chunk");
+
+    if (ret == ResultCode::kNeedsMoreInput) {
+      PERFETTO_DCHECK(result.bytes_written == 0);
+      return base::OkStatus();
+    }
+    bytes_written_ += result.bytes_written;
+    output_state_ = kMidStream;
+
+    if (bytes_written_ == kUncompressedBufferSize || ret == ResultCode::kEof) {
+      TraceBlob blob =
+          TraceBlob::TakeOwnership(std::move(buffer_), bytes_written_);
+      RETURN_IF_ERROR(inner_->Parse(TraceBlobView(std::move(blob))));
+    }
+
+    // We support multiple gzip streams in a single gzip file (which is valid
+    // according to RFC1952 section 2.2): in that case, we just need to reset
+    // the decompressor to begin processing the next stream: all other variables
+    // can be preserved.
+    if (ret == ResultCode::kEof) {
+      decompressor_.Reset();
+      output_state_ = kStreamBoundary;
+
+      if (decompressor_.AvailIn() == 0) {
+        return base::OkStatus();
+      }
+    }
+  }
+}
+
+base::Status GzipTraceParser::NotifyEndOfFile() {
+  if (output_state_ != kStreamBoundary || decompressor_.AvailIn() > 0) {
+    return base::ErrStatus("GZIP stream incomplete, trace is likely corrupt");
+  }
+  PERFETTO_CHECK(!buffer_);
+  return inner_ ? inner_->NotifyEndOfFile() : base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/archive/gzip_trace_parser.h b/src/trace_processor/importers/archive/gzip_trace_parser.h
new file mode 100644
index 0000000..17fdc4c
--- /dev/null
+++ b/src/trace_processor/importers/archive/gzip_trace_parser.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 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
+ *
+ *      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_ARCHIVE_GZIP_TRACE_PARSER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ARCHIVE_GZIP_TRACE_PARSER_H_
+
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+
+#include "perfetto/base/status.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/util/gzip_utils.h"
+
+namespace perfetto::trace_processor {
+
+class TraceProcessorContext;
+
+class GzipTraceParser : public ChunkedTraceReader {
+ public:
+  explicit GzipTraceParser(TraceProcessorContext*);
+  explicit GzipTraceParser(std::unique_ptr<ChunkedTraceReader>);
+  ~GzipTraceParser() override;
+
+  // ChunkedTraceReader implementation
+  base::Status Parse(TraceBlobView) override;
+  base::Status NotifyEndOfFile() override;
+
+  base::Status ParseUnowned(const uint8_t*, size_t);
+
+ private:
+  TraceProcessorContext* const context_;
+  util::GzipDecompressor decompressor_;
+  std::unique_ptr<ChunkedTraceReader> inner_;
+
+  std::unique_ptr<uint8_t[]> buffer_;
+  size_t bytes_written_ = 0;
+
+  bool first_chunk_parsed_ = false;
+  enum { kStreamBoundary, kMidStream } output_state_ = kStreamBoundary;
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_ARCHIVE_GZIP_TRACE_PARSER_H_
diff --git a/src/trace_processor/importers/archive/tar_trace_reader.cc b/src/trace_processor/importers/archive/tar_trace_reader.cc
new file mode 100644
index 0000000..d0cf11f
--- /dev/null
+++ b/src/trace_processor/importers/archive/tar_trace_reader.cc
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/archive/tar_trace_reader.h"
+
+#include <algorithm>
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/forwarding_trace_parser.h"
+#include "src/trace_processor/importers/archive/archive_entry.h"
+#include "src/trace_processor/importers/common/trace_file_tracker.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/status_macros.h"
+#include "src/trace_processor/util/trace_type.h"
+
+namespace perfetto::trace_processor {
+namespace {
+
+constexpr char kUstarMagic[] = {'u', 's', 't', 'a', 'r', '\0'};
+constexpr char kGnuMagic[] = {'u', 's', 't', 'a', 'r', ' ', ' ', '\0'};
+
+constexpr char TYPE_FLAG_REGULAR = '0';
+constexpr char TYPE_FLAG_AREGULAR = '\0';
+constexpr char TYPE_FLAG_GNU_LONG_NAME = 'L';
+
+template <size_t Size>
+std::optional<uint64_t> ExtractUint64(const char (&ptr)[Size]) {
+  static_assert(Size <= 64 / 3);
+  if (*ptr == 0) {
+    return std::nullopt;
+  }
+  uint64_t value = 0;
+  for (size_t i = 0; i < Size && ptr[i] != 0; ++i) {
+    if (ptr[i] > '7' || ptr[i] < '0') {
+      return std::nullopt;
+    }
+    value <<= 3;
+    value += static_cast<uint64_t>(ptr[i] - '0');
+  }
+  return value;
+}
+
+enum class TarType { kUnknown, kUstar, kGnu };
+
+struct alignas(1) Header {
+  char name[100];
+  char mode[8];
+  char uid[8];
+  char gid[8];
+  char size[12];
+  char mtime[12];
+  char checksum[8];
+  char type_flag[1];
+  char link_name[100];
+  union {
+    struct UstarMagic {
+      char magic[6];
+      char version[2];
+    } ustar;
+    char gnu[8];
+  } magic;
+  char user_name[32];
+  char group_name[32];
+  char dev_major[8];
+  char dev_minor[8];
+  char prefix[155];
+  char padding[12];
+
+  TarType GetTarFileType() const {
+    if (memcmp(magic.gnu, kGnuMagic, sizeof(kGnuMagic)) == 0) {
+      return TarType::kGnu;
+    }
+    if (memcmp(magic.ustar.magic, kUstarMagic, sizeof(kUstarMagic)) == 0) {
+      return TarType::kUstar;
+    }
+    return TarType::kUnknown;
+  }
+};
+
+constexpr size_t kHeaderSize = 512;
+static_assert(sizeof(Header) == kHeaderSize);
+
+bool IsAllZeros(const TraceBlobView& data) {
+  const uint8_t* start = data.data();
+  const uint8_t* end = data.data() + data.size();
+  return std::find_if(start, end, [](uint8_t v) { return v != 0; }) == end;
+}
+
+template <size_t Size>
+std::string ExtractString(const char (&start)[Size]) {
+  const char* end = start + Size;
+  end = std::find(start, end, 0);
+  return std::string(start, end);
+}
+
+}  // namespace
+
+TarTraceReader::TarTraceReader(TraceProcessorContext* context)
+    : context_(context) {}
+
+TarTraceReader::~TarTraceReader() = default;
+
+util::Status TarTraceReader::Parse(TraceBlobView blob) {
+  ParseResult result = ParseResult::kOk;
+  buffer_.PushBack(std::move(blob));
+  while (!buffer_.empty() && result == ParseResult::kOk) {
+    switch (state_) {
+      case State::kMetadata:
+      case State::kZeroMetadata: {
+        ASSIGN_OR_RETURN(result, ParseMetadata());
+        break;
+      }
+      case State::kContent: {
+        ASSIGN_OR_RETURN(result, ParseContent());
+        break;
+      }
+      case State::kDone:
+        // We are done, ignore any more data
+        buffer_.PopFrontUntil(buffer_.end_offset());
+    }
+  }
+  return base::OkStatus();
+}
+
+base::Status TarTraceReader::NotifyEndOfFile() {
+  if (state_ != State::kDone) {
+    return base::ErrStatus("Premature end of TAR file");
+  }
+
+  for (auto& file : ordered_files_) {
+    auto chunk_reader =
+        std::make_unique<ForwardingTraceParser>(context_, file.second.id);
+    auto& parser = *chunk_reader;
+    context_->chunk_readers.push_back(std::move(chunk_reader));
+
+    for (auto& data : file.second.data) {
+      RETURN_IF_ERROR(parser.Parse(std::move(data)));
+    }
+    RETURN_IF_ERROR(parser.NotifyEndOfFile());
+    // Make sure the ForwardingTraceParser determined the same trace type as we
+    // did.
+    PERFETTO_CHECK(parser.trace_type() == file.first.trace_type);
+  }
+
+  return base::OkStatus();
+}
+
+base::StatusOr<TarTraceReader::ParseResult> TarTraceReader::ParseMetadata() {
+  PERFETTO_CHECK(!metadata_.has_value());
+  auto blob = buffer_.SliceOff(buffer_.start_offset(), kHeaderSize);
+  if (!blob) {
+    return ParseResult::kNeedsMoreData;
+  }
+  buffer_.PopFrontBytes(kHeaderSize);
+  const Header& header = *reinterpret_cast<const Header*>(blob->data());
+
+  TarType type = header.GetTarFileType();
+
+  if (type == TarType::kUnknown) {
+    if (!IsAllZeros(*blob)) {
+      return base::ErrStatus("Invalid magic value");
+    }
+    // EOF is signaled by two consecutive zero headers.
+    if (state_ == State::kMetadata) {
+      // Fist time we see all zeros. NExt parser loop will enter ParseMetadata
+      // again and decide whether it is the real end or maybe a ral header
+      // comes.
+      state_ = State::kZeroMetadata;
+    } else {
+      // Previous header was zeros, thus we are done.
+      PERFETTO_CHECK(state_ == State::kZeroMetadata);
+      state_ = State::kDone;
+    }
+    return ParseResult::kOk;
+  }
+
+  if (type == TarType::kUstar && (header.magic.ustar.version[0] != '0' ||
+                                  header.magic.ustar.version[1] != '0')) {
+    return base::ErrStatus("Invalid version: %c%c",
+                           header.magic.ustar.version[0],
+                           header.magic.ustar.version[1]);
+  }
+
+  std::optional<uint64_t> size = ExtractUint64(header.size);
+  if (!size.has_value()) {
+    return base::ErrStatus("Failed to parse octal size field.");
+  }
+
+  metadata_.emplace();
+  metadata_->size = *size;
+  metadata_->type_flag = *header.type_flag;
+
+  if (long_name_) {
+    metadata_->name = std::move(*long_name_);
+    long_name_.reset();
+  } else {
+    metadata_->name =
+        ExtractString(header.prefix) + "/" + ExtractString(header.name);
+  }
+
+  switch (metadata_->type_flag) {
+    case TYPE_FLAG_REGULAR:
+    case TYPE_FLAG_AREGULAR:
+    case TYPE_FLAG_GNU_LONG_NAME:
+      state_ = State::kContent;
+      break;
+
+    default:
+      if (metadata_->size != 0) {
+        return base::ErrStatus("Unsupported file type: 0x%02x",
+                               metadata_->type_flag);
+      }
+      state_ = State::kMetadata;
+      break;
+  }
+
+  return ParseResult::kOk;
+}
+
+base::StatusOr<TarTraceReader::ParseResult> TarTraceReader::ParseContent() {
+  PERFETTO_CHECK(metadata_.has_value());
+
+  size_t data_and_padding_size = base::AlignUp(metadata_->size, kHeaderSize);
+  if (buffer_.avail() < data_and_padding_size) {
+    return ParseResult::kNeedsMoreData;
+  }
+
+  if (metadata_->type_flag == TYPE_FLAG_GNU_LONG_NAME) {
+    TraceBlobView data =
+        *buffer_.SliceOff(buffer_.start_offset(), metadata_->size);
+    long_name_ = std::string(reinterpret_cast<const char*>(data.data()),
+                             metadata_->size);
+  } else {
+    AddFile(*metadata_,
+            *buffer_.SliceOff(
+                buffer_.start_offset(),
+                std::min(static_cast<uint64_t>(512), metadata_->size)),
+            buffer_.MultiSliceOff(buffer_.start_offset(), metadata_->size));
+  }
+
+  buffer_.PopFrontBytes(data_and_padding_size);
+
+  metadata_.reset();
+  state_ = State::kMetadata;
+  return ParseResult::kOk;
+}
+
+void TarTraceReader::AddFile(const Metadata& metadata,
+                             TraceBlobView header,
+                             std::vector<TraceBlobView> data) {
+  auto file_id = context_->trace_file_tracker->AddFile(metadata.name);
+  context_->trace_file_tracker->SetSize(file_id, metadata.size);
+  ordered_files_.emplace(
+      ArchiveEntry{metadata.name, ordered_files_.size(),
+                   GuessTraceType(header.data(), header.size())},
+      File{file_id, std::move(data)});
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/archive/tar_trace_reader.h b/src/trace_processor/importers/archive/tar_trace_reader.h
new file mode 100644
index 0000000..6760774
--- /dev/null
+++ b/src/trace_processor/importers/archive/tar_trace_reader.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_ARCHIVE_TAR_TRACE_READER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ARCHIVE_TAR_TRACE_READER_H_
+
+#include <cstddef>
+#include <cstdint>
+#include <map>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/archive/archive_entry.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
+#include "src/trace_processor/util/trace_blob_view_reader.h"
+
+namespace perfetto::trace_processor {
+
+class TraceProcessorContext;
+
+class TarTraceReader : public ChunkedTraceReader {
+ public:
+  explicit TarTraceReader(TraceProcessorContext*);
+  ~TarTraceReader() override;
+
+  // ChunkedTraceReader implementation
+  base::Status Parse(TraceBlobView) override;
+  base::Status NotifyEndOfFile() override;
+
+ private:
+  struct Metadata {
+    std::string name;
+    uint64_t size;
+    char type_flag;
+  };
+  enum class ParseResult {
+    kOk,
+    kNeedsMoreData,
+  };
+
+  struct File {
+    tables::TraceFileTable::Id id;
+    std::vector<TraceBlobView> data;
+  };
+
+  enum class State { kMetadata, kContent, kZeroMetadata, kDone };
+
+  base::StatusOr<ParseResult> ParseMetadata();
+  base::StatusOr<ParseResult> ParseContent();
+  base::StatusOr<ParseResult> ParseLongName();
+  base::StatusOr<ParseResult> ParsePadding();
+
+  void AddFile(const Metadata& metadata,
+               TraceBlobView header,
+               std::vector<TraceBlobView> data);
+
+  TraceProcessorContext* const context_;
+  State state_{State::kMetadata};
+  util::TraceBlobViewReader buffer_;
+  std::optional<Metadata> metadata_;
+  std::optional<std::string> long_name_;
+  std::map<ArchiveEntry, File> ordered_files_;
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_ARCHIVE_TAR_TRACE_READER_H_
diff --git a/src/trace_processor/importers/archive/zip_trace_reader.cc b/src/trace_processor/importers/archive/zip_trace_reader.cc
new file mode 100644
index 0000000..6077d00
--- /dev/null
+++ b/src/trace_processor/importers/archive/zip_trace_reader.cc
@@ -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.
+ */
+
+#include "src/trace_processor/importers/archive/zip_trace_reader.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/trace_processor/trace_blob.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/forwarding_trace_parser.h"
+#include "src/trace_processor/importers/android_bugreport/android_bugreport_reader.h"
+#include "src/trace_processor/importers/archive/archive_entry.h"
+#include "src/trace_processor/importers/common/trace_file_tracker.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/status_macros.h"
+#include "src/trace_processor/util/trace_type.h"
+#include "src/trace_processor/util/zip_reader.h"
+
+namespace perfetto::trace_processor {
+
+ZipTraceReader::ZipTraceReader(TraceProcessorContext* context)
+    : context_(context) {}
+ZipTraceReader::~ZipTraceReader() = default;
+
+base::Status ZipTraceReader::Parse(TraceBlobView blob) {
+  return zip_reader_.Parse(std::move(blob));
+}
+
+base::Status ZipTraceReader::NotifyEndOfFile() {
+  std::vector<util::ZipFile> files = zip_reader_.TakeFiles();
+
+  // Android bug reports are ZIP files and its files do not get handled
+  // separately.
+  if (AndroidBugreportReader::IsAndroidBugReport(files)) {
+    return AndroidBugreportReader::Parse(context_, std::move(files));
+  }
+
+  // TODO(carlscab): There is a lot of unnecessary copying going on here.
+  // ZipTraceReader can directly parse the ZIP file and given that we know the
+  // decompressed size we could directly decompress into TraceBlob chunks and
+  // send them to the tokenizer.
+  std::vector<uint8_t> buffer;
+  std::map<ArchiveEntry, File> ordered_files;
+  for (size_t i = 0; i < files.size(); ++i) {
+    util::ZipFile& zip_file = files[i];
+    auto id = context_->trace_file_tracker->AddFile(zip_file.name());
+    context_->trace_file_tracker->SetSize(id, zip_file.compressed_size());
+    RETURN_IF_ERROR(files[i].Decompress(&buffer));
+    TraceBlobView data(TraceBlob::CopyFrom(buffer.data(), buffer.size()));
+    ArchiveEntry entry{zip_file.name(), i,
+                       GuessTraceType(data.data(), data.size())};
+    ordered_files.emplace(entry, File{id, std::move(data)});
+  }
+
+  for (auto& file : ordered_files) {
+    auto chunk_reader =
+        std::make_unique<ForwardingTraceParser>(context_, file.second.id);
+    auto& parser = *chunk_reader;
+    context_->chunk_readers.push_back(std::move(chunk_reader));
+
+    RETURN_IF_ERROR(parser.Parse(std::move(file.second.data)));
+    RETURN_IF_ERROR(parser.NotifyEndOfFile());
+    // Make sure the ForwardingTraceParser determined the same trace type as we
+    // did.
+    PERFETTO_CHECK(parser.trace_type() == file.first.trace_type);
+  }
+
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/archive/zip_trace_reader.h b/src/trace_processor/importers/archive/zip_trace_reader.h
new file mode 100644
index 0000000..d36fd5c
--- /dev/null
+++ b/src/trace_processor/importers/archive/zip_trace_reader.h
@@ -0,0 +1,57 @@
+/*
+ * 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_ARCHIVE_ZIP_TRACE_READER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ARCHIVE_ZIP_TRACE_READER_H_
+
+#include <cstddef>
+#include <map>
+
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/archive/archive_entry.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
+#include "src/trace_processor/util/zip_reader.h"
+
+namespace perfetto::trace_processor {
+
+class ForwardingTraceParser;
+class TraceProcessorContext;
+
+// Forwards files contained in a ZIP to the appropiate ChunkedTraceReader. It is
+// guaranteed that proto traces will be parsed first.
+class ZipTraceReader : public ChunkedTraceReader {
+ public:
+  explicit ZipTraceReader(TraceProcessorContext* context);
+  ~ZipTraceReader() override;
+
+  // ChunkedTraceReader implementation
+  base::Status Parse(TraceBlobView) override;
+  base::Status NotifyEndOfFile() override;
+
+ private:
+  struct File {
+    tables::TraceFileTable::Id id;
+    TraceBlobView data;
+  };
+  TraceProcessorContext* const context_;
+  util::ZipReader zip_reader_;
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_ARCHIVE_ZIP_TRACE_READER_H_
diff --git a/src/trace_processor/importers/art_method/BUILD.gn b/src/trace_processor/importers/art_method/BUILD.gn
new file mode 100644
index 0000000..57f6c62
--- /dev/null
+++ b/src/trace_processor/importers/art_method/BUILD.gn
@@ -0,0 +1,45 @@
+# 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("../../../../gn/test.gni")
+
+source_set("art_method_event") {
+  sources = [ "art_method_event.h" ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../../containers",
+  ]
+}
+
+source_set("art_method") {
+  sources = [
+    "art_method_parser_impl.cc",
+    "art_method_parser_impl.h",
+    "art_method_tokenizer.cc",
+    "art_method_tokenizer.h",
+  ]
+  deps = [
+    ":art_method_event",
+    "../../../../gn:default_deps",
+    "../../../../protos/perfetto/common:zero",
+    "../../../base",
+    "../../containers",
+    "../../importers/common",
+    "../../sorter",
+    "../../storage",
+    "../../types",
+    "../../util",
+    "../../util:trace_blob_view_reader",
+  ]
+}
diff --git a/src/trace_processor/importers/art_method/art_method_event.h b/src/trace_processor/importers/art_method/art_method_event.h
new file mode 100644
index 0000000..4ad10a7
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_event.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_ART_METHOD_ART_METHOD_EVENT_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_EVENT_H_
+
+#include <cstdint>
+#include <optional>
+
+#include "src/trace_processor/containers/string_pool.h"
+
+namespace perfetto::trace_processor::art_method {
+
+struct alignas(8) ArtMethodEvent {
+  uint32_t tid;
+  std::optional<StringPool::Id> comm;
+  StringPool::Id method;
+  enum { kEnter, kExit } action;
+  std::optional<StringPool::Id> pathname;
+  std::optional<uint32_t> line_number;
+};
+
+}  // namespace perfetto::trace_processor::art_method
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_EVENT_H_
diff --git a/src/trace_processor/importers/art_method/art_method_parser_impl.cc b/src/trace_processor/importers/art_method/art_method_parser_impl.cc
new file mode 100644
index 0000000..2d55095
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_parser_impl.cc
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/art_method/art_method_parser_impl.h"
+
+#include <cstdint>
+
+#include "src/trace_processor/importers/art_method/art_method_event.h"
+#include "src/trace_processor/importers/common/args_tracker.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/stack_profile_tracker.h"
+#include "src/trace_processor/importers/common/track_tracker.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/types/variadic.h"
+
+namespace perfetto::trace_processor::art_method {
+
+ArtMethodParserImpl::ArtMethodParserImpl(TraceProcessorContext* context)
+    : context_(context),
+      pathname_id_(context->storage->InternString("pathname")),
+      line_number_id_(context->storage->InternString("line_number")) {}
+
+ArtMethodParserImpl::~ArtMethodParserImpl() = default;
+
+void ArtMethodParserImpl::ParseArtMethodEvent(int64_t ts, ArtMethodEvent e) {
+  UniqueTid utid = context_->process_tracker->GetOrCreateThread(e.tid);
+  if (e.comm) {
+    context_->process_tracker->UpdateThreadNameAndMaybeProcessName(
+        e.tid, *e.comm, ThreadNamePriority::kOther);
+  }
+  TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
+  switch (e.action) {
+    case ArtMethodEvent::kEnter:
+      context_->slice_tracker->Begin(
+          ts, track_id, kNullStringId, e.method,
+          [this, &e](ArgsTracker::BoundInserter* i) {
+            if (e.pathname) {
+              i->AddArg(pathname_id_, Variadic::String(*e.pathname));
+            }
+            if (e.line_number) {
+              i->AddArg(line_number_id_, Variadic::Integer(*e.line_number));
+            }
+          });
+      break;
+    case ArtMethodEvent::kExit:
+      context_->slice_tracker->End(ts, track_id);
+      break;
+  }
+}
+
+}  // namespace perfetto::trace_processor::art_method
diff --git a/src/trace_processor/importers/art_method/art_method_parser_impl.h b/src/trace_processor/importers/art_method/art_method_parser_impl.h
new file mode 100644
index 0000000..09759cd
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_parser_impl.h
@@ -0,0 +1,45 @@
+/*
+ * 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_ART_METHOD_ART_METHOD_PARSER_IMPL_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_PARSER_IMPL_H_
+
+#include <cstdint>
+
+#include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/importers/art_method/art_method_event.h"
+#include "src/trace_processor/importers/common/trace_parser.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::art_method {
+
+class ArtMethodParserImpl : public ArtMethodParser {
+ public:
+  explicit ArtMethodParserImpl(TraceProcessorContext*);
+  ~ArtMethodParserImpl() override;
+
+  void ParseArtMethodEvent(int64_t ts, ArtMethodEvent) override;
+
+ private:
+  TraceProcessorContext* const context_;
+
+  StringPool::Id pathname_id_;
+  StringPool::Id line_number_id_;
+};
+
+}  // namespace perfetto::trace_processor::art_method
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_PARSER_IMPL_H_
diff --git a/src/trace_processor/importers/art_method/art_method_tokenizer.cc b/src/trace_processor/importers/art_method/art_method_tokenizer.cc
new file mode 100644
index 0000000..bdcbd1d
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_tokenizer.cc
@@ -0,0 +1,623 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/art_method/art_method_tokenizer.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_splitter.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/ext/base/utils.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/art_method/art_method_event.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/status_macros.h"
+#include "src/trace_processor/util/trace_blob_view_reader.h"
+
+#include "protos/perfetto/common/builtin_clock.pbzero.h"
+
+namespace perfetto::trace_processor::art_method {
+namespace {
+
+constexpr uint32_t kTraceMagic = 0x574f4c53;  // 'SLOW'
+constexpr uint32_t kStreamingVersionMask = 0xF0U;
+constexpr uint32_t kTraceHeaderLength = 32;
+
+constexpr uint32_t kMethodsCode = 1;
+constexpr uint32_t kThreadsCode = 2;
+constexpr uint32_t kSummaryCode = 3;
+
+std::string_view ToStringView(const TraceBlobView& tbv) {
+  return {reinterpret_cast<const char*>(tbv.data()), tbv.size()};
+}
+
+std::string ConstructPathname(const std::string& class_name,
+                              const std::string& pathname) {
+  size_t index = class_name.rfind('/');
+  if (index != std::string::npos && base::EndsWith(pathname, ".java")) {
+    return class_name.substr(0, index + 1) + pathname;
+  }
+  return pathname;
+}
+
+uint64_t ToLong(const TraceBlobView& tbv) {
+  uint64_t x = 0;
+  memcpy(base::AssumeLittleEndian(&x), tbv.data(), tbv.size());
+  return x;
+}
+
+uint32_t ToInt(const TraceBlobView& tbv) {
+  uint32_t x = 0;
+  memcpy(base::AssumeLittleEndian(&x), tbv.data(), tbv.size());
+  return x;
+}
+
+uint16_t ToShort(const TraceBlobView& tbv) {
+  uint16_t x = 0;
+  memcpy(base::AssumeLittleEndian(&x), tbv.data(), tbv.size());
+  return x;
+}
+
+}  // namespace
+
+ArtMethodTokenizer::ArtMethodTokenizer(TraceProcessorContext* ctx)
+    : context_(ctx) {}
+ArtMethodTokenizer::~ArtMethodTokenizer() = default;
+
+base::Status ArtMethodTokenizer::Parse(TraceBlobView blob) {
+  reader_.PushBack(std::move(blob));
+  if (sub_parser_.index() == base::variant_index<SubParser, Detect>()) {
+    auto smagic = reader_.SliceOff(reader_.start_offset(), 4);
+    if (!smagic) {
+      return base::OkStatus();
+    }
+    uint32_t magic = ToInt(*smagic);
+    sub_parser_ = magic == kTraceMagic ? SubParser{Streaming{this}}
+                                       : SubParser{NonStreaming{this}};
+    context_->clock_tracker->SetTraceTimeClock(
+        protos::pbzero::BUILTIN_CLOCK_MONOTONIC);
+  }
+  if (sub_parser_.index() == base::variant_index<SubParser, Streaming>()) {
+    return std::get<Streaming>(sub_parser_).Parse();
+  }
+  return std::get<NonStreaming>(sub_parser_).Parse();
+}
+
+base::Status ArtMethodTokenizer::ParseMethodLine(std::string_view l) {
+  auto tokens = base::SplitString(base::TrimWhitespace(std::string(l)), "\t");
+  auto id = base::StringToUInt32(tokens[0], 16);
+  if (!id) {
+    return base::ErrStatus(
+        "ART method trace: unable to parse method id as integer: %s",
+        tokens[0].c_str());
+  }
+
+  std::string class_name = tokens[1];
+  std::string method_name;
+  std::string signature;
+  std::optional<StringId> pathname;
+  std::optional<uint32_t> line_number;
+  // Below logic was taken from:
+  // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:perflib/src/main/java/com/android/tools/perflib/vmtrace/VmTraceParser.java;l=251
+  // It's not clear why this complexity is strictly needed (maybe backcompat
+  // or certain configurations of method tracing) but it's best to stick
+  // closely to the official parser implementation.
+  if (tokens.size() == 6) {
+    method_name = tokens[2];
+    signature = tokens[3];
+    pathname = context_->storage->InternString(
+        base::StringView(ConstructPathname(class_name, tokens[4])));
+    line_number = base::StringToUInt32(tokens[5]);
+  } else if (tokens.size() > 2) {
+    if (base::StartsWith(tokens[3], "(")) {
+      method_name = tokens[2];
+      signature = tokens[3];
+      if (tokens.size() >= 5) {
+        pathname = context_->storage->InternString(base::StringView(tokens[4]));
+      }
+    } else {
+      pathname = context_->storage->InternString(base::StringView(tokens[2]));
+      line_number = base::StringToUInt32(tokens[3]);
+    }
+  }
+  base::StackString<2048> slice_name("%s.%s: %s", class_name.c_str(),
+                                     method_name.c_str(), signature.c_str());
+  method_map_[*id] = {
+      context_->storage->InternString(slice_name.string_view()),
+      pathname,
+      line_number,
+  };
+  return base::OkStatus();
+}
+
+base::Status ArtMethodTokenizer::ParseOptionLine(std::string_view l) {
+  std::string line(l);
+  auto res = base::SplitString(line, "=");
+  if (res.size() != 2) {
+    return base::ErrStatus(
+        "ART method tracing: unable to parse option (line %s)", line.c_str());
+  }
+  if (res[0] == "clock") {
+    if (res[1] == "dual") {
+      clock_ = kDual;
+    } else if (res[1] == "wall") {
+      clock_ = kWall;
+    } else if (res[1] == "thread-cpu") {
+      return base::ErrStatus(
+          "ART method tracing: thread-cpu clock is *not* supported. Use wall "
+          "or dual clocks");
+    } else {
+      return base::ErrStatus("ART method tracing: unknown clock %s",
+                             res[1].c_str());
+    }
+  }
+  return base::OkStatus();
+}
+
+base::Status ArtMethodTokenizer::ParseRecord(uint32_t tid,
+                                             const TraceBlobView& record) {
+  ArtMethodEvent evt{};
+  evt.tid = tid;
+  if (auto* it = thread_map_.Find(tid); it && !it->comm_used) {
+    evt.comm = it->comm;
+    it->comm_used = true;
+  }
+
+  uint32_t methodid_action = ToInt(record.slice_off(0, 4));
+  uint32_t ts_delta = clock_ == kDual ? ToInt(record.slice_off(8, 4))
+                                      : ToInt(record.slice_off(4, 4));
+
+  uint32_t action = methodid_action & 0x03;
+  uint32_t method_id = methodid_action & ~0x03u;
+
+  const auto& m = method_map_[method_id];
+  evt.method = m.name;
+  evt.pathname = m.pathname;
+  evt.line_number = m.line_number;
+  switch (action) {
+    case 0:
+      evt.action = ArtMethodEvent::kEnter;
+      break;
+    case 1:
+    case 2:
+      evt.action = ArtMethodEvent::kExit;
+      break;
+  }
+  ASSIGN_OR_RETURN(int64_t ts, context_->clock_tracker->ToTraceTime(
+                                   protos::pbzero::BUILTIN_CLOCK_MONOTONIC,
+                                   (ts_ + ts_delta) * 1000));
+  context_->sorter->PushArtMethodEvent(ts, evt);
+  return base::OkStatus();
+}
+
+base::Status ArtMethodTokenizer::ParseThread(uint32_t tid,
+                                             const std::string& comm) {
+  thread_map_.Insert(
+      tid, {context_->storage->InternString(base::StringView(comm)), false});
+  return base::OkStatus();
+}
+
+base::Status ArtMethodTokenizer::Streaming::Parse() {
+  auto it = tokenizer_->reader_.GetIterator();
+  PERFETTO_CHECK(it.MaybeAdvance(it_offset_));
+  for (bool cnt = true; cnt;) {
+    switch (mode_) {
+      case kHeaderStart: {
+        ASSIGN_OR_RETURN(cnt, ParseHeaderStart(it));
+        break;
+      }
+      case kData: {
+        ASSIGN_OR_RETURN(cnt, ParseData(it));
+        break;
+      }
+      case kSummaryDone: {
+        mode_ = kDone;
+        cnt = false;
+        break;
+      }
+      case kDone: {
+        return base::ErrStatus(
+            "ART method trace: unexpected data after eof marker");
+      }
+    }
+    if (cnt) {
+      it_offset_ = it.file_offset();
+    }
+  }
+  return base::OkStatus();
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::Streaming::ParseHeaderStart(
+    Iterator& it) {
+  auto header = it.MaybeRead(kTraceHeaderLength);
+  if (!header) {
+    return false;
+  }
+  uint32_t magic = ToInt(header->slice_off(0, 4));
+  if (magic != kTraceMagic) {
+    return base::ErrStatus("ART Method trace: expected start-header magic");
+  }
+  tokenizer_->version_ =
+      ToShort(header->slice_off(4, 2)) ^ kStreamingVersionMask;
+  tokenizer_->ts_ = static_cast<int64_t>(ToLong(header->slice_off(8, 8)));
+  switch (tokenizer_->version_) {
+    case 1:
+      tokenizer_->record_size_ = 9;
+      break;
+    case 2:
+      tokenizer_->record_size_ = 10;
+      break;
+    case 3:
+      tokenizer_->record_size_ = ToShort(header->slice_off(16, 2));
+      break;
+    default:
+      PERFETTO_FATAL("Illegal version %u", tokenizer_->version_);
+  }
+  mode_ = kData;
+  return true;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::Streaming::ParseData(Iterator& it) {
+  std::optional<TraceBlobView> op_tbv = it.MaybeRead(2);
+  if (!op_tbv) {
+    return false;
+  }
+  uint32_t op = ToShort(*op_tbv);
+  if (op != 0) {
+    // Just skip past the record: this will be handled later.
+    // -2 because we already the tid above which forms part of the record.
+    return it.MaybeAdvance(tokenizer_->record_size_ - 2);
+  }
+  std::optional<TraceBlobView> code_tbv = it.MaybeRead(1);
+  if (!code_tbv) {
+    return false;
+  }
+  uint8_t code = *code_tbv->data();
+  switch (code) {
+    case kSummaryCode: {
+      std::optional<TraceBlobView> summary_len_tbv = it.MaybeRead(4);
+      if (!summary_len_tbv) {
+        return false;
+      }
+      uint32_t summary_len = ToInt(*summary_len_tbv);
+      std::optional<TraceBlobView> summary_tbv = it.MaybeRead(summary_len);
+      if (!summary_tbv) {
+        return false;
+      }
+      RETURN_IF_ERROR(ParseSummary(ToStringView(*summary_tbv)));
+      mode_ = kSummaryDone;
+      return true;
+    }
+    case kMethodsCode: {
+      std::optional<TraceBlobView> method_len_tbv = it.MaybeRead(2);
+      if (!method_len_tbv) {
+        return false;
+      }
+      uint32_t method_len = ToShort(*method_len_tbv);
+      std::optional<TraceBlobView> method_tbv = it.MaybeRead(method_len);
+      if (!method_tbv) {
+        return false;
+      }
+      RETURN_IF_ERROR(tokenizer_->ParseMethodLine(ToStringView(*method_tbv)));
+      return true;
+    }
+    case kThreadsCode: {
+      std::optional<TraceBlobView> tid_tbv = it.MaybeRead(2);
+      if (!tid_tbv) {
+        return false;
+      }
+      std::optional<TraceBlobView> comm_len_tbv = it.MaybeRead(2);
+      if (!comm_len_tbv) {
+        return false;
+      }
+      uint32_t comm_len = ToShort(*comm_len_tbv);
+      std::optional<TraceBlobView> comm_tbv = it.MaybeRead(comm_len);
+      if (!comm_tbv) {
+        return false;
+      }
+      RETURN_IF_ERROR(tokenizer_->ParseThread(
+          ToShort(*tid_tbv), std::string(ToStringView(*comm_tbv))));
+      return true;
+    }
+    default:
+      return base::ErrStatus("ART method trace: unknown opcode encountered %d",
+                             code);
+  }
+}
+
+base::Status ArtMethodTokenizer::Streaming::ParseSummary(
+    std::string_view summary) const {
+  base::StringSplitter s(std::string(summary), '\n');
+
+  // First two lines should be version and line number respectively.
+  if (!s.Next() || !s.Next() || !s.Next()) {
+    return base::ErrStatus(
+        "ART method trace: unexpected format of summary section");
+  }
+
+  // Parse lines until we hit "*threads" as the line.
+  for (;;) {
+    std::string_view line(s.cur_token(), s.cur_token_size());
+    if (line == "*threads") {
+      return base::OkStatus();
+    }
+    RETURN_IF_ERROR(tokenizer_->ParseOptionLine(line));
+    if (!s.Next()) {
+      return base::ErrStatus(
+          "ART method trace: reached end of file before EOF marker");
+    }
+  }
+}
+
+base::Status ArtMethodTokenizer::Streaming::NotifyEndOfFile() {
+  if (mode_ != kDone) {
+    return base::ErrStatus("ART Method trace: trace is incomplete");
+  }
+
+  auto it = tokenizer_->reader_.GetIterator();
+  PERFETTO_CHECK(it.MaybeAdvance(kTraceHeaderLength));
+  for (;;) {
+    std::optional<TraceBlobView> tid_tbv = it.MaybeRead(2);
+    uint32_t tid = ToShort(*tid_tbv);
+    if (tid == 0) {
+      uint8_t code = *it.MaybeRead(1)->data();
+      switch (code) {
+        case kSummaryCode:
+          return base::OkStatus();
+        case kMethodsCode: {
+          PERFETTO_CHECK(it.MaybeAdvance(ToShort(*it.MaybeRead(2))));
+          break;
+        }
+        case kThreadsCode: {
+          // Advance past the tid.
+          PERFETTO_CHECK(it.MaybeAdvance(2));
+          PERFETTO_CHECK(it.MaybeAdvance(ToShort(*it.MaybeRead(2))));
+          break;
+        }
+        default:
+          PERFETTO_FATAL("Should not be reached");
+      }
+      continue;
+    }
+    RETURN_IF_ERROR(tokenizer_->ParseRecord(
+        tid, *it.MaybeRead(tokenizer_->record_size_ - 2)));
+  }
+}
+
+base::Status ArtMethodTokenizer::NonStreaming::Parse() {
+  auto it = tokenizer_->reader_.GetIterator();
+  for (bool cnt = true; cnt;) {
+    switch (mode_) {
+      case kHeaderStart: {
+        ASSIGN_OR_RETURN(cnt, ParseHeaderStart(it));
+        break;
+      }
+      case kHeaderVersion: {
+        ASSIGN_OR_RETURN(cnt, ParseHeaderVersion(it));
+        break;
+      }
+      case kHeaderOptions: {
+        ASSIGN_OR_RETURN(cnt, ParseHeaderOptions(it));
+        break;
+      }
+      case kHeaderThreads: {
+        ASSIGN_OR_RETURN(cnt, ParseHeaderThreads(it));
+        break;
+      }
+      case kHeaderMethods: {
+        ASSIGN_OR_RETURN(cnt, ParseHeaderMethods(it));
+        break;
+      }
+      case kDataHeader: {
+        ASSIGN_OR_RETURN(cnt, ParseDataHeader(it));
+        break;
+      }
+      case kData: {
+        size_t s = it.file_offset();
+        for (size_t i = s;; i += tokenizer_->record_size_) {
+          auto record =
+              tokenizer_->reader_.SliceOff(i, tokenizer_->record_size_);
+          if (!record) {
+            PERFETTO_CHECK(it.MaybeAdvance(i - s));
+            cnt = false;
+            break;
+          }
+          uint32_t tid = tokenizer_->version_ == 1
+                             ? record->data()[0]
+                             : ToShort(record->slice_off(0, 2));
+          RETURN_IF_ERROR(tokenizer_->ParseRecord(
+              tid, record->slice_off(2, record->size() - 2)));
+        }
+        break;
+      }
+    }
+  }
+  tokenizer_->reader_.PopFrontUntil(it.file_offset());
+  return base::OkStatus();
+}
+
+base::Status ArtMethodTokenizer::NonStreaming::NotifyEndOfFile() const {
+  if (mode_ == NonStreaming::kData && tokenizer_->reader_.empty()) {
+    return base::OkStatus();
+  }
+  return base::ErrStatus("ART Method trace: trace is incomplete");
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::NonStreaming::ParseHeaderStart(
+    Iterator& it) {
+  auto raw = it.MaybeFindAndRead('\n');
+  if (!raw) {
+    return false;
+  }
+  RETURN_IF_ERROR(ParseHeaderSectionLine(ToStringView(*raw)));
+  return true;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::NonStreaming::ParseHeaderVersion(
+    Iterator& it) {
+  auto line = it.MaybeFindAndRead('\n');
+  if (!line) {
+    return false;
+  }
+  std::string version_str(ToStringView(*line));
+  auto version = base::StringToInt32(version_str);
+  if (!version || *version < 1 || *version > 3) {
+    return base::ErrStatus("ART Method trace: trace version (%s) not supported",
+                           version_str.c_str());
+  }
+  tokenizer_->version_ = static_cast<uint32_t>(*version);
+  mode_ = kHeaderOptions;
+  return true;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::NonStreaming::ParseHeaderOptions(
+    Iterator& it) {
+  for (auto r = it.MaybeFindAndRead('\n'); r; r = it.MaybeFindAndRead('\n')) {
+    std::string_view l = ToStringView(*r);
+    if (l[0] == '*') {
+      RETURN_IF_ERROR(ParseHeaderSectionLine(l));
+      return true;
+    }
+    RETURN_IF_ERROR(tokenizer_->ParseOptionLine(l));
+  }
+  return false;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::NonStreaming::ParseHeaderThreads(
+    Iterator& it) {
+  for (auto r = it.MaybeFindAndRead('\n'); r; r = it.MaybeFindAndRead('\n')) {
+    std::string_view l = ToStringView(*r);
+    if (l[0] == '*') {
+      RETURN_IF_ERROR(ParseHeaderSectionLine(l));
+      return true;
+    }
+    std::string line(l);
+    auto tokens = base::SplitString(line, "\t");
+    if (tokens.size() != 2) {
+      return base::ErrStatus(
+          "ART method tracing: expected only one tab in thread line (context: "
+          "%s)",
+          line.c_str());
+    }
+    std::optional<uint32_t> tid = base::StringToUInt32(tokens[0]);
+    if (!tid) {
+      return base::ErrStatus(
+          "ART method tracing: failed parse tid in thread line (context: %s)",
+          tokens[0].c_str());
+    }
+    RETURN_IF_ERROR(tokenizer_->ParseThread(*tid, tokens[1]));
+  }
+  return false;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::NonStreaming::ParseHeaderMethods(
+    Iterator& it) {
+  for (auto r = it.MaybeFindAndRead('\n'); r; r = it.MaybeFindAndRead('\n')) {
+    std::string_view l = ToStringView(*r);
+    if (l[0] == '*') {
+      RETURN_IF_ERROR(ParseHeaderSectionLine(l));
+      return true;
+    }
+    RETURN_IF_ERROR(tokenizer_->ParseMethodLine(l));
+  }
+  return false;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::NonStreaming::ParseDataHeader(
+    Iterator& it) {
+  auto header = it.MaybeRead(kTraceHeaderLength);
+  if (!header) {
+    return false;
+  }
+  uint32_t magic = ToInt(header->slice_off(0, 4));
+  if (magic != kTraceMagic) {
+    return base::ErrStatus("ART Method trace: expected start-header magic");
+  }
+  uint16_t version = ToShort(header->slice_off(4, 2));
+  if (version != tokenizer_->version_) {
+    return base::ErrStatus(
+        "ART Method trace: trace version does not match data version");
+  }
+  tokenizer_->ts_ = static_cast<int64_t>(ToLong(header->slice_off(8, 8)));
+  switch (tokenizer_->version_) {
+    case 1:
+      tokenizer_->record_size_ = 9;
+      break;
+    case 2:
+      tokenizer_->record_size_ = 10;
+      break;
+    case 3:
+      tokenizer_->record_size_ = ToShort(header->slice_off(16, 2));
+      break;
+    default:
+      PERFETTO_FATAL("Illegal version %u", tokenizer_->version_);
+  }
+  mode_ = kData;
+  return true;
+}
+
+base::Status ArtMethodTokenizer::NonStreaming::ParseHeaderSectionLine(
+    std::string_view line) {
+  if (line == "*version") {
+    mode_ = kHeaderVersion;
+    return base::OkStatus();
+  }
+  if (line == "*threads") {
+    mode_ = kHeaderThreads;
+    return base::OkStatus();
+  }
+  if (line == "*methods") {
+    mode_ = kHeaderMethods;
+    return base::OkStatus();
+  }
+  if (line == "*end") {
+    mode_ = kDataHeader;
+    return base::OkStatus();
+  }
+  return base::ErrStatus(
+      "ART Method trace: unexpected line (%s) when expecting section header "
+      "(line starting with *)",
+      std::string(line).c_str());
+}
+
+base::Status ArtMethodTokenizer::NotifyEndOfFile() {
+  switch (sub_parser_.index()) {
+    case base::variant_index<SubParser, Detect>():
+      return base::ErrStatus("ART Method trace: trace is incomplete");
+    case base::variant_index<SubParser, Streaming>():
+      return std::get<Streaming>(sub_parser_).NotifyEndOfFile();
+    case base::variant_index<SubParser, NonStreaming>():
+      return std::get<NonStreaming>(sub_parser_).NotifyEndOfFile();
+  }
+  PERFETTO_FATAL("For GCC");
+}
+
+}  // namespace perfetto::trace_processor::art_method
diff --git a/src/trace_processor/importers/art_method/art_method_tokenizer.h b/src/trace_processor/importers/art_method/art_method_tokenizer.h
new file mode 100644
index 0000000..d79807b
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_tokenizer.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_TOKENIZER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_TOKENIZER_H_
+
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <variant>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/importers/common/trace_parser.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/trace_blob_view_reader.h"
+
+namespace perfetto::trace_processor::art_method {
+
+class ArtMethodTokenizer : public ChunkedTraceReader {
+ public:
+  explicit ArtMethodTokenizer(TraceProcessorContext*);
+  ~ArtMethodTokenizer() override;
+
+  base::Status Parse(TraceBlobView) override;
+  base::Status NotifyEndOfFile() override;
+
+ private:
+  using Iterator = util::TraceBlobViewReader::Iterator;
+  struct Method {
+    StringId name;
+    std::optional<StringId> pathname;
+    std::optional<uint32_t> line_number;
+  };
+  struct Thread {
+    StringId comm;
+    bool comm_used;
+  };
+  struct Detect {};
+  struct NonStreaming {
+    base::Status Parse();
+    base::Status NotifyEndOfFile() const;
+
+    base::StatusOr<bool> ParseHeaderStart(Iterator&);
+    base::StatusOr<bool> ParseHeaderVersion(Iterator&);
+    base::StatusOr<bool> ParseHeaderOptions(Iterator&);
+    base::StatusOr<bool> ParseHeaderThreads(Iterator&);
+    base::StatusOr<bool> ParseHeaderMethods(Iterator&);
+    base::StatusOr<bool> ParseDataHeader(Iterator&);
+
+    base::Status ParseHeaderSectionLine(std::string_view);
+
+    ArtMethodTokenizer* tokenizer_;
+    enum {
+      kHeaderStart,
+      kHeaderVersion,
+      kHeaderOptions,
+      kHeaderThreads,
+      kHeaderMethods,
+      kDataHeader,
+      kData,
+    } mode_ = kHeaderStart;
+  };
+  struct Streaming {
+    base::Status Parse();
+    base::Status NotifyEndOfFile();
+
+    base::StatusOr<bool> ParseHeaderStart(Iterator&);
+    base::StatusOr<bool> ParseData(Iterator&);
+    base::Status ParseSummary(std::string_view) const;
+
+    ArtMethodTokenizer* tokenizer_;
+    enum {
+      kHeaderStart,
+      kData,
+      kSummaryDone,
+      kDone,
+    } mode_ = kHeaderStart;
+    size_t it_offset_ = 0;
+  };
+  using SubParser = std::variant<Detect, NonStreaming, Streaming>;
+
+  [[nodiscard]] base::Status ParseMethodLine(std::string_view);
+  [[nodiscard]] base::Status ParseOptionLine(std::string_view);
+  [[nodiscard]] base::Status ParseThread(uint32_t tid, const std::string&);
+  [[nodiscard]] base::Status ParseRecord(uint32_t tid, const TraceBlobView&);
+
+  TraceProcessorContext* const context_;
+  util::TraceBlobViewReader reader_;
+
+  SubParser sub_parser_;
+  enum {
+    kWall,
+    kDual,
+  } clock_ = kWall;
+
+  uint32_t version_ = std::numeric_limits<uint32_t>::max();
+  int64_t ts_ = std::numeric_limits<int64_t>::max();
+  uint32_t record_size_ = std::numeric_limits<uint32_t>::max();
+  base::FlatHashMap<uint32_t, Method> method_map_;
+  base::FlatHashMap<uint32_t, Thread> thread_map_;
+};
+
+}  // namespace perfetto::trace_processor::art_method
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_TOKENIZER_H_
diff --git a/src/trace_processor/importers/common/BUILD.gn b/src/trace_processor/importers/common/BUILD.gn
index c2f61da..8be9508 100644
--- a/src/trace_processor/importers/common/BUILD.gn
+++ b/src/trace_processor/importers/common/BUILD.gn
@@ -41,6 +41,8 @@
     "global_args_tracker.h",
     "jit_cache.cc",
     "jit_cache.h",
+    "legacy_v8_cpu_profile_tracker.cc",
+    "legacy_v8_cpu_profile_tracker.h",
     "machine_tracker.cc",
     "machine_tracker.h",
     "mapping_tracker.cc",
@@ -54,8 +56,6 @@
     "sched_event_state.h",
     "sched_event_tracker.cc",
     "sched_event_tracker.h",
-    "scoped_active_trace_file.cc",
-    "scoped_active_trace_file.h",
     "slice_tracker.cc",
     "slice_tracker.h",
     "slice_translation_table.cc",
@@ -71,6 +71,7 @@
     "trace_parser.cc",
     "track_tracker.cc",
     "track_tracker.h",
+    "tracks.h",
     "virtual_memory_mapping.cc",
     "virtual_memory_mapping.h",
   ]
@@ -96,7 +97,6 @@
     "../../util:profiler_util",
     "../../util:trace_type",
     "../fuchsia:fuchsia_record",
-    "../perf:record",
     "../systrace:systrace_line",
   ]
 }
diff --git a/src/trace_processor/importers/common/args_tracker.h b/src/trace_processor/importers/common/args_tracker.h
index 912d574..bacd67b 100644
--- a/src/trace_processor/importers/common/args_tracker.h
+++ b/src/trace_processor/importers/common/args_tracker.h
@@ -211,6 +211,10 @@
                      id);
   }
 
+  BoundInserter AddArgsTo(tables::CpuTable::Id id) {
+    return AddArgsTo(context_->storage->mutable_cpu_table(), id);
+  }
+
   // Returns a CompactArgSet which contains the args inserted into this
   // ArgsTracker. Requires that every arg in this tracker was inserted for the
   // "arg_set_id" column given by |column| at the given |row_number|.
diff --git a/src/trace_processor/importers/common/async_track_set_tracker.cc b/src/trace_processor/importers/common/async_track_set_tracker.cc
index 432bd39..dab0af0 100644
--- a/src/trace_processor/importers/common/async_track_set_tracker.cc
+++ b/src/trace_processor/importers/common/async_track_set_tracker.cc
@@ -16,12 +16,12 @@
 
 #include "src/trace_processor/importers/common/async_track_set_tracker.h"
 
+#include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 AsyncTrackSetTracker::AsyncTrackSetTracker(TraceProcessorContext* context)
     : android_source_(context->storage->InternString("android")),
@@ -181,11 +181,15 @@
 
 TrackId AsyncTrackSetTracker::CreateTrackForSet(const TrackSet& set) {
   switch (set.scope) {
-    case TrackSetScope::kGlobal:
-      // TODO(lalitm): propogate source from callers rather than just passing
-      // kNullStringId here.
-      return context_->track_tracker->CreateGlobalAsyncTrack(
-          set.global_track_name, kNullStringId);
+    case TrackSetScope::kGlobal: {
+      // TODO(lalitm): propogate source from callers as a dimension
+      TrackTracker::DimensionsBuilder builder =
+          context_->track_tracker->CreateDimensionsBuilder();
+      builder.AppendName(set.global_track_name);
+      return context_->track_tracker->CreateTrack(
+          tracks::unknown, std::move(builder).Build(),
+          TrackTracker::LegacyStringIdName{set.global_track_name});
+    }
     case TrackSetScope::kProcess:
       // TODO(lalitm): propogate source from callers rather than just passing
       // kNullStringId here.
@@ -193,11 +197,28 @@
           set.nesting_behaviour == NestingBehaviour::kLegacySaturatingUnnestable
               ? android_source_
               : kNullStringId;
-      return context_->track_tracker->CreateProcessAsyncTrack(
-          set.process_tuple.name, set.process_tuple.upid, source);
+
+      const StringId name =
+          context_->process_track_translation_table->TranslateName(
+              set.process_tuple.name);
+      TrackTracker::DimensionsBuilder dims_builder =
+          context_->track_tracker->CreateDimensionsBuilder();
+      dims_builder.AppendName(name);
+      dims_builder.AppendUpid(set.process_tuple.upid);
+      TrackTracker::Dimensions dims_id = std::move(dims_builder).Build();
+
+      TrackId id = context_->track_tracker->CreateProcessTrack(
+          tracks::unknown, set.process_tuple.upid, dims_id,
+          TrackTracker::LegacyStringIdName{name});
+
+      if (!source.is_null()) {
+        context_->args_tracker->AddArgsTo(id).AddArg(source,
+                                                     Variadic::String(source));
+      }
+      return id;
   }
+
   PERFETTO_FATAL("For GCC");
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/common/clock_tracker.h b/src/trace_processor/importers/common/clock_tracker.h
index 36a4a9a..4a5a28e 100644
--- a/src/trace_processor/importers/common/clock_tracker.h
+++ b/src/trace_processor/importers/common/clock_tracker.h
@@ -176,7 +176,7 @@
   }
 
   // Apply the clock offset to convert remote trace times to host trace time.
-  int64_t ToHostTraceTime(int64_t timestamp) {
+  PERFETTO_ALWAYS_INLINE int64_t ToHostTraceTime(int64_t timestamp) {
     if (PERFETTO_LIKELY(!context_->machine_id())) {
       // No need to convert host timestamps.
       return timestamp;
@@ -188,7 +188,9 @@
     return timestamp - clock_offset;
   }
 
-  base::StatusOr<int64_t> ToTraceTime(ClockId clock_id, int64_t timestamp) {
+  PERFETTO_ALWAYS_INLINE base::StatusOr<int64_t> ToTraceTime(
+      ClockId clock_id,
+      int64_t timestamp) {
     if (PERFETTO_UNLIKELY(!trace_time_clock_id_used_for_conversion_)) {
       context_->metadata_tracker->SetMetadata(
           metadata::trace_time_clock_id,
@@ -197,8 +199,9 @@
     }
     trace_time_clock_id_used_for_conversion_ = true;
 
-    if (clock_id == trace_time_clock_id_)
+    if (clock_id == trace_time_clock_id_) {
       return ToHostTraceTime(timestamp);
+    }
 
     ASSIGN_OR_RETURN(int64_t ts,
                      Convert(clock_id, timestamp, trace_time_clock_id_));
diff --git a/src/trace_processor/importers/common/event_tracker.cc b/src/trace_processor/importers/common/event_tracker.cc
index 4b7e817..2ebc62b 100644
--- a/src/trace_processor/importers/common/event_tracker.cc
+++ b/src/trace_processor/importers/common/event_tracker.cc
@@ -88,13 +88,13 @@
 
     TrackId track_id = kInvalidTrackId;
     if (upid.has_value()) {
-      track_id = context_->track_tracker->InternProcessCounterTrack(
+      track_id = context_->track_tracker->LegacyInternProcessCounterTrack(
           pending_counter.name_id, *upid);
     } else {
       // If we still don't know which process this thread belongs to, fall back
       // onto creating a thread counter track. It's too late to drop data
       // because the counter values have already been inserted.
-      track_id = context_->track_tracker->InternThreadCounterTrack(
+      track_id = context_->track_tracker->LegacyInternThreadCounterTrack(
           pending_counter.name_id, utid);
     }
     auto& counter = *context_->storage->mutable_counter_table();
diff --git a/src/trace_processor/importers/common/event_tracker_unittest.cc b/src/trace_processor/importers/common/event_tracker_unittest.cc
index 64a3bdc..3f99c78 100644
--- a/src/trace_processor/importers/common/event_tracker_unittest.cc
+++ b/src/trace_processor/importers/common/event_tracker_unittest.cc
@@ -53,9 +53,9 @@
 TEST_F(EventTrackerTest, CounterDuration) {
   uint32_t cpu = 3;
   int64_t timestamp = 100;
-  StringId name_id = kNullStringId;
 
-  TrackId track = context.track_tracker->InternCpuCounterTrack(name_id, cpu);
+  TrackId track =
+      context.track_tracker->InternCpuCounterTrack(tracks::cpu_frequency, cpu);
   context.event_tracker->PushCounter(timestamp, 1000, track);
   context.event_tracker->PushCounter(timestamp + 1, 4000, track);
   context.event_tracker->PushCounter(timestamp + 3, 5000, track);
diff --git a/src/trace_processor/importers/common/global_args_tracker.h b/src/trace_processor/importers/common/global_args_tracker.h
index 78bae43..2fd9f36 100644
--- a/src/trace_processor/importers/common/global_args_tracker.h
+++ b/src/trace_processor/importers/common/global_args_tracker.h
@@ -20,6 +20,7 @@
 #include <cstdint>
 #include <type_traits>
 #include <vector>
+#include "perfetto/base/logging.h"
 #include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/hash.h"
 #include "perfetto/ext/base/small_vector.h"
@@ -63,7 +64,7 @@
                 "Args must be trivially destructible");
 
   struct ArgHasher {
-    uint64_t operator()(const Arg& arg) const noexcept {
+    uint64_t operator()(const CompactArg& arg) const noexcept {
       base::Hasher hash;
       hash.Update(arg.key.raw_id());
       // We don't hash arg.flat_key because it's a subsequence of arg.key.
@@ -99,33 +100,47 @@
 
   explicit GlobalArgsTracker(TraceStorage* storage);
 
+  ArgSetId AddArgSet(const std::vector<Arg>& args,
+                     uint32_t begin,
+                     uint32_t end) {
+    return AddArgSet(args.data() + begin, args.data() + end, sizeof(Arg));
+  }
+  ArgSetId AddArgSet(Arg* args, uint32_t begin, uint32_t end) {
+    return AddArgSet(args + begin, args + end, sizeof(Arg));
+  }
+  ArgSetId AddArgSet(CompactArg* args, uint32_t begin, uint32_t end) {
+    return AddArgSet(args + begin, args + end, sizeof(CompactArg));
+  }
+
+ private:
+  using ArgSetHash = uint64_t;
+
   // Assumes that the interval [begin, end) of |args| has args with the same key
   // grouped together.
-  ArgSetId AddArgSet(const Arg* args, uint32_t begin, uint32_t end) {
-    base::SmallVector<uint32_t, 64> valid_indexes;
+  ArgSetId AddArgSet(const void* start, const void* end, uint32_t stride) {
+    base::SmallVector<const CompactArg*, 64> valid;
 
     // TODO(eseckler): Also detect "invalid" key combinations in args sets (e.g.
     // "foo" and "foo.bar" in the same arg set)?
-    for (uint32_t i = begin; i < end; i++) {
-      if (!valid_indexes.empty() &&
-          args[valid_indexes.back()].key == args[i].key) {
+    for (const void* ptr = start; ptr != end;
+         ptr = reinterpret_cast<const uint8_t*>(ptr) + stride) {
+      const auto& arg = *reinterpret_cast<const CompactArg*>(ptr);
+      if (!valid.empty() && valid.back()->key == arg.key) {
         // Last arg had the same key as this one. In case of kSkipIfExists, skip
         // this arg. In case of kAddOrUpdate, remove the last arg and add this
         // arg instead.
-        if (args[i].update_policy == UpdatePolicy::kSkipIfExists) {
+        if (arg.update_policy == UpdatePolicy::kSkipIfExists) {
           continue;
-        } else {
-          PERFETTO_DCHECK(args[i].update_policy == UpdatePolicy::kAddOrUpdate);
-          valid_indexes.pop_back();
         }
+        PERFETTO_DCHECK(arg.update_policy == UpdatePolicy::kAddOrUpdate);
+        valid.pop_back();
       }
-
-      valid_indexes.emplace_back(i);
+      valid.emplace_back(&arg);
     }
 
     base::Hasher hash;
-    for (uint32_t i : valid_indexes) {
-      hash.Update(ArgHasher()(args[i]));
+    for (const auto* it : valid) {
+      hash.Update(ArgHasher()(*it));
     }
 
     auto& arg_table = *storage_->mutable_arg_table();
@@ -141,9 +156,8 @@
     // 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 (uint32_t i : valid_indexes) {
-      const auto& arg = args[i];
-
+    for (const CompactArg* ptr : valid) {
+      const auto& arg = *ptr;
       tables::ArgTable::Row row;
       row.arg_set_id = id;
       row.flat_key = arg.flat_key;
@@ -179,16 +193,6 @@
     return id;
   }
 
-  // Exposed for making tests easier to write.
-  ArgSetId AddArgSet(const std::vector<Arg>& args,
-                     uint32_t begin,
-                     uint32_t end) {
-    return AddArgSet(args.data(), begin, end);
-  }
-
- private:
-  using ArgSetHash = uint64_t;
-
   base::FlatHashMap<ArgSetHash, uint32_t, base::AlreadyHashed<ArgSetHash>>
       arg_row_for_hash_;
 
diff --git a/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc b/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc
new file mode 100644
index 0000000..9206d87
--- /dev/null
+++ b/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
+
+#include <cstdint>
+#include <optional>
+#include <utility>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_view.h"
+#include "src/trace_processor/importers/common/mapping_tracker.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/profiler_tables_py.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor {
+
+LegacyV8CpuProfileTracker::LegacyV8CpuProfileTracker(
+    TraceProcessorContext* context)
+    : context_(context) {}
+
+void LegacyV8CpuProfileTracker::SetStartTsForSessionAndPid(uint64_t session_id,
+                                                           uint32_t pid,
+                                                           int64_t ts) {
+  auto [it, inserted] = state_by_session_and_pid_.Insert(
+      std::make_pair(session_id, pid),
+      State{ts, base::FlatHashMap<uint32_t, CallsiteId>(), nullptr});
+  it->ts = ts;
+  if (inserted) {
+    it->mapping = &context_->mapping_tracker->CreateDummyMapping("");
+  }
+}
+
+base::Status LegacyV8CpuProfileTracker::AddCallsite(
+    uint64_t session_id,
+    uint32_t pid,
+    uint32_t raw_callsite_id,
+    std::optional<uint32_t> parent_raw_callsite_id,
+    base::StringView script_url,
+    base::StringView function_name) {
+  auto* state = state_by_session_and_pid_.Find(std::make_pair(session_id, pid));
+  if (!state) {
+    return base::ErrStatus(
+        "v8 profile id does not exist: cannot insert callsite");
+  }
+  FrameId frame_id =
+      state->mapping->InternDummyFrame(function_name, script_url);
+  CallsiteId callsite_id;
+  if (parent_raw_callsite_id) {
+    auto* parent_id = state->callsites.Find(*parent_raw_callsite_id);
+    if (!parent_id) {
+      return base::ErrStatus(
+          "v8 profile parent id does not exist: cannot insert callsite");
+    }
+    auto row =
+        context_->storage->stack_profile_callsite_table().FindById(*parent_id);
+    callsite_id = context_->stack_profile_tracker->InternCallsite(
+        *parent_id, frame_id, row->depth() + 1);
+  } else {
+    callsite_id = context_->stack_profile_tracker->InternCallsite(std::nullopt,
+                                                                  frame_id, 0);
+  }
+  if (!state->callsites.Insert(raw_callsite_id, callsite_id).second) {
+    return base::ErrStatus("v8 profile: callsite with id already exists");
+  }
+  return base::OkStatus();
+}
+
+base::StatusOr<int64_t> LegacyV8CpuProfileTracker::AddDeltaAndGetTs(
+    uint64_t session_id,
+    uint32_t pid,
+    int64_t delta_ts) {
+  auto* state = state_by_session_and_pid_.Find(std::make_pair(session_id, pid));
+  if (!state) {
+    return base::ErrStatus(
+        "v8 profile id does not exist: cannot compute timestamp from delta");
+  }
+  state->ts += delta_ts;
+  return state->ts;
+}
+
+base::Status LegacyV8CpuProfileTracker::AddSample(int64_t ts,
+                                                  uint64_t session_id,
+                                                  uint32_t pid,
+                                                  uint32_t tid,
+                                                  uint32_t raw_callsite_id) {
+  auto* state = state_by_session_and_pid_.Find(std::make_pair(session_id, pid));
+  if (!state) {
+    return base::ErrStatus("v8 callsite id does not exist: cannot add sample");
+  }
+  auto* id = state->callsites.Find(raw_callsite_id);
+  if (!id) {
+    return base::ErrStatus("v8 callsite id does not exist: cannot add sample");
+  }
+  UniqueTid utid = context_->process_tracker->UpdateThread(tid, pid);
+  auto* samples = context_->storage->mutable_cpu_profile_stack_sample_table();
+  samples->Insert({ts, *id, utid, 0});
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h b/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h
new file mode 100644
index 0000000..bdb2e78
--- /dev/null
+++ b/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h
@@ -0,0 +1,87 @@
+/*
+ * 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_COMMON_LEGACY_V8_CPU_PROFILE_TRACKER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_LEGACY_V8_CPU_PROFILE_TRACKER_H_
+
+#include <cstdint>
+#include <optional>
+#include <utility>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/hash.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_view.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor {
+
+// Stores interned callsites for given pid for legacy v8 samples.
+class LegacyV8CpuProfileTracker {
+ public:
+  explicit LegacyV8CpuProfileTracker(TraceProcessorContext*);
+
+  // Sets the start timestamp for the given pid.
+  void SetStartTsForSessionAndPid(uint64_t session_id,
+                                  uint32_t pid,
+                                  int64_t ts);
+
+  // Adds the callsite with for the given session and pid and given raw callsite
+  // id.
+  base::Status AddCallsite(uint64_t session_id,
+                           uint32_t pid,
+                           uint32_t raw_callsite_id,
+                           std::optional<uint32_t> parent_raw_callsite_id,
+                           base::StringView script_url,
+                           base::StringView function_name);
+
+  // Increments the current timestamp for the given session and pid by
+  // |delta_ts| and returns the resulting full timestamp.
+  base::StatusOr<int64_t> AddDeltaAndGetTs(uint64_t session_id,
+                                           uint32_t pid,
+                                           int64_t delta_ts);
+
+  // Adds the sample with for the given session and pid/tid and given raw
+  // callsite id.
+  base::Status AddSample(int64_t ts,
+                         uint64_t session_id,
+                         uint32_t pid,
+                         uint32_t tid,
+                         uint32_t raw_callsite_id);
+
+ private:
+  struct State {
+    int64_t ts;
+    base::FlatHashMap<uint32_t, CallsiteId> callsites;
+    DummyMemoryMapping* mapping;
+  };
+  struct Hasher {
+    uint64_t operator()(const std::pair<uint64_t, uint32_t>& res) {
+      return base::Hasher::Combine(res.first, res.second);
+    }
+  };
+  base::FlatHashMap<std::pair<uint64_t, uint32_t>, State, Hasher>
+      state_by_session_and_pid_;
+
+  TraceProcessorContext* const context_;
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_LEGACY_V8_CPU_PROFILE_TRACKER_H_
diff --git a/src/trace_processor/importers/common/mapping_tracker.cc b/src/trace_processor/importers/common/mapping_tracker.cc
index 02976f9..6e6fbf3 100644
--- a/src/trace_processor/importers/common/mapping_tracker.cc
+++ b/src/trace_processor/importers/common/mapping_tracker.cc
@@ -25,6 +25,7 @@
 #include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/importers/common/address_range.h"
 #include "src/trace_processor/importers/common/jit_cache.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/util/build_id.h"
@@ -161,14 +162,15 @@
       });
 }
 
-VirtualMemoryMapping* MappingTracker::GetDummyMapping() {
-  if (!dummy_mapping_) {
-    CreateMappingParams params;
-    params.memory_range =
-        AddressRange::FromStartAndSize(0, std::numeric_limits<uint64_t>::max());
-    dummy_mapping_ = &InternMemoryMapping(params);
-  }
-  return dummy_mapping_;
+DummyMemoryMapping& MappingTracker::CreateDummyMapping(std::string name) {
+  CreateMappingParams params;
+  params.name = std::move(name);
+  params.memory_range =
+      AddressRange::FromStartAndSize(0, std::numeric_limits<uint64_t>::max());
+  std::unique_ptr<DummyMemoryMapping> mapping(
+      new DummyMemoryMapping(context_, std::move(params)));
+
+  return AddMapping(std::move(mapping));
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/common/mapping_tracker.h b/src/trace_processor/importers/common/mapping_tracker.h
index 4791dba..c655d57 100644
--- a/src/trace_processor/importers/common/mapping_tracker.h
+++ b/src/trace_processor/importers/common/mapping_tracker.h
@@ -68,6 +68,10 @@
   UserMemoryMapping& CreateUserMemoryMapping(UniquePid upid,
                                              CreateMappingParams params);
 
+  // Sometimes we just need a mapping and we are lacking trace data to create a
+  // proper one. Use this mapping in those cases.
+  DummyMemoryMapping& CreateDummyMapping(std::string name);
+
   // Create an "other" mapping. Returned reference will be valid for the
   // duration of this instance.
   VirtualMemoryMapping& InternMemoryMapping(CreateMappingParams params);
@@ -91,10 +95,6 @@
   // Jitted ranges will only be applied to UserMemoryMappings
   void AddJitRange(UniquePid upid, AddressRange range, JitCache* jit_cache);
 
-  // Sometimes we just need a mapping and we are lacking trace data to create a
-  // proper one. Use this mapping in those cases.
-  VirtualMemoryMapping* GetDummyMapping();
-
  private:
   template <typename MappingImpl>
   MappingImpl& AddMapping(std::unique_ptr<MappingImpl> mapping);
@@ -140,8 +140,6 @@
   KernelMemoryMapping* kernel_ = nullptr;
 
   base::FlatHashMap<UniquePid, AddressRangeMap<JitCache*>> jit_caches_;
-
-  VirtualMemoryMapping* dummy_mapping_ = nullptr;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/common/parser_types.h b/src/trace_processor/importers/common/parser_types.h
index 7290626..917479a 100644
--- a/src/trace_processor/importers/common/parser_types.h
+++ b/src/trace_processor/importers/common/parser_types.h
@@ -17,14 +17,19 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_PARSER_TYPES_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_PARSER_TYPES_H_
 
-#include <stdint.h>
+#include <array>
+#include <cstdint>
+#include <functional>
+#include <optional>
+#include <string>
+#include <utility>
 
+#include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 struct alignas(8) InlineSchedSwitch {
   int64_t prev_state;
@@ -32,6 +37,11 @@
   int32_t next_prio;
   StringPool::Id next_comm;
 };
+static_assert(sizeof(InlineSchedSwitch) == 24);
+
+// We enforce the exact size as it's critical for peak-memory use when sorting
+// data in trace processor that this struct is as small as possible.
+static_assert(sizeof(InlineSchedSwitch) == 24);
 
 struct alignas(8) InlineSchedWaking {
   int32_t pid;
@@ -40,18 +50,29 @@
   StringPool::Id comm;
   uint16_t common_flags;
 };
+
+// We enforce the exact size as it's critical for peak-memory use when sorting
+// data in trace processor that this struct is as small as possible.
 static_assert(sizeof(InlineSchedWaking) == 16);
 
 struct alignas(8) JsonEvent {
   std::string value;
 };
+static_assert(sizeof(JsonEvent) % 8 == 0);
 
-struct TracePacketData {
+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;
 };
+static_assert(sizeof(TracePacketData) % 8 == 0);
 
-struct TrackEventData {
+struct alignas(8) TrackEventData {
   TrackEventData(TraceBlobView pv,
                  RefPtr<PacketSequenceStateGeneration> generation)
       : trace_packet_data{std::move(pv), std::move(generation)} {}
@@ -75,8 +96,16 @@
   double counter_value = 0;
   std::array<double, kMaxNumExtraCounters> extra_counter_values = {};
 };
+static_assert(sizeof(TracePacketData) % 8 == 0);
 
-}  // namespace trace_processor
-}  // namespace perfetto
+struct alignas(8) LegacyV8CpuProfileEvent {
+  uint64_t session_id;
+  uint32_t pid;
+  uint32_t tid;
+  uint32_t callsite_id;
+};
+static_assert(sizeof(LegacyV8CpuProfileEvent) % 8 == 0);
+
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_PARSER_TYPES_H_
diff --git a/src/trace_processor/importers/common/scoped_active_trace_file.cc b/src/trace_processor/importers/common/scoped_active_trace_file.cc
deleted file mode 100644
index 7249261..0000000
--- a/src/trace_processor/importers/common/scoped_active_trace_file.cc
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/importers/common/scoped_active_trace_file.h"
-
-#include <cstddef>
-#include <cstdint>
-#include <string>
-
-#include "perfetto/ext/base/string_view.h"
-#include "src/trace_processor/importers/common/trace_file_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"
-
-namespace perfetto::trace_processor {
-ScopedActiveTraceFile::~ScopedActiveTraceFile() {
-  if (is_valid_) {
-    context_->trace_file_tracker->EndFile(row_);
-  }
-}
-
-void ScopedActiveTraceFile::SetName(const std::string& name) {
-  row_.set_name(context_->storage->InternString(base::StringView(name)));
-}
-
-void ScopedActiveTraceFile::SetTraceType(TraceType type) {
-  row_.set_trace_type(context_->storage->InternString(TraceTypeToString(type)));
-}
-
-void ScopedActiveTraceFile::SetSize(size_t size) {
-  row_.set_size(static_cast<int64_t>(size));
-}
-
-void ScopedActiveTraceFile::AddSize(size_t size) {
-  row_.set_size(static_cast<int64_t>(size) + row_.size());
-}
-
-}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/common/scoped_active_trace_file.h b/src/trace_processor/importers/common/scoped_active_trace_file.h
deleted file mode 100644
index 0006fcf..0000000
--- a/src/trace_processor/importers/common/scoped_active_trace_file.h
+++ /dev/null
@@ -1,77 +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.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_SCOPED_ACTIVE_TRACE_FILE_H_
-#define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_SCOPED_ACTIVE_TRACE_FILE_H_
-
-#include <string>
-
-#include "src/trace_processor/tables/metadata_tables_py.h"
-#include "src/trace_processor/util/trace_type.h"
-
-namespace perfetto::trace_processor {
-
-class TraceProcessorContext;
-
-// RAII like object that represents a file currently being parsed. When
-// instances of this object go out of scope they will notify the
-// TraceFileTracker that we are done parsing the file.
-// This class also acts a handler for setting file related properties.
-class ScopedActiveTraceFile {
- public:
-  ~ScopedActiveTraceFile();
-
-  ScopedActiveTraceFile(const ScopedActiveTraceFile&) = delete;
-  ScopedActiveTraceFile& operator=(const ScopedActiveTraceFile&) = delete;
-
-  ScopedActiveTraceFile(ScopedActiveTraceFile&& o)
-      : context_(o.context_), row_(o.row_), is_valid_(o.is_valid_) {
-    o.is_valid_ = false;
-  }
-
-  ScopedActiveTraceFile& operator=(ScopedActiveTraceFile&& o) {
-    context_ = o.context_;
-    row_ = o.row_;
-    is_valid_ = o.is_valid_;
-    o.is_valid_ = false;
-    return *this;
-  }
-
-  void SetTraceType(TraceType type);
-
-  // For streamed files this method can be called for each chunk to update the
-  // file size incrementally.
-  void AddSize(size_t delta);
-
- private:
-  friend class TraceFileTracker;
-  ScopedActiveTraceFile(TraceProcessorContext* context,
-                        tables::TraceFileTable::RowReference row)
-      : context_(context), row_(row), is_valid_(true) {}
-
-  // Sets the file name. If this method is not called (sometimes we do not know
-  // the file name, e.g. streaming data) the name is set to null.
-  void SetName(const std::string& name);
-  void SetSize(size_t size);
-
-  TraceProcessorContext* context_;
-  tables::TraceFileTable::RowReference row_;
-  bool is_valid_;
-};
-
-}  // namespace perfetto::trace_processor
-
-#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_SCOPED_ACTIVE_TRACE_FILE_H_
diff --git a/src/trace_processor/importers/common/slice_tracker.cc b/src/trace_processor/importers/common/slice_tracker.cc
index 5f3af01..443b8fa 100644
--- a/src/trace_processor/importers/common/slice_tracker.cc
+++ b/src/trace_processor/importers/common/slice_tracker.cc
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+#include <cstdint>
 #include <limits>
 
 #include <stdint.h>
@@ -28,6 +29,9 @@
 
 namespace perfetto {
 namespace trace_processor {
+namespace {
+constexpr uint32_t kMaxDepth = 512;
+}
 
 SliceTracker::SliceTracker(TraceProcessorContext* context)
     : legacy_unnestable_begin_count_string_id_(
@@ -177,7 +181,7 @@
 
   SliceId id = inserter();
   tables::SliceTable::RowReference ref = *slices->FindById(id);
-  if (depth >= std::numeric_limits<uint8_t>::max()) {
+  if (depth >= kMaxDepth) {
     auto parent_name = context_->storage->GetString(
         parent_ref->name().value_or(kNullStringId));
     auto name =
@@ -191,7 +195,7 @@
 
   // Post fill all the relevant columns. All the other columns should have
   // been filled by the inserter.
-  ref.set_depth(static_cast<uint8_t>(depth));
+  ref.set_depth(static_cast<uint32_t>(depth));
   ref.set_parent_stack_id(parent_stack_id);
   ref.set_stack_id(GetStackHash(stack));
   if (parent_id)
diff --git a/src/trace_processor/importers/common/thread_state_tracker.cc b/src/trace_processor/importers/common/thread_state_tracker.cc
index a0d98ce..907894b 100644
--- a/src/trace_processor/importers/common/thread_state_tracker.cc
+++ b/src/trace_processor/importers/common/thread_state_tracker.cc
@@ -98,7 +98,9 @@
 void ThreadStateTracker::PushNewTaskEvent(int64_t event_ts,
                                           UniqueTid utid,
                                           UniqueTid waker_utid) {
-  AddOpenState(event_ts, utid, runnable_string_id_, std::nullopt, waker_utid);
+  // open a runnable state with a non-interrupt wakeup from the cloning thread.
+  AddOpenState(event_ts, utid, runnable_string_id_, /*cpu=*/std::nullopt,
+               waker_utid, /*common_flags=*/0);
 }
 
 void ThreadStateTracker::PushBlockedReason(
diff --git a/src/trace_processor/importers/common/trace_file_tracker.cc b/src/trace_processor/importers/common/trace_file_tracker.cc
index e51371a..2bf02b7 100644
--- a/src/trace_processor/importers/common/trace_file_tracker.cc
+++ b/src/trace_processor/importers/common/trace_file_tracker.cc
@@ -17,11 +17,14 @@
 #include "src/trace_processor/importers/common/trace_file_tracker.h"
 
 #include <cstddef>
+#include <cstdint>
+#include <optional>
 #include <string>
 #include <vector>
 
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
-#include "src/trace_processor/storage/metadata.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"
@@ -29,46 +32,49 @@
 
 namespace perfetto::trace_processor {
 
-ScopedActiveTraceFile TraceFileTracker::StartNewFile() {
-  tables::TraceFileTable::Row row;
-  if (!ancestors_.empty()) {
-    row.parent_id = ancestors_.back();
-  }
-
-  row.size = 0;
-  row.trace_type =
-      context_->storage->InternString(TraceTypeToString(kUnknownTraceType));
-
-  auto ref =
-      context_->storage->mutable_trace_file_table()->Insert(row).row_reference;
-
-  ancestors_.push_back(ref.id());
-  return ScopedActiveTraceFile(context_, std::move(ref));
+tables::TraceFileTable::Id TraceFileTracker::AddFile(const std::string& name) {
+  return AddFileImpl(context_->storage->InternString(base::StringView(name)));
 }
 
-ScopedActiveTraceFile TraceFileTracker::StartNewFile(const std::string& name,
-                                                     TraceType type,
-                                                     size_t size) {
-  auto file = StartNewFile();
-  file.SetName(name);
-  file.SetTraceType(type);
-  file.SetSize(size);
-  return file;
+tables::TraceFileTable::Id TraceFileTracker::AddFileImpl(StringId name) {
+  std::optional<tables::TraceFileTable::Id> parent =
+      parsing_stack_.empty() ? std::nullopt
+                             : std::make_optional(parsing_stack_.back());
+  return context_->storage->mutable_trace_file_table()
+      ->Insert({parent, name, 0,
+                context_->storage->InternString(
+                    TraceTypeToString(kUnknownTraceType)),
+                std::nullopt})
+      .id;
 }
 
-void TraceFileTracker::EndFile(
-    const tables::TraceFileTable::ConstRowReference& row) {
-  PERFETTO_CHECK(!ancestors_.empty());
-  PERFETTO_CHECK(ancestors_.back() == row.id());
+void TraceFileTracker::SetSize(tables::TraceFileTable::Id id, uint64_t size) {
+  auto row = *context_->storage->mutable_trace_file_table()->FindById(id);
+  row.set_size(static_cast<int64_t>(size));
+}
+
+void TraceFileTracker::StartParsing(tables::TraceFileTable::Id id,
+                                    TraceType trace_type) {
+  parsing_stack_.push_back(id);
+  auto row = *context_->storage->mutable_trace_file_table()->FindById(id);
+  row.set_trace_type(
+      context_->storage->InternString(TraceTypeToString(trace_type)));
+  row.set_processing_order(static_cast<int64_t>(processing_order_++));
+}
+
+void TraceFileTracker::DoneParsing(tables::TraceFileTable::Id id, size_t size) {
+  PERFETTO_CHECK(!parsing_stack_.empty() && parsing_stack_.back() == id);
+  parsing_stack_.pop_back();
+  auto row = *context_->storage->mutable_trace_file_table()->FindById(id);
+  row.set_size(static_cast<int64_t>(size));
 
   // First file (root)
-  if (row.id().value == 0) {
+  if (id.value == 0) {
     context_->metadata_tracker->SetMetadata(metadata::trace_size_bytes,
                                             Variadic::Integer(row.size()));
     context_->metadata_tracker->SetMetadata(metadata::trace_type,
                                             Variadic::String(row.trace_type()));
   }
-  ancestors_.pop_back();
 }
 
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/common/trace_file_tracker.h b/src/trace_processor/importers/common/trace_file_tracker.h
index 0e2f5f9..4c6be5f 100644
--- a/src/trace_processor/importers/common/trace_file_tracker.h
+++ b/src/trace_processor/importers/common/trace_file_tracker.h
@@ -20,7 +20,7 @@
 #include <string>
 #include <vector>
 
-#include "src/trace_processor/importers/common/scoped_active_trace_file.h"
+#include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/util/trace_type.h"
 
@@ -36,24 +36,18 @@
   explicit TraceFileTracker(TraceProcessorContext* context)
       : context_(context) {}
 
-  // Notifies the start of a new file that we are about to parse. It returns a
-  // RAII like object that will notify the end of processing when it goes out of
-  // scope.
-  // NOTE: Files must be ended in reverse order of being started.
-  ScopedActiveTraceFile StartNewFile();
-
-  // Convenience version of the above that should be used when all the file
-  // properties are known upfront.
-  ScopedActiveTraceFile StartNewFile(const std::string& name,
-                                     TraceType type,
-                                     size_t size);
+  tables::TraceFileTable::Id AddFile(const std::string& name);
+  tables::TraceFileTable::Id AddFile() { return AddFileImpl(kNullStringId); }
+  void SetSize(tables::TraceFileTable::Id id, uint64_t size);
+  void StartParsing(tables::TraceFileTable::Id id, TraceType trace_type);
+  void DoneParsing(tables::TraceFileTable::Id id, size_t size);
 
  private:
-  void EndFile(const tables::TraceFileTable::ConstRowReference& row);
+  tables::TraceFileTable::Id AddFileImpl(StringId name);
 
-  friend class ScopedActiveTraceFile;
   TraceProcessorContext* const context_;
-  std::vector<tables::TraceFileTable::Id> ancestors_;
+  size_t processing_order_ = 0;
+  std::vector<tables::TraceFileTable::Id> parsing_stack_;
 };
 
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/common/trace_parser.cc b/src/trace_processor/importers/common/trace_parser.cc
index f497278..2754a2e 100644
--- a/src/trace_processor/importers/common/trace_parser.cc
+++ b/src/trace_processor/importers/common/trace_parser.cc
@@ -16,14 +16,17 @@
 
 #include "src/trace_processor/importers/common/trace_parser.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
-ProtoTraceParser::~ProtoTraceParser() = default;
-JsonTraceParser::~JsonTraceParser() = default;
-FuchsiaRecordParser::~FuchsiaRecordParser() = default;
-PerfRecordParser::~PerfRecordParser() = default;
 AndroidLogEventParser::~AndroidLogEventParser() = default;
+FuchsiaRecordParser::~FuchsiaRecordParser() = default;
+InstrumentsRowParser::~InstrumentsRowParser() = default;
+JsonTraceParser::~JsonTraceParser() = default;
+PerfRecordParser::~PerfRecordParser() = default;
+ProtoTraceParser::~ProtoTraceParser() = default;
+SpeRecordParser::~SpeRecordParser() = default;
+GeckoTraceParser::~GeckoTraceParser() = default;
+ArtMethodParser::~ArtMethodParser() = default;
+PerfTextTraceParser::~PerfTextTraceParser() = default;
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/common/trace_parser.h b/src/trace_processor/importers/common/trace_parser.h
index 87fd8ff..a672a07 100644
--- a/src/trace_processor/importers/common/trace_parser.h
+++ b/src/trace_processor/importers/common/trace_parser.h
@@ -17,13 +17,25 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACE_PARSER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACE_PARSER_H_
 
-#include <stdint.h>
+#include <cstdint>
 #include <string>
 
 namespace perfetto::trace_processor {
 namespace perf_importer {
 struct Record;
 }
+namespace instruments_importer {
+struct Row;
+}
+namespace gecko_importer {
+struct GeckoEvent;
+}
+namespace art_method {
+struct ArtMethodEvent;
+}
+namespace perf_text_importer {
+struct PerfTextEvent;
+}
 
 struct AndroidLogEvent;
 class PacketSequenceStateGeneration;
@@ -34,6 +46,7 @@
 struct InlineSchedWaking;
 struct TracePacketData;
 struct TrackEventData;
+struct LegacyV8CpuProfileEvent;
 
 class ProtoTraceParser {
  public:
@@ -51,6 +64,7 @@
   virtual ~JsonTraceParser();
   virtual void ParseJsonPacket(int64_t, std::string) = 0;
   virtual void ParseSystraceLine(int64_t, SystraceLine) = 0;
+  virtual void ParseLegacyV8ProfileEvent(int64_t, LegacyV8CpuProfileEvent) = 0;
 };
 
 class FuchsiaRecordParser {
@@ -65,12 +79,43 @@
   virtual void ParsePerfRecord(int64_t, perf_importer::Record) = 0;
 };
 
+class SpeRecordParser {
+ public:
+  virtual ~SpeRecordParser();
+  virtual void ParseSpeRecord(int64_t, TraceBlobView) = 0;
+};
+
+class InstrumentsRowParser {
+ public:
+  virtual ~InstrumentsRowParser();
+  virtual void ParseInstrumentsRow(int64_t, instruments_importer::Row) = 0;
+};
+
 class AndroidLogEventParser {
  public:
   virtual ~AndroidLogEventParser();
   virtual void ParseAndroidLogEvent(int64_t, AndroidLogEvent) = 0;
 };
 
+class GeckoTraceParser {
+ public:
+  virtual ~GeckoTraceParser();
+  virtual void ParseGeckoEvent(int64_t, gecko_importer::GeckoEvent) = 0;
+};
+
+class ArtMethodParser {
+ public:
+  virtual ~ArtMethodParser();
+  virtual void ParseArtMethodEvent(int64_t, art_method::ArtMethodEvent) = 0;
+};
+
+class PerfTextTraceParser {
+ public:
+  virtual ~PerfTextTraceParser();
+  virtual void ParsePerfTextEvent(int64_t,
+                                  perf_text_importer::PerfTextEvent) = 0;
+};
+
 }  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACE_PARSER_H_
diff --git a/src/trace_processor/importers/common/track_tracker.cc b/src/trace_processor/importers/common/track_tracker.cc
index 9d32e04..6b02ff8 100644
--- a/src/trace_processor/importers/common/track_tracker.cc
+++ b/src/trace_processor/importers/common/track_tracker.cc
@@ -16,22 +16,25 @@
 
 #include "src/trace_processor/importers/common/track_tracker.h"
 
+#include <cstddef>
 #include <cstdint>
 #include <optional>
+#include <string>
 #include <utility>
 
+#include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/cpu_tracker.h"
 #include "src/trace_processor/importers/common/process_track_translation_table.h"
+#include "src/trace_processor/importers/common/tracks.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/tables/profiler_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"
 
-namespace perfetto {
-namespace trace_processor {
-
+namespace perfetto::trace_processor {
 namespace {
 
 const char* GetNameForGroup(TrackTracker::Group group) {
@@ -60,6 +63,39 @@
   PERFETTO_FATAL("For GCC");
 }
 
+bool IsLegacyStringIdNameAllowed(tracks::TrackClassification classification) {
+  // **DO NOT** add new values here. Use TrackTracker::AutoName instead.
+  return classification == tracks::android_energy_estimation_breakdown ||
+         classification ==
+             tracks::android_energy_estimation_breakdown_per_uid ||
+         classification == tracks::unknown;
+}
+
+bool IsLegacyCharArrayNameAllowed(tracks::TrackClassification classification) {
+  // **DO NOT** add new values here. Use TrackTracker::AutoName instead.
+  return classification == tracks::cpu_capacity ||
+         classification == tracks::cpu_frequency ||
+         classification == tracks::cpu_frequency_throttle ||
+         classification == tracks::cpu_funcgraph ||
+         classification == tracks::cpu_idle ||
+         classification == tracks::cpu_irq ||
+         classification == tracks::cpu_mali_irq ||
+         classification == tracks::cpu_max_frequency_limit ||
+         classification == tracks::cpu_min_frequency_limit ||
+         classification == tracks::cpu_napi_gro ||
+         classification == tracks::cpu_nr_running ||
+         classification == tracks::cpu_stat ||
+         classification == tracks::cpu_softirq ||
+         classification == tracks::cpu_utilization ||
+         classification == tracks::gpu_frequency ||
+         classification == tracks::interconnect_events ||
+         classification == tracks::irq_counter ||
+         classification == tracks::linux_rpm ||
+         classification == tracks::pkvm_hypervisor ||
+         classification == tracks::softirq_counter ||
+         classification == tracks::triggers;
+}
+
 }  // namespace
 
 TrackTracker::TrackTracker(TraceProcessorContext* context)
@@ -69,279 +105,188 @@
           context->storage->InternString("trace_id_is_process_scoped")),
       source_scope_key_(context->storage->InternString("source_scope")),
       category_key_(context->storage->InternString("category")),
+      scope_id_(context->storage->InternString("scope")),
+      cookie_id_(context->storage->InternString("cookie")),
       fuchsia_source_(context->storage->InternString("fuchsia")),
       chrome_source_(context->storage->InternString("chrome")),
+      utid_id_(context->storage->InternString("utid")),
+      upid_id_(context->storage->InternString("upid")),
+      ucpu_id_(context->storage->InternString("ucpu")),
+      uid_id_(context->storage->InternString("uid")),
+      gpu_id_(context->storage->InternString("gpu")),
+      name_id_(context->storage->InternString("name")),
       context_(context) {}
 
-TrackId TrackTracker::InternThreadTrack(UniqueTid utid) {
-  auto it = thread_tracks_.find(utid);
-  if (it != thread_tracks_.end())
-    return it->second;
+TrackId TrackTracker::CreateTrack(tracks::TrackClassification classification,
+                                  std::optional<Dimensions> dimensions,
+                                  const TrackName& name) {
+  tables::TrackTable::Row row(StringIdFromTrackName(classification, name));
+  row.classification =
+      context_->storage->InternString(tracks::ToString(classification));
+  if (dimensions) {
+    row.dimension_arg_set_id = dimensions->arg_set_id;
+  }
+  row.machine_id = context_->machine_id();
 
-  tables::ThreadTrackTable::Row row;
+  return context_->storage->mutable_track_table()->Insert(row).id;
+}
+
+TrackId TrackTracker::CreateCounterTrack(
+    tracks::TrackClassification classification,
+    std::optional<Dimensions> dimensions,
+    const TrackName& name) {
+  tables::CounterTrackTable::Row row(
+      StringIdFromTrackName(classification, name));
+  row.classification =
+      context_->storage->InternString(tracks::ToString(classification));
+  if (dimensions) {
+    row.dimension_arg_set_id = dimensions->arg_set_id;
+  }
+  row.machine_id = context_->machine_id();
+
+  return context_->storage->mutable_counter_track_table()->Insert(row).id;
+}
+
+TrackId TrackTracker::CreateProcessTrack(
+    tracks::TrackClassification classification,
+    UniquePid upid,
+    std::optional<Dimensions> dims,
+    const TrackName& name) {
+  Dimensions dims_id =
+      dims ? *dims : SingleDimension(upid_id_, Variadic::Integer(upid));
+
+  tables::ProcessTrackTable::Row row(
+      StringIdFromTrackName(classification, name));
+  row.upid = upid;
+  row.dimension_arg_set_id = dims_id.arg_set_id;
+  row.classification =
+      context_->storage->InternString(tracks::ToString(classification));
+  row.machine_id = context_->machine_id();
+
+  return context_->storage->mutable_process_track_table()->Insert(row).id;
+}
+
+TrackId TrackTracker::CreateProcessCounterTrack(
+    tracks::TrackClassification classification,
+    UniquePid upid,
+    std::optional<Dimensions> dims,
+    const TrackName& name) {
+  Dimensions dims_id =
+      dims ? *dims : SingleDimension(upid_id_, Variadic::Integer(upid));
+
+  tables::ProcessCounterTrackTable::Row row(
+      StringIdFromTrackName(classification, name));
+  row.upid = upid;
+  row.machine_id = context_->machine_id();
+  row.dimension_arg_set_id = dims_id.arg_set_id;
+  row.classification =
+      context_->storage->InternString(tracks::ToString(classification));
+
+  return context_->storage->mutable_process_counter_track_table()
+      ->Insert(row)
+      .id;
+}
+
+TrackId TrackTracker::CreateThreadTrack(
+    tracks::TrackClassification classification,
+    UniqueTid utid,
+    const TrackName& name) {
+  Dimensions dims_id = SingleDimension(utid_id_, Variadic::Integer(utid));
+
+  tables::ThreadTrackTable::Row row(
+      StringIdFromTrackName(classification, name));
+  row.utid = utid;
+  row.classification =
+      context_->storage->InternString(tracks::ToString(classification));
+  row.dimension_arg_set_id = dims_id.arg_set_id;
+  row.machine_id = context_->machine_id();
+
+  return context_->storage->mutable_thread_track_table()->Insert(row).id;
+}
+
+TrackId TrackTracker::CreateThreadCounterTrack(
+    tracks::TrackClassification classification,
+    UniqueTid utid,
+    const TrackName& name) {
+  Dimensions dims_id = SingleDimension(utid_id_, Variadic::Integer(utid));
+
+  tables::ThreadCounterTrackTable::Row row(
+      StringIdFromTrackName(classification, name));
   row.utid = utid;
   row.machine_id = context_->machine_id();
-  auto id = context_->storage->mutable_thread_track_table()->Insert(row).id;
-  thread_tracks_[utid] = id;
-  return id;
+  row.dimension_arg_set_id = dims_id.arg_set_id;
+  row.classification =
+      context_->storage->InternString(tracks::ToString(classification));
+
+  return context_->storage->mutable_thread_counter_track_table()
+      ->Insert(row)
+      .id;
 }
 
-TrackId TrackTracker::InternProcessTrack(UniquePid upid) {
-  auto it = process_tracks_.find(upid);
-  if (it != process_tracks_.end())
-    return it->second;
+TrackId TrackTracker::InternTrack(tracks::TrackClassification classification,
+                                  std::optional<Dimensions> dimensions,
+                                  const TrackName& name,
+                                  const SetArgsCallback& callback) {
+  auto* it = tracks_.Find({classification, dimensions});
+  if (it)
+    return *it;
 
-  tables::ProcessTrackTable::Row row;
-  row.upid = upid;
-  row.machine_id = context_->machine_id();
-  auto id = context_->storage->mutable_process_track_table()->Insert(row).id;
-  process_tracks_[upid] = id;
-  return id;
-}
-
-TrackId TrackTracker::InternFuchsiaAsyncTrack(StringId name,
-                                              uint32_t upid,
-                                              int64_t correlation_id) {
-  return InternLegacyChromeAsyncTrack(name, upid, correlation_id, false,
-                                      StringId());
-}
-
-TrackId TrackTracker::InternCpuTrack(StringId name, uint32_t cpu) {
-  auto it = cpu_tracks_.find(std::make_pair(name, cpu));
-  if (it != cpu_tracks_.end()) {
-    return it->second;
-  }
-
-  tables::CpuTrackTable::Row row(name);
-  row.ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
-  row.machine_id = context_->machine_id();
-  auto id = context_->storage->mutable_cpu_track_table()->Insert(row).id;
-  cpu_tracks_[std::make_pair(name, cpu)] = id;
-
-  return id;
-}
-
-TrackId TrackTracker::InternGpuTrack(const tables::GpuTrackTable::Row& row) {
-  GpuTrackTuple tuple{row.name, row.scope, row.context_id.value_or(0)};
-
-  auto it = gpu_tracks_.find(tuple);
-  if (it != gpu_tracks_.end())
-    return it->second;
-
-  auto row_copy = row;
-  row_copy.machine_id = context_->machine_id();
-  auto id = context_->storage->mutable_gpu_track_table()->Insert(row_copy).id;
-  gpu_tracks_[tuple] = id;
-  return id;
-}
-
-TrackId TrackTracker::InternGpuWorkPeriodTrack(
-    const tables::GpuWorkPeriodTrackTable::Row& row) {
-  GpuWorkPeriodTrackTuple tuple{row.name, row.gpu_id, row.uid};
-
-  auto it = gpu_work_period_tracks_.find(tuple);
-  if (it != gpu_work_period_tracks_.end())
-    return it->second;
-
-  auto id =
-      context_->storage->mutable_gpu_work_period_track_table()->Insert(row).id;
-  gpu_work_period_tracks_[tuple] = id;
-  return id;
-}
-
-TrackId TrackTracker::InternLegacyChromeAsyncTrack(
-    StringId raw_name,
-    uint32_t upid,
-    int64_t trace_id,
-    bool trace_id_is_process_scoped,
-    StringId source_scope) {
-  ChromeTrackTuple tuple;
-  if (trace_id_is_process_scoped)
-    tuple.upid = upid;
-  tuple.trace_id = trace_id;
-  tuple.source_scope = source_scope;
-
-  const StringId name =
-      context_->process_track_translation_table->TranslateName(raw_name);
-  auto it = chrome_tracks_.find(tuple);
-  if (it != chrome_tracks_.end()) {
-    if (name != kNullStringId) {
-      // The track may have been created for an end event without name. In that
-      // case, update it with this event's name.
-      auto& tracks = *context_->storage->mutable_track_table();
-      auto rr = *tracks.FindById(it->second);
-      if (rr.name() == kNullStringId) {
-        rr.set_name(name);
-      }
-    }
-    return it->second;
-  }
-
-  // Legacy async tracks are always drawn in the context of a process, even if
-  // the ID's scope is global.
-  tables::ProcessTrackTable::Row track(name);
-  track.upid = upid;
-  track.machine_id = context_->machine_id();
-  TrackId id =
-      context_->storage->mutable_process_track_table()->Insert(track).id;
-  chrome_tracks_[tuple] = id;
-
-  context_->args_tracker->AddArgsTo(id)
-      .AddArg(source_key_, Variadic::String(chrome_source_))
-      .AddArg(trace_id_key_, Variadic::Integer(trace_id))
-      .AddArg(trace_id_is_process_scoped_key_,
-              Variadic::Boolean(trace_id_is_process_scoped))
-      .AddArg(source_scope_key_, Variadic::String(source_scope));
-
-  return id;
-}
-
-TrackId TrackTracker::CreateGlobalAsyncTrack(StringId name, StringId source) {
-  tables::TrackTable::Row row(name);
-  row.machine_id = context_->machine_id();
-  auto id = context_->storage->mutable_track_table()->Insert(row).id;
-  if (!source.is_null()) {
-    context_->args_tracker->AddArgsTo(id).AddArg(source_key_,
-                                                 Variadic::String(source));
-  }
-  return id;
-}
-
-TrackId TrackTracker::CreateProcessAsyncTrack(StringId raw_name,
-                                              UniquePid upid,
-                                              StringId source) {
-  const StringId name =
-      context_->process_track_translation_table->TranslateName(raw_name);
-  tables::ProcessTrackTable::Row row(name);
-  row.upid = upid;
-  row.machine_id = context_->machine_id();
-  auto id = context_->storage->mutable_process_track_table()->Insert(row).id;
-  if (!source.is_null()) {
-    context_->args_tracker->AddArgsTo(id).AddArg(source_key_,
-                                                 Variadic::String(source));
-  }
-  return id;
-}
-
-TrackId TrackTracker::InternLegacyChromeProcessInstantTrack(UniquePid upid) {
-  auto it = chrome_process_instant_tracks_.find(upid);
-  if (it != chrome_process_instant_tracks_.end())
-    return it->second;
-
-  tables::ProcessTrackTable::Row row;
-  row.upid = upid;
-  row.machine_id = context_->machine_id();
-  auto id = context_->storage->mutable_process_track_table()->Insert(row).id;
-  chrome_process_instant_tracks_[upid] = id;
-
-  context_->args_tracker->AddArgsTo(id).AddArg(
-      source_key_, Variadic::String(chrome_source_));
-
-  return id;
-}
-
-TrackId TrackTracker::GetOrCreateLegacyChromeGlobalInstantTrack() {
-  if (!chrome_global_instant_track_id_) {
-    tables::TrackTable::Row row;
-    row.machine_id = context_->machine_id();
-    chrome_global_instant_track_id_ =
-        context_->storage->mutable_track_table()->Insert(row).id;
-
-    context_->args_tracker->AddArgsTo(*chrome_global_instant_track_id_)
-        .AddArg(source_key_, Variadic::String(chrome_source_));
-  }
-  return *chrome_global_instant_track_id_;
-}
-
-TrackId TrackTracker::GetOrCreateTriggerTrack() {
-  if (trigger_track_id_) {
-    return *trigger_track_id_;
-  }
-  tables::TrackTable::Row row;
-  row.name = context_->storage->InternString("Trace Triggers");
-  row.machine_id = context_->machine_id();
-  trigger_track_id_ = context_->storage->mutable_track_table()->Insert(row).id;
-  return *trigger_track_id_;
-}
-
-TrackId TrackTracker::GetOrCreateInterconnectTrack() {
-  if (interconnect_events_track_id_) {
-    return *interconnect_events_track_id_;
-  }
-  tables::TrackTable::Row row;
-  row.name = context_->storage->InternString("Interconnect Events");
-  row.machine_id = context_->machine_id();
-  interconnect_events_track_id_ =
-      context_->storage->mutable_track_table()->Insert(row).id;
-  return *interconnect_events_track_id_;
-}
-
-TrackId TrackTracker::InternGlobalCounterTrack(TrackTracker::Group group,
-                                               StringId name,
-                                               SetArgsCallback callback,
-                                               StringId unit,
-                                               StringId description) {
-  auto it = global_counter_tracks_by_name_.find(name);
-  if (it != global_counter_tracks_by_name_.end()) {
-    return it->second;
-  }
-
-  tables::CounterTrackTable::Row row(name);
-  row.parent_id = InternTrackForGroup(group);
-  row.unit = unit;
-  row.description = description;
-  row.machine_id = context_->machine_id();
-  TrackId track =
-      context_->storage->mutable_counter_track_table()->Insert(row).id;
-  global_counter_tracks_by_name_[name] = track;
+  TrackId id = CreateTrack(classification, dimensions, name);
+  tracks_[{classification, dimensions}] = id;
   if (callback) {
-    auto inserter = context_->args_tracker->AddArgsTo(track);
+    ArgsTracker::BoundInserter inserter = context_->args_tracker->AddArgsTo(id);
     callback(inserter);
   }
-  return track;
+  return id;
 }
 
-TrackId TrackTracker::InternCpuCounterTrack(StringId name, uint32_t cpu) {
-  auto it = cpu_counter_tracks_.find(std::make_pair(name, cpu));
-  if (it != cpu_counter_tracks_.end()) {
-    return it->second;
-  }
+TrackId TrackTracker::InternCounterTrack(
+    tracks::TrackClassification classification,
+    std::optional<Dimensions> dimensions,
+    const TrackName& name) {
+  auto* it = tracks_.Find({classification, dimensions});
+  if (it)
+    return *it;
 
-  tables::CpuCounterTrackTable::Row row(name);
-  row.ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
-  row.machine_id = context_->machine_id();
-
-  TrackId track =
-      context_->storage->mutable_cpu_counter_track_table()->Insert(row).id;
-  cpu_counter_tracks_[std::make_pair(name, cpu)] = track;
-  return track;
+  TrackId id = CreateCounterTrack(classification, dimensions, name);
+  tracks_[{classification, dimensions}] = id;
+  return id;
 }
 
-TrackId TrackTracker::InternThreadCounterTrack(StringId name, UniqueTid utid) {
-  auto it = utid_counter_tracks_.find(std::make_pair(name, utid));
-  if (it != utid_counter_tracks_.end()) {
-    return it->second;
-  }
+TrackId TrackTracker::InternProcessTrack(
+    tracks::TrackClassification classification,
+    UniquePid upid,
+    const TrackName& name) {
+  Dimensions dims_id = SingleDimension(upid_id_, Variadic::Integer(upid));
 
-  tables::ThreadCounterTrackTable::Row row(name);
-  row.utid = utid;
-  row.machine_id = context_->machine_id();
+  auto* it = tracks_.Find({classification, dims_id});
+  if (it)
+    return *it;
 
-  TrackId track =
-      context_->storage->mutable_thread_counter_track_table()->Insert(row).id;
-  utid_counter_tracks_[std::make_pair(name, utid)] = track;
-  return track;
+  TrackId track_id =
+      CreateProcessTrack(classification, upid, std::nullopt, name);
+  tracks_[{classification, dims_id}] = track_id;
+  return track_id;
 }
 
-TrackId TrackTracker::InternProcessCounterTrack(StringId raw_name,
-                                                UniquePid upid,
-                                                StringId unit,
-                                                StringId description) {
+TrackId TrackTracker::LegacyInternProcessCounterTrack(StringId raw_name,
+                                                      UniquePid upid,
+                                                      StringId unit,
+                                                      StringId description) {
   const StringId name =
       context_->process_track_translation_table->TranslateName(raw_name);
-  auto it = upid_counter_tracks_.find(std::make_pair(name, upid));
-  if (it != upid_counter_tracks_.end()) {
-    return it->second;
+
+  TrackMapKey key;
+  key.classification = tracks::unknown;
+
+  DimensionsBuilder dims_builder = CreateDimensionsBuilder();
+  dims_builder.AppendUpid(upid);
+  dims_builder.AppendName(name);
+  key.dimensions = std::move(dims_builder).Build();
+
+  auto* it = tracks_.Find(key);
+  if (it) {
+    return *it;
   }
 
   tables::ProcessCounterTrackTable::Row row(name);
@@ -349,130 +294,273 @@
   row.unit = unit;
   row.description = description;
   row.machine_id = context_->machine_id();
-
-  TrackId track =
+  row.classification =
+      context_->storage->InternString(tracks::ToString(key.classification));
+  row.dimension_arg_set_id = key.dimensions->arg_set_id;
+  TrackId track_id =
       context_->storage->mutable_process_counter_track_table()->Insert(row).id;
-  upid_counter_tracks_[std::make_pair(name, upid)] = track;
-  return track;
+
+  tracks_[key] = track_id;
+  return track_id;
 }
 
-TrackId TrackTracker::InternIrqCounterTrack(StringId name, int32_t irq) {
-  auto it = irq_counter_tracks_.find(std::make_pair(name, irq));
-  if (it != irq_counter_tracks_.end()) {
-    return it->second;
+TrackId TrackTracker::InternThreadTrack(UniqueTid utid, const TrackName& name) {
+  Dimensions dims = SingleDimension(utid_id_, Variadic::Integer(utid));
+
+  auto* it = tracks_.Find({tracks::thread, dims});
+  if (it)
+    return *it;
+  TrackId track_id = CreateThreadTrack(tracks::thread, utid, name);
+  tracks_[{tracks::thread, dims}] = track_id;
+  return track_id;
+}
+
+TrackId TrackTracker::LegacyInternThreadCounterTrack(StringId name,
+                                                     UniqueTid utid) {
+  TrackMapKey key;
+  key.classification = tracks::unknown;
+
+  DimensionsBuilder dims_builder = CreateDimensionsBuilder();
+  dims_builder.AppendUtid(utid);
+  dims_builder.AppendName(name);
+  key.dimensions = std::move(dims_builder).Build();
+
+  auto* it = tracks_.Find(key);
+  if (it) {
+    return *it;
   }
 
-  tables::IrqCounterTrackTable::Row row(name);
-  row.irq = irq;
+  TrackId track_id =
+      CreateThreadCounterTrack(tracks::unknown, utid, LegacyStringIdName{name});
+  tracks_[key] = track_id;
+  return track_id;
+}
+
+TrackId TrackTracker::InternCpuTrack(tracks::TrackClassification classification,
+                                     uint32_t cpu,
+                                     const TrackName& name) {
+  auto ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
+  Dimensions dims_id = SingleDimension(ucpu_id_, Variadic::Integer(ucpu.value));
+
+  auto* it = tracks_.Find({classification, dims_id});
+  if (it) {
+    return *it;
+  }
+
+  tables::CpuTrackTable::Row row(StringIdFromTrackName(classification, name));
+  row.ucpu = ucpu;
   row.machine_id = context_->machine_id();
+  row.classification =
+      context_->storage->InternString(tracks::ToString(classification));
+  row.dimension_arg_set_id = dims_id.arg_set_id;
 
-  TrackId track =
-      context_->storage->mutable_irq_counter_track_table()->Insert(row).id;
-  irq_counter_tracks_[std::make_pair(name, irq)] = track;
-  return track;
+  TrackId track_id =
+      context_->storage->mutable_cpu_track_table()->Insert(row).id;
+  tracks_[{classification, dims_id}] = track_id;
+  return track_id;
 }
 
-TrackId TrackTracker::InternSoftirqCounterTrack(StringId name,
-                                                int32_t softirq) {
-  auto it = softirq_counter_tracks_.find(std::make_pair(name, softirq));
-  if (it != softirq_counter_tracks_.end()) {
-    return it->second;
+TrackId TrackTracker::InternGlobalTrack(
+    tracks::TrackClassification classification,
+    const TrackName& name,
+    const SetArgsCallback& callback) {
+  return InternTrack(classification, std::nullopt, name, callback);
+}
+
+TrackId TrackTracker::LegacyInternGpuTrack(
+    const tables::GpuTrackTable::Row& row) {
+  DimensionsBuilder dims_builder = CreateDimensionsBuilder();
+  dims_builder.AppendGpu(row.context_id.value_or(0));
+  if (row.scope != kNullStringId) {
+    dims_builder.AppendDimension(scope_id_, Variadic::String(row.scope));
+  }
+  dims_builder.AppendName(row.name);
+  Dimensions dims_id = std::move(dims_builder).Build();
+
+  TrackMapKey key;
+  key.classification = tracks::unknown;
+  key.dimensions = dims_id;
+
+  auto* it = tracks_.Find(key);
+  if (it)
+    return *it;
+
+  auto row_copy = row;
+  row_copy.classification =
+      context_->storage->InternString(tracks::ToString(tracks::unknown));
+  row_copy.dimension_arg_set_id = dims_id.arg_set_id;
+  row_copy.machine_id = context_->machine_id();
+
+  TrackId track_id =
+      context_->storage->mutable_gpu_track_table()->Insert(row_copy).id;
+  tracks_[key] = track_id;
+  return track_id;
+}
+
+TrackId TrackTracker::LegacyInternGlobalCounterTrack(TrackTracker::Group group,
+                                                     StringId name,
+                                                     SetArgsCallback callback,
+                                                     StringId unit,
+                                                     StringId description) {
+  TrackMapKey key;
+  key.classification = tracks::unknown;
+  key.dimensions = SingleDimension(name_id_, Variadic::String(name));
+
+  auto* it = tracks_.Find(key);
+  if (it) {
+    return *it;
   }
 
-  tables::SoftirqCounterTrackTable::Row row(name);
-  row.softirq = softirq;
+  tables::CounterTrackTable::Row row(name);
+  row.parent_id = InternTrackForGroup(group);
+  row.unit = unit;
+  row.description = description;
   row.machine_id = context_->machine_id();
+  row.classification =
+      context_->storage->InternString(tracks::ToString(tracks::unknown));
 
   TrackId track =
-      context_->storage->mutable_softirq_counter_track_table()->Insert(row).id;
-  softirq_counter_tracks_[std::make_pair(name, softirq)] = track;
+      context_->storage->mutable_counter_track_table()->Insert(row).id;
+  tracks_[key] = track;
+
+  if (callback) {
+    auto inserter = context_->args_tracker->AddArgsTo(track);
+    callback(inserter);
+  }
+
   return track;
 }
 
-TrackId TrackTracker::InternGpuCounterTrack(StringId name, uint32_t gpu_id) {
-  auto it = gpu_counter_tracks_.find(std::make_pair(name, gpu_id));
-  if (it != gpu_counter_tracks_.end()) {
-    return it->second;
-  }
-  TrackId track = CreateGpuCounterTrack(name, gpu_id);
-  gpu_counter_tracks_[std::make_pair(name, gpu_id)] = track;
-  return track;
-}
+TrackId TrackTracker::InternCpuCounterTrack(
+    tracks::TrackClassification classification,
+    uint32_t cpu,
+    const TrackName& name) {
+  auto ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
+  StringId name_id = StringIdFromTrackName(classification, name);
 
-TrackId TrackTracker::InternEnergyCounterTrack(StringId name,
-                                               int32_t consumer_id,
-                                               StringId consumer_type,
-                                               int32_t ordinal) {
-  auto it = energy_counter_tracks_.find(std::make_pair(name, consumer_id));
-  if (it != energy_counter_tracks_.end()) {
-    return it->second;
+  TrackMapKey key;
+  key.classification = classification;
+
+  DimensionsBuilder dims_builder = CreateDimensionsBuilder();
+  dims_builder.AppendUcpu(ucpu);
+  dims_builder.AppendName(name_id);
+  key.dimensions = std::move(dims_builder).Build();
+
+  auto* it = tracks_.Find(key);
+  if (it) {
+    return *it;
   }
-  tables::EnergyCounterTrackTable::Row row(name);
-  row.consumer_id = consumer_id;
-  row.consumer_type = consumer_type;
-  row.ordinal = ordinal;
+
+  tables::CpuCounterTrackTable::Row row(name_id);
+  row.ucpu = ucpu;
   row.machine_id = context_->machine_id();
-  TrackId track =
-      context_->storage->mutable_energy_counter_track_table()->Insert(row).id;
-  energy_counter_tracks_[std::make_pair(name, consumer_id)] = track;
-  return track;
+  row.classification =
+      context_->storage->InternString(tracks::ToString(classification));
+  row.dimension_arg_set_id = key.dimensions->arg_set_id;
+
+  TrackId track_id =
+      context_->storage->mutable_cpu_counter_track_table()->Insert(row).id;
+  tracks_[key] = track_id;
+  return track_id;
 }
 
-TrackId TrackTracker::InternEnergyPerUidCounterTrack(StringId name,
-                                                     int32_t consumer_id,
-                                                     int32_t uid) {
-  auto it = energy_per_uid_counter_tracks_.find(std::make_pair(name, uid));
-  if (it != energy_per_uid_counter_tracks_.end()) {
-    return it->second;
+TrackId TrackTracker::LegacyInternCpuIdleStateTrack(uint32_t cpu,
+                                                    StringId state) {
+  auto ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
+  DimensionsBuilder dims_builder = CreateDimensionsBuilder();
+  dims_builder.AppendDimension(
+      context_->storage->InternString("cpu_idle_state"),
+      Variadic::String(state));
+  dims_builder.AppendUcpu(ucpu);
+  Dimensions dims_id = std::move(dims_builder).Build();
+
+  tracks::TrackClassification classification = tracks::cpu_idle_state;
+
+  auto* it = tracks_.Find({classification, dims_id});
+  if (it) {
+    return *it;
   }
 
-  tables::EnergyPerUidCounterTrackTable::Row row(name);
-  row.consumer_id = consumer_id;
-  row.uid = uid;
+  std::string name =
+      "cpuidle." + context_->storage->GetString(state).ToStdString();
+
+  tables::CpuCounterTrackTable::Row row(
+      context_->storage->InternString(name.c_str()));
+  row.ucpu = ucpu;
   row.machine_id = context_->machine_id();
-  TrackId track =
-      context_->storage->mutable_energy_per_uid_counter_track_table()
-          ->Insert(row)
-          .id;
-  energy_per_uid_counter_tracks_[std::make_pair(name, uid)] = track;
-  return track;
+  row.classification =
+      context_->storage->InternString(tracks::ToString(classification));
+  row.dimension_arg_set_id = dims_id.arg_set_id;
+
+  TrackId track_id =
+      context_->storage->mutable_cpu_counter_track_table()->Insert(row).id;
+  tracks_[{classification, dims_id}] = track_id;
+  return track_id;
 }
 
-TrackId TrackTracker::InternLinuxDeviceTrack(StringId name) {
-  if (auto it = linux_device_tracks_.find(name);
-      it != linux_device_tracks_.end()) {
-    return it->second;
+TrackId TrackTracker::InternGpuCounterTrack(
+    tracks::TrackClassification classification,
+    uint32_t gpu_id,
+    const TrackName& name) {
+  Dimensions dims_id = SingleDimension(gpu_id_, Variadic::Integer(gpu_id));
+
+  auto* it = tracks_.Find({classification, dims_id});
+  if (it) {
+    return *it;
   }
 
-  tables::LinuxDeviceTrackTable::Row row(name);
-  TrackId track =
-      context_->storage->mutable_linux_device_track_table()->Insert(row).id;
-  linux_device_tracks_[name] = track;
-  return track;
+  tables::GpuCounterTrackTable::Row row(
+      StringIdFromTrackName(classification, name));
+  row.gpu_id = gpu_id;
+  row.machine_id = context_->machine_id();
+  row.dimension_arg_set_id = dims_id.arg_set_id;
+  row.classification =
+      context_->storage->InternString(tracks::ToString(classification));
+
+  TrackId track_id =
+      context_->storage->mutable_gpu_counter_track_table()->Insert(row).id;
+
+  tracks_[{classification, dims_id}] = track_id;
+  return track_id;
 }
 
-TrackId TrackTracker::CreateGpuCounterTrack(StringId name,
-                                            uint32_t gpu_id,
-                                            StringId description,
-                                            StringId unit) {
+TrackId TrackTracker::LegacyCreateGpuCounterTrack(StringId name,
+                                                  uint32_t gpu_id,
+                                                  StringId description,
+                                                  StringId unit) {
   tables::GpuCounterTrackTable::Row row(name);
   row.gpu_id = gpu_id;
   row.description = description;
   row.unit = unit;
   row.machine_id = context_->machine_id();
+  row.classification =
+      context_->storage->InternString(tracks::ToString(tracks::unknown));
+  row.dimension_arg_set_id =
+      SingleDimension(gpu_id_, Variadic::Integer(gpu_id)).arg_set_id;
 
   return context_->storage->mutable_gpu_counter_track_table()->Insert(row).id;
 }
 
-TrackId TrackTracker::CreatePerfCounterTrack(
+TrackId TrackTracker::LegacyCreatePerfCounterTrack(
     StringId name,
     tables::PerfSessionTable::Id perf_session_id,
     uint32_t cpu,
     bool is_timebase) {
+  auto ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
+  DimensionsBuilder dims_builder = CreateDimensionsBuilder();
+  dims_builder.AppendUcpu(ucpu);
+  dims_builder.AppendDimension(
+      context_->storage->InternString("perf_session_id"),
+      Variadic::Integer(perf_session_id.value));
+  Dimensions dims_id = std::move(dims_builder).Build();
+
   tables::PerfCounterTrackTable::Row row(name);
   row.perf_session_id = perf_session_id;
   row.cpu = cpu;
   row.is_timebase = is_timebase;
+  row.dimension_arg_set_id = dims_id.arg_set_id;
+  row.classification =
+      context_->storage->InternString(tracks::ToString(tracks::unknown));
   row.machine_id = context_->machine_id();
   return context_->storage->mutable_perf_counter_track_table()->Insert(row).id;
 }
@@ -484,13 +572,87 @@
     return *group_id;
   }
 
-  StringId id = context_->storage->InternString(GetNameForGroup(group));
-  tables::TrackTable::Row row{id};
-  row.machine_id = context_->machine_id();
-  TrackId track_id = context_->storage->mutable_track_table()->Insert(row).id;
+  StringId name_id = context_->storage->InternString(GetNameForGroup(group));
+  TrackId track_id =
+      InternTrack(tracks::unknown, std::nullopt, LegacyStringIdName{name_id});
   group_track_ids_[group_idx] = track_id;
   return track_id;
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+TrackId TrackTracker::LegacyInternLegacyChromeAsyncTrack(
+    StringId raw_name,
+    uint32_t upid,
+    int64_t trace_id,
+    bool trace_id_is_process_scoped,
+    StringId source_scope) {
+  DimensionsBuilder dims_builder = CreateDimensionsBuilder();
+  dims_builder.AppendDimension(scope_id_, Variadic::String(source_scope));
+  if (trace_id_is_process_scoped) {
+    dims_builder.AppendUpid(upid);
+  }
+  dims_builder.AppendDimension(cookie_id_, Variadic::Integer(trace_id));
+
+  const StringId name =
+      context_->process_track_translation_table->TranslateName(raw_name);
+
+  TrackMapKey key;
+  key.classification = tracks::unknown;
+  key.dimensions = std::move(dims_builder).Build();
+
+  auto* it = tracks_.Find(key);
+  if (it) {
+    if (name != kNullStringId) {
+      // The track may have been created for an end event without name. In
+      // that case, update it with this event's name.
+      auto& tracks = *context_->storage->mutable_track_table();
+      auto rr = *tracks.FindById(*it);
+      if (rr.name() == kNullStringId) {
+        rr.set_name(name);
+      }
+    }
+    return *it;
+  }
+
+  // Legacy async tracks are always drawn in the context of a process, even if
+  // the ID's scope is global.
+  tables::ProcessTrackTable::Row track(name);
+  track.upid = upid;
+  track.classification =
+      context_->storage->InternString(tracks::ToString(tracks::unknown));
+  track.dimension_arg_set_id = key.dimensions->arg_set_id;
+  track.machine_id = context_->machine_id();
+
+  TrackId id =
+      context_->storage->mutable_process_track_table()->Insert(track).id;
+  tracks_[key] = id;
+
+  context_->args_tracker->AddArgsTo(id)
+      .AddArg(source_key_, Variadic::String(chrome_source_))
+      .AddArg(trace_id_key_, Variadic::Integer(trace_id))
+      .AddArg(trace_id_is_process_scoped_key_,
+              Variadic::Boolean(trace_id_is_process_scoped))
+      .AddArg(source_scope_key_, Variadic::String(source_scope));
+
+  return id;
+}
+
+StringId TrackTracker::StringIdFromTrackName(
+    tracks::TrackClassification classification,
+    const TrackTracker::TrackName& name) {
+  switch (name.index()) {
+    case base::variant_index<TrackName, AutoName>():
+      return kNullStringId;
+    case base::variant_index<TrackName, LegacyStringIdName>():
+      PERFETTO_DCHECK(IsLegacyStringIdNameAllowed(classification));
+      return std::get<LegacyStringIdName>(name).id;
+    case base::variant_index<TrackName, LegacyCharArrayName>():
+      PERFETTO_DCHECK(IsLegacyCharArrayNameAllowed(classification));
+      return context_->storage->InternString(
+          std::get<LegacyCharArrayName>(name).name);
+    case base::variant_index<TrackName, FromTraceName>():
+      return std::get<FromTraceName>(name).id;
+  }
+  PERFETTO_FATAL("For GCC");
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/common/track_tracker.h b/src/trace_processor/importers/common/track_tracker.h
index 261b562..d4d14f0 100644
--- a/src/trace_processor/importers/common/track_tracker.h
+++ b/src/trace_processor/importers/common/track_tracker.h
@@ -17,19 +17,105 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACK_TRACKER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACK_TRACKER_H_
 
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <limits>
 #include <optional>
+#include <tuple>
+#include <variant>
 
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/hash.h"
+#include "perfetto/ext/base/string_utils.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
+#include "src/trace_processor/importers/common/global_args_tracker.h"
+#include "src/trace_processor/importers/common/tracks.h"
 #include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/tables/profiler_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"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 // Tracks and stores tracks based on track types, ids and scopes.
 class TrackTracker {
  public:
+  using SetArgsCallback = std::function<void(ArgsTracker::BoundInserter&)>;
+
+  // Dimensions of the data in a track. Used as an argument in
+  // `TrackTracker::InternTrack()`. Use `TrackTracker::DimensionsBuilder` to
+  // create.
+  struct Dimensions {
+    ArgSetId arg_set_id;
+
+    bool operator==(const Dimensions& o) const {
+      return arg_set_id == o.arg_set_id;
+    }
+  };
+
+  // Used to create `Dimensions` required to intern a new track.
+  class DimensionsBuilder {
+   public:
+    explicit DimensionsBuilder(TrackTracker* tt) : tt_(tt) {}
+
+    // Append CPU dimension of a track.
+    void AppendUcpu(tables::CpuTable::Id ucpu) {
+      AppendDimension(tt_->ucpu_id_, Variadic::Integer(ucpu.value));
+    }
+
+    // Append Utid (unique tid) dimension of a track.
+    void AppendUtid(UniqueTid utid) {
+      AppendDimension(tt_->utid_id_, Variadic::Integer(utid));
+    }
+
+    // Append Upid (unique pid) dimension of a track.
+    void AppendUpid(UniquePid upid) {
+      AppendDimension(tt_->upid_id_, Variadic::Integer(upid));
+    }
+
+    // Append gpu id dimension of a track.
+    void AppendGpu(int64_t gpu) {
+      AppendDimension(tt_->gpu_id_, Variadic::Integer(gpu));
+    }
+
+    // Append Uid (user id) dimension of a track.
+    void AppendUid(int64_t uid) {
+      AppendDimension(tt_->uid_id_, Variadic::Integer(uid));
+    }
+
+    // Append name dimension of a track. Only use in cases where name is a
+    // dimension, it is not a way to force the name of the track in a table.
+    void AppendName(StringId name) {
+      AppendDimension(tt_->name_id_, Variadic::String(name));
+    }
+
+    // Append custom dimension. Only use if none of the other Append functions
+    // are suitable.
+    void AppendDimension(StringId key, const Variadic& val) {
+      GlobalArgsTracker::CompactArg& arg = args_[count_args++];
+      arg.flat_key = key;
+      arg.key = key;
+      arg.value = val;
+    }
+
+    // Build to fetch the `Dimensions` value of the Appended dimensions. Pushes
+    // the dimensions into args table. Use the result in
+    // `TrackTracker::InternTrack`.
+    Dimensions Build() && {
+      return Dimensions{tt_->context_->global_args_tracker->AddArgSet(
+          args_.data(), 0, count_args)};
+    }
+
+   private:
+    TrackTracker* tt_;
+    std::array<GlobalArgsTracker::CompactArg, 64> args_;
+    uint32_t count_args = 0;
+  };
+
   // Enum which groups global tracks to avoid an explosion of tracks at the top
   // level.
   // Try and keep members of this enum high level as every entry here
@@ -48,203 +134,234 @@
     // Keep this last.
     kSizeSentinel,
   };
-  using SetArgsCallback = std::function<void(ArgsTracker::BoundInserter&)>;
+
+  // Indicates that the track name will be automatically generated by the trace
+  // processor.
+  // All tracks *MUST* use this option unless it is explicitly approved by a
+  // trace processor maintainer.
+  struct AutoName {};
+  // Indicates that the track name comes from the trace directly with no
+  // modification.
+  // *MUST NOT* be used without explicit appoval from a trace processor
+  // maintainer.
+  struct FromTraceName {
+    StringId id;
+  };
+  // Indicates that the track name is synthesied in trace processor as a
+  // StringId and works this way due to legacy reasons.
+  //
+  // Tracks *MUST NOT* use this method: this only exists for legacy tracks which
+  // we named before the introduction of classification/dimension system.
+  struct LegacyStringIdName {
+    StringId id;
+  };
+  // Indicates that the track name is synthesied in trace processor as a
+  // StackString and works this way due to legacy reasons.
+  //
+  // Tracks *MUST NOT* use this method: this only exists for legacy tracks which
+  // we named before the introduction of classification/dimension system.
+  struct LegacyCharArrayName {
+    template <size_t N>
+    explicit LegacyCharArrayName(const char (&_name)[N]) {
+      static_assert(N > 0 && N <= 512);
+      base::StringCopy(name, _name, N);
+    }
+    template <size_t N>
+    explicit LegacyCharArrayName(const base::StackString<N>& _name) {
+      static_assert(N > 0 && N <= 512);
+      base::StringCopy(name, _name.c_str(), N);
+    }
+    char name[512];
+  };
+  using TrackName = std::
+      variant<AutoName, FromTraceName, LegacyStringIdName, LegacyCharArrayName>;
 
   explicit TrackTracker(TraceProcessorContext*);
 
+  DimensionsBuilder CreateDimensionsBuilder() {
+    return DimensionsBuilder(this);
+  }
+
+  // Interns track into TrackTable. If the track created with below arguments
+  // already exists, returns the TrackTable::Id of the track.
+  //
+  // `name` is the display name of the track in trace processor and should
+  // always be `AutoName()` unless approved by a trace processor maintainer.
+  TrackId InternTrack(tracks::TrackClassification,
+                      std::optional<Dimensions>,
+                      const TrackName& name = AutoName(),
+                      const SetArgsCallback& callback = {});
+
+  // Interns a track with the given classification and one dimension into the
+  // `track` table. This is useful when interning global tracks which have a
+  // single uncommon dimension attached to them.
+  TrackId InternSingleDimensionTrack(tracks::TrackClassification classification,
+                                     StringId key,
+                                     const Variadic& value,
+                                     const TrackName& name = AutoName(),
+                                     const SetArgsCallback& callback = {}) {
+    return InternTrack(classification, SingleDimension(key, value), name,
+                       callback);
+  }
+
+  // Interns counter track into TrackTable. If the track created with below
+  // arguments already exists, returns the TrackTable::Id of the track.
+  TrackId InternCounterTrack(tracks::TrackClassification,
+                             std::optional<Dimensions>,
+                             const TrackName& = AutoName());
+
+  // Interns a unique track into the storage.
+  TrackId InternGlobalTrack(tracks::TrackClassification,
+                            const TrackName& = AutoName(),
+                            const SetArgsCallback& callback = {});
+
   // Interns a thread track into the storage.
-  TrackId InternThreadTrack(UniqueTid utid);
+  TrackId InternThreadTrack(UniqueTid, const TrackName& = AutoName());
 
   // Interns a process track into the storage.
-  TrackId InternProcessTrack(UniquePid upid);
+  TrackId InternProcessTrack(tracks::TrackClassification,
+                             UniquePid,
+                             const TrackName& = AutoName());
 
-  // Interns a Fuchsia async track into the storage.
-  TrackId InternFuchsiaAsyncTrack(StringId name,
-                                  uint32_t upid,
-                                  int64_t correlation_id);
-
-  // Interns a global track keyed by CPU + name into the storage.
-  TrackId InternCpuTrack(StringId name, uint32_t cpu);
-
-  // Interns a given GPU track into the storage.
-  TrackId InternGpuTrack(const tables::GpuTrackTable::Row& row);
-
-  // Interns a GPU work period track into the storage.
-  TrackId InternGpuWorkPeriodTrack(
-      const tables::GpuWorkPeriodTrackTable::Row& row);
-
-  // Interns a legacy Chrome async event track into the storage.
-  TrackId InternLegacyChromeAsyncTrack(StringId name,
-                                       uint32_t upid,
-                                       int64_t trace_id,
-                                       bool trace_id_is_process_scoped,
-                                       StringId source_scope);
-
-  // Interns a track for legacy Chrome process-scoped instant events into the
-  // storage.
-  TrackId InternLegacyChromeProcessInstantTrack(UniquePid upid);
-
-  // Lazily creates the track for legacy Chrome global instant events.
-  TrackId GetOrCreateLegacyChromeGlobalInstantTrack();
-
-  // Returns the ID of the implicit trace-global default track for triggers
-  // received by the service.
-  TrackId GetOrCreateTriggerTrack();
-
-  // Returns the ID of the track for Google Interconnect events
-  TrackId GetOrCreateInterconnectTrack();
-
-  // Interns a global counter track into the storage.
-  TrackId InternGlobalCounterTrack(Group group,
-                                   StringId name,
-                                   SetArgsCallback = {},
-                                   StringId unit = kNullStringId,
-                                   StringId description = kNullStringId);
+  // Interns a global track keyed by track type + CPU into the storage.
+  TrackId InternCpuTrack(tracks::TrackClassification,
+                         uint32_t cpu,
+                         const TrackName& = AutoName());
 
   // Interns a counter track associated with a cpu into the storage.
-  TrackId InternCpuCounterTrack(StringId name, uint32_t cpu);
-
-  // Interns a counter track associated with a thread into the storage.
-  TrackId InternThreadCounterTrack(StringId name, UniqueTid utid);
-
-  // Interns a counter track associated with a process into the storage.
-  TrackId InternProcessCounterTrack(StringId name,
-                                    UniquePid upid,
-                                    StringId unit = kNullStringId,
-                                    StringId description = kNullStringId);
-
-  // Interns a counter track associated with an irq into the storage.
-  TrackId InternIrqCounterTrack(StringId name, int32_t irq);
-
-  // Interns a counter track associated with an softirq into the storage.
-  TrackId InternSoftirqCounterTrack(StringId name, int32_t softirq);
+  TrackId InternCpuCounterTrack(tracks::TrackClassification,
+                                uint32_t cpu,
+                                const TrackName& = AutoName());
 
   // Interns a counter track associated with a GPU into the storage.
-  TrackId InternGpuCounterTrack(StringId name, uint32_t gpu_id);
-
-  // Interns energy counter track associated with a
-  // Energy breakdown into the storage.
-  TrackId InternEnergyCounterTrack(StringId name,
-                                   int32_t consumer_id,
-                                   StringId consumer_type,
-                                   int32_t ordinal);
-
-  // Interns a per process energy consumer counter track associated with a
-  // Energy Uid into the storage.
-  TrackId InternEnergyPerUidCounterTrack(StringId name,
-                                         int32_t consumer_id,
-                                         int32_t uid);
-
-  // Interns a track associated with a Linux device (where a Linux device
-  // implies a kernel-level device managed by a Linux driver).
-  TrackId InternLinuxDeviceTrack(StringId name);
-
-  // Creates a counter track associated with a GPU into the storage.
-  TrackId CreateGpuCounterTrack(StringId name,
+  TrackId InternGpuCounterTrack(tracks::TrackClassification,
                                 uint32_t gpu_id,
-                                StringId description = StringId::Null(),
-                                StringId unit = StringId::Null());
+                                const TrackName& = AutoName());
 
-  // Creates a counter track for values within perf samples.
-  // The tracks themselves are managed by PerfSampleTracker.
-  TrackId CreatePerfCounterTrack(StringId name,
-                                 tables::PerfSessionTable::Id perf_session_id,
-                                 uint32_t cpu,
-                                 bool is_timebase);
+  // Everything below this point are legacy functions and should no longer be
+  // used.
 
-  // NOTE:
-  // The below method should only be called by AsyncTrackSetTracker
+  TrackId LegacyInternLegacyChromeAsyncTrack(StringId name,
+                                             uint32_t upid,
+                                             int64_t trace_id,
+                                             bool trace_id_is_process_scoped,
+                                             StringId source_scope);
 
-  // Creates and inserts a global async track into the storage.
-  TrackId CreateGlobalAsyncTrack(StringId name, StringId source);
+  TrackId LegacyInternCpuIdleStateTrack(uint32_t cpu, StringId state);
 
-  // Creates and inserts a Android async track into the storage.
-  TrackId CreateProcessAsyncTrack(StringId name,
-                                  UniquePid upid,
-                                  StringId source);
+  TrackId LegacyCreateGpuCounterTrack(StringId name,
+                                      uint32_t gpu_id,
+                                      StringId description = StringId::Null(),
+                                      StringId unit = StringId::Null());
+
+  TrackId LegacyCreatePerfCounterTrack(StringId name,
+                                       tables::PerfSessionTable::Id,
+                                       uint32_t cpu,
+                                       bool is_timebase);
+
+  TrackId LegacyInternThreadCounterTrack(StringId name, UniqueTid);
+
+  TrackId LegacyInternGpuTrack(const tables::GpuTrackTable::Row&);
+
+  TrackId LegacyInternProcessCounterTrack(StringId name,
+                                          UniquePid,
+                                          StringId unit = kNullStringId,
+                                          StringId description = kNullStringId);
+
+  TrackId LegacyInternGlobalCounterTrack(Group,
+                                         StringId name,
+                                         SetArgsCallback = {},
+                                         StringId unit = kNullStringId,
+                                         StringId description = kNullStringId);
 
  private:
-  struct GpuTrackTuple {
-    StringId track_name;
-    StringId scope;
-    int64_t context_id;
+  friend class AsyncTrackSetTracker;
+  friend class TrackEventTracker;
 
-    friend bool operator<(const GpuTrackTuple& l, const GpuTrackTuple& r) {
-      return std::tie(l.track_name, l.scope, l.context_id) <
-             std::tie(r.track_name, r.scope, r.context_id);
+  struct TrackMapKey {
+    tracks::TrackClassification classification;
+    std::optional<Dimensions> dimensions;
+
+    bool operator==(const TrackMapKey& k) const {
+      return std::tie(classification, dimensions) ==
+             std::tie(k.classification, k.dimensions);
     }
   };
-  struct GpuWorkPeriodTrackTuple {
-    StringId track_name;
-    uint32_t gpu_id;
-    int32_t uid;
 
-    friend bool operator<(const GpuWorkPeriodTrackTuple& l,
-                          const GpuWorkPeriodTrackTuple& r) {
-      return std::tie(l.track_name, l.gpu_id, l.uid) <
-             std::tie(r.track_name, r.gpu_id, r.uid);
+  struct MapHasher {
+    size_t operator()(const TrackMapKey& l) const {
+      perfetto::base::Hasher hasher;
+      hasher.Update(static_cast<uint32_t>(l.classification));
+      hasher.Update(l.dimensions ? l.dimensions->arg_set_id : -1ll);
+      return hasher.digest();
     }
   };
-  struct ChromeTrackTuple {
-    std::optional<int64_t> upid;
-    int64_t trace_id = 0;
-    StringId source_scope = StringId::Null();
 
-    friend bool operator<(const ChromeTrackTuple& l,
-                          const ChromeTrackTuple& r) {
-      return std::tie(l.trace_id, l.upid, l.source_scope) <
-             std::tie(r.trace_id, r.upid, r.source_scope);
-    }
-  };
   static constexpr size_t kGroupCount =
       static_cast<uint32_t>(Group::kSizeSentinel);
 
-  TrackId InternTrackForGroup(Group group);
+  TrackId CreateTrack(tracks::TrackClassification,
+                      std::optional<Dimensions>,
+                      const TrackName&);
+
+  TrackId CreateCounterTrack(tracks::TrackClassification,
+                             std::optional<Dimensions>,
+                             const TrackName&);
+
+  TrackId CreateThreadTrack(tracks::TrackClassification,
+                            UniqueTid,
+                            const TrackName&);
+
+  TrackId CreateThreadCounterTrack(tracks::TrackClassification,
+                                   UniqueTid,
+                                   const TrackName&);
+
+  TrackId CreateProcessTrack(tracks::TrackClassification,
+                             UniquePid,
+                             std::optional<Dimensions>,
+                             const TrackName&);
+
+  TrackId CreateProcessCounterTrack(tracks::TrackClassification,
+                                    UniquePid,
+                                    std::optional<Dimensions>,
+                                    const TrackName&);
+
+  TrackId InternTrackForGroup(Group);
+
+  StringId StringIdFromTrackName(tracks::TrackClassification classification,
+                                 const TrackTracker::TrackName& name);
+
+  Dimensions SingleDimension(StringId key, const Variadic& val) {
+    std::array args{GlobalArgsTracker::CompactArg{key, key, val}};
+    return Dimensions{
+        context_->global_args_tracker->AddArgSet(args.data(), 0, 1)};
+  }
 
   std::array<std::optional<TrackId>, kGroupCount> group_track_ids_;
 
-  std::map<UniqueTid, TrackId> thread_tracks_;
-  std::map<UniquePid, TrackId> process_tracks_;
-  std::map<int64_t /* correlation_id */, TrackId> fuchsia_async_tracks_;
+  base::FlatHashMap<TrackMapKey, TrackId, MapHasher> tracks_;
 
-  std::map<std::pair<StringId, uint32_t /* cpu */>, TrackId> cpu_tracks_;
+  const StringId source_key_;
+  const StringId trace_id_key_;
+  const StringId trace_id_is_process_scoped_key_;
+  const StringId source_scope_key_;
+  const StringId category_key_;
+  const StringId scope_id_;
+  const StringId cookie_id_;
 
-  std::map<GpuTrackTuple, TrackId> gpu_tracks_;
-  std::map<ChromeTrackTuple, TrackId> chrome_tracks_;
-  std::map<UniquePid, TrackId> chrome_process_instant_tracks_;
-  std::map<std::pair<StringId, int32_t /*uid*/>, TrackId> uid_tracks_;
-  std::map<GpuWorkPeriodTrackTuple, TrackId> gpu_work_period_tracks_;
+  const StringId fuchsia_source_;
+  const StringId chrome_source_;
 
-  std::map<StringId, TrackId> global_counter_tracks_by_name_;
-  std::map<std::pair<StringId, uint32_t>, TrackId> cpu_counter_tracks_;
-  std::map<std::pair<StringId, UniqueTid>, TrackId> utid_counter_tracks_;
-  std::map<std::pair<StringId, UniquePid>, TrackId> upid_counter_tracks_;
-  std::map<std::pair<StringId, int32_t>, TrackId> irq_counter_tracks_;
-  std::map<std::pair<StringId, int32_t>, TrackId> softirq_counter_tracks_;
-  std::map<std::pair<StringId, uint32_t>, TrackId> gpu_counter_tracks_;
-  std::map<std::pair<StringId, int32_t>, TrackId> energy_counter_tracks_;
-  std::map<std::pair<StringId, int32_t>, TrackId> uid_counter_tracks_;
-  std::map<std::pair<StringId, int32_t>, TrackId>
-      energy_per_uid_counter_tracks_;
-  std::map<StringId, TrackId> linux_device_tracks_;
-
-  std::optional<TrackId> chrome_global_instant_track_id_;
-  std::optional<TrackId> trigger_track_id_;
-  std::optional<TrackId> interconnect_events_track_id_;
-
-  const StringId source_key_ = kNullStringId;
-  const StringId trace_id_key_ = kNullStringId;
-  const StringId trace_id_is_process_scoped_key_ = kNullStringId;
-  const StringId source_scope_key_ = kNullStringId;
-  const StringId category_key_ = kNullStringId;
-
-  const StringId fuchsia_source_ = kNullStringId;
-  const StringId chrome_source_ = kNullStringId;
+  const StringId utid_id_;
+  const StringId upid_id_;
+  const StringId ucpu_id_;
+  const StringId uid_id_;
+  const StringId gpu_id_;
+  const StringId name_id_;
 
   TraceProcessorContext* const context_;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACK_TRACKER_H_
diff --git a/src/trace_processor/importers/common/tracks.h b/src/trace_processor/importers/common/tracks.h
new file mode 100644
index 0000000..ce4fbcd
--- /dev/null
+++ b/src/trace_processor/importers/common/tracks.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACKS_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACKS_H_
+
+#include <array>
+#include <cstddef>
+
+namespace perfetto::trace_processor::tracks {
+
+// The classification of a track indicates the "type of data" the track
+// contains.
+//
+// Every track is uniquely identified by the the combination of the
+// classification and a set of dimensions: classifications allow identifying a
+// set of tracks with the same type of data within the whole universe of tracks
+// while dimensions allow distinguishing between different tracks in that set.
+#define PERFETTO_TP_TRACKS(F)                    \
+  F(android_energy_estimation_breakdown_per_uid) \
+  F(android_energy_estimation_breakdown)         \
+  F(android_gpu_work_period)                     \
+  F(android_lmk)                                 \
+  F(chrome_process_instant)                      \
+  F(cpu_capacity)                                \
+  F(cpu_frequency_throttle)                      \
+  F(cpu_frequency)                               \
+  F(cpu_funcgraph)                               \
+  F(cpu_idle_state)                              \
+  F(cpu_idle)                                    \
+  F(cpu_irq)                                     \
+  F(cpu_nr_running)                              \
+  F(cpu_mali_irq)                                \
+  F(cpu_max_frequency_limit)                     \
+  F(cpu_min_frequency_limit)                     \
+  F(cpu_napi_gro)                                \
+  F(cpu_softirq)                                 \
+  F(cpu_stat)                                    \
+  F(cpu_utilization)                             \
+  F(gpu_frequency)                               \
+  F(interconnect_events)                         \
+  F(irq_counter)                                 \
+  F(legacy_chrome_global_instants)               \
+  F(linux_device_frequency)                      \
+  F(linux_rpm)                                   \
+  F(pixel_cpm_trace)                             \
+  F(pkvm_hypervisor)                             \
+  F(softirq_counter)                             \
+  F(thread)                                      \
+  F(track_event)                                 \
+  F(triggers)                                    \
+  F(unknown)
+
+#define PERFETTO_TP_TRACKS_CLASSIFICATION_ENUM(name) name,
+enum TrackClassification : size_t {
+  PERFETTO_TP_TRACKS(PERFETTO_TP_TRACKS_CLASSIFICATION_ENUM)
+};
+
+#define PERFETTO_TP_TRACKS_CLASSIFICATION_STR(name) #name,
+constexpr std::array kTrackClassificationStr{
+    PERFETTO_TP_TRACKS(PERFETTO_TP_TRACKS_CLASSIFICATION_STR)};
+
+constexpr const char* ToString(TrackClassification c) {
+  return kTrackClassificationStr[c];
+}
+
+}  // namespace perfetto::trace_processor::tracks
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACKS_H_
diff --git a/src/trace_processor/importers/common/virtual_memory_mapping.cc b/src/trace_processor/importers/common/virtual_memory_mapping.cc
index 0485243..562049e 100644
--- a/src/trace_processor/importers/common/virtual_memory_mapping.cc
+++ b/src/trace_processor/importers/common/virtual_memory_mapping.cc
@@ -23,6 +23,7 @@
 #include <string>
 #include <utility>
 
+#include "perfetto/base/logging.h"
 #include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/importers/common/address_range.h"
 #include "src/trace_processor/importers/common/jit_cache.h"
@@ -119,5 +120,40 @@
   return {frame_id, true};
 }
 
+DummyMemoryMapping::~DummyMemoryMapping() = default;
+
+DummyMemoryMapping::DummyMemoryMapping(TraceProcessorContext* context,
+                                       CreateMappingParams params)
+    : VirtualMemoryMapping(context, std::move(params)) {}
+
+FrameId DummyMemoryMapping::InternDummyFrame(base::StringView function_name,
+                                             base::StringView source_file) {
+  DummyFrameKey key{context()->storage->InternString(function_name),
+                    context()->storage->InternString(source_file)};
+
+  if (FrameId* id = interned_dummy_frames_.Find(key); id) {
+    return *id;
+  }
+
+  uint32_t symbol_set_id = context()->storage->symbol_table().row_count();
+
+  tables::SymbolTable::Id symbol_id =
+      context()
+          ->storage->mutable_symbol_table()
+          ->Insert({symbol_set_id, key.function_name_id, key.source_file_id})
+          .id;
+
+  PERFETTO_CHECK(symbol_set_id == symbol_id.value);
+
+  const FrameId frame_id =
+      context()
+          ->storage->mutable_stack_profile_frame_table()
+          ->Insert({key.function_name_id, mapping_id(), 0, symbol_set_id})
+          .id;
+  interned_dummy_frames_.Insert(key, frame_id);
+
+  return frame_id;
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/common/virtual_memory_mapping.h b/src/trace_processor/importers/common/virtual_memory_mapping.h
index 498a9ef..1676437 100644
--- a/src/trace_processor/importers/common/virtual_memory_mapping.h
+++ b/src/trace_processor/importers/common/virtual_memory_mapping.h
@@ -86,6 +86,8 @@
   VirtualMemoryMapping(TraceProcessorContext* context,
                        CreateMappingParams params);
 
+  TraceProcessorContext* context() const { return context_; }
+
  private:
   friend class MappingTracker;
 
@@ -149,6 +151,42 @@
   const UniquePid upid_;
 };
 
+// Dummy mapping to be able to create frames when we have no real pc addresses
+// or real mappings.
+class DummyMemoryMapping : public VirtualMemoryMapping {
+ public:
+  ~DummyMemoryMapping() override;
+
+  // Interns a frame based solely on function name and source file. This is
+  // useful for profilers that do not emit an address nor a mapping.
+  FrameId InternDummyFrame(base::StringView function_name,
+                           base::StringView source_file);
+
+ private:
+  friend class MappingTracker;
+  DummyMemoryMapping(TraceProcessorContext* context,
+                     CreateMappingParams params);
+
+  struct DummyFrameKey {
+    StringId function_name_id;
+    StringId source_file_id;
+
+    bool operator==(const DummyFrameKey& o) const {
+      return function_name_id == o.function_name_id &&
+             source_file_id == o.source_file_id;
+    }
+
+    struct Hasher {
+      size_t operator()(const DummyFrameKey& k) const {
+        return static_cast<size_t>(base::Hasher::Combine(
+            k.function_name_id.raw_id(), k.source_file_id.raw_id()));
+      }
+    };
+  };
+  base::FlatHashMap<DummyFrameKey, FrameId, DummyFrameKey::Hasher>
+      interned_dummy_frames_;
+};
+
 }  // namespace trace_processor
 }  // namespace perfetto
 
diff --git a/src/trace_processor/importers/ftrace/BUILD.gn b/src/trace_processor/importers/ftrace/BUILD.gn
index 19354a7..79544b1 100644
--- a/src/trace_processor/importers/ftrace/BUILD.gn
+++ b/src/trace_processor/importers/ftrace/BUILD.gn
@@ -47,6 +47,8 @@
     "iostat_tracker.h",
     "mali_gpu_event_tracker.cc",
     "mali_gpu_event_tracker.h",
+    "pixel_mm_kswapd_event_tracker.cc",
+    "pixel_mm_kswapd_event_tracker.h",
     "pkvm_hyp_cpu_tracker.cc",
     "pkvm_hyp_cpu_tracker.h",
     "rss_stat_tracker.cc",
@@ -72,6 +74,7 @@
     "../../../protozero",
     "../../sorter",
     "../../storage",
+    "../../tables",
     "../../types",
     "../common",
     "../common:parser_types",
diff --git a/src/trace_processor/importers/ftrace/drm_tracker.cc b/src/trace_processor/importers/ftrace/drm_tracker.cc
index aeb7de6..f23ca75 100644
--- a/src/trace_processor/importers/ftrace/drm_tracker.cc
+++ b/src/trace_processor/importers/ftrace/drm_tracker.cc
@@ -126,7 +126,7 @@
   base::StackString<256> track_name("vblank-%d", crtc);
   StringId track_name_id =
       context_->storage->InternString(track_name.string_view());
-  return context_->track_tracker->InternGpuTrack(
+  return context_->track_tracker->LegacyInternGpuTrack(
       tables::GpuTrackTable::Row(track_name_id));
 }
 
@@ -163,7 +163,7 @@
   base::StackString<64> track_name("sched-%.*s", int(name.size()), name.data());
   StringId track_name_id =
       context_->storage->InternString(track_name.string_view());
-  TrackId track_id = context_->track_tracker->InternGpuTrack(
+  TrackId track_id = context_->track_tracker->LegacyInternGpuTrack(
       tables::GpuTrackTable::Row(track_name_id));
 
   // no std::make_unique until C++14..
@@ -260,7 +260,7 @@
                                    name.data(), context);
   StringId track_name_id =
       context_->storage->InternString(track_name.string_view());
-  TrackId track_id = context_->track_tracker->InternGpuTrack(
+  TrackId track_id = context_->track_tracker->LegacyInternGpuTrack(
       tables::GpuTrackTable::Row(track_name_id));
 
   // no std::make_unique until C++14..
diff --git a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
index 4e346e1..53085f9 100644
--- a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
@@ -24,7 +24,7 @@
 namespace trace_processor {
 namespace {
 
-std::array<FtraceMessageDescriptor, 538> descriptors{{
+std::array<FtraceMessageDescriptor, 544> descriptors{{
     {nullptr, 0, {}},
     {nullptr, 0, {}},
     {nullptr, 0, {}},
@@ -5353,26 +5353,32 @@
     },
     {
         "sched_switch_with_ctrs",
-        17,
+        23,
         {
             {},
             {"old_pid", ProtoSchemaType::kInt32},
             {"new_pid", ProtoSchemaType::kInt32},
-            {"cctr", ProtoSchemaType::kUint32},
-            {"ctr0", ProtoSchemaType::kUint32},
-            {"ctr1", ProtoSchemaType::kUint32},
-            {"ctr2", ProtoSchemaType::kUint32},
-            {"ctr3", ProtoSchemaType::kUint32},
+            {"cctr", ProtoSchemaType::kUint64},
+            {"ctr0", ProtoSchemaType::kUint64},
+            {"ctr1", ProtoSchemaType::kUint64},
+            {"ctr2", ProtoSchemaType::kUint64},
+            {"ctr3", ProtoSchemaType::kUint64},
             {"lctr0", ProtoSchemaType::kUint32},
             {"lctr1", ProtoSchemaType::kUint32},
-            {"ctr4", ProtoSchemaType::kUint32},
-            {"ctr5", ProtoSchemaType::kUint32},
+            {"ctr4", ProtoSchemaType::kUint64},
+            {"ctr5", ProtoSchemaType::kUint64},
             {"prev_comm", ProtoSchemaType::kString},
             {"prev_pid", ProtoSchemaType::kInt32},
             {"cyc", ProtoSchemaType::kUint32},
             {"inst", ProtoSchemaType::kUint32},
             {"stallbm", ProtoSchemaType::kUint32},
             {"l3dm", ProtoSchemaType::kUint32},
+            {"next_pid", ProtoSchemaType::kInt32},
+            {"next_comm", ProtoSchemaType::kString},
+            {"prev_state", ProtoSchemaType::kInt64},
+            {"amu0", ProtoSchemaType::kUint64},
+            {"amu1", ProtoSchemaType::kUint64},
+            {"amu2", ProtoSchemaType::kUint64},
         },
     },
     {
@@ -5950,6 +5956,66 @@
             {"active", ProtoSchemaType::kUint64},
         },
     },
+    {
+        "pixel_mm_kswapd_wake",
+        1,
+        {
+            {},
+            {"whatever", ProtoSchemaType::kInt32},
+        },
+    },
+    {
+        "pixel_mm_kswapd_done",
+        2,
+        {
+            {},
+            {"delta_nr_scanned", ProtoSchemaType::kUint64},
+            {"delta_nr_reclaimed", ProtoSchemaType::kUint64},
+        },
+    },
+    {
+        "sched_wakeup_task_attr",
+        5,
+        {
+            {},
+            {"pid", ProtoSchemaType::kInt32},
+            {"cpu_affinity", ProtoSchemaType::kUint64},
+            {"task_util", ProtoSchemaType::kUint64},
+            {"uclamp_min", ProtoSchemaType::kUint64},
+            {"vruntime", ProtoSchemaType::kUint64},
+        },
+    },
+    {
+        "devfreq_frequency",
+        5,
+        {
+            {},
+            {"dev_name", ProtoSchemaType::kString},
+            {"freq", ProtoSchemaType::kUint64},
+            {"prev_freq", ProtoSchemaType::kUint64},
+            {"busy_time", ProtoSchemaType::kUint64},
+            {"total_time", ProtoSchemaType::kUint64},
+        },
+    },
+    {
+        "kprobe_event",
+        2,
+        {
+            {},
+            {"name", ProtoSchemaType::kString},
+            {"type", ProtoSchemaType::kInt32},
+        },
+    },
+    {
+        "param_set_value_cpm",
+        3,
+        {
+            {},
+            {"body", ProtoSchemaType::kString},
+            {"value", ProtoSchemaType::kUint32},
+            {"timestamp", ProtoSchemaType::kInt64},
+        },
+    },
 }};
 
 }  // namespace
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index 1ef8874..d9b130e 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -15,14 +15,29 @@
  */
 
 #include "src/trace_processor/importers/ftrace/ftrace_parser.h"
-#include <optional>
 
+#include <algorithm>
+#include <array>
+#include <cinttypes>
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "perfetto/base/compiler.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/protozero/field.h"
 #include "perfetto/protozero/proto_decoder.h"
-
+#include "perfetto/protozero/proto_utils.h"
+#include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/basic_types.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/async_track_set_tracker.h"
 #include "src/trace_processor/importers/common/cpu_tracker.h"
@@ -32,27 +47,35 @@
 #include "src/trace_processor/importers/common/system_info_tracker.h"
 #include "src/trace_processor/importers/common/thread_state_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
+#include "src/trace_processor/importers/common/tracks.h"
 #include "src/trace_processor/importers/ftrace/binder_tracker.h"
+#include "src/trace_processor/importers/ftrace/ftrace_descriptors.h"
 #include "src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.h"
+#include "src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.h"
 #include "src/trace_processor/importers/ftrace/v4l2_tracker.h"
 #include "src/trace_processor/importers/ftrace/virtio_video_tracker.h"
 #include "src/trace_processor/importers/i2c/i2c_tracker.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/importers/syscalls/syscall_tracker.h"
 #include "src/trace_processor/importers/systrace/systrace_parser.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/types/softirq_action.h"
 #include "src/trace_processor/types/tcp_state.h"
+#include "src/trace_processor/types/variadic.h"
+#include "src/trace_processor/types/version_number.h"
 
 #include "protos/perfetto/common/gpu_counter_descriptor.pbzero.h"
 #include "protos/perfetto/trace/ftrace/android_fs.pbzero.h"
 #include "protos/perfetto/trace/ftrace/bcl_exynos.pbzero.h"
 #include "protos/perfetto/trace/ftrace/binder.pbzero.h"
 #include "protos/perfetto/trace/ftrace/cma.pbzero.h"
+#include "protos/perfetto/trace/ftrace/cpm_trace.pbzero.h"
 #include "protos/perfetto/trace/ftrace/cpuhp.pbzero.h"
 #include "protos/perfetto/trace/ftrace/cros_ec.pbzero.h"
 #include "protos/perfetto/trace/ftrace/dcvsh.pbzero.h"
+#include "protos/perfetto/trace/ftrace/devfreq.pbzero.h"
 #include "protos/perfetto/trace/ftrace/dmabuf_heap.pbzero.h"
 #include "protos/perfetto/trace/ftrace/dpu.pbzero.h"
 #include "protos/perfetto/trace/ftrace/fastrpc.pbzero.h"
@@ -101,9 +124,12 @@
 
 namespace {
 
+using protos::pbzero::perfetto_pbzero_enum_KprobeEvent::KprobeType;
 using protozero::ConstBytes;
 using protozero::ProtoDecoder;
 
+constexpr char kInternconnectTrackName[] = "Interconnect Events";
+
 struct FtraceEventAndFieldId {
   uint32_t event_id;
   uint32_t field_id;
@@ -269,29 +295,24 @@
          str.compare(0, prefix.size(), prefix) == 0;
 }
 
-// Constructs the display string for device PM callback slices.
+// Constructs the callback phase name for device PM callback slices.
 //
-// Format: "<driver name> <device name> <event type>[:<callback phase>]"
-//
-// Note: The optional `<callback phase>` is extracted from the `pm_ops` field
-// of the `device_pm_callback_start` tracepoint.
-std::string ConstructDpmCallbackSliceName(const std::string& driver_name,
-                                          const std::string& device_name,
-                                          const std::string& pm_ops,
-                                          const std::string& event_type) {
-  std::string slice_name_base =
-      driver_name + " " + device_name + " " + event_type;
+// Format: "<event type>[:<callback phase>]"
+// Examples: suspend, suspend:late, resume:noirq etc.
+std::string ConstructCallbackPhaseName(const std::string& pm_ops,
+                                       const std::string& event_type) {
+  std::string callback_phase = event_type;
 
   // The Linux kernel has a limitation where the `pm_ops` field in the
   // tracepoint is left empty if the phase is either prepare/complete.
   if (pm_ops == "") {
     if (event_type == "suspend")
-      return slice_name_base + ":prepare";
+      return callback_phase + ":prepare";
     else if (event_type == "resume")
-      return slice_name_base + ":complete";
+      return callback_phase + ":complete";
   }
 
-  // Extract callback phase (if present) for slice display name.
+  // Extract phase (if present) for slice details.
   //
   // The `pm_ops` string may contain both callback phase and callback type, but
   // only phase is needed. A prefix match is used due to potential absence of
@@ -299,11 +320,11 @@
   const std::vector<std::string> valid_phases = {"early", "late", "noirq"};
   for (const std::string& valid_phase : valid_phases) {
     if (StrStartsWith(pm_ops, valid_phase)) {
-      return slice_name_base + ":" + valid_phase;
+      return callback_phase + ":" + valid_phase;
     }
   }
 
-  return slice_name_base;
+  return callback_phase;
 }
 
 }  // namespace
@@ -318,14 +339,13 @@
       pkvm_hyp_cpu_tracker_(context),
       gpu_work_period_tracker_(context),
       thermal_tracker_(context),
+      pixel_mm_kswapd_event_tracker_(context),
       sched_wakeup_name_id_(context->storage->InternString("sched_wakeup")),
       sched_waking_name_id_(context->storage->InternString("sched_waking")),
       cpu_id_(context->storage->InternString("cpu")),
-      cpu_freq_name_id_(context->storage->InternString("cpufreq")),
-      cpu_freq_throttle_name_id_(
-          context->storage->InternString("cpufreq_throttle")),
-      gpu_freq_name_id_(context->storage->InternString("gpufreq")),
-      cpu_idle_name_id_(context->storage->InternString("cpuidle")),
+      ucpu_id_(context->storage->InternString("ucpu")),
+      linux_device_name_id_(
+          context->storage->InternString("linux_device_name")),
       suspend_resume_name_id_(
           context->storage->InternString("Suspend/Resume Latency")),
       suspend_resume_minimal_name_id_(
@@ -434,7 +454,21 @@
       runtime_status_active_id_(context->storage->InternString("Active")),
       runtime_status_suspending_id_(
           context->storage->InternString("Suspending")),
-      runtime_status_resuming_id_(context->storage->InternString("Resuming")) {
+      runtime_status_resuming_id_(context->storage->InternString("Resuming")),
+      suspend_resume_main_event_id_(
+          context->storage->InternString("Main Kernel Suspend Event")),
+      suspend_resume_device_pm_event_id_(
+          context->storage->InternString("Device PM Suspend Event")),
+      suspend_resume_utid_arg_name_(context->storage->InternString("utid")),
+      suspend_resume_device_arg_name_(
+          context->storage->InternString("device_name")),
+      suspend_resume_driver_arg_name_(
+          context->storage->InternString("driver_name")),
+      suspend_resume_callback_phase_arg_name_(
+          context->storage->InternString("callback_phase")),
+      suspend_resume_event_type_arg_name_(
+          context->storage->InternString("event_type")),
+      device_name_id_(context->storage->InternString("device_name")) {
   // 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++) {
@@ -627,6 +661,28 @@
     }
   }
 
+  protos::pbzero::FtraceKprobeStats::Decoder kprobe_stats(evt.kprobe_stats());
+  storage->SetStats(stats::ftrace_kprobe_hits_begin + phase,
+                    kprobe_stats.hits());
+  storage->SetStats(stats::ftrace_kprobe_misses_begin + phase,
+                    kprobe_stats.misses());
+  if (is_end) {
+    auto kprobe_hits_begin = storage->GetStats(stats::ftrace_kprobe_hits_begin);
+    auto kprobe_hits_end = kprobe_stats.hits();
+    if (kprobe_hits_begin) {
+      int64_t delta_hits = kprobe_hits_end - kprobe_hits_begin;
+      storage->SetStats(stats::ftrace_kprobe_hits_delta, delta_hits);
+    }
+
+    auto kprobe_misses_begin =
+        storage->GetStats(stats::ftrace_kprobe_misses_begin);
+    auto kprobe_misses_end = kprobe_stats.misses();
+    if (kprobe_misses_begin) {
+      int64_t delta_misses = kprobe_misses_end - kprobe_misses_begin;
+      storage->SetStats(stats::ftrace_kprobe_misses_delta, delta_misses);
+    }
+  }
+
   // Compute atrace + ftrace setup errors. We do two things here:
   // 1. We add up all the errors and put the counter in the stats table (which
   //    can hold only numerals).
@@ -1073,7 +1129,7 @@
         break;
       }
       case FtraceEvent::kSuspendResumeFieldNumber: {
-        ParseSuspendResume(ts, fld_bytes);
+        ParseSuspendResume(ts, cpu, pid, fld_bytes);
         break;
       }
       case FtraceEvent::kSuspendResumeMinimalFieldNumber: {
@@ -1215,6 +1271,10 @@
         ParseTrustyEnqueueNop(pid, ts, fld_bytes);
         break;
       }
+      case FtraceEvent::kDevfreqFrequencyFieldNumber: {
+        ParseDeviceFrequency(ts, fld_bytes);
+        break;
+      }
       case FtraceEvent::kMaliMaliKCPUCQSSETFieldNumber:
       case FtraceEvent::kMaliMaliKCPUCQSWAITSTARTFieldNumber:
       case FtraceEvent::kMaliMaliKCPUCQSWAITENDFieldNumber:
@@ -1289,7 +1349,7 @@
         break;
       }
       case FtraceEvent::kDevicePmCallbackStartFieldNumber: {
-        ParseDevicePmCallbackStart(ts, fld_bytes);
+        ParseDevicePmCallbackStart(ts, cpu, pid, fld_bytes);
         break;
       }
       case FtraceEvent::kDevicePmCallbackEndFieldNumber: {
@@ -1300,6 +1360,23 @@
         ParseBclIrq(ts, fld_bytes);
         break;
       }
+      case FtraceEvent::kPixelMmKswapdWakeFieldNumber: {
+        pixel_mm_kswapd_event_tracker_.ParsePixelMmKswapdWake(ts, pid);
+        break;
+      }
+      case FtraceEvent::kPixelMmKswapdDoneFieldNumber: {
+        pixel_mm_kswapd_event_tracker_.ParsePixelMmKswapdDone(ts, pid,
+                                                              fld_bytes);
+        break;
+      }
+      case FtraceEvent::kKprobeEventFieldNumber: {
+        ParseKprobe(ts, pid, fld_bytes);
+        break;
+      }
+      case FtraceEvent::kParamSetValueCpmFieldNumber: {
+        ParseParamSetValueCpm(fld_bytes);
+        break;
+      }
       default:
         break;
     }
@@ -1387,9 +1464,23 @@
     }
   }
 
-  // Calculate the timestamp used to skip events since, while still populating
+  // Calculate the timestamp used to skip early events, while still populating
   // the |ftrace_events| table.
-  switch (context_->config.soft_drop_ftrace_data_before) {
+  SoftDropFtraceDataBefore soft_drop_before =
+      context_->config.soft_drop_ftrace_data_before;
+
+  // TODO(b/344969928): Workaround, can be removed when perfetto v47+ traces are
+  // the norm in Android.
+  base::StringView unique_session_name =
+      context_->metadata_tracker->GetMetadata(metadata::unique_session_name)
+          .value_or(SqlValue::String(""))
+          .AsString();
+  if (unique_session_name ==
+      base::StringView("session_with_lightweight_battery_tracing")) {
+    soft_drop_before = SoftDropFtraceDataBefore::kNoDrop;
+  }
+
+  switch (soft_drop_before) {
     case SoftDropFtraceDataBefore::kNoDrop: {
       soft_drop_ftrace_data_before_ts_ = 0;
       break;
@@ -1565,6 +1656,33 @@
       next_pid, ss.next_comm(), ss.next_prio());
 }
 
+void FtraceParser::ParseKprobe(int64_t timestamp,
+                               uint32_t pid,
+                               ConstBytes blob) {
+  protos::pbzero::KprobeEvent::Decoder kp(blob.data, blob.size);
+
+  auto kprobe_type = static_cast<KprobeType>(kp.type());
+  StringId name_id = context_->storage->InternString(kp.name());
+  UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
+  TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
+  switch (kprobe_type) {
+    case KprobeType::KPROBE_TYPE_BEGIN:
+      context_->slice_tracker->Begin(timestamp, track_id,
+                                     kNullStringId /* cat */, name_id);
+      break;
+    case KprobeType::KPROBE_TYPE_END:
+      context_->slice_tracker->End(timestamp, track_id, kNullStringId /* cat */,
+                                   name_id);
+      break;
+    case KprobeType::KPROBE_TYPE_INSTANT:
+      context_->slice_tracker->Scoped(timestamp, track_id, kNullStringId,
+                                      name_id, 0);
+      break;
+    case KprobeType::KPROBE_TYPE_UNKNOWN:
+      break;
+  }
+}
+
 void FtraceParser::ParseSchedWaking(int64_t timestamp,
                                     uint32_t pid,
                                     ConstBytes blob) {
@@ -1588,8 +1706,8 @@
   protos::pbzero::CpuFrequencyFtraceEvent::Decoder freq(blob.data, blob.size);
   uint32_t cpu = freq.cpu_id();
   uint32_t new_freq_khz = freq.state();
-  TrackId track =
-      context_->track_tracker->InternCpuCounterTrack(cpu_freq_name_id_, cpu);
+  TrackId track = context_->track_tracker->InternCpuCounterTrack(
+      tracks::cpu_frequency, cpu, TrackTracker::LegacyCharArrayName{"cpufreq"});
   context_->event_tracker->PushCounter(timestamp, new_freq_khz, track);
 }
 
@@ -1598,7 +1716,8 @@
   uint32_t cpu = static_cast<uint32_t>(freq.cpu());
   double new_freq_khz = static_cast<double>(freq.freq());
   TrackId track = context_->track_tracker->InternCpuCounterTrack(
-      cpu_freq_throttle_name_id_, cpu);
+      tracks::cpu_frequency_throttle, cpu,
+      TrackTracker::LegacyCharArrayName{"cpufreq_throttle"});
   context_->event_tracker->PushCounter(timestamp, new_freq_khz, track);
 }
 
@@ -1606,8 +1725,8 @@
   protos::pbzero::GpuFrequencyFtraceEvent::Decoder freq(blob.data, blob.size);
   uint32_t gpu = freq.gpu_id();
   uint32_t new_freq = freq.state();
-  TrackId track =
-      context_->track_tracker->InternGpuCounterTrack(gpu_freq_name_id_, gpu);
+  TrackId track = context_->track_tracker->InternGpuCounterTrack(
+      tracks::gpu_frequency, gpu, TrackTracker::LegacyCharArrayName{"gpufreq"});
   context_->event_tracker->PushCounter(timestamp, new_freq, track);
 }
 
@@ -1617,8 +1736,8 @@
   uint32_t gpu = freq.gpu_id();
   // Source data is frequency / 1000, so we correct that here:
   double new_freq = static_cast<double>(freq.gpu_freq()) * 1000.0;
-  TrackId track =
-      context_->track_tracker->InternGpuCounterTrack(gpu_freq_name_id_, gpu);
+  TrackId track = context_->track_tracker->InternGpuCounterTrack(
+      tracks::gpu_frequency, gpu, TrackTracker::LegacyCharArrayName{"gpufreq"});
   context_->event_tracker->PushCounter(timestamp, new_freq, track);
 }
 
@@ -1626,8 +1745,8 @@
   protos::pbzero::CpuIdleFtraceEvent::Decoder idle(blob.data, blob.size);
   uint32_t cpu = idle.cpu_id();
   uint32_t new_state = idle.state();
-  TrackId track =
-      context_->track_tracker->InternCpuCounterTrack(cpu_idle_name_id_, cpu);
+  TrackId track = context_->track_tracker->InternCpuCounterTrack(
+      tracks::cpu_idle, cpu, TrackTracker::LegacyCharArrayName{"cpuidle"});
   context_->event_tracker->PushCounter(timestamp, new_state, track);
 }
 
@@ -1781,7 +1900,9 @@
 
 void FtraceParser::ParseGoogleIccEvent(int64_t timestamp, ConstBytes blob) {
   protos::pbzero::GoogleIccEventFtraceEvent::Decoder evt(blob.data, blob.size);
-  TrackId track_id = context_->track_tracker->GetOrCreateInterconnectTrack();
+  TrackId track_id = context_->track_tracker->InternGlobalTrack(
+      tracks::interconnect_events,
+      TrackTracker::LegacyCharArrayName(kInternconnectTrackName));
   StringId slice_name_id =
       context_->storage->InternString(base::StringView(evt.event()));
   context_->slice_tracker->Scoped(timestamp, track_id, google_icc_event_id_,
@@ -1790,7 +1911,9 @@
 
 void FtraceParser::ParseGoogleIrmEvent(int64_t timestamp, ConstBytes blob) {
   protos::pbzero::GoogleIrmEventFtraceEvent::Decoder evt(blob.data, blob.size);
-  TrackId track_id = context_->track_tracker->GetOrCreateInterconnectTrack();
+  TrackId track_id = context_->track_tracker->InternGlobalTrack(
+      tracks::interconnect_events,
+      TrackTracker::LegacyCharArrayName{kInternconnectTrackName});
   StringId slice_name_id =
       context_->storage->InternString(base::StringView(evt.event()));
   context_->slice_tracker->Scoped(timestamp, track_id, google_irm_event_id_,
@@ -1822,7 +1945,7 @@
   }
 
   // Push the global counter.
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kMemory, global_name_id);
   context_->event_tracker->PushCounter(timestamp,
                                        static_cast<double>(total_bytes), track);
@@ -1830,8 +1953,8 @@
   // Push the change counter.
   // TODO(b/121331269): these should really be instant events.
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
-  track =
-      context_->track_tracker->InternThreadCounterTrack(change_name_id, utid);
+  track = context_->track_tracker->LegacyInternThreadCounterTrack(
+      change_name_id, utid);
   context_->event_tracker->PushCounter(
       timestamp, static_cast<double>(change_bytes), track);
 
@@ -1860,7 +1983,7 @@
                                 protozero::ConstBytes data) {
   protos::pbzero::IonStatFtraceEvent::Decoder ion(data.data, data.size);
   // Push the global counter.
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kMemory, ion_total_id_);
   context_->event_tracker->PushCounter(
       timestamp, static_cast<double>(ion.total_allocated()), track);
@@ -1868,8 +1991,8 @@
   // Push the change counter.
   // TODO(b/121331269): these should really be instant events.
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
-  track =
-      context_->track_tracker->InternThreadCounterTrack(ion_change_id_, utid);
+  track = context_->track_tracker->LegacyInternThreadCounterTrack(
+      ion_change_id_, utid);
   context_->event_tracker->PushCounter(timestamp,
                                        static_cast<double>(ion.len()), track);
 
@@ -1894,44 +2017,44 @@
   protos::pbzero::BclIrqTriggerFtraceEvent::Decoder bcl(data.data, data.size);
   int throttle = bcl.throttle();
   // id
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kBatteryMitigation, bcl_irq_id_);
   context_->event_tracker->PushCounter(ts, throttle ? bcl.id() : -1, track);
   // throttle
-  track = context_->track_tracker->InternGlobalCounterTrack(
+  track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kBatteryMitigation, bcl_irq_throttle_);
   context_->event_tracker->PushCounter(ts, throttle, track);
   // cpu0_limit
-  track = context_->track_tracker->InternGlobalCounterTrack(
+  track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kBatteryMitigation, bcl_irq_cpu0_);
   context_->event_tracker->PushCounter(ts, throttle ? bcl.cpu0_limit() : 0,
                                        track);
   // cpu1_limit
-  track = context_->track_tracker->InternGlobalCounterTrack(
+  track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kBatteryMitigation, bcl_irq_cpu1_);
   context_->event_tracker->PushCounter(ts, throttle ? bcl.cpu1_limit() : 0,
                                        track);
   // cpu2_limit
-  track = context_->track_tracker->InternGlobalCounterTrack(
+  track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kBatteryMitigation, bcl_irq_cpu2_);
   context_->event_tracker->PushCounter(ts, throttle ? bcl.cpu2_limit() : 0,
                                        track);
   // tpu_limit
-  track = context_->track_tracker->InternGlobalCounterTrack(
+  track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kBatteryMitigation, bcl_irq_tpu_);
   context_->event_tracker->PushCounter(ts, throttle ? bcl.tpu_limit() : 0,
                                        track);
   // gpu_limit
-  track = context_->track_tracker->InternGlobalCounterTrack(
+  track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kBatteryMitigation, bcl_irq_gpu_);
   context_->event_tracker->PushCounter(ts, throttle ? bcl.gpu_limit() : 0,
                                        track);
   // voltage
-  track = context_->track_tracker->InternGlobalCounterTrack(
+  track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kBatteryMitigation, bcl_irq_voltage_);
   context_->event_tracker->PushCounter(ts, bcl.voltage(), track);
   // capacity
-  track = context_->track_tracker->InternGlobalCounterTrack(
+  track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kBatteryMitigation, bcl_irq_capacity_);
   context_->event_tracker->PushCounter(ts, bcl.capacity(), track);
 }
@@ -1942,7 +2065,7 @@
   protos::pbzero::DmaHeapStatFtraceEvent::Decoder dma_heap(data.data,
                                                            data.size);
   // Push the global counter.
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kMemory, dma_heap_total_id_);
   context_->event_tracker->PushCounter(
       timestamp, static_cast<double>(dma_heap.total_allocated()), track);
@@ -1950,8 +2073,8 @@
   // Push the change counter.
   // TODO(b/121331269): these should really be instant events.
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
-  track = context_->track_tracker->InternThreadCounterTrack(dma_heap_change_id_,
-                                                            utid);
+  track = context_->track_tracker->LegacyInternThreadCounterTrack(
+      dma_heap_change_id_, utid);
 
   auto opt_counter_id = context_->event_tracker->PushCounter(
       timestamp, static_cast<double>(dma_heap.len()), track);
@@ -2282,7 +2405,7 @@
                                       clock_name.data(), int(subtitle.size()),
                                       subtitle.data());
   StringId name = context_->storage->InternString(counter_name.c_str());
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kClockFrequency, name);
   context_->event_tracker->PushCounter(timestamp, static_cast<double>(rate),
                                        track);
@@ -2484,16 +2607,16 @@
                                         int64_t timestamp,
                                         protozero::ConstBytes blob) {
   protos::pbzero::IrqHandlerEntryFtraceEvent::Decoder evt(blob.data, blob.size);
-  base::StackString<255> track_name("Irq Cpu %d", cpu);
-  StringId track_name_id =
-      context_->storage->InternString(track_name.string_view());
 
   base::StringView irq_name = evt.name();
   base::StackString<255> slice_name("IRQ (%.*s)", int(irq_name.size()),
                                     irq_name.data());
   StringId slice_name_id =
       context_->storage->InternString(slice_name.string_view());
-  TrackId track = context_->track_tracker->InternCpuTrack(track_name_id, cpu);
+  TrackId track = context_->track_tracker->InternCpuTrack(
+      tracks::cpu_irq, cpu,
+      TrackTracker::LegacyCharArrayName{
+          base::StackString<255>("Irq Cpu %u", cpu)});
   context_->slice_tracker->Begin(timestamp, track, irq_id_, slice_name_id);
 }
 
@@ -2501,10 +2624,10 @@
                                        int64_t timestamp,
                                        protozero::ConstBytes blob) {
   protos::pbzero::IrqHandlerExitFtraceEvent::Decoder evt(blob.data, blob.size);
-  base::StackString<255> track_name("Irq Cpu %d", cpu);
-  StringId track_name_id =
-      context_->storage->InternString(track_name.string_view());
-  TrackId track = context_->track_tracker->InternCpuTrack(track_name_id, cpu);
+  TrackId track = context_->track_tracker->InternCpuTrack(
+      tracks::cpu_irq, cpu,
+      TrackTracker::LegacyCharArrayName{
+          base::StackString<255>("Irq Cpu %u", cpu)});
 
   base::StackString<255> status("%s", evt.ret() == 1 ? "handled" : "unhandled");
   StringId status_id = context_->storage->InternString(status.string_view());
@@ -2519,9 +2642,6 @@
                                      int64_t timestamp,
                                      protozero::ConstBytes blob) {
   protos::pbzero::SoftirqEntryFtraceEvent::Decoder evt(blob.data, blob.size);
-  base::StackString<255> track_name("SoftIrq Cpu %d", cpu);
-  StringId track_name_id =
-      context_->storage->InternString(track_name.string_view());
   auto num_actions = sizeof(kActionNames) / sizeof(*kActionNames);
   if (evt.vec() >= num_actions) {
     PERFETTO_DFATAL("No action name at index %d for softirq event.", evt.vec());
@@ -2529,7 +2649,10 @@
   }
   base::StringView slice_name = kActionNames[evt.vec()];
   StringId slice_name_id = context_->storage->InternString(slice_name);
-  TrackId track = context_->track_tracker->InternCpuTrack(track_name_id, cpu);
+  TrackId track = context_->track_tracker->InternCpuTrack(
+      tracks::cpu_softirq, cpu,
+      TrackTracker::LegacyCharArrayName{
+          base::StackString<255>("SoftIrq Cpu %u", cpu)});
   context_->slice_tracker->Begin(timestamp, track, irq_id_, slice_name_id);
 }
 
@@ -2537,10 +2660,10 @@
                                     int64_t timestamp,
                                     protozero::ConstBytes blob) {
   protos::pbzero::SoftirqExitFtraceEvent::Decoder evt(blob.data, blob.size);
-  base::StackString<255> track_name("SoftIrq Cpu %d", cpu);
-  StringId track_name_id =
-      context_->storage->InternString(track_name.string_view());
-  TrackId track = context_->track_tracker->InternCpuTrack(track_name_id, cpu);
+  TrackId track = context_->track_tracker->InternCpuTrack(
+      tracks::cpu_softirq, cpu,
+      TrackTracker::LegacyCharArrayName{
+          base::StackString<255>("SoftIrq Cpu %u", cpu)});
   auto vec = evt.vec();
   auto args_inserter = [this, vec](ArgsTracker::BoundInserter* inserter) {
     inserter->AddArg(vec_arg_id_, Variadic::Integer(vec));
@@ -2557,7 +2680,7 @@
   const uint32_t pid = gpu_mem_total.pid();
   if (pid == 0) {
     // Pid 0 is used to indicate the global total
-    track = context_->track_tracker->InternGlobalCounterTrack(
+    track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kMemory, gpu_mem_total_name_id_, {},
         gpu_mem_total_unit_id_, gpu_mem_total_global_desc_id_);
   } else {
@@ -2581,7 +2704,7 @@
     UniquePid upid = *context_->storage->thread_table()[*opt_utid].upid();
     PERFETTO_DCHECK(context_->storage->process_table()[upid].pid() == pid);
 
-    track = context_->track_tracker->InternProcessCounterTrack(
+    track = context_->track_tracker->LegacyInternProcessCounterTrack(
         gpu_mem_total_name_id_, upid, gpu_mem_total_unit_id_,
         gpu_mem_total_proc_desc_id_);
   }
@@ -2636,7 +2759,7 @@
   }
 
   // Push the global counter.
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kMemory, total_name);
   context_->event_tracker->PushCounter(
       timestamp, static_cast<double>(event.total_allocated()), track);
@@ -2644,7 +2767,7 @@
   // Push the change counter.
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
   TrackId delta_track =
-      context_->track_tracker->InternThreadCounterTrack(name, utid);
+      context_->track_tracker->LegacyInternThreadCounterTrack(name, utid);
   context_->event_tracker->PushCounter(
       timestamp, static_cast<double>(event.len()), delta_track);
 }
@@ -2670,7 +2793,7 @@
   nic_received_bytes_[name] += event.len();
 
   uint64_t nic_received_kilobytes = nic_received_bytes_[name] / 1024;
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kNetwork, name);
   std::optional<CounterId> id = context_->event_tracker->PushCounter(
       timestamp, static_cast<double>(nic_received_kilobytes), track);
@@ -2702,7 +2825,7 @@
   nic_transmitted_bytes_[name] += evt.len();
 
   uint64_t nic_transmitted_kilobytes = nic_transmitted_bytes_[name] / 1024;
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kNetwork, name);
   std::optional<CounterId> id = context_->event_tracker->PushCounter(
       timestamp, static_cast<double>(nic_transmitted_kilobytes), track);
@@ -2797,12 +2920,12 @@
                                             protozero::ConstBytes blob) {
   protos::pbzero::NapiGroReceiveEntryFtraceEvent::Decoder evt(blob.data,
                                                               blob.size);
-  base::StackString<255> track_name("Napi Gro Cpu %d", cpu);
-  StringId track_name_id =
-      context_->storage->InternString(track_name.string_view());
   base::StringView net_device = evt.name();
   StringId slice_name_id = context_->storage->InternString(net_device);
-  TrackId track = context_->track_tracker->InternCpuTrack(track_name_id, cpu);
+  TrackId track = context_->track_tracker->InternCpuTrack(
+      tracks::cpu_napi_gro, cpu,
+      TrackTracker::LegacyCharArrayName{
+          base::StackString<255>("Napi Gro Cpu %u", cpu)});
   auto len = evt.len();
   auto args_inserter = [this, len](ArgsTracker::BoundInserter* inserter) {
     inserter->AddArg(len_arg_id_, Variadic::Integer(len));
@@ -2816,10 +2939,10 @@
                                            protozero::ConstBytes blob) {
   protos::pbzero::NapiGroReceiveExitFtraceEvent::Decoder evt(blob.data,
                                                              blob.size);
-  base::StackString<255> track_name("Napi Gro Cpu %d", cpu);
-  StringId track_name_id =
-      context_->storage->InternString(track_name.string_view());
-  TrackId track = context_->track_tracker->InternCpuTrack(track_name_id, cpu);
+  TrackId track = context_->track_tracker->InternCpuTrack(
+      tracks::cpu_napi_gro, cpu,
+      TrackTracker::LegacyCharArrayName{
+          base::StackString<255>("Napi Gro Cpu %u", cpu)});
   auto ret = evt.ret();
   auto args_inserter = [this, ret](ArgsTracker::BoundInserter* inserter) {
     inserter->AddArg(ret_arg_id_, Variadic::Integer(ret));
@@ -2832,21 +2955,18 @@
                                            protozero::ConstBytes blob) {
   protos::pbzero::CpuFrequencyLimitsFtraceEvent::Decoder evt(blob.data,
                                                              blob.size);
-  base::StackString<255> max_counter_name("Cpu %" PRIu32 " Max Freq Limit",
-                                          evt.cpu_id());
-  base::StackString<255> min_counter_name("Cpu %" PRIu32 " Min Freq Limit",
-                                          evt.cpu_id());
-  // Push max freq to global counter.
-  StringId max_name = context_->storage->InternString(max_counter_name.c_str());
-  TrackId max_track =
-      context_->track_tracker->InternCpuCounterTrack(max_name, evt.cpu_id());
+
+  TrackId max_track = context_->track_tracker->InternCpuCounterTrack(
+      tracks::cpu_max_frequency_limit, evt.cpu_id(),
+      TrackTracker::LegacyCharArrayName{
+          base::StackString<255>("Cpu %u Max Freq Limit", evt.cpu_id())});
   context_->event_tracker->PushCounter(
       timestamp, static_cast<double>(evt.max_freq()), max_track);
 
-  // Push min freq to global counter.
-  StringId min_name = context_->storage->InternString(min_counter_name.c_str());
-  TrackId min_track =
-      context_->track_tracker->InternCpuCounterTrack(min_name, evt.cpu_id());
+  TrackId min_track = context_->track_tracker->InternCpuCounterTrack(
+      tracks::cpu_min_frequency_limit, evt.cpu_id(),
+      TrackTracker::LegacyCharArrayName{
+          base::StackString<255>("Cpu %u Min Freq Limit", evt.cpu_id())});
   context_->event_tracker->PushCounter(
       timestamp, static_cast<double>(evt.min_freq()), min_track);
 }
@@ -2861,7 +2981,7 @@
   }
   num_of_kfree_skb_ip_prot += 1;
 
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kNetwork, kfree_skb_name_id_);
   std::optional<CounterId> id = context_->event_tracker->PushCounter(
       timestamp, static_cast<double>(num_of_kfree_skb_ip_prot), track);
@@ -2881,7 +3001,7 @@
                                                               blob.size);
 
   // Push the global counter.
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kDeviceState,
       context_->storage->InternString(
           base::StringView("cros_ec.cros_ec_sensorhub_data." +
@@ -2922,7 +3042,7 @@
       clk_state = 2;
       break;
   }
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kNetwork, ufs_clkgating_id_);
   context_->event_tracker->PushCounter(timestamp,
                                        static_cast<double>(clk_state), track);
@@ -3244,7 +3364,7 @@
   uint32_t num = evt.doorbell() > 0
                      ? static_cast<uint32_t>(PERFETTO_POPCOUNT(evt.doorbell()))
                      : (evt.str_t() == 1 ? 0 : 1);
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kIo, ufs_command_count_id_);
   context_->event_tracker->PushCounter(timestamp, static_cast<double>(num),
                                        track);
@@ -3314,6 +3434,8 @@
 }
 
 void FtraceParser::ParseSuspendResume(int64_t timestamp,
+                                      uint32_t cpu,
+                                      uint32_t tid,
                                       protozero::ConstBytes blob) {
   protos::pbzero::SuspendResumeFtraceEvent::Decoder evt(blob.data, blob.size);
 
@@ -3361,8 +3483,26 @@
 
   TrackId start_id =
       context_->async_track_set_tracker->Begin(async_track, cookie);
+
+  auto args_inserter = [&](ArgsTracker::BoundInserter* inserter) {
+    inserter->AddArg(suspend_resume_utid_arg_name_,
+                     Variadic::UnsignedInteger(
+                         context_->process_tracker->GetOrCreateThread(tid)));
+    inserter->AddArg(suspend_resume_event_type_arg_name_,
+                     Variadic::String(suspend_resume_main_event_id_));
+    auto ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
+    inserter->AddArg(ucpu_id_, Variadic::UnsignedInteger(ucpu.value));
+
+    // These fields are set to null as this is not a device PM callback event.
+    inserter->AddArg(suspend_resume_device_arg_name_,
+                     Variadic::String(kNullStringId));
+    inserter->AddArg(suspend_resume_driver_arg_name_,
+                     Variadic::String(kNullStringId));
+    inserter->AddArg(suspend_resume_callback_phase_arg_name_,
+                     Variadic::String(kNullStringId));
+  };
   context_->slice_tracker->Begin(timestamp, start_id, suspend_resume_name_id_,
-                                 slice_name_id);
+                                 slice_name_id, args_inserter);
   ongoing_suspend_resume_actions[current_action] = true;
 }
 
@@ -3389,31 +3529,24 @@
 void FtraceParser::ParseSchedCpuUtilCfs(int64_t timestamp,
                                         protozero::ConstBytes blob) {
   protos::pbzero::SchedCpuUtilCfsFtraceEvent::Decoder evt(blob.data, blob.size);
-  base::StackString<255> util_track_name("Cpu %" PRIu32 " Util", evt.cpu());
-  StringId util_track_name_id =
-      context_->storage->InternString(util_track_name.string_view());
-
   TrackId util_track = context_->track_tracker->InternCpuCounterTrack(
-      util_track_name_id, evt.cpu());
+      tracks::cpu_utilization, evt.cpu(),
+      TrackTracker::LegacyCharArrayName{
+          base::StackString<255>("Cpu %u Util", evt.cpu())});
   context_->event_tracker->PushCounter(
       timestamp, static_cast<double>(evt.cpu_util()), util_track);
 
-  base::StackString<255> cap_track_name("Cpu %" PRIu32 " Cap", evt.cpu());
-  StringId cap_track_name_id =
-      context_->storage->InternString(cap_track_name.string_view());
-
   TrackId cap_track = context_->track_tracker->InternCpuCounterTrack(
-      cap_track_name_id, evt.cpu());
+      tracks::cpu_capacity, evt.cpu(),
+      TrackTracker::LegacyCharArrayName{
+          base::StackString<255>("Cpu %u Cap", evt.cpu())});
   context_->event_tracker->PushCounter(
       timestamp, static_cast<double>(evt.capacity()), cap_track);
 
-  base::StackString<255> nrr_track_name("Cpu %" PRIu32 " Nr Running",
-                                        evt.cpu());
-  StringId nrr_track_name_id =
-      context_->storage->InternString(nrr_track_name.string_view());
-
   TrackId nrr_track = context_->track_tracker->InternCpuCounterTrack(
-      nrr_track_name_id, evt.cpu());
+      tracks::cpu_nr_running, evt.cpu(),
+      TrackTracker::LegacyCharArrayName{
+          base::StackString<255>("Cpu %u Nr Running", evt.cpu())});
   context_->event_tracker->PushCounter(
       timestamp, static_cast<double>(evt.nr_running()), nrr_track);
 }
@@ -3437,10 +3570,10 @@
     // 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).
-    base::StackString<255> track_name("swapper%" PRIu32 "-funcgraph", cpu);
-    StringId track_name_id =
-        context_->storage->InternString(track_name.string_view());
-    track = context_->track_tracker->InternCpuTrack(track_name_id, cpu);
+    track = context_->track_tracker->InternCpuTrack(
+        tracks::cpu_funcgraph, cpu,
+        TrackTracker::LegacyCharArrayName{
+            base::StackString<255>("swapper%u -funcgraph", cpu)});
   }
 
   context_->slice_tracker->Begin(timestamp, track, kNullStringId, name_id);
@@ -3462,10 +3595,10 @@
     track = context_->track_tracker->InternThreadTrack(utid);
   } else {
     // special case: see |ParseFuncgraphEntry|
-    base::StackString<255> track_name("swapper%" PRIu32 "-funcgraph", cpu);
-    StringId track_name_id =
-        context_->storage->InternString(track_name.string_view());
-    track = context_->track_tracker->InternCpuTrack(track_name_id, cpu);
+    track = context_->track_tracker->InternCpuTrack(
+        tracks::cpu_funcgraph, cpu,
+        TrackTracker::LegacyCharArrayName{
+            base::StackString<255>("swapper%u -funcgraph", cpu)});
   }
 
   context_->slice_tracker->End(timestamp, track, kNullStringId, name_id);
@@ -3551,15 +3684,15 @@
 
   // Device here refers to anything managed by a Linux kernel driver.
   std::string device_name = rpm_event.name().ToStdString();
-  int32_t rpm_status = rpm_event.status();
-  StringId device_name_string_id =
-      context_->storage->InternString(device_name.c_str());
-  TrackId track_id =
-      context_->track_tracker->InternLinuxDeviceTrack(device_name_string_id);
+  StringId device_name_string_id = context_->storage->InternString(device_name);
+  TrackId track_id = context_->track_tracker->InternSingleDimensionTrack(
+      tracks::linux_rpm, linux_device_name_id_,
+      Variadic::String(device_name_string_id),
+      TrackTracker::LegacyStringIdName{device_name_string_id});
 
   // A `runtime_status` event implies a potential change in state. Hence, if an
   // active slice exists for this device, end that slice.
-  if (devices_with_active_rpm_slice_.find(device_name) !=
+  if (devices_with_active_rpm_slice_.find(device_name_string_id) !=
       devices_with_active_rpm_slice_.end()) {
     context_->slice_tracker->End(ts, track_id);
   }
@@ -3567,27 +3700,30 @@
   // To reduce visual clutter, the "SUSPENDED" state will be omitted from the
   // visualization, as devices typically spend the majority of their time in
   // this state.
+  int32_t rpm_status = rpm_event.status();
   if (rpm_status == RPM_SUSPENDED) {
-    devices_with_active_rpm_slice_.erase(device_name);
+    devices_with_active_rpm_slice_.erase(device_name_string_id);
     return;
   }
 
   context_->slice_tracker->Begin(ts, track_id, /*category=*/kNullStringId,
                                  /*raw_name=*/GetRpmStatusStringId(rpm_status));
-  devices_with_active_rpm_slice_.insert(device_name);
+  devices_with_active_rpm_slice_.insert(device_name_string_id);
 }
 
 // Parses `device_pm_callback_start` events and begins corresponding slices in
 // the suspend / resume latency UI track.
 void FtraceParser::ParseDevicePmCallbackStart(int64_t ts,
+                                              uint32_t cpu,
+                                              uint32_t tid,
                                               protozero::ConstBytes blob) {
   protos::pbzero::DevicePmCallbackStartFtraceEvent::Decoder dpm_event(
       blob.data, blob.size);
 
   // Device here refers to anything managed by a Linux kernel driver.
   std::string device_name = dpm_event.device().ToStdString();
+  std::string driver_name = dpm_event.driver().ToStdString();
   int64_t cookie;
-
   if (suspend_resume_cookie_map_.Find(device_name) == nullptr) {
     cookie = static_cast<int64_t>(suspend_resume_cookie_map_.size());
     suspend_resume_cookie_map_[device_name] = cookie;
@@ -3595,18 +3731,39 @@
     cookie = suspend_resume_cookie_map_[device_name];
   }
 
-  std::string slice_name = ConstructDpmCallbackSliceName(
-      dpm_event.driver().ToStdString(), device_name,
-      dpm_event.pm_ops().ToStdString(),
-      GetDpmCallbackEventString(dpm_event.event()));
-  StringId slice_name_id = context_->storage->InternString(slice_name.c_str());
-
   auto async_track = context_->async_track_set_tracker->InternGlobalTrackSet(
       suspend_resume_name_id_);
   TrackId track_id =
       context_->async_track_set_tracker->Begin(async_track, cookie);
+
+  std::string slice_name = device_name + " " + driver_name;
+  StringId slice_name_id = context_->storage->InternString(slice_name.c_str());
+
+  std::string callback_phase = ConstructCallbackPhaseName(
+      /*pm_ops=*/dpm_event.pm_ops().ToStdString(),
+      /*event_type=*/GetDpmCallbackEventString(dpm_event.event()));
+
+  auto args_inserter = [&](ArgsTracker::BoundInserter* inserter) {
+    inserter->AddArg(suspend_resume_utid_arg_name_,
+                     Variadic::UnsignedInteger(
+                         context_->process_tracker->GetOrCreateThread(tid)));
+    inserter->AddArg(suspend_resume_event_type_arg_name_,
+                     Variadic::String(suspend_resume_device_pm_event_id_));
+    auto ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
+    inserter->AddArg(ucpu_id_, Variadic::UnsignedInteger(ucpu.value));
+    inserter->AddArg(
+        suspend_resume_device_arg_name_,
+        Variadic::String(context_->storage->InternString(device_name.c_str())));
+    inserter->AddArg(
+        suspend_resume_driver_arg_name_,
+        Variadic::String(context_->storage->InternString(driver_name.c_str())));
+    inserter->AddArg(suspend_resume_callback_phase_arg_name_,
+                     Variadic::String(context_->storage->InternString(
+                         callback_phase.c_str())));
+  };
+
   context_->slice_tracker->Begin(ts, track_id, suspend_resume_name_id_,
-                                 slice_name_id);
+                                 slice_name_id, args_inserter);
 }
 
 // Parses `device_pm_callback_end` events and ends corresponding slices in the
@@ -3660,4 +3817,40 @@
   return name_id;
 }
 
+void FtraceParser::ParseDeviceFrequency(int64_t ts,
+                                        protozero::ConstBytes blob) {
+  protos::pbzero::DevfreqFrequencyFtraceEvent::Decoder event(blob);
+  std::string dev_name = event.dev_name().ToStdString();
+
+  constexpr char kDelimiter[] = "devfreq_";
+  auto position = dev_name.find(kDelimiter);
+  if (position == std::string::npos) {
+    return;
+  }
+
+  // Get device name by getting substring after delimiter and keep existing
+  // naming convention (e.g. cpufreq, gpufreq) consistent by adding a suffix
+  // to the devfreq name (e.g. dsufreq, bcifreq)
+  StringId device_name = context_->storage->InternString(
+      (dev_name.substr(position + sizeof(kDelimiter) - 1) + "freq").c_str());
+
+  TrackId track_id = context_->track_tracker->InternSingleDimensionTrack(
+      tracks::linux_device_frequency, device_name_id_,
+      Variadic::String(device_name));
+  context_->event_tracker->PushCounter(ts, static_cast<double>(event.freq()),
+                                       track_id);
+}
+
+void FtraceParser::ParseParamSetValueCpm(protozero::ConstBytes blob) {
+  protos::pbzero::ParamSetValueCpmFtraceEvent::Decoder event(blob);
+  TrackTracker::DimensionsBuilder dims_builder =
+      context_->track_tracker->CreateDimensionsBuilder();
+  // Store event body which denotes the name of the track.
+  dims_builder.AppendName(context_->storage->InternString(event.body()));
+  TrackId track_id = context_->track_tracker->InternTrack(
+      tracks::pixel_cpm_trace, std::move(dims_builder).Build());
+  context_->event_tracker->PushCounter(static_cast<int64_t>(event.timestamp()),
+                                       event.value(), 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 ad121da..6a08f65 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.h
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.h
@@ -33,10 +33,12 @@
 #include "src/trace_processor/importers/ftrace/gpu_work_period_tracker.h"
 #include "src/trace_processor/importers/ftrace/iostat_tracker.h"
 #include "src/trace_processor/importers/ftrace/mali_gpu_event_tracker.h"
+#include "src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.h"
 #include "src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.h"
 #include "src/trace_processor/importers/ftrace/rss_stat_tracker.h"
 #include "src/trace_processor/importers/ftrace/thermal_tracker.h"
 #include "src/trace_processor/importers/ftrace/virtio_gpu_tracker.h"
+#include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
 namespace perfetto {
@@ -71,6 +73,7 @@
                              protozero::ConstBytes,
                              PacketSequenceStateGeneration*);
   void ParseSchedSwitch(uint32_t cpu, int64_t timestamp, protozero::ConstBytes);
+  void ParseKprobe(int64_t timestamp, uint32_t pid, protozero::ConstBytes);
   void ParseSchedWaking(int64_t timestamp, uint32_t pid, protozero::ConstBytes);
   void ParseSchedProcessFree(int64_t timestamp, protozero::ConstBytes);
   void ParseCpuFreq(int64_t timestamp, protozero::ConstBytes);
@@ -234,7 +237,10 @@
 
   void ParseWakeSourceActivate(int64_t timestamp, protozero::ConstBytes);
   void ParseWakeSourceDeactivate(int64_t timestamp, protozero::ConstBytes);
-  void ParseSuspendResume(int64_t timestamp, protozero::ConstBytes);
+  void ParseSuspendResume(int64_t timestamp,
+                          uint32_t cpu,
+                          uint32_t pid,
+                          protozero::ConstBytes);
   void ParseSuspendResumeMinimal(int64_t timestamp, protozero::ConstBytes);
   void ParseSchedCpuUtilCfs(int64_t timestamp, protozero::ConstBytes);
 
@@ -300,13 +306,18 @@
                                    protozero::ConstBytes);
   StringId GetRpmStatusStringId(int32_t rpm_status_val);
   void ParseRpmStatus(int64_t ts, protozero::ConstBytes);
-  void ParseDevicePmCallbackStart(int64_t ts, protozero::ConstBytes);
+  void ParseDevicePmCallbackStart(int64_t ts,
+                                  uint32_t cpu,
+                                  uint32_t pid,
+                                  protozero::ConstBytes);
   void ParseDevicePmCallbackEnd(int64_t ts, protozero::ConstBytes);
   void ParsePanelWriteGeneric(int64_t timestamp,
                               uint32_t pid,
                               protozero::ConstBytes);
   void ParseGoogleIccEvent(int64_t timestamp, protozero::ConstBytes);
   void ParseGoogleIrmEvent(int64_t timestamp, protozero::ConstBytes);
+  void ParseDeviceFrequency(int64_t ts, protozero::ConstBytes blob);
+  void ParseParamSetValueCpm(protozero::ConstBytes blob);
 
   TraceProcessorContext* context_;
   RssStatTracker rss_stat_tracker_;
@@ -317,14 +328,13 @@
   PkvmHypervisorCpuTracker pkvm_hyp_cpu_tracker_;
   GpuWorkPeriodTracker gpu_work_period_tracker_;
   ThermalTracker thermal_tracker_;
+  PixelMmKswapdEventTracker pixel_mm_kswapd_event_tracker_;
 
   const StringId sched_wakeup_name_id_;
   const StringId sched_waking_name_id_;
   const StringId cpu_id_;
-  const StringId cpu_freq_name_id_;
-  const StringId cpu_freq_throttle_name_id_;
-  const StringId gpu_freq_name_id_;
-  const StringId cpu_idle_name_id_;
+  const StringId ucpu_id_;
+  const StringId linux_device_name_id_;
   const StringId suspend_resume_name_id_;
   const StringId suspend_resume_minimal_name_id_;
   const StringId suspend_resume_minimal_slice_name_id_;
@@ -414,6 +424,15 @@
   const StringId runtime_status_active_id_;
   const StringId runtime_status_suspending_id_;
   const StringId runtime_status_resuming_id_;
+  const StringId suspend_resume_main_event_id_;
+  const StringId suspend_resume_device_pm_event_id_;
+  const StringId suspend_resume_utid_arg_name_;
+  const StringId suspend_resume_device_arg_name_;
+  const StringId suspend_resume_driver_arg_name_;
+  const StringId suspend_resume_callback_phase_arg_name_;
+  const StringId suspend_resume_event_type_arg_name_;
+  const StringId device_name_id_;
+
   std::vector<StringId> syscall_arg_name_ids_;
 
   struct FtraceMessageStrings {
@@ -483,7 +502,7 @@
 
   // Tracks Linux devices with active runtime power management (RPM) status
   // slices.
-  std::unordered_set<std::string> devices_with_active_rpm_slice_;
+  std::unordered_set<StringId> devices_with_active_rpm_slice_;
 
   // Tracks unique identifiers ("cookies") to create separate async tracks for
   // the Suspend/Resume UI track. The separation prevents unnestable slices from
diff --git a/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc b/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
index 39c01ab..50a85da 100644
--- a/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
@@ -15,14 +15,26 @@
  */
 
 #include "src/trace_processor/importers/ftrace/ftrace_tokenizer.h"
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <utility>
+#include <vector>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
+#include "perfetto/protozero/field.h"
 #include "perfetto/protozero/proto_decoder.h"
 #include "perfetto/protozero/proto_utils.h"
+#include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/basic_types.h"
+#include "perfetto/trace_processor/ref_counted.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/common/clock_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/parser_types.h"
+#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
 #include "src/trace_processor/storage/metadata.h"
 #include "src/trace_processor/storage/stats.h"
@@ -31,6 +43,7 @@
 #include "src/trace_processor/util/status_macros.h"
 
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
+#include "protos/perfetto/trace/ftrace/cpm_trace.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/power.pbzero.h"
@@ -51,17 +64,6 @@
 
 static constexpr uint32_t kFtraceGlobalClockIdForOldKernels = 64;
 
-PERFETTO_ALWAYS_INLINE base::StatusOr<int64_t> ResolveTraceTime(
-    TraceProcessorContext* context,
-    ClockTracker::ClockId clock_id,
-    int64_t ts) {
-  // On most traces (i.e. P+), the clock should be BOOTTIME.
-  if (PERFETTO_LIKELY(clock_id == BuiltinClock::BUILTIN_CLOCK_BOOTTIME &&
-                      !context->machine_id()))
-    return ts;
-  return context->clock_tracker->ToTraceTime(clock_id, ts);
-}
-
 // Fast path for parsing the event id of an ftrace event.
 // Speculate on the fact that, if the timestamp was found, the common pid
 // will appear immediately after and the event id immediately after that.
@@ -180,14 +182,17 @@
   if (!per_cpu_seen_first_bundle_[cpu]) {
     per_cpu_seen_first_bundle_[cpu] = true;
 
-    // if this cpu's timestamp is the new max, update the metadata table entry
-    if (decoder.has_last_read_event_timestamp()) {
+    // If this cpu's timestamp is the new max, update the metadata table entry.
+    // previous_bundle_end_timestamp is the replacement for
+    // last_read_event_timestamp on perfetto v47+, at most one will be set.
+    if (decoder.has_previous_bundle_end_timestamp() ||
+        decoder.has_last_read_event_timestamp()) {
+      uint64_t raw_ts = decoder.has_previous_bundle_end_timestamp()
+                            ? decoder.previous_bundle_end_timestamp()
+                            : decoder.last_read_event_timestamp();
       int64_t timestamp = 0;
-      ASSIGN_OR_RETURN(
-          timestamp,
-          ResolveTraceTime(
-              context_, clock_id,
-              static_cast<int64_t>(decoder.last_read_event_timestamp())));
+      ASSIGN_OR_RETURN(timestamp, context_->clock_tracker->ToTraceTime(
+                                      clock_id, static_cast<int64_t>(raw_ts)));
 
       std::optional<SqlValue> curr_latest_timestamp =
           context_->metadata_tracker->GetMetadata(
@@ -275,12 +280,15 @@
     TokenizeFtraceThermalExynosAcpmBulk(cpu, std::move(event),
                                         std::move(state));
     return;
+  } else if (PERFETTO_UNLIKELY(event_id ==
+                               protos::pbzero::FtraceEvent::
+                                   kParamSetValueCpmFieldNumber)) {
+    TokenizeFtraceParamSetValueCpm(cpu, std::move(event), std::move(state));
+    return;
   }
 
-  int64_t int64_timestamp = static_cast<int64_t>(raw_timestamp);
-  base::StatusOr<int64_t> timestamp =
-      ResolveTraceTime(context_, clock_id, int64_timestamp);
-
+  auto timestamp = context_->clock_tracker->ToTraceTime(
+      clock_id, static_cast<int64_t>(raw_timestamp));
   // ClockTracker will increment some error stats if it failed to convert the
   // timestamp so just return.
   if (!timestamp.ok()) {
@@ -336,15 +344,18 @@
     int64_t event_timestamp = timestamp_acc;
 
     // index into the interned string table
-    PERFETTO_DCHECK(*comm_it < string_table.size());
+    if (PERFETTO_UNLIKELY(*comm_it >= string_table.size())) {
+      parse_error = true;
+      break;
+    }
     event.next_comm = string_table[*comm_it];
 
     event.prev_state = *pstate_it;
     event.next_pid = *npid_it;
     event.next_prio = *nprio_it;
 
-    base::StatusOr<int64_t> timestamp =
-        ResolveTraceTime(context_, clock_id, event_timestamp);
+    auto timestamp =
+        context_->clock_tracker->ToTraceTime(clock_id, event_timestamp);
     if (!timestamp.ok()) {
       DlogWithLimit(timestamp.status());
       return;
@@ -388,7 +399,10 @@
     int64_t event_timestamp = timestamp_acc;
 
     // index into the interned string table
-    PERFETTO_DCHECK(*comm_it < string_table.size());
+    if (PERFETTO_UNLIKELY(*comm_it >= string_table.size())) {
+      parse_error = true;
+      break;
+    }
     event.comm = string_table[*comm_it];
 
     event.pid = *pid_it;
@@ -400,8 +414,8 @@
       common_flags_it++;
     }
 
-    base::StatusOr<int64_t> timestamp =
-        ResolveTraceTime(context_, clock_id, event_timestamp);
+    auto timestamp =
+        context_->clock_tracker->ToTraceTime(clock_id, event_timestamp);
     if (!timestamp.ok()) {
       DlogWithLimit(timestamp.status());
       return;
@@ -440,19 +454,12 @@
     RefPtr<PacketSequenceStateGeneration> state) {
   // Special handling of valid gpu_work_period tracepoint events which contain
   // timestamp values for the GPU time period nested inside the event data.
-  const uint8_t* data = event.data();
-  const size_t length = event.length();
-
-  ProtoDecoder decoder(data, length);
-  auto ts_field =
-      decoder.FindField(protos::pbzero::FtraceEvent::kGpuWorkPeriodFieldNumber);
-  if (!ts_field.valid()) {
-    context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
-    return;
-  }
+  auto ts_field = GetFtraceEventField(
+      protos::pbzero::FtraceEvent::kGpuWorkPeriodFieldNumber, event);
+  if (!ts_field.has_value()) return;
 
   protos::pbzero::GpuWorkPeriodFtraceEvent::Decoder gpu_work_event(
-      ts_field.data(), ts_field.size());
+      ts_field.value().data(), ts_field.value().size());
   if (!gpu_work_event.has_start_time_ns()) {
     context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
     return;
@@ -461,9 +468,9 @@
 
   // Enforce clock type for the event data to be CLOCK_MONOTONIC_RAW
   // as specified, to calculate the timestamp correctly.
-  int64_t int64_timestamp = static_cast<int64_t>(raw_timestamp);
-  base::StatusOr<int64_t> timestamp = ResolveTraceTime(
-      context_, BuiltinClock::BUILTIN_CLOCK_MONOTONIC_RAW, int64_timestamp);
+  auto timestamp = context_->clock_tracker->ToTraceTime(
+      BuiltinClock::BUILTIN_CLOCK_MONOTONIC_RAW,
+      static_cast<int64_t>(raw_timestamp));
 
   // ClockTracker will increment some error stats if it failed to convert the
   // timestamp so just return.
@@ -482,19 +489,13 @@
     RefPtr<PacketSequenceStateGeneration> state) {
   // Special handling of valid thermal_exynos_acpm_bulk tracepoint events which
   // contains the right timestamp value nested inside the event data.
-  const uint8_t* data = event.data();
-  const size_t length = event.length();
-
-  ProtoDecoder decoder(data, length);
-  auto ts_field = decoder.FindField(
-      protos::pbzero::FtraceEvent::kThermalExynosAcpmBulkFieldNumber);
-  if (!ts_field.valid()) {
-    context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
-    return;
-  }
+  auto ts_field = GetFtraceEventField(
+      protos::pbzero::FtraceEvent::kThermalExynosAcpmBulkFieldNumber, event);
+  if (!ts_field.has_value()) return;
 
   protos::pbzero::ThermalExynosAcpmBulkFtraceEvent::Decoder
-      thermal_exynos_acpm_bulk_event(ts_field.data(), ts_field.size());
+      thermal_exynos_acpm_bulk_event(ts_field.value().data(),
+                                     ts_field.value().size());
   if (!thermal_exynos_acpm_bulk_event.has_timestamp()) {
     context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
     return;
@@ -505,5 +506,42 @@
                                     std::move(state), context_->machine_id());
 }
 
+void FtraceTokenizer::TokenizeFtraceParamSetValueCpm(
+    uint32_t cpu, TraceBlobView event,
+    RefPtr<PacketSequenceStateGeneration> state) {
+  // Special handling of valid param_set_value_cpm tracepoint events which
+  // contains the right timestamp value nested inside the event data.
+  auto ts_field = GetFtraceEventField(
+      protos::pbzero::FtraceEvent::kParamSetValueCpmFieldNumber, event);
+  if (!ts_field.has_value()) return;
+
+  protos::pbzero::ParamSetValueCpmFtraceEvent::Decoder
+      param_set_value_cpm_event(ts_field.value().data(),
+                                ts_field.value().size());
+  if (!param_set_value_cpm_event.has_timestamp()) {
+    context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
+    return;
+  }
+  int64_t timestamp =
+      static_cast<int64_t>(param_set_value_cpm_event.timestamp());
+  context_->sorter->PushFtraceEvent(cpu, timestamp, std::move(event),
+                                    std::move(state), context_->machine_id());
+}
+
+std::optional<protozero::Field> FtraceTokenizer::GetFtraceEventField(
+    uint32_t event_id, const TraceBlobView& event) {
+  //  Extract ftrace event field by decoding event trace blob.
+  const uint8_t* data = event.data();
+  const size_t length = event.length();
+
+  ProtoDecoder decoder(data, length);
+  auto ts_field = decoder.FindField(event_id);
+  if (!ts_field.valid()) {
+    context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
+    return std::nullopt;
+  }
+  return ts_field;
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/ftrace/ftrace_tokenizer.h b/src/trace_processor/importers/ftrace/ftrace_tokenizer.h
index 6aff47d..d8780b1 100644
--- a/src/trace_processor/importers/ftrace/ftrace_tokenizer.h
+++ b/src/trace_processor/importers/ftrace/ftrace_tokenizer.h
@@ -17,6 +17,7 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_FTRACE_TOKENIZER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_FTRACE_TOKENIZER_H_
 
+#include <optional>
 #include <vector>
 
 #include "perfetto/trace_processor/trace_blob_view.h"
@@ -68,6 +69,11 @@
       uint32_t cpu,
       TraceBlobView event,
       RefPtr<PacketSequenceStateGeneration> state);
+  void TokenizeFtraceParamSetValueCpm(
+      uint32_t cpu, TraceBlobView event,
+      RefPtr<PacketSequenceStateGeneration> state);
+  std::optional<protozero::Field> GetFtraceEventField(
+      uint32_t event_id, const TraceBlobView& event);
 
   void DlogWithLimit(const base::Status& status) {
     static std::atomic<uint32_t> dlog_count(0);
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 86db07b..922b21e 100644
--- a/src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc
+++ b/src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc
@@ -16,36 +16,39 @@
 
 #include "src/trace_processor/importers/ftrace/gpu_work_period_tracker.h"
 
+#include <cstdint>
+
 #include "perfetto/ext/base/string_utils.h"
-#include "protos/perfetto/common/gpu_counter_descriptor.pbzero.h"
+#include "perfetto/protozero/field.h"
 #include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
 #include "protos/perfetto/trace/ftrace/power.pbzero.h"
 #include "src/trace_processor/importers/common/async_track_set_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.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/track_tracker.h"
+#include "src/trace_processor/importers/common/tracks.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/slice_tables_py.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 GpuWorkPeriodTracker::GpuWorkPeriodTracker(TraceProcessorContext* context)
-    : context_(context),
-      gpu_work_period_id_(context->storage->InternString("GPU Work Period")) {}
+    : context_(context) {}
 
 void GpuWorkPeriodTracker::ParseGpuWorkPeriodEvent(int64_t timestamp,
                                                    protozero::ConstBytes blob) {
-  protos::pbzero::GpuWorkPeriodFtraceEvent::Decoder evt(blob.data, blob.size);
+  protos::pbzero::GpuWorkPeriodFtraceEvent::Decoder evt(blob);
 
-  tables::GpuWorkPeriodTrackTable::Row track(gpu_work_period_id_);
-  track.uid = static_cast<int32_t>(evt.uid());
-  track.gpu_id = evt.gpu_id();
-  track.machine_id = context_->machine_id();
-  TrackId track_id = context_->track_tracker->InternGpuWorkPeriodTrack(track);
+  TrackTracker::DimensionsBuilder dims_builder =
+      context_->track_tracker->CreateDimensionsBuilder();
+  dims_builder.AppendGpu(evt.gpu_id());
+  dims_builder.AppendUid(static_cast<int32_t>(evt.uid()));
+  TrackId track_id = context_->track_tracker->InternTrack(
+      tracks::android_gpu_work_period, std::move(dims_builder).Build());
 
-  const int64_t duration =
+  const auto duration =
       static_cast<int64_t>(evt.end_time_ns() - evt.start_time_ns());
-  const int64_t active_duration =
+  const auto active_duration =
       static_cast<int64_t>(evt.total_active_duration_ns());
   const double active_percent = 100.0 * (static_cast<double>(active_duration) /
                                          static_cast<double>(duration));
@@ -66,5 +69,4 @@
                                        row);
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/ftrace/gpu_work_period_tracker.h b/src/trace_processor/importers/ftrace/gpu_work_period_tracker.h
index a0001e3..861c90b 100644
--- a/src/trace_processor/importers/ftrace/gpu_work_period_tracker.h
+++ b/src/trace_processor/importers/ftrace/gpu_work_period_tracker.h
@@ -17,11 +17,11 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_GPU_WORK_PERIOD_TRACKER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_GPU_WORK_PERIOD_TRACKER_H_
 
-#include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/util/descriptors.h"
+#include <cstdint>
 
-namespace perfetto {
-namespace trace_processor {
+#include "perfetto/protozero/field.h"
+
+namespace perfetto::trace_processor {
 
 class TraceProcessorContext;
 
@@ -32,10 +32,8 @@
 
  private:
   TraceProcessorContext* context_;
-  const StringId gpu_work_period_id_;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_GPU_WORK_PERIOD_TRACKER_H_
diff --git a/src/trace_processor/importers/ftrace/iostat_tracker.cc b/src/trace_processor/importers/ftrace/iostat_tracker.cc
index d3c40d8..e0ae458 100644
--- a/src/trace_processor/importers/ftrace/iostat_tracker.cc
+++ b/src/trace_processor/importers/ftrace/iostat_tracker.cc
@@ -43,7 +43,7 @@
                                                    uint64_t value) {
     std::string track_name = tagPrefix + "." + std::string(counter_name);
     StringId string_id = context_->storage->InternString(track_name.c_str());
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kIo, string_id);
     context_->event_tracker->PushCounter(timestamp, static_cast<double>(value),
                                          track);
@@ -83,7 +83,7 @@
                                                    uint64_t value) {
     std::string track_name = tagPrefix + "." + std::string(counter_name);
     StringId string_id = context_->storage->InternString(track_name.c_str());
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kIo, string_id);
     context_->event_tracker->PushCounter(timestamp, static_cast<double>(value),
                                          track);
diff --git a/src/trace_processor/importers/ftrace/mali_gpu_event_tracker.cc b/src/trace_processor/importers/ftrace/mali_gpu_event_tracker.cc
index ad33a20..44c8d9f 100644
--- a/src/trace_processor/importers/ftrace/mali_gpu_event_tracker.cc
+++ b/src/trace_processor/importers/ftrace/mali_gpu_event_tracker.cc
@@ -154,11 +154,10 @@
   // Since these events are called from an interrupt context they cannot be
   // associated to a single process or thread. Add to a custom Mali Irq track
   // instead.
-  base::StackString<255> track_name("Mali Irq Cpu %d", cpu);
-  StringId track_name_id =
-      context_->storage->InternString(track_name.string_view());
-  TrackId track_id =
-      context_->track_tracker->InternCpuTrack(track_name_id, cpu);
+  TrackId track_id = context_->track_tracker->InternCpuTrack(
+      tracks::cpu_mali_irq, cpu,
+      TrackTracker::LegacyCharArrayName{
+          base::StackString<255>("Mali Irq Cpu %u", cpu)});
 
   switch (field_id) {
     case FtraceEvent::kMaliMaliCSFINTERRUPTSTARTFieldNumber: {
@@ -178,7 +177,7 @@
 void MaliGpuEventTracker::ParseMaliGpuMcuStateEvent(int64_t timestamp,
                                                     uint32_t field_id) {
   tables::GpuTrackTable::Row track_info(mcu_state_track_name_);
-  TrackId track_id = context_->track_tracker->InternGpuTrack(track_info);
+  TrackId track_id = context_->track_tracker->LegacyInternGpuTrack(track_info);
 
   if (field_id < kFirstMcuStateId || field_id > kLastMcuStateId) {
     PERFETTO_FATAL("Mali MCU state ID out of range");
diff --git a/src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.cc b/src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.cc
new file mode 100644
index 0000000..60f85a7
--- /dev/null
+++ b/src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.cc
@@ -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.
+ */
+
+#include "src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.h"
+
+#include <cmath>
+#include <cstdint>
+
+#include "perfetto/protozero/field.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
+#include "protos/perfetto/trace/ftrace/pixel_mm.pbzero.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/track_tracker.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/variadic.h"
+
+namespace perfetto::trace_processor {
+
+PixelMmKswapdEventTracker::PixelMmKswapdEventTracker(
+    TraceProcessorContext* context)
+    : context_(context),
+      kswapd_efficiency_name_(
+          context->storage->InternString("kswapd_efficiency")),
+      efficiency_pct_name_(context->storage->InternString("efficiency %")),
+      pages_scanned_name_(context->storage->InternString("pages scanned")),
+      pages_reclaimed_name_(context->storage->InternString("pages reclaimed")) {
+}
+
+void PixelMmKswapdEventTracker::ParsePixelMmKswapdWake(int64_t timestamp,
+                                                       uint32_t pid) {
+  UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
+  TrackId details_track = context_->track_tracker->InternThreadTrack(utid);
+
+  context_->slice_tracker->Begin(timestamp, details_track, kNullStringId,
+                                 kswapd_efficiency_name_);
+}
+
+void PixelMmKswapdEventTracker::ParsePixelMmKswapdDone(
+    int64_t timestamp,
+    uint32_t pid,
+    protozero::ConstBytes blob) {
+  UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
+  TrackId details_track = context_->track_tracker->InternThreadTrack(utid);
+
+  protos::pbzero::PixelMmKswapdDoneFtraceEvent::Decoder decoder(blob.data,
+                                                                blob.size);
+
+  context_->slice_tracker->End(
+      timestamp, details_track, kNullStringId, kswapd_efficiency_name_,
+      [this, &decoder](ArgsTracker::BoundInserter* inserter) {
+        if (decoder.has_delta_nr_scanned()) {
+          inserter->AddArg(
+              pages_scanned_name_,
+              Variadic::UnsignedInteger(decoder.delta_nr_scanned()));
+        }
+        if (decoder.has_delta_nr_reclaimed()) {
+          inserter->AddArg(
+              pages_reclaimed_name_,
+              Variadic::UnsignedInteger(decoder.delta_nr_reclaimed()));
+        }
+
+        if (decoder.has_delta_nr_reclaimed() &&
+            decoder.has_delta_nr_scanned()) {
+          double efficiency =
+              static_cast<double>(decoder.delta_nr_reclaimed()) * 100 /
+              static_cast<double>(decoder.delta_nr_scanned());
+
+          inserter->AddArg(efficiency_pct_name_,
+                           Variadic::UnsignedInteger(
+                               static_cast<uint64_t>(std::round(efficiency))));
+        }
+      });
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.h b/src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.h
new file mode 100644
index 0000000..dfdd6a0
--- /dev/null
+++ b/src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.h
@@ -0,0 +1,48 @@
+/*
+ * 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_FTRACE_PIXEL_MM_KSWAPD_EVENT_TRACKER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_PIXEL_MM_KSWAPD_EVENT_TRACKER_H_
+
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/util/descriptors.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+class TraceProcessorContext;
+
+class PixelMmKswapdEventTracker {
+ public:
+  explicit PixelMmKswapdEventTracker(TraceProcessorContext*);
+
+  void ParsePixelMmKswapdWake(int64_t timestamp, uint32_t pid);
+  void ParsePixelMmKswapdDone(int64_t timestamp,
+                              uint32_t pid,
+                              protozero::ConstBytes);
+
+ private:
+  TraceProcessorContext* context_;
+  const StringId kswapd_efficiency_name_;
+  const StringId efficiency_pct_name_;
+  const StringId pages_scanned_name_;
+  const StringId pages_reclaimed_name_;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_PIXEL_MM_KSWAPD_EVENT_TRACKER_H_
diff --git a/src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.cc b/src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.cc
index 66769e6..cf0d7f2 100644
--- a/src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.cc
+++ b/src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.cc
@@ -20,12 +20,18 @@
 #include "perfetto/ext/base/string_utils.h"
 #include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
 #include "protos/perfetto/trace/ftrace/hyp.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_tracker.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
+namespace {
+
+TrackTracker::LegacyCharArrayName GetTrackName(uint32_t cpu) {
+  return TrackTracker::LegacyCharArrayName{
+      base::StackString<255>("pkVM Hypervisor CPU %u", cpu)};
+}
+
+}  // namespace
 
 PkvmHypervisorCpuTracker::PkvmHypervisorCpuTracker(
     TraceProcessorContext* context)
@@ -78,21 +84,23 @@
 
 void PkvmHypervisorCpuTracker::ParseHypEnter(uint32_t cpu, int64_t timestamp) {
   // TODO(b/249050813): handle bad events (e.g. 2 hyp_enter in a row)
-
-  TrackId track_id = GetHypCpuTrackId(cpu);
+  TrackId track_id = context_->track_tracker->InternCpuTrack(
+      tracks::pkvm_hypervisor, cpu, GetTrackName(cpu));
   context_->slice_tracker->Begin(timestamp, track_id, category_, slice_name_);
 }
 
 void PkvmHypervisorCpuTracker::ParseHypExit(uint32_t cpu, int64_t timestamp) {
   // TODO(b/249050813): handle bad events (e.g. 2 hyp_exit in a row)
-  TrackId track_id = GetHypCpuTrackId(cpu);
+  TrackId track_id = context_->track_tracker->InternCpuTrack(
+      tracks::pkvm_hypervisor, cpu, GetTrackName(cpu));
   context_->slice_tracker->End(timestamp, track_id);
 }
 
 void PkvmHypervisorCpuTracker::ParseHostHcall(uint32_t cpu,
                                               protozero::ConstBytes blob) {
   protos::pbzero::HostHcallFtraceEvent::Decoder evt(blob.data, blob.size);
-  TrackId track_id = GetHypCpuTrackId(cpu);
+  TrackId track_id = context_->track_tracker->InternCpuTrack(
+      tracks::pkvm_hypervisor, cpu, GetTrackName(cpu));
 
   auto args_inserter = [this, &evt](ArgsTracker::BoundInserter* inserter) {
     StringId host_hcall = context_->storage->InternString("host_hcall");
@@ -109,7 +117,8 @@
 void PkvmHypervisorCpuTracker::ParseHostSmc(uint32_t cpu,
                                             protozero::ConstBytes blob) {
   protos::pbzero::HostSmcFtraceEvent::Decoder evt(blob.data, blob.size);
-  TrackId track_id = GetHypCpuTrackId(cpu);
+  TrackId track_id = context_->track_tracker->InternCpuTrack(
+      tracks::pkvm_hypervisor, cpu, GetTrackName(cpu));
 
   auto args_inserter = [this, &evt](ArgsTracker::BoundInserter* inserter) {
     StringId host_smc = context_->storage->InternString("host_smc");
@@ -126,7 +135,8 @@
 void PkvmHypervisorCpuTracker::ParseHostMemAbort(uint32_t cpu,
                                                  protozero::ConstBytes blob) {
   protos::pbzero::HostMemAbortFtraceEvent::Decoder evt(blob.data, blob.size);
-  TrackId track_id = GetHypCpuTrackId(cpu);
+  TrackId track_id = context_->track_tracker->InternCpuTrack(
+      tracks::pkvm_hypervisor, cpu, GetTrackName(cpu));
 
   auto args_inserter = [this, &evt](ArgsTracker::BoundInserter* inserter) {
     StringId host_mem_abort = context_->storage->InternString("host_mem_abort");
@@ -140,11 +150,4 @@
                                    args_inserter);
 }
 
-TrackId PkvmHypervisorCpuTracker::GetHypCpuTrackId(uint32_t cpu) {
-  base::StackString<255> track_name("pkVM Hypervisor CPU %d", cpu);
-  StringId track = context_->storage->InternString(track_name.string_view());
-  return context_->track_tracker->InternCpuTrack(track, cpu);
-}
-
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.h b/src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.h
index bf5845b..7508044 100644
--- a/src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.h
+++ b/src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.h
@@ -46,8 +46,6 @@
   void ParseHostSmc(uint32_t cpu, protozero::ConstBytes blob);
   void ParseHostMemAbort(uint32_t cpu, protozero::ConstBytes blob);
 
-  TrackId GetHypCpuTrackId(uint32_t cpu);
-
   TraceProcessorContext* context_;
   const StringId category_;
   const StringId slice_name_;
diff --git a/src/trace_processor/importers/ftrace/thermal_tracker.cc b/src/trace_processor/importers/ftrace/thermal_tracker.cc
index f2fe7bb..b97d001 100644
--- a/src/trace_processor/importers/ftrace/thermal_tracker.cc
+++ b/src/trace_processor/importers/ftrace/thermal_tracker.cc
@@ -104,7 +104,7 @@
 void ThermalTracker::PushCounter(int64_t timestamp,
                                  StringId counter_id,
                                  double value) {
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kThermals, counter_id);
   context_->event_tracker->PushCounter(timestamp, value, track);
 }
diff --git a/src/trace_processor/importers/ftrace/virtio_gpu_tracker.cc b/src/trace_processor/importers/ftrace/virtio_gpu_tracker.cc
index c66ab62..d142d3d 100644
--- a/src/trace_processor/importers/ftrace/virtio_gpu_tracker.cc
+++ b/src/trace_processor/importers/ftrace/virtio_gpu_tracker.cc
@@ -160,7 +160,7 @@
 
 void VirtioGpuTracker::VirtioGpuQueue::HandleNumFree(int64_t timestamp,
                                                      uint32_t num_free) {
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kVirtio, num_free_id_);
   context_->event_tracker->PushCounter(timestamp, static_cast<double>(num_free),
                                        track);
@@ -201,7 +201,7 @@
 
   int64_t duration = timestamp - *start_timestamp;
 
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kVirtio, latency_id_);
   context_->event_tracker->PushCounter(timestamp, static_cast<double>(duration),
                                        track);
diff --git a/src/trace_processor/importers/fuchsia/BUILD.gn b/src/trace_processor/importers/fuchsia/BUILD.gn
index 47d430b..8aa0826 100644
--- a/src/trace_processor/importers/fuchsia/BUILD.gn
+++ b/src/trace_processor/importers/fuchsia/BUILD.gn
@@ -39,7 +39,9 @@
     "../../../../gn:default_deps",
     "../../sorter",
     "../../storage",
+    "../../tables",
     "../../types",
+    "../../util:trace_type",
     "../common",
     "../proto:minimal",
   ]
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.cc b/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.cc
index c74f2e5..beed31e 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.cc
+++ b/src/trace_processor/importers/fuchsia/fuchsia_trace_parser.cc
@@ -332,7 +332,7 @@
               StringId counter_name_id = context_->storage->InternString(
                   base::StringView(counter_name_str));
               TrackId track =
-                  context_->track_tracker->InternProcessCounterTrack(
+                  context_->track_tracker->LegacyInternProcessCounterTrack(
                       counter_name_id, upid);
               context_->event_tracker->PushCounter(ts, counter_value, track);
             }
@@ -386,8 +386,9 @@
           }
           UniquePid upid =
               procs->GetOrCreateProcess(static_cast<uint32_t>(tinfo.pid));
-          TrackId track_id = context_->track_tracker->InternFuchsiaAsyncTrack(
-              name, upid, correlation_id);
+          TrackId track_id =
+              context_->track_tracker->LegacyInternLegacyChromeAsyncTrack(
+                  name, upid, correlation_id, false, kNullStringId);
           slices->Begin(ts, track_id, cat, name, std::move(insert_args));
           break;
         }
@@ -399,8 +400,9 @@
           }
           UniquePid upid =
               procs->GetOrCreateProcess(static_cast<uint32_t>(tinfo.pid));
-          TrackId track_id = context_->track_tracker->InternFuchsiaAsyncTrack(
-              name, upid, correlation_id);
+          TrackId track_id =
+              context_->track_tracker->LegacyInternLegacyChromeAsyncTrack(
+                  name, upid, correlation_id, false, kNullStringId);
           slices->Scoped(ts, track_id, cat, name, 0, std::move(insert_args));
           break;
         }
@@ -412,8 +414,9 @@
           }
           UniquePid upid =
               procs->GetOrCreateProcess(static_cast<uint32_t>(tinfo.pid));
-          TrackId track_id = context_->track_tracker->InternFuchsiaAsyncTrack(
-              name, upid, correlation_id);
+          TrackId track_id =
+              context_->track_tracker->LegacyInternLegacyChromeAsyncTrack(
+                  name, upid, correlation_id, false, kNullStringId);
           slices->End(ts, track_id, cat, name, std::move(insert_args));
           break;
         }
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h
index f15a725..fb669a9 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h
+++ b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h
@@ -17,14 +17,22 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_FUCHSIA_FUCHSIA_TRACE_TOKENIZER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_FUCHSIA_FUCHSIA_TRACE_TOKENIZER_H_
 
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "perfetto/base/status.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
-#include "src/trace_processor/importers/fuchsia/fuchsia_trace_utils.h"
+#include "src/trace_processor/importers/fuchsia/fuchsia_record.h"
 #include "src/trace_processor/importers/proto/proto_trace_reader.h"
 #include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/types/task_state.h"
+#include "src/trace_processor/tables/sched_tables_py.h"
+#include "src/trace_processor/util/trace_type.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class TraceProcessorContext;
 
@@ -37,7 +45,7 @@
   ~FuchsiaTraceTokenizer() override;
 
   // ChunkedTraceReader implementation
-  util::Status Parse(TraceBlobView) override;
+  base::Status Parse(TraceBlobView) override;
   base::Status NotifyEndOfFile() override;
 
  private:
@@ -130,7 +138,6 @@
   std::unordered_map<uint64_t, Thread> threads_;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_FUCHSIA_FUCHSIA_TRACE_TOKENIZER_H_
diff --git a/src/trace_processor/importers/gecko/BUILD.gn b/src/trace_processor/importers/gecko/BUILD.gn
new file mode 100644
index 0000000..5ebd2b8
--- /dev/null
+++ b/src/trace_processor/importers/gecko/BUILD.gn
@@ -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("../../../../gn/test.gni")
+
+source_set("gecko_event") {
+  sources = [ "gecko_event.h" ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../../containers",
+    "../../tables",
+  ]
+}
+
+if (enable_perfetto_trace_processor_json) {
+  source_set("gecko") {
+    sources = [
+      "gecko_trace_parser_impl.cc",
+      "gecko_trace_parser_impl.h",
+      "gecko_trace_tokenizer.cc",
+      "gecko_trace_tokenizer.h",
+    ]
+    deps = [
+      ":gecko_event",
+      "../../../../gn:default_deps",
+      "../../../../gn:jsoncpp",
+      "../../../../include/perfetto/trace_processor:storage",
+      "../../../../protos/perfetto/trace:zero",
+      "../../../base",
+      "../../importers/common",
+      "../../sorter",
+      "../../storage",
+      "../../tables",
+      "../../types",
+      "../json:minimal",
+    ]
+  }
+}
diff --git a/src/trace_processor/importers/gecko/gecko_event.h b/src/trace_processor/importers/gecko/gecko_event.h
new file mode 100644
index 0000000..52dacb1
--- /dev/null
+++ b/src/trace_processor/importers/gecko/gecko_event.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_GECKO_GECKO_EVENT_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_GECKO_GECKO_EVENT_H_
+
+#include <cstdint>
+#include <variant>
+
+#include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/tables/profiler_tables_py.h"
+
+namespace perfetto::trace_processor::gecko_importer {
+
+struct alignas(8) GeckoEvent {
+  struct ThreadMetadata {
+    uint32_t tid;
+    uint32_t pid;
+    StringPool::Id name;
+  };
+  struct StackSample {
+    uint32_t tid;
+    tables::StackProfileCallsiteTable::Id callsite_id;
+  };
+  using OneOf = std::variant<ThreadMetadata, StackSample>;
+  OneOf oneof;
+};
+
+}  // namespace perfetto::trace_processor::gecko_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_GECKO_GECKO_EVENT_H_
diff --git a/src/trace_processor/importers/gecko/gecko_trace_parser_impl.cc b/src/trace_processor/importers/gecko/gecko_trace_parser_impl.cc
new file mode 100644
index 0000000..66eb0fc
--- /dev/null
+++ b/src/trace_processor/importers/gecko/gecko_trace_parser_impl.cc
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/gecko/gecko_trace_parser_impl.h"
+
+#include <cstdint>
+
+#include "perfetto/base/compiler.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/importers/gecko/gecko_event.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/profiler_tables_py.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::gecko_importer {
+
+namespace {
+
+template <typename T>
+constexpr uint32_t GeckoOneOf() {
+  return base::variant_index<GeckoEvent::OneOf, T>();
+}
+
+}  // namespace
+
+GeckoTraceParserImpl::GeckoTraceParserImpl(TraceProcessorContext* context)
+    : context_(context) {}
+
+GeckoTraceParserImpl::~GeckoTraceParserImpl() = default;
+
+void GeckoTraceParserImpl::ParseGeckoEvent(int64_t ts, GeckoEvent evt) {
+  switch (evt.oneof.index()) {
+    case GeckoOneOf<GeckoEvent::ThreadMetadata>(): {
+      auto thread = std::get<GeckoEvent::ThreadMetadata>(evt.oneof);
+      UniqueTid utid =
+          context_->process_tracker->UpdateThread(thread.tid, thread.pid);
+      context_->process_tracker->UpdateThreadNameByUtid(
+          utid, thread.name, ThreadNamePriority::kOther);
+      break;
+    }
+    case GeckoOneOf<GeckoEvent::StackSample>():
+      auto sample = std::get<GeckoEvent::StackSample>(evt.oneof);
+      auto* ss = context_->storage->mutable_cpu_profile_stack_sample_table();
+      tables::CpuProfileStackSampleTable::Row row;
+      row.ts = ts;
+      row.callsite_id = sample.callsite_id;
+      row.utid = context_->process_tracker->GetOrCreateThread(sample.tid);
+      ss->Insert(row);
+      break;
+  }
+}
+
+}  // namespace perfetto::trace_processor::gecko_importer
diff --git a/src/trace_processor/importers/gecko/gecko_trace_parser_impl.h b/src/trace_processor/importers/gecko/gecko_trace_parser_impl.h
new file mode 100644
index 0000000..de5b378
--- /dev/null
+++ b/src/trace_processor/importers/gecko/gecko_trace_parser_impl.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_GECKO_GECKO_TRACE_PARSER_IMPL_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_GECKO_GECKO_TRACE_PARSER_IMPL_H_
+
+#include <cstdint>
+
+#include "src/trace_processor/importers/common/trace_parser.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::gecko_importer {
+
+class GeckoTraceParserImpl : public GeckoTraceParser {
+ public:
+  explicit GeckoTraceParserImpl(TraceProcessorContext*);
+  ~GeckoTraceParserImpl() override;
+
+  void ParseGeckoEvent(int64_t ts, GeckoEvent) override;
+
+ private:
+  TraceProcessorContext* const context_;
+};
+
+}  // namespace perfetto::trace_processor::gecko_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_GECKO_GECKO_TRACE_PARSER_IMPL_H_
diff --git a/src/trace_processor/importers/gecko/gecko_trace_tokenizer.cc b/src/trace_processor/importers/gecko/gecko_trace_tokenizer.cc
new file mode 100644
index 0000000..32945f4
--- /dev/null
+++ b/src/trace_processor/importers/gecko/gecko_trace_tokenizer.cc
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/gecko/gecko_trace_tokenizer.h"
+
+#include <json/value.h>
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "protos/perfetto/trace/clock_snapshot.pbzero.h"
+#include "src/trace_processor/importers/common/mapping_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
+#include "src/trace_processor/importers/gecko/gecko_event.h"
+#include "src/trace_processor/importers/json/json_utils.h"
+#include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto::trace_processor::gecko_importer {
+namespace {
+
+struct Callsite {
+  CallsiteId id;
+  uint32_t depth;
+};
+
+}  // namespace
+
+GeckoTraceTokenizer::GeckoTraceTokenizer(TraceProcessorContext* ctx)
+    : context_(ctx) {}
+GeckoTraceTokenizer::~GeckoTraceTokenizer() = default;
+
+base::Status GeckoTraceTokenizer::Parse(TraceBlobView blob) {
+  pending_json_.append(reinterpret_cast<const char*>(blob.data()), blob.size());
+  return base::OkStatus();
+}
+
+base::Status GeckoTraceTokenizer::NotifyEndOfFile() {
+  std::optional<Json::Value> opt_value =
+      json::ParseJsonString(base::StringView(pending_json_));
+  if (!opt_value) {
+    return base::ErrStatus(
+        "Syntactic error while Gecko trace; please use an external JSON tool "
+        "(e.g. jq) to understand the source of the error.");
+  }
+  context_->clock_tracker->SetTraceTimeClock(
+      protos::pbzero::ClockSnapshot::Clock::MONOTONIC);
+
+  DummyMemoryMapping* dummy_mapping = nullptr;
+  base::FlatHashMap<std::string, DummyMemoryMapping*> mappings;
+
+  const Json::Value& value = *opt_value;
+  std::vector<FrameId> frame_ids;
+  std::vector<Callsite> callsites;
+  for (const auto& t : value["threads"]) {
+    // The trace uses per-thread indices, we reuse the vector for perf reasons
+    // to prevent reallocs on every thread.
+    frame_ids.clear();
+    callsites.clear();
+
+    const auto& strings = t["stringTable"];
+    const auto& frames = t["frameTable"];
+    const auto& frames_schema = frames["schema"];
+    uint32_t location_idx = frames_schema["location"].asUInt();
+    for (const auto& frame : frames["data"]) {
+      base::StringView name = strings[frame[location_idx].asUInt()].asCString();
+
+      constexpr std::string_view kMappingStart = " (in ";
+      size_t mapping_meta_start = name.find(
+          base::StringView(kMappingStart.data(), kMappingStart.size()));
+      if (mapping_meta_start == base::StringView::npos &&
+          name.data()[name.size() - 1] == ')') {
+        if (!dummy_mapping) {
+          dummy_mapping =
+              &context_->mapping_tracker->CreateDummyMapping("gecko");
+        }
+        frame_ids.push_back(
+            dummy_mapping->InternDummyFrame(name, base::StringView()));
+        continue;
+      }
+
+      DummyMemoryMapping* mapping;
+      size_t mapping_start = mapping_meta_start + kMappingStart.size();
+      size_t mapping_end = name.find(')', mapping_start);
+      std::string mapping_name =
+          name.substr(mapping_start, mapping_end - mapping_start).ToStdString();
+      if (auto* mapping_ptr = mappings.Find(mapping_name); mapping_ptr) {
+        mapping = *mapping_ptr;
+      } else {
+        mapping = &context_->mapping_tracker->CreateDummyMapping(mapping_name);
+        mappings.Insert(mapping_name, mapping);
+      }
+      frame_ids.push_back(mapping->InternDummyFrame(
+          name.substr(0, mapping_meta_start), base::StringView()));
+    }
+
+    const auto& stacks = t["stackTable"];
+    const auto& stacks_schema = stacks["schema"];
+    uint32_t prefix_index = stacks_schema["prefix"].asUInt();
+    uint32_t frame_index = stacks_schema["frame"].asUInt();
+    for (const auto& frame : stacks["data"]) {
+      const auto& prefix = frame[prefix_index];
+      std::optional<CallsiteId> prefix_id;
+      uint32_t depth = 0;
+      if (!prefix.isNull()) {
+        const auto& c = callsites[prefix.asUInt()];
+        prefix_id = c.id;
+        depth = c.depth + 1;
+      }
+      CallsiteId cid = context_->stack_profile_tracker->InternCallsite(
+          prefix_id, frame_ids[frame[frame_index].asUInt()], depth);
+      callsites.push_back({cid, depth});
+    }
+
+    const auto& samples = t["samples"];
+    const auto& samples_schema = samples["schema"];
+    uint32_t stack_index = samples_schema["stack"].asUInt();
+    uint32_t time_index = samples_schema["time"].asUInt();
+    bool added_metadata = false;
+    for (const auto& sample : samples["data"]) {
+      uint32_t stack_idx = sample[stack_index].asUInt();
+      auto ts =
+          static_cast<int64_t>(sample[time_index].asDouble() * 1000 * 1000);
+      if (!added_metadata) {
+        context_->sorter->PushGeckoEvent(
+            ts, GeckoEvent{GeckoEvent::ThreadMetadata{
+                    t["tid"].asUInt(), t["pid"].asUInt(),
+                    context_->storage->InternString(t["name"].asCString())}});
+        added_metadata = true;
+      }
+      ASSIGN_OR_RETURN(
+          int64_t converted,
+          context_->clock_tracker->ToTraceTime(
+              protos::pbzero::ClockSnapshot::Clock::MONOTONIC, ts));
+      context_->sorter->PushGeckoEvent(
+          converted, GeckoEvent{GeckoEvent::StackSample{
+                         t["tid"].asUInt(), callsites[stack_idx].id}});
+    }
+  }
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor::gecko_importer
diff --git a/src/trace_processor/importers/gecko/gecko_trace_tokenizer.h b/src/trace_processor/importers/gecko/gecko_trace_tokenizer.h
new file mode 100644
index 0000000..c6050d5
--- /dev/null
+++ b/src/trace_processor/importers/gecko/gecko_trace_tokenizer.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_GECKO_GECKO_TRACE_TOKENIZER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_GECKO_GECKO_TRACE_TOKENIZER_H_
+
+#include <string>
+
+#include "perfetto/base/status.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::gecko_importer {
+
+class GeckoTraceTokenizer : public ChunkedTraceReader {
+ public:
+  explicit GeckoTraceTokenizer(TraceProcessorContext*);
+  ~GeckoTraceTokenizer() override;
+
+  base::Status Parse(TraceBlobView) override;
+  base::Status NotifyEndOfFile() override;
+
+ private:
+  TraceProcessorContext* const context_;
+  std::string pending_json_;
+};
+
+}  // namespace perfetto::trace_processor::gecko_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_GECKO_GECKO_TRACE_TOKENIZER_H_
diff --git a/src/trace_processor/importers/gzip/BUILD.gn b/src/trace_processor/importers/gzip/BUILD.gn
deleted file mode 100644
index d2fad21..0000000
--- a/src/trace_processor/importers/gzip/BUILD.gn
+++ /dev/null
@@ -1,28 +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.
-
-source_set("full") {
-  sources = [
-    "gzip_trace_parser.cc",
-    "gzip_trace_parser.h",
-  ]
-  deps = [
-    "../..:storage_minimal",
-    "../../../../gn:default_deps",
-    "../../../base",
-    "../../util",
-    "../../util:gzip",
-    "../common",
-  ]
-}
diff --git a/src/trace_processor/importers/gzip/gzip_trace_parser.cc b/src/trace_processor/importers/gzip/gzip_trace_parser.cc
deleted file mode 100644
index dfa45e0..0000000
--- a/src/trace_processor/importers/gzip/gzip_trace_parser.cc
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 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
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/importers/gzip/gzip_trace_parser.h"
-
-#include <cstdint>
-#include <cstring>
-#include <memory>
-#include <string>
-#include <utility>
-
-#include "perfetto/base/logging.h"
-#include "perfetto/base/status.h"
-#include "perfetto/ext/base/string_utils.h"
-#include "perfetto/ext/base/string_view.h"
-#include "perfetto/trace_processor/trace_blob.h"
-#include "perfetto/trace_processor/trace_blob_view.h"
-#include "src/trace_processor/forwarding_trace_parser.h"
-#include "src/trace_processor/importers/common/chunked_trace_reader.h"
-#include "src/trace_processor/util/gzip_utils.h"
-#include "src/trace_processor/util/status_macros.h"
-
-namespace perfetto::trace_processor {
-
-namespace {
-
-using ResultCode = util::GzipDecompressor::ResultCode;
-
-}  // namespace
-
-GzipTraceParser::GzipTraceParser(TraceProcessorContext* context)
-    : context_(context) {}
-
-GzipTraceParser::GzipTraceParser(std::unique_ptr<ChunkedTraceReader> reader)
-    : context_(nullptr), inner_(std::move(reader)) {}
-
-GzipTraceParser::~GzipTraceParser() = default;
-
-base::Status GzipTraceParser::Parse(TraceBlobView blob) {
-  return ParseUnowned(blob.data(), blob.size());
-}
-
-base::Status GzipTraceParser::ParseUnowned(const uint8_t* data, size_t size) {
-  const uint8_t* start = data;
-  size_t len = size;
-
-  if (!inner_) {
-    PERFETTO_CHECK(context_);
-    inner_.reset(new ForwardingTraceParser(context_));
-  }
-
-  if (!first_chunk_parsed_) {
-    // .ctrace files begin with: "TRACE:\n" or "done. TRACE:\n" strip this if
-    // present.
-    base::StringView beginning(reinterpret_cast<const char*>(start), size);
-
-    static const char* kSystraceFileHeader = "TRACE:\n";
-    size_t offset = Find(kSystraceFileHeader, beginning);
-    if (offset != std::string::npos) {
-      start += strlen(kSystraceFileHeader) + offset;
-      len -= strlen(kSystraceFileHeader) + offset;
-    }
-    first_chunk_parsed_ = true;
-  }
-
-  // Our default uncompressed buffer size is 32MB as it allows for good
-  // throughput.
-  constexpr size_t kUncompressedBufferSize = 32ul * 1024 * 1024;
-
-  needs_more_input_ = false;
-  decompressor_.Feed(start, len);
-
-  for (auto ret = ResultCode::kOk; ret != ResultCode::kEof;) {
-    if (!buffer_) {
-      buffer_.reset(new uint8_t[kUncompressedBufferSize]);
-      bytes_written_ = 0;
-    }
-
-    auto result =
-        decompressor_.ExtractOutput(buffer_.get() + bytes_written_,
-                                    kUncompressedBufferSize - bytes_written_);
-    ret = result.ret;
-    if (ret == ResultCode::kError)
-      return base::ErrStatus("Failed to decompress trace chunk");
-
-    if (ret == ResultCode::kNeedsMoreInput) {
-      PERFETTO_DCHECK(result.bytes_written == 0);
-      needs_more_input_ = true;
-      return base::OkStatus();
-    }
-    bytes_written_ += result.bytes_written;
-
-    if (bytes_written_ == kUncompressedBufferSize || ret == ResultCode::kEof) {
-      TraceBlob blob =
-          TraceBlob::TakeOwnership(std::move(buffer_), bytes_written_);
-      RETURN_IF_ERROR(inner_->Parse(TraceBlobView(std::move(blob))));
-    }
-  }
-  return base::OkStatus();
-}
-
-base::Status GzipTraceParser::NotifyEndOfFile() {
-  // TODO(lalitm): this should really be an error returned to the caller but
-  // due to historical implementation, NotifyEndOfFile does not return a
-  // base::Status.
-  if (needs_more_input_) {
-    return base::ErrStatus("GZIP stream incomplete, trace is likely corrupt");
-  }
-  PERFETTO_DCHECK(!buffer_);
-
-  if (!inner_) {
-    base::OkStatus();
-  }
-  return inner_->NotifyEndOfFile();
-}
-
-}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/gzip/gzip_trace_parser.h b/src/trace_processor/importers/gzip/gzip_trace_parser.h
deleted file mode 100644
index 1cd862d..0000000
--- a/src/trace_processor/importers/gzip/gzip_trace_parser.h
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 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
- *
- *      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_GZIP_GZIP_TRACE_PARSER_H_
-#define SRC_TRACE_PROCESSOR_IMPORTERS_GZIP_GZIP_TRACE_PARSER_H_
-
-#include <cstddef>
-#include <cstdint>
-#include <memory>
-
-#include "perfetto/base/status.h"
-#include "src/trace_processor/importers/common/chunked_trace_reader.h"
-#include "src/trace_processor/util/gzip_utils.h"
-
-namespace perfetto::trace_processor {
-
-class TraceProcessorContext;
-
-class GzipTraceParser : public ChunkedTraceReader {
- public:
-  explicit GzipTraceParser(TraceProcessorContext*);
-  explicit GzipTraceParser(std::unique_ptr<ChunkedTraceReader>);
-  ~GzipTraceParser() override;
-
-  // ChunkedTraceReader implementation
-  base::Status Parse(TraceBlobView) override;
-  base::Status NotifyEndOfFile() override;
-
-  base::Status ParseUnowned(const uint8_t*, size_t);
-
-  bool needs_more_input() const { return needs_more_input_; }
-
- private:
-  TraceProcessorContext* const context_;
-  util::GzipDecompressor decompressor_;
-  std::unique_ptr<ChunkedTraceReader> inner_;
-
-  std::unique_ptr<uint8_t[]> buffer_;
-  size_t bytes_written_ = 0;
-
-  bool first_chunk_parsed_ = false;
-  bool needs_more_input_ = false;
-};
-
-}  // namespace perfetto::trace_processor
-
-#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_GZIP_GZIP_TRACE_PARSER_H_
diff --git a/src/trace_processor/importers/instruments/BUILD.gn b/src/trace_processor/importers/instruments/BUILD.gn
new file mode 100644
index 0000000..a0a408f
--- /dev/null
+++ b/src/trace_processor/importers/instruments/BUILD.gn
@@ -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("../../../../gn/test.gni")
+
+source_set("row") {
+  sources = [ "row.h" ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../../containers",
+    "../../util:build_id",
+  ]
+}
+
+if (enable_perfetto_trace_processor_mac_instruments) {
+  source_set("instruments") {
+    sources = [
+      "instruments_xml_tokenizer.cc",
+      "instruments_xml_tokenizer.h",
+      "row_data_tracker.cc",
+      "row_data_tracker.h",
+      "row_parser.cc",
+      "row_parser.h",
+    ]
+    public_deps = [ ":row" ]
+    deps = [
+      "../../../../gn:default_deps",
+      "../../../../gn:expat",
+      "../../../../include/perfetto/ext/base:base",
+      "../../../../include/perfetto/public",
+      "../../../../include/perfetto/trace_processor:trace_processor",
+      "../../../../protos/perfetto/trace:zero",
+      "../../sorter",
+      "../../storage",
+      "../../types",
+      "../common:common",
+    ]
+  }
+}
diff --git a/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc
new file mode 100644
index 0000000..06038b7
--- /dev/null
+++ b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc
@@ -0,0 +1,507 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/instruments/instruments_xml_tokenizer.h"
+
+#include <cctype>
+#include <map>
+
+#include <expat.h>
+#include <stdint.h>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/public/fnv1a.h"
+#include "protos/perfetto/trace/clock_snapshot.pbzero.h"
+#include "src/trace_processor/importers/common/clock_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/importers/instruments/row.h"
+#include "src/trace_processor/importers/instruments/row_data_tracker.h"
+#include "src/trace_processor/sorter/trace_sorter.h"
+
+#if !PERFETTO_BUILDFLAG(PERFETTO_TP_INSTRUMENTS)
+#error \
+    "This file should not be built when enable_perfetto_trace_processor_mac_instruments=false"
+#endif
+
+namespace perfetto::trace_processor::instruments_importer {
+
+namespace {
+
+std::string MakeTrimmed(const char* chars, int len) {
+  while (len > 0 && std::isspace(*chars)) {
+    chars++;
+    len--;
+  }
+  while (len > 0 && std::isspace(chars[len - 1])) {
+    len--;
+  }
+  return std::string(chars, static_cast<size_t>(len));
+}
+
+}  // namespace
+
+// The Instruments XML tokenizer reads instruments traces exported with:
+//
+//   xctrace export --input /path/to/profile.trace --xpath
+//     '//trace-toc/run/data/table[@schema="os-signpost and
+//        @category="PointsOfInterest"] |
+//      //trace-toc/run/data/table[@schema="time-sample"]'
+//
+// This exports two tables:
+//   1. Points of interest signposts
+//   2. Time samples
+//
+// The first is used for clock synchronization -- perfetto emits signpost events
+// during tracing which allow synchronization of the xctrace clock (relative to
+// start of profiling) with the perfetto boottime clock. The second contains
+// the samples themselves.
+//
+// The expected format of the rows in the clock sync table is:
+//
+//     <row>
+//       <event-time>1234</event-time>
+//       <subsystem>dev.perfetto.clock_sync</subsystem>
+//       <os-log-metadata>
+//         <uint64>5678</uint64>
+//       </os-log-metadata>
+//     </row>
+//
+// There may be other rows with other data (from other subsystems), and
+// additional data in the row (such as thread data and other metadata) -- this
+// can be safely ignored.
+//
+// The expected format of the rows in the time sample table is:
+//
+//     <row>
+//       <sample-time>1234</sample-time>
+//       <thread fmt="Thread name">
+//         <tid>1</tid>
+//         <process fmt="Process name">
+//           <pid>1<pid>
+//         </process>
+//       </thread>
+//       <core>0</core>
+//       <backtrace>
+//         <frame addr="0x120001234">
+//           <binary
+//             name="MyBinary" UUID="01234567-89ABC-CDEF-0123-456789ABCDEF"
+//             load-addr="0x120000000" path="/path/to/MyBinary.app/MyBinary" />
+//         </frame>
+//         ... more frames ...
+//     </row>
+//
+// Here we do not expect other rows with other data -- every row should have a
+// backtrace, and we use the presence of a backtrace to distinguish time samples
+// and clock sync eventst. However, there can be additional data in the row
+// (such as other metadata) -- this can be safely ignored.
+//
+// In addition, the XML format annotates elements with ids, to later reuse the
+// same data by id without needing to repeat its contents. For example, you
+// might have thread data for a sample:
+//
+//     <thread id="11" fmt="My Thread"><tid id="12">10</tid>...</thread>
+//
+// and subsequent samples on that thread will simply have
+//
+//     <thread ref="11" />
+//
+// This means that most elements have to have their pertinent data cached by id,
+// including any data store in child elements (which themselves also have to
+// be cached by id, like the <tid> in the example above).
+//
+// This importer reads the XML data using a streaming XML parser, which means
+// it has to maintain some parsing state (such as the current stack of tags, or
+// the current element for which we are reading data).
+class InstrumentsXmlTokenizer::Impl {
+ public:
+  explicit Impl(TraceProcessorContext* context)
+      : context_(context), data_(RowDataTracker::GetOrCreate(context_)) {
+    parser_ = XML_ParserCreate(nullptr);
+    XML_SetElementHandler(parser_, ElementStart, ElementEnd);
+    XML_SetCharacterDataHandler(parser_, CharacterData);
+    XML_SetUserData(parser_, this);
+
+    const char* subsystem = "dev.perfetto.instruments_clock";
+    clock_ = static_cast<ClockTracker::ClockId>(
+        PerfettoFnv1a(subsystem, strlen(subsystem)) | 0x80000000);
+
+    // Use the above clock if we can, in case there is no other trace and
+    // no clock sync events.
+    context_->clock_tracker->SetTraceTimeClock(clock_);
+  }
+  ~Impl() { XML_ParserFree(parser_); }
+
+  base::Status Parse(TraceBlobView view) {
+    if (!XML_Parse(parser_, reinterpret_cast<const char*>(view.data()),
+                   static_cast<int>(view.length()), false)) {
+      return base::ErrStatus("XML parse error at line %lu: %s\n",
+                             XML_GetCurrentLineNumber(parser_),
+                             XML_ErrorString(XML_GetErrorCode(parser_)));
+    }
+    return base::OkStatus();
+  }
+
+  base::Status End() {
+    if (!XML_Parse(parser_, nullptr, 0, true)) {
+      return base::ErrStatus("XML parse error at end, line %lu: %s\n",
+                             XML_GetCurrentLineNumber(parser_),
+                             XML_ErrorString(XML_GetErrorCode(parser_)));
+    }
+    return base::OkStatus();
+  }
+
+ private:
+  static void ElementStart(void* data, const char* el, const char** attr) {
+    reinterpret_cast<Impl*>(data)->ElementStart(el, attr);
+  }
+  static void ElementEnd(void* data, const char* el) {
+    reinterpret_cast<Impl*>(data)->ElementEnd(el);
+  }
+  static void CharacterData(void* data, const char* chars, int len) {
+    reinterpret_cast<Impl*>(data)->CharacterData(chars, len);
+  }
+
+  void ElementStart(const char* el, const char** attrs) {
+    tag_stack_.emplace_back(el);
+    std::string_view tag_name = tag_stack_.back();
+
+    if (tag_name == "row") {
+      current_row_ = Row{};
+    } else if (tag_name == "thread") {
+      MaybeCachedRef<ThreadId> thread_lookup =
+          GetOrInsertByRef(attrs, thread_ref_to_thread_);
+      if (thread_lookup.is_new) {
+        auto new_thread = data_.NewThread();
+        thread_lookup.ref = new_thread.id;
+
+        for (int i = 2; attrs[i]; i += 2) {
+          std::string key(attrs[i]);
+          if (key == "fmt") {
+            new_thread.ptr->fmt = InternString(attrs[i + 1]);
+          }
+        }
+
+        current_new_thread_ = new_thread.id;
+      }
+      current_row_.thread = thread_lookup.ref;
+    } else if (tag_name == "process") {
+      MaybeCachedRef<ProcessId> process_lookup =
+          GetOrInsertByRef(attrs, process_ref_to_process_);
+      if (process_lookup.is_new) {
+        // Can only be processing a new process when processing a new thread.
+        PERFETTO_DCHECK(current_new_thread_ != kNullId);
+        auto new_process = data_.NewProcess();
+        process_lookup.ref = new_process.id;
+
+        for (int i = 2; attrs[i]; i += 2) {
+          std::string key(attrs[i]);
+          if (key == "fmt") {
+            new_process.ptr->fmt = InternString(attrs[i + 1]);
+          }
+        }
+
+        current_new_process_ = new_process.id;
+      }
+      if (current_new_thread_) {
+        data_.GetThread(current_new_thread_)->process = process_lookup.ref;
+      }
+    } else if (tag_name == "core") {
+      MaybeCachedRef<uint32_t> core_id_lookup =
+          GetOrInsertByRef(attrs, core_ref_to_core_);
+      if (core_id_lookup.is_new) {
+        current_new_core_id_ = &core_id_lookup.ref;
+      } else {
+        current_row_.core_id = core_id_lookup.ref;
+      }
+    } else if (tag_name == "sample-time" || tag_name == "event-time") {
+      // Share time lookup logic between sample times and event times, including
+      // updating the current row's sample time for both.
+      MaybeCachedRef<int64_t> time_lookup =
+          GetOrInsertByRef(attrs, sample_time_ref_to_time_);
+      if (time_lookup.is_new) {
+        current_time_ref_ = &time_lookup.ref;
+      } else {
+        current_row_.timestamp_ = time_lookup.ref;
+      }
+    } else if (tag_name == "subsystem") {
+      MaybeCachedRef<std::string> subsystem_lookup =
+          GetOrInsertByRef(attrs, subsystem_ref_to_subsystem_);
+      current_subsystem_ref_ = &subsystem_lookup.ref;
+    } else if (tag_name == "uint64") {
+      // The only uint64 we care about is the one for the clock sync, which is
+      // expected to contain exactly one uint64 value -- we'll
+      // map all uint64 to a single value and check against the subsystem
+      // when the row is closed.
+      MaybeCachedRef<uint64_t> uint64_lookup =
+          GetOrInsertByRef(attrs, os_log_metadata_or_uint64_ref_to_uint64_);
+      if (uint64_lookup.is_new) {
+        current_uint64_ref_ = &uint64_lookup.ref;
+      } else {
+        if (current_os_log_metadata_uint64_ref_) {
+          // Update the os-log-metadata's uint64 value with this uint64 value.
+          *current_os_log_metadata_uint64_ref_ = uint64_lookup.ref;
+        }
+      }
+    } else if (tag_name == "os-log-metadata") {
+      // The only os-log-metadata we care about is the one with the single
+      // uint64 clock sync value, so also map this to uint64 values with its own
+      // id.
+      MaybeCachedRef<uint64_t> uint64_lookup =
+          GetOrInsertByRef(attrs, os_log_metadata_or_uint64_ref_to_uint64_);
+      current_os_log_metadata_uint64_ref_ = &uint64_lookup.ref;
+    } else if (tag_name == "backtrace") {
+      MaybeCachedRef<BacktraceId> backtrace_lookup =
+          GetOrInsertByRef(attrs, backtrace_ref_to_backtrace_);
+      if (backtrace_lookup.is_new) {
+        backtrace_lookup.ref = data_.NewBacktrace().id;
+      }
+      current_row_.backtrace = backtrace_lookup.ref;
+    } else if (tag_name == "frame") {
+      MaybeCachedRef<BacktraceFrameId> frame_lookup =
+          GetOrInsertByRef(attrs, frame_ref_to_frame_);
+      if (frame_lookup.is_new) {
+        IdPtr<Frame> new_frame = data_.NewFrame();
+        frame_lookup.ref = new_frame.id;
+        for (int i = 2; attrs[i]; i += 2) {
+          std::string key(attrs[i]);
+          if (key == "addr") {
+            new_frame.ptr->addr = strtoll(attrs[i + 1], nullptr, 16);
+          }
+        }
+        current_new_frame_ = new_frame.id;
+      }
+      data_.GetBacktrace(current_row_.backtrace)
+          ->frames.push_back(frame_lookup.ref);
+    } else if (tag_name == "binary") {
+      // Can only be processing a binary when processing a new frame.
+      PERFETTO_DCHECK(current_new_frame_ != kNullId);
+
+      MaybeCachedRef<BinaryId> binary_lookup =
+          GetOrInsertByRef(attrs, binary_ref_to_binary_);
+      if (binary_lookup.is_new) {
+        auto new_binary = data_.NewBinary();
+        binary_lookup.ref = new_binary.id;
+        for (int i = 2; attrs[i]; i += 2) {
+          std::string key(attrs[i]);
+          if (key == "path") {
+            new_binary.ptr->path = std::string(attrs[i + 1]);
+          } else if (key == "UUID") {
+            new_binary.ptr->uuid =
+                BuildId::FromHex(base::StringView(attrs[i + 1]));
+          } else if (key == "load-addr") {
+            new_binary.ptr->load_addr = strtoll(attrs[i + 1], nullptr, 16);
+          }
+        }
+        new_binary.ptr->max_addr = new_binary.ptr->load_addr;
+      }
+      PERFETTO_DCHECK(data_.GetFrame(current_new_frame_)->binary == kNullId);
+      data_.GetFrame(current_new_frame_)->binary = binary_lookup.ref;
+    }
+  }
+
+  void ElementEnd(const char* el) {
+    PERFETTO_DCHECK(el == tag_stack_.back());
+    std::string tag_name = std::move(tag_stack_.back());
+    tag_stack_.pop_back();
+
+    if (tag_name == "row") {
+      if (current_row_.backtrace) {
+        // Rows with backtraces are assumed to be time samples.
+        base::StatusOr<int64_t> trace_ts =
+            ToTraceTimestamp(current_row_.timestamp_);
+        if (!trace_ts.ok()) {
+          PERFETTO_DLOG("Skipping timestamp %" PRId64 ", no clock snapshot yet",
+                        current_row_.timestamp_);
+        } else {
+          context_->sorter->PushInstrumentsRow(*trace_ts,
+                                               std::move(current_row_));
+        }
+      } else if (current_subsystem_ref_ != nullptr) {
+        // Rows without backtraces are assumed to be signpost events -- filter
+        // these for `dev.perfetto.clock_sync` events.
+        if (*current_subsystem_ref_ == "dev.perfetto.clock_sync") {
+          PERFETTO_DCHECK(current_os_log_metadata_uint64_ref_ != nullptr);
+          uint64_t clock_sync_timestamp = *current_os_log_metadata_uint64_ref_;
+          if (latest_clock_sync_timestamp_ > clock_sync_timestamp) {
+            PERFETTO_DLOG("Skipping timestamp %" PRId64
+                          ", non-monotonic sync deteced",
+                          current_row_.timestamp_);
+          } else {
+            latest_clock_sync_timestamp_ = clock_sync_timestamp;
+            auto status = context_->clock_tracker->AddSnapshot(
+                {{clock_, current_row_.timestamp_},
+                 {protos::pbzero::ClockSnapshot::Clock::BOOTTIME,
+                  static_cast<int64_t>(latest_clock_sync_timestamp_)}});
+            if (!status.ok()) {
+              PERFETTO_FATAL("Error adding clock snapshot: %s",
+                             status.status().c_message());
+            }
+          }
+        }
+        current_subsystem_ref_ = nullptr;
+        current_os_log_metadata_uint64_ref_ = nullptr;
+        current_uint64_ref_ = nullptr;
+      }
+    } else if (current_new_frame_ != kNullId && tag_name == "frame") {
+      Frame* frame = data_.GetFrame(current_new_frame_);
+      if (frame->binary) {
+        Binary* binary = data_.GetBinary(frame->binary);
+        // We don't know what the binary's mapping end is, but we know that the
+        // current frame is inside of it, so use that.
+        PERFETTO_DCHECK(frame->addr > binary->load_addr);
+        if (frame->addr > binary->max_addr) {
+          binary->max_addr = frame->addr;
+        }
+      }
+      current_new_frame_ = kNullId;
+    } else if (current_new_thread_ != kNullId && tag_name == "thread") {
+      current_new_thread_ = kNullId;
+    } else if (current_new_process_ != kNullId && tag_name == "process") {
+      current_new_process_ = kNullId;
+    } else if (current_new_core_id_ != nullptr && tag_name == "core") {
+      current_new_core_id_ = nullptr;
+    }
+  }
+
+  void CharacterData(const char* chars, int len) {
+    std::string_view tag_name = tag_stack_.back();
+    if (current_time_ref_ != nullptr &&
+        (tag_name == "sample-time" || tag_name == "event-time")) {
+      std::string s = MakeTrimmed(chars, len);
+      current_row_.timestamp_ = *current_time_ref_ = stoll(s);
+      current_time_ref_ = nullptr;
+    } else if (current_new_thread_ != kNullId && tag_name == "tid") {
+      std::string s = MakeTrimmed(chars, len);
+      data_.GetThread(current_new_thread_)->tid = stoi(s);
+    } else if (current_new_process_ != kNullId && tag_name == "pid") {
+      std::string s = MakeTrimmed(chars, len);
+      data_.GetProcess(current_new_process_)->pid = stoi(s);
+    } else if (current_new_core_id_ != nullptr && tag_name == "core") {
+      std::string s = MakeTrimmed(chars, len);
+      *current_new_core_id_ = static_cast<uint32_t>(stoul(s));
+    } else if (current_subsystem_ref_ != nullptr && tag_name == "subsystem") {
+      std::string s = MakeTrimmed(chars, len);
+      *current_subsystem_ref_ = s;
+    } else if (current_uint64_ref_ != nullptr &&
+               current_os_log_metadata_uint64_ref_ != nullptr &&
+               tag_name == "uint64") {
+      std::string s = MakeTrimmed(chars, len);
+      *current_os_log_metadata_uint64_ref_ = *current_uint64_ref_ = stoull(s);
+    }
+  }
+
+  base::StatusOr<int64_t> ToTraceTimestamp(int64_t time) {
+    base::StatusOr<int64_t> trace_ts =
+        context_->clock_tracker->ToTraceTime(clock_, time);
+
+    if (PERFETTO_LIKELY(trace_ts.ok())) {
+      latest_timestamp_ = std::max(latest_timestamp_, *trace_ts);
+    }
+
+    return trace_ts;
+  }
+
+  StringId InternString(base::StringView string_view) {
+    return context_->storage->InternString(string_view);
+  }
+  StringId InternString(const char* string) {
+    return InternString(base::StringView(string));
+  }
+  StringId InternString(const char* data, size_t len) {
+    return InternString(base::StringView(data, len));
+  }
+
+  template <typename Value>
+  struct MaybeCachedRef {
+    Value& ref;
+    bool is_new;
+  };
+  // Implement the element caching mechanism. Either insert an element by its
+  // id attribute into the given map, or look up the element in the cache by its
+  // ref attribute. The returned value is a reference into the map, to allow
+  // in-place modification.
+  template <typename Value>
+  MaybeCachedRef<Value> GetOrInsertByRef(const char** attrs,
+                                         std::map<unsigned long, Value>& map) {
+    PERFETTO_DCHECK(attrs[0] != nullptr);
+    PERFETTO_DCHECK(attrs[1] != nullptr);
+    const char* key = attrs[0];
+    // The id or ref attribute has to be the first attribute on the element.
+    PERFETTO_DCHECK(strcmp(key, "ref") == 0 || strcmp(key, "id") == 0);
+    unsigned long id = strtoul(attrs[1], nullptr, 10);
+    // If the first attribute key is `id`, then this is a new entry in the
+    // cache -- otherwise, for lookup by ref, it should already exist.
+    bool is_new = strcmp(key, "id") == 0;
+    PERFETTO_DCHECK(is_new == (map.find(id) == map.end()));
+    return {map[id], is_new};
+  }
+
+  TraceProcessorContext* context_;
+  RowDataTracker& data_;
+
+  XML_Parser parser_;
+  std::vector<std::string> tag_stack_;
+  int64_t latest_timestamp_;
+
+  // These maps store the cached element data. These currently have to be
+  // std::map, because they require pointer stability under insertion,
+  // as the various `current_foo_` pointers below point directly into the map
+  // data.
+  //
+  // TODO(leszeks): Relax this pointer stability requirement, and use
+  // base::FlatHashMap.
+  // TODO(leszeks): Consider merging these into a single map from ID to
+  // a variant (or similar).
+  std::map<unsigned long, ThreadId> thread_ref_to_thread_;
+  std::map<unsigned long, ProcessId> process_ref_to_process_;
+  std::map<unsigned long, uint32_t> core_ref_to_core_;
+  std::map<unsigned long, int64_t> sample_time_ref_to_time_;
+  std::map<unsigned long, BinaryId> binary_ref_to_binary_;
+  std::map<unsigned long, BacktraceFrameId> frame_ref_to_frame_;
+  std::map<unsigned long, BacktraceId> backtrace_ref_to_backtrace_;
+  std::map<unsigned long, std::string> subsystem_ref_to_subsystem_;
+  std::map<unsigned long, uint64_t> os_log_metadata_or_uint64_ref_to_uint64_;
+
+  Row current_row_;
+  int64_t* current_time_ref_ = nullptr;
+  ThreadId current_new_thread_ = kNullId;
+  ProcessId current_new_process_ = kNullId;
+  uint32_t* current_new_core_id_ = nullptr;
+  BacktraceFrameId current_new_frame_ = kNullId;
+
+  ClockTracker::ClockId clock_;
+  std::string* current_subsystem_ref_ = nullptr;
+  uint64_t* current_os_log_metadata_uint64_ref_ = nullptr;
+  uint64_t* current_uint64_ref_ = nullptr;
+  uint64_t latest_clock_sync_timestamp_ = 0;
+};
+
+InstrumentsXmlTokenizer::InstrumentsXmlTokenizer(TraceProcessorContext* context)
+    : impl_(new Impl(context)) {}
+InstrumentsXmlTokenizer::~InstrumentsXmlTokenizer() {
+  delete impl_;
+}
+
+base::Status InstrumentsXmlTokenizer::Parse(TraceBlobView view) {
+  return impl_->Parse(std::move(view));
+}
+
+[[nodiscard]] base::Status InstrumentsXmlTokenizer::NotifyEndOfFile() {
+  return impl_->End();
+}
+
+}  // namespace perfetto::trace_processor::instruments_importer
diff --git a/src/trace_processor/importers/instruments/instruments_xml_tokenizer.h b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.h
new file mode 100644
index 0000000..be044dd
--- /dev/null
+++ b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_INSTRUMENTS_XML_TOKENIZER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_INSTRUMENTS_XML_TOKENIZER_H_
+
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+class InstrumentsXmlTokenizer : public ChunkedTraceReader {
+ public:
+  explicit InstrumentsXmlTokenizer(TraceProcessorContext*);
+  ~InstrumentsXmlTokenizer() override;
+
+  base::Status Parse(TraceBlobView) override;
+
+  [[nodiscard]] base::Status NotifyEndOfFile() override;
+
+ private:
+  class Impl;
+
+  class Impl* impl_;
+};
+
+}  // namespace perfetto::trace_processor::instruments_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_INSTRUMENTS_XML_TOKENIZER_H_
diff --git a/src/trace_processor/importers/instruments/row.h b/src/trace_processor/importers/instruments/row.h
new file mode 100644
index 0000000..531a36d
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_H_
+
+#include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/util/build_id.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+// TODO(leszeks): Would be nice if these were strong type aliases, to be
+// type safe.
+using ThreadId = uint32_t;
+using ProcessId = uint32_t;
+using BacktraceId = uint32_t;
+using BacktraceFrameId = uint32_t;
+using BinaryId = uint32_t;
+
+constexpr uint32_t kNullId = 0u;
+
+struct Binary {
+  std::string path;
+  BuildId uuid = BuildId::FromRaw(std::string(""));
+  long long load_addr = 0;
+  long long max_addr = 0;
+};
+
+struct Frame {
+  long long addr = 0;
+  BinaryId binary = kNullId;
+};
+
+struct Process {
+  int pid = 0;
+  StringPool::Id fmt = StringPool::Id::Null();
+};
+
+struct Thread {
+  int tid = 0;
+  StringPool::Id fmt = StringPool::Id::Null();
+  ProcessId process = kNullId;
+};
+
+struct Backtrace {
+  std::vector<BacktraceFrameId> frames;
+};
+
+struct alignas(8) Row {
+  int64_t timestamp_;
+  uint32_t core_id;
+  ThreadId thread = kNullId;
+  BacktraceId backtrace = kNullId;
+};
+
+}  // namespace perfetto::trace_processor::instruments_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_H_
diff --git a/src/trace_processor/importers/instruments/row_data_tracker.cc b/src/trace_processor/importers/instruments/row_data_tracker.cc
new file mode 100644
index 0000000..ff17b7d
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row_data_tracker.cc
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/instruments/row_data_tracker.h"
+
+#include "perfetto/base/status.h"
+
+#if !PERFETTO_BUILDFLAG(PERFETTO_TP_INSTRUMENTS)
+#error \
+    "This file should not be built when enable_perfetto_trace_processor_mac_instruments=false"
+#endif
+
+namespace perfetto::trace_processor::instruments_importer {
+
+RowDataTracker::RowDataTracker() {}
+RowDataTracker::~RowDataTracker() = default;
+
+IdPtr<Thread> RowDataTracker::NewThread() {
+  ThreadId id = static_cast<ThreadId>(threads_.size());
+  Thread* ptr = &threads_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Thread* RowDataTracker::GetThread(ThreadId id) {
+  PERFETTO_DCHECK(id != kNullId);
+  return &threads_[id - 1];
+}
+
+IdPtr<Process> RowDataTracker::NewProcess() {
+  ProcessId id = static_cast<ProcessId>(processes_.size());
+  Process* ptr = &processes_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Process* RowDataTracker::GetProcess(ProcessId id) {
+  PERFETTO_DCHECK(id != kNullId);
+  return &processes_[id - 1];
+}
+
+IdPtr<Frame> RowDataTracker::NewFrame() {
+  BacktraceFrameId id = static_cast<BacktraceFrameId>(frames_.size());
+  Frame* ptr = &frames_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Frame* RowDataTracker::GetFrame(BacktraceFrameId id) {
+  PERFETTO_DCHECK(id != kNullId);
+  return &frames_[id - 1];
+}
+
+IdPtr<Backtrace> RowDataTracker::NewBacktrace() {
+  BacktraceId id = static_cast<BacktraceId>(backtraces_.size());
+  Backtrace* ptr = &backtraces_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Backtrace* RowDataTracker::GetBacktrace(BacktraceId id) {
+  PERFETTO_DCHECK(id != kNullId);
+  return &backtraces_[id - 1];
+}
+
+IdPtr<Binary> RowDataTracker::NewBinary() {
+  BinaryId id = static_cast<BinaryId>(binaries_.size());
+  Binary* ptr = &binaries_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Binary* RowDataTracker::GetBinary(BinaryId id) {
+  // Frames are allowed to have null binaries.
+  if (id == kNullId)
+    return nullptr;
+  return &binaries_[id - 1];
+}
+
+}  // namespace perfetto::trace_processor::instruments_importer
diff --git a/src/trace_processor/importers/instruments/row_data_tracker.h b/src/trace_processor/importers/instruments/row_data_tracker.h
new file mode 100644
index 0000000..247326e
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row_data_tracker.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_DATA_TRACKER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_DATA_TRACKER_H_
+
+#include "src/trace_processor/importers/instruments/row.h"
+#include "src/trace_processor/types/destructible.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+template <typename T>
+struct IdPtr {
+  uint32_t id;
+  T* ptr;
+};
+
+// Keeps track of row data.
+class RowDataTracker : public Destructible {
+ public:
+  static RowDataTracker& GetOrCreate(TraceProcessorContext* context) {
+    if (!context->instruments_row_data_tracker) {
+      context->instruments_row_data_tracker.reset(new RowDataTracker());
+    }
+    return static_cast<RowDataTracker&>(*context->instruments_row_data_tracker);
+  }
+  ~RowDataTracker() override;
+
+  IdPtr<Thread> NewThread();
+  Thread* GetThread(ThreadId id);
+
+  IdPtr<Process> NewProcess();
+  Process* GetProcess(ProcessId id);
+
+  IdPtr<Frame> NewFrame();
+  Frame* GetFrame(BacktraceFrameId id);
+
+  IdPtr<Backtrace> NewBacktrace();
+  Backtrace* GetBacktrace(BacktraceId id);
+
+  IdPtr<Binary> NewBinary();
+  Binary* GetBinary(BinaryId id);
+
+ private:
+  explicit RowDataTracker();
+
+  std::vector<Thread> threads_;
+  std::vector<Process> processes_;
+  std::vector<Frame> frames_;
+  std::vector<Backtrace> backtraces_;
+  std::vector<Binary> binaries_;
+};
+
+}  // namespace perfetto::trace_processor::instruments_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_DATA_TRACKER_H_
diff --git a/src/trace_processor/importers/instruments/row_parser.cc b/src/trace_processor/importers/instruments/row_parser.cc
new file mode 100644
index 0000000..39d7a92
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row_parser.cc
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/instruments/row_parser.h"
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/string_view.h"
+#include "src/trace_processor/importers/common/mapping_tracker.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/importers/instruments/row.h"
+#include "src/trace_processor/importers/instruments/row_data_tracker.h"
+
+#if !PERFETTO_BUILDFLAG(PERFETTO_TP_INSTRUMENTS)
+#error \
+    "This file should not be built when enable_perfetto_trace_processor_mac_instruments=false"
+#endif
+
+namespace perfetto::trace_processor::instruments_importer {
+
+RowParser::RowParser(TraceProcessorContext* context)
+    : context_(context), data_(RowDataTracker::GetOrCreate(context)) {}
+
+void RowParser::ParseInstrumentsRow(int64_t ts, instruments_importer::Row row) {
+  if (!row.backtrace) {
+    return;
+  }
+
+  Thread* thread = data_.GetThread(row.thread);
+  Process* process = data_.GetProcess(thread->process);
+  uint32_t tid = static_cast<uint32_t>(thread->tid);
+  uint32_t pid = static_cast<uint32_t>(process->pid);
+
+  UniqueTid utid = context_->process_tracker->UpdateThread(tid, pid);
+  UniquePid upid = context_->process_tracker->GetOrCreateProcess(pid);
+
+  // TODO(leszeks): Avoid setting thread/process name if we've already seen this
+  // Thread* / Process*.
+  context_->process_tracker->UpdateThreadNameByUtid(utid, thread->fmt,
+                                                    ThreadNamePriority::kOther);
+  context_->process_tracker->SetProcessNameIfUnset(upid, process->fmt);
+
+  auto& stack_profile_tracker = *context_->stack_profile_tracker;
+
+  Backtrace* backtrace = data_.GetBacktrace(row.backtrace);
+  std::optional<CallsiteId> parent;
+  uint32_t depth = 0;
+  auto leaf = backtrace->frames.rend() - 1;
+  for (auto it = backtrace->frames.rbegin(); it != backtrace->frames.rend();
+       ++it) {
+    Frame* frame = data_.GetFrame(*it);
+    Binary* binary = data_.GetBinary(frame->binary);
+
+    uint64_t rel_pc = static_cast<uint64_t>(frame->addr);
+    if (frame->binary) {
+      rel_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;
+    }
+
+    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 {
+          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});
+        }
+      }
+      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;
+
+    parent = stack_profile_tracker.InternCallsite(parent, frame_id, depth);
+    depth++;
+  }
+
+  context_->storage->mutable_instruments_sample_table()->Insert(
+      {ts, utid, row.core_id, parent});
+}
+
+DummyMemoryMapping* RowParser::GetDummyMapping(UniquePid upid) {
+  if (auto it = dummy_mappings_.Find(upid); it) {
+    return *it;
+  }
+
+  DummyMemoryMapping* mapping =
+      &context_->mapping_tracker->CreateDummyMapping("");
+  dummy_mappings_.Insert(upid, mapping);
+  return mapping;
+}
+
+}  // namespace perfetto::trace_processor::instruments_importer
diff --git a/src/trace_processor/importers/instruments/row_parser.h b/src/trace_processor/importers/instruments/row_parser.h
new file mode 100644
index 0000000..ead208c
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row_parser.h
@@ -0,0 +1,54 @@
+/*
+ * 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_INSTRUMENTS_ROW_PARSER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_PARSER_H_
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "src/trace_processor/importers/common/trace_parser.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
+#include "src/trace_processor/importers/instruments/row.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+class RowDataTracker;
+
+class RowParser : public InstrumentsRowParser {
+ public:
+  explicit RowParser(TraceProcessorContext*);
+  ~RowParser() override = default;
+
+  void ParseInstrumentsRow(int64_t, instruments_importer::Row) override;
+
+ private:
+  DummyMemoryMapping* GetDummyMapping(UniquePid upid);
+
+  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_;
+  base::FlatHashMap<BinaryId, VirtualMemoryMapping*> binary_to_mapping_;
+  base::FlatHashMap<UniquePid, DummyMemoryMapping*> dummy_mappings_;
+};
+
+}  // namespace perfetto::trace_processor::instruments_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_PARSER_H_
diff --git a/src/trace_processor/importers/json/BUILD.gn b/src/trace_processor/importers/json/BUILD.gn
index ec0813d..c7921d2 100644
--- a/src/trace_processor/importers/json/BUILD.gn
+++ b/src/trace_processor/importers/json/BUILD.gn
@@ -22,30 +22,8 @@
   ]
   deps = [
     "../../../../gn:default_deps",
-    "../common",
-  ]
-  if (enable_perfetto_trace_processor_json) {
-    public_deps = [ "../../../../gn:jsoncpp" ]
-  }
-}
-
-source_set("full") {
-  sources = [
-    "json_trace_parser_impl.cc",
-    "json_trace_parser_impl.h",
-    "json_trace_tokenizer.cc",
-    "json_trace_tokenizer.h",
-  ]
-  deps = [
-    ":minimal",
-    "../../../../gn:default_deps",
-    "../../sorter",
     "../../storage",
-    "../../tables",
-    "../../types",
     "../common",
-    "../systrace:full",
-    "../systrace:systrace_line",
   ]
   if (enable_perfetto_trace_processor_json) {
     public_deps = [ "../../../../gn:jsoncpp" ]
@@ -53,6 +31,30 @@
 }
 
 if (enable_perfetto_trace_processor_json) {
+  source_set("json") {
+    sources = [
+      "json_trace_parser_impl.cc",
+      "json_trace_parser_impl.h",
+      "json_trace_tokenizer.cc",
+      "json_trace_tokenizer.h",
+    ]
+    deps = [
+      ":minimal",
+      "../../../../gn:default_deps",
+      "../../sorter",
+      "../../storage",
+      "../../tables",
+      "../../types",
+      "../common",
+      "../common:parser_types",
+      "../systrace:full",
+      "../systrace:systrace_line",
+    ]
+    if (enable_perfetto_trace_processor_json) {
+      public_deps = [ "../../../../gn:jsoncpp" ]
+    }
+  }
+
   perfetto_unittest_source_set("unittests") {
     testonly = true
     sources = [
@@ -60,7 +62,7 @@
       "json_utils_unittest.cc",
     ]
     deps = [
-      ":full",
+      ":json",
       ":minimal",
       "../../../../gn:default_deps",
       "../../../../gn:gtest_and_gmock",
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 37e56c8..1890498 100644
--- a/src/trace_processor/importers/json/json_trace_parser_impl.cc
+++ b/src/trace_processor/importers/json/json_trace_parser_impl.cc
@@ -24,11 +24,14 @@
 
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/hash.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/string_view.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/legacy_v8_cpu_profile_tracker.h"
+#include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
@@ -39,10 +42,8 @@
 #include "src/trace_processor/tables/slice_tables_py.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 namespace {
 
 std::optional<uint64_t> MaybeExtractFlowIdentifier(const Json::Value& value,
@@ -60,7 +61,6 @@
 }
 
 }  // namespace
-#endif  // PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 
 JsonTraceParserImpl::JsonTraceParserImpl(TraceProcessorContext* context)
     : context_(context), systrace_line_parser_(context) {}
@@ -75,7 +75,6 @@
                                           std::string string_value) {
   PERFETTO_DCHECK(json::IsJsonSupported());
 
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
   auto opt_value = json::ParseJsonString(base::StringView(string_value));
   if (!opt_value) {
     context_->storage->IncrementStats(stats::json_parser_failure);
@@ -128,7 +127,7 @@
 
   std::string id = value.isMember("id") ? value["id"].asString() : "";
 
-  base::StringView cat = value.isMember("cat")
+  base::StringView cat = value.isMember("cat") && value["cat"].isString()
                              ? base::StringView(value["cat"].asCString())
                              : base::StringView();
   StringId cat_id = storage->InternString(cat);
@@ -205,14 +204,14 @@
         const std::string& real_id = id.empty() ? global : id;
         int64_t cookie = static_cast<int64_t>(
             base::Hasher::Combine(cat_id.raw_id(), real_id));
-        track_id = context_->track_tracker->InternLegacyChromeAsyncTrack(
+        track_id = context_->track_tracker->LegacyInternLegacyChromeAsyncTrack(
             name_id, upid, cookie, false /* source_id_is_process_scoped */,
             kNullStringId /* source_scope */);
       } else {
         PERFETTO_DCHECK(!local.empty());
         int64_t cookie =
             static_cast<int64_t>(base::Hasher::Combine(cat_id.raw_id(), local));
-        track_id = context_->track_tracker->InternLegacyChromeAsyncTrack(
+        track_id = context_->track_tracker->LegacyInternLegacyChromeAsyncTrack(
             name_id, upid, cookie, true /* source_id_is_process_scoped */,
             kNullStringId /* source_scope */);
       }
@@ -289,17 +288,24 @@
 
       TrackId track_id;
       if (scope == "g") {
-        track_id = context_->track_tracker
-                       ->GetOrCreateLegacyChromeGlobalInstantTrack();
+        track_id = context_->track_tracker->InternGlobalTrack(
+            tracks::legacy_chrome_global_instants, TrackTracker::AutoName(),
+            [this](ArgsTracker::BoundInserter& inserter) {
+              inserter.AddArg(
+                  context_->storage->InternString("source"),
+                  Variadic::String(context_->storage->InternString("chrome")));
+            });
       } else if (scope == "p") {
         if (!opt_pid) {
           context_->storage->IncrementStats(stats::json_parser_failure);
           break;
         }
         UniquePid upid = context_->process_tracker->GetOrCreateProcess(pid);
-        track_id =
-            context_->track_tracker->InternLegacyChromeProcessInstantTrack(
-                upid);
+        track_id = context_->track_tracker->InternProcessTrack(
+            tracks::chrome_process_instant, upid);
+        context_->args_tracker->AddArgsTo(track_id).AddArg(
+            context_->storage->InternString("source"),
+            Variadic::String(context_->storage->InternString("chrome")));
       } else if (scope == "t" || scope.data() == nullptr) {
         if (!opt_tid) {
           context_->storage->IncrementStats(stats::json_parser_failure);
@@ -381,18 +387,11 @@
       }
     }
   }
-#else
-  perfetto::base::ignore_result(timestamp);
-  perfetto::base::ignore_result(context_);
-  perfetto::base::ignore_result(string_value);
-  PERFETTO_ELOG("Cannot parse JSON trace due to missing JSON support");
-#endif  // PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 }
 
 void JsonTraceParserImpl::MaybeAddFlow(TrackId track_id,
                                        const Json::Value& event) {
   PERFETTO_DCHECK(json::IsJsonSupported());
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
   auto opt_bind_id = MaybeExtractFlowIdentifier(event, /* version2 = */ true);
   if (opt_bind_id) {
     FlowTracker* flow_tracker = context_->flow_tracker.get();
@@ -410,11 +409,18 @@
       context_->storage->IncrementStats(stats::flow_without_direction);
     }
   }
-#else
-  perfetto::base::ignore_result(track_id);
-  perfetto::base::ignore_result(event);
-#endif  // PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+void JsonTraceParserImpl::ParseLegacyV8ProfileEvent(
+    int64_t ts,
+    LegacyV8CpuProfileEvent event) {
+  base::Status status = context_->legacy_v8_cpu_profile_tracker->AddSample(
+      ts, event.session_id, event.pid, event.tid, event.callsite_id);
+  if (!status.ok()) {
+    context_->storage->IncrementStats(
+        stats::legacy_v8_cpu_profile_invalid_sample);
+  }
+  context_->args_tracker->Flush();
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/json/json_trace_parser_impl.h b/src/trace_processor/importers/json/json_trace_parser_impl.h
index 4c0e269..8b89897 100644
--- a/src/trace_processor/importers/json/json_trace_parser_impl.h
+++ b/src/trace_processor/importers/json/json_trace_parser_impl.h
@@ -17,21 +17,19 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACE_PARSER_IMPL_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACE_PARSER_IMPL_H_
 
-#include <stdint.h>
-
-#include <memory>
-#include <tuple>
+#include <cstdint>
+#include <string>
 
 #include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/systrace/systrace_line.h"
 #include "src/trace_processor/importers/systrace/systrace_line_parser.h"
+#include "src/trace_processor/storage/trace_storage.h"
 
 namespace Json {
 class Value;
 }
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class TraceProcessorContext;
 
@@ -43,8 +41,9 @@
   ~JsonTraceParserImpl() override;
 
   // TraceParser implementation.
-  void ParseJsonPacket(int64_t timestamp, std::string string_value) override;
-  void ParseSystraceLine(int64_t timestamp, SystraceLine line) override;
+  void ParseJsonPacket(int64_t, std::string) override;
+  void ParseSystraceLine(int64_t, SystraceLine) override;
+  void ParseLegacyV8ProfileEvent(int64_t, LegacyV8CpuProfileEvent) override;
 
  private:
   TraceProcessorContext* const context_;
@@ -53,7 +52,6 @@
   void MaybeAddFlow(TrackId track_id, const Json::Value& event);
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACE_PARSER_IMPL_H_
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer.cc b/src/trace_processor/importers/json/json_trace_tokenizer.cc
index 8345a9e..8ec36af 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer.cc
+++ b/src/trace_processor/importers/json/json_trace_tokenizer.cc
@@ -16,23 +16,32 @@
 
 #include "src/trace_processor/importers/json/json_trace_tokenizer.h"
 
+#include <cctype>
+#include <cstddef>
+#include <cstdint>
 #include <memory>
+#include <optional>
+#include <string>
 
-#include "perfetto/base/build_config.h"
+#include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/string_utils.h"
-
+#include "perfetto/ext/base/string_view.h"
+#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/json/json_utils.h"
-#include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/sorter/trace_sorter.h"  // IWYU pragma: keep
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/util/status_macros.h"
 
-namespace perfetto {
-namespace trace_processor {
-
+namespace perfetto::trace_processor {
 namespace {
 
+std::string FormatErrorContext(const char* s, const char* e) {
+  return {s, static_cast<size_t>(e - s)};
+}
+
 base::Status AppendUnescapedCharacter(char c,
                                       bool is_escaping,
                                       std::string* key) {
@@ -64,7 +73,7 @@
         key->append("\\u");
         break;
       default:
-        return base::ErrStatus("Illegal character in JSON");
+        return base::ErrStatus("Illegal character in JSON %c", c);
     }
   } else if (c != '\\') {
     key->push_back(c);
@@ -213,7 +222,7 @@
         return ReadDictRes::kEndOfTrace;
       if (--braces > 0)
         continue;
-      size_t len = static_cast<size_t>((s + 1) - dict_begin);
+      auto len = static_cast<size_t>((s + 1) - dict_begin);
       *value = base::StringView(dict_begin, len);
       *next = s + 1;
       return ReadDictRes::kFoundDict;
@@ -324,11 +333,12 @@
         state = kInsideDict;
         continue;
       }
-      return base::ErrStatus("Unexpected character before JSON dict");
+      return base::ErrStatus("Unexpected character before JSON dict: '%c'", *s);
     }
 
-    if (state == kAfterDict)
-      return base::ErrStatus("Unexpected character after JSON dict");
+    if (state == kAfterDict) {
+      return base::ErrStatus("Unexpected character after JSON dict: '%c'", *s);
+    }
 
     PERFETTO_DCHECK(state == kInsideDict);
     PERFETTO_DCHECK(s < end);
@@ -347,18 +357,22 @@
     if (res == ReadKeyRes::kFatalError) {
       return base::ErrStatus(
           "Failure parsing JSON: encountered fatal error while parsing key for "
-          "value");
+          "value: '%s'",
+          FormatErrorContext(s, end).c_str());
     }
 
     if (res == ReadKeyRes::kNeedsMoreData) {
-      return base::ErrStatus("Failure parsing JSON: partial JSON dictionary");
+      return base::ErrStatus(
+          "Failure parsing JSON: partial JSON dictionary: '%s'",
+          FormatErrorContext(s, end).c_str());
     }
 
     PERFETTO_DCHECK(res == ReadKeyRes::kFoundKey);
 
     if (*s == '[') {
       return base::ErrStatus(
-          "Failure parsing JSON: unsupported JSON dictionary with array");
+          "Failure parsing JSON: unsupported JSON dictionary with array: '%s'",
+          FormatErrorContext(s, end).c_str());
     }
 
     std::string value_str;
@@ -369,14 +383,17 @@
           dict_res == ReadDictRes::kEndOfArray ||
           dict_res == ReadDictRes::kEndOfTrace) {
         return base::ErrStatus(
-            "Failure parsing JSON: unable to parse dictionary");
+            "Failure parsing JSON: unable to parse dictionary: '%s'",
+            FormatErrorContext(s, end).c_str());
       }
       value_str = dict_str.ToStdString();
     } else if (*s == '"') {
       auto str_res = ReadOneJsonString(s, end, &value_str, &s);
       if (str_res == ReadStringRes::kNeedsMoreData ||
           str_res == ReadStringRes::kFatalError) {
-        return base::ErrStatus("Failure parsing JSON: unable to parse string");
+        return base::ErrStatus(
+            "Failure parsing JSON: unable to parse string: '%s",
+            FormatErrorContext(s, end).c_str());
       }
     } else {
       const char* value_start = s;
@@ -396,8 +413,10 @@
     }
   }
 
-  if (state != kAfterDict)
-    return base::ErrStatus("Failure parsing JSON: malformed dictionary");
+  if (state != kAfterDict) {
+    return base::ErrStatus("Failure parsing JSON: malformed dictionary: '%s'",
+                           FormatErrorContext(start, end).c_str());
+  }
 
   *value = std::nullopt;
   return base::OkStatus();
@@ -532,27 +551,88 @@
         break;
     }
 
+    // Metadata events may omit ts. In all other cases error:
+    std::optional<std::string> opt_raw_ph;
+    RETURN_IF_ERROR(ExtractValueForJsonKey(unparsed, "ph", &opt_raw_ph));
+    if (PERFETTO_UNLIKELY(opt_raw_ph == "P")) {
+      RETURN_IF_ERROR(ParseV8SampleEvent(unparsed));
+      continue;
+    }
+
     std::optional<std::string> opt_raw_ts;
     RETURN_IF_ERROR(ExtractValueForJsonKey(unparsed, "ts", &opt_raw_ts));
     std::optional<int64_t> opt_ts =
         opt_raw_ts ? json::CoerceToTs(*opt_raw_ts) : std::nullopt;
+    std::optional<std::string> opt_raw_dur;
+    RETURN_IF_ERROR(ExtractValueForJsonKey(unparsed, "dur", &opt_raw_dur));
+    std::optional<int64_t> opt_dur =
+        opt_raw_dur ? json::CoerceToTs(*opt_raw_dur) : std::nullopt;
     int64_t ts = 0;
     if (opt_ts.has_value()) {
       ts = opt_ts.value();
     } else {
-      // Metadata events may omit ts. In all other cases error:
-      std::optional<std::string> opt_raw_ph;
-      RETURN_IF_ERROR(ExtractValueForJsonKey(unparsed, "ph", &opt_raw_ph));
       if (!opt_raw_ph || *opt_raw_ph != "M") {
         context_->storage->IncrementStats(stats::json_tokenizer_failure);
         continue;
       }
     }
-    context_->sorter->PushJsonValue(ts, unparsed.ToStdString());
+    context_->sorter->PushJsonValue(ts, unparsed.ToStdString(), opt_dur);
   }
   return SetOutAndReturn(next, out);
 }
 
+base::Status JsonTraceTokenizer::ParseV8SampleEvent(base::StringView unparsed) {
+  auto opt_evt = json::ParseJsonString(unparsed);
+  if (!opt_evt) {
+    return base::OkStatus();
+  }
+  const auto& evt = *opt_evt;
+  std::optional<uint32_t> id = base::StringToUInt32(evt["id"].asString(), 16);
+  if (!id) {
+    return base::OkStatus();
+  }
+  uint32_t pid = evt["pid"].asUInt();
+  uint32_t tid = evt["tid"].asUInt();
+  const auto& val = evt["args"]["data"];
+  if (val.isMember("startTime")) {
+    context_->legacy_v8_cpu_profile_tracker->SetStartTsForSessionAndPid(
+        *id, pid, val["startTime"].asInt64() * 1000);
+    return base::OkStatus();
+  }
+  const auto& profile = val["cpuProfile"];
+  for (const auto& n : profile["nodes"]) {
+    uint32_t node_id = n["id"].asUInt();
+    std::optional<uint32_t> parent_node_id =
+        n.isMember("parent") ? std::make_optional(n["parent"].asUInt())
+                             : std::nullopt;
+    const auto& frame = n["callFrame"];
+    base::StringView url =
+        frame.isMember("url") ? frame["url"].asCString() : base::StringView();
+    base::StringView function_name = frame["functionName"].asCString();
+    base::Status status = context_->legacy_v8_cpu_profile_tracker->AddCallsite(
+        *id, pid, node_id, parent_node_id, url, function_name);
+    if (!status.ok()) {
+      context_->storage->IncrementStats(
+          stats::legacy_v8_cpu_profile_invalid_callsite);
+      continue;
+    }
+  }
+  const auto& samples = profile["samples"];
+  const auto& deltas = val["timeDeltas"];
+  if (samples.size() != deltas.size()) {
+    return base::ErrStatus(
+        "v8 legacy profile: samples and timestamps do not have same size");
+  }
+  for (uint32_t i = 0; i < samples.size(); ++i) {
+    ASSIGN_OR_RETURN(int64_t ts,
+                     context_->legacy_v8_cpu_profile_tracker->AddDeltaAndGetTs(
+                         *id, pid, deltas[i].asInt64() * 1000));
+    context_->sorter->PushLegacyV8CpuProfileEvent(ts, *id, pid, tid,
+                                                  samples[i].asUInt());
+  }
+  return base::OkStatus();
+}
+
 base::Status JsonTraceTokenizer::HandleDictionaryKey(const char* start,
                                                      const char* end,
                                                      const char** out) {
@@ -668,10 +748,11 @@
 }
 
 base::Status JsonTraceTokenizer::NotifyEndOfFile() {
-  return position_ == TracePosition::kEof
+  return position_ == TracePosition::kEof ||
+                 (position_ == TracePosition::kInsideTraceEventsArray &&
+                  format_ == TraceFormat::kOnlyTraceEvents)
              ? base::OkStatus()
              : base::ErrStatus("JSON trace file is incomplete");
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer.h b/src/trace_processor/importers/json/json_trace_tokenizer.h
index 8672def..398b67d 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer.h
+++ b/src/trace_processor/importers/json/json_trace_tokenizer.h
@@ -17,18 +17,21 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACE_TOKENIZER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACE_TOKENIZER_H_
 
-#include <stdint.h>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <vector>
 
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
 #include "src/trace_processor/importers/systrace/systrace_line_tokenizer.h"
-#include "src/trace_processor/storage/trace_storage.h"
 
 namespace Json {
 class Value;
 }
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class TraceProcessorContext;
 
@@ -137,6 +140,8 @@
                              const char* end,
                              const char** out);
 
+  base::Status ParseV8SampleEvent(base::StringView unparsed);
+
   base::Status HandleTraceEvent(const char* start,
                                 const char* end,
                                 const char** out);
@@ -162,7 +167,6 @@
   std::vector<char> buffer_;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACE_TOKENIZER_H_
diff --git a/src/trace_processor/importers/json/json_utils.cc b/src/trace_processor/importers/json/json_utils.cc
index d2e1c18..d4157a1 100644
--- a/src/trace_processor/importers/json/json_utils.cc
+++ b/src/trace_processor/importers/json/json_utils.cc
@@ -29,14 +29,6 @@
 namespace trace_processor {
 namespace json {
 
-bool IsJsonSupported() {
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
-  return true;
-#else
-  return false;
-#endif
-}
-
 std::optional<int64_t> CoerceToTs(const Json::Value& value) {
   PERFETTO_DCHECK(IsJsonSupported());
 
diff --git a/src/trace_processor/importers/json/json_utils.h b/src/trace_processor/importers/json/json_utils.h
index b9d0762..3c1d8fa 100644
--- a/src/trace_processor/importers/json/json_utils.h
+++ b/src/trace_processor/importers/json/json_utils.h
@@ -17,12 +17,14 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_UTILS_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_UTILS_H_
 
-#include <stdint.h>
+#include <cstdint>
 #include <optional>
+#include <string>
 
+#include "perfetto/base/build_config.h"
 #include "perfetto/ext/base/string_view.h"
-
 #include "src/trace_processor/importers/common/args_tracker.h"
+#include "src/trace_processor/storage/trace_storage.h"
 
 #if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
 #include <json/value.h>
@@ -32,13 +34,17 @@
 }  // namespace Json
 #endif
 
-namespace perfetto {
-namespace trace_processor {
-namespace json {
+namespace perfetto::trace_processor::json {
 
 // Returns whether JSON related functioanlity is supported with the current
 // build flags.
-bool IsJsonSupported();
+constexpr bool IsJsonSupported() {
+#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
+  return true;
+#else
+  return false;
+#endif
+}
 
 std::optional<int64_t> CoerceToTs(const Json::Value& value);
 std::optional<int64_t> CoerceToTs(const std::string& value);
@@ -61,8 +67,6 @@
                         TraceStorage* storage,
                         ArgsTracker::BoundInserter* inserter);
 
-}  // namespace json
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor::json
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_UTILS_H_
diff --git a/src/trace_processor/importers/json/json_utils_unittest.cc b/src/trace_processor/importers/json/json_utils_unittest.cc
index 5b17cc3..3d9be27 100644
--- a/src/trace_processor/importers/json/json_utils_unittest.cc
+++ b/src/trace_processor/importers/json/json_utils_unittest.cc
@@ -16,13 +16,12 @@
 
 #include "src/trace_processor/importers/json/json_utils.h"
 
+#include <json/config.h>
 #include <json/value.h>
 
 #include "test/gtest_and_gmock.h"
 
-namespace perfetto {
-namespace trace_processor {
-namespace json {
+namespace perfetto::trace_processor::json {
 namespace {
 
 TEST(JsonTraceUtilsTest, CoerceToUint32) {
@@ -78,6 +77,4 @@
 }
 
 }  // namespace
-}  // namespace json
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor::json
diff --git a/src/trace_processor/importers/perf/BUILD.gn b/src/trace_processor/importers/perf/BUILD.gn
index 5fd5d25..c0d0bd4 100644
--- a/src/trace_processor/importers/perf/BUILD.gn
+++ b/src/trace_processor/importers/perf/BUILD.gn
@@ -30,11 +30,13 @@
     "../../../../gn:default_deps",
     "../../../../include/perfetto/ext/base:base",
     "../../../../include/perfetto/trace_processor:trace_processor",
+    "../../../../protos/perfetto/common:zero",
     "../../../../protos/perfetto/trace/profiling:zero",
     "../../storage",
     "../../tables:tables_python",
     "../../types",
     "../../util:build_id",
+    "../common:common",
     "../common:parser_types",
   ]
 }
@@ -59,8 +61,22 @@
   sources = [
     "attrs_section_reader.cc",
     "attrs_section_reader.h",
+    "aux_data_tokenizer.cc",
+    "aux_data_tokenizer.h",
+    "aux_record.cc",
+    "aux_record.h",
+    "aux_stream_manager.cc",
+    "aux_stream_manager.h",
+    "auxtrace_info_record.cc",
+    "auxtrace_info_record.h",
+    "auxtrace_record.cc",
+    "auxtrace_record.h",
+    "etm_tokenizer.cc",
+    "etm_tokenizer.h",
     "features.cc",
     "features.h",
+    "itrace_start_record.cc",
+    "itrace_start_record.h",
     "mmap_record.cc",
     "mmap_record.h",
     "perf_data_tokenizer.cc",
@@ -70,11 +86,21 @@
     "record_parser.h",
     "sample.cc",
     "sample.h",
+    "sample_id.cc",
+    "sample_id.h",
+    "spe.h",
+    "spe_record_parser.cc",
+    "spe_record_parser.h",
+    "spe_tokenizer.cc",
+    "spe_tokenizer.h",
+    "time_conv_record.h",
+    "util.h",
   ]
   public_deps = [ ":record" ]
   deps = [
     ":tracker",
     "../../../../gn:default_deps",
+    "../../../../protos/perfetto/common:zero",
     "../../../../protos/perfetto/trace:zero",
     "../../../../protos/perfetto/trace/profiling:zero",
     "../../../../protos/third_party/simpleperf:zero",
@@ -93,6 +119,7 @@
 perfetto_unittest_source_set("unittests") {
   testonly = true
   sources = [
+    "aux_stream_manager_unittest.cc",
     "perf_session_unittest.cc",
     "reader_unittest.cc",
   ]
diff --git a/src/trace_processor/importers/perf/aux_data_tokenizer.cc b/src/trace_processor/importers/perf/aux_data_tokenizer.cc
new file mode 100644
index 0000000..9f7176c
--- /dev/null
+++ b/src/trace_processor/importers/perf/aux_data_tokenizer.cc
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf/aux_data_tokenizer.h"
+
+#include <cstdint>
+#include <memory>
+
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/perf/aux_record.h"
+#include "src/trace_processor/importers/perf/aux_stream_manager.h"
+#include "src/trace_processor/importers/perf/itrace_start_record.h"
+#include "src/trace_processor/storage/stats.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+AuxDataTokenizer::~AuxDataTokenizer() = default;
+AuxDataTokenizerFactory::~AuxDataTokenizerFactory() = default;
+
+DummyAuxDataTokenizer::DummyAuxDataTokenizer(TraceProcessorContext* context,
+                                             AuxStream*)
+    : context_(context) {}
+
+void DummyAuxDataTokenizer::OnDataLoss(uint64_t size) {
+  context_->storage->IncrementStats(stats::perf_aux_lost,
+                                    static_cast<int>(size));
+}
+base::Status DummyAuxDataTokenizer::Parse(AuxRecord, TraceBlobView data) {
+  context_->storage->IncrementStats(stats::perf_aux_ignored,
+                                    static_cast<int>(data.size()));
+  return base::OkStatus();
+}
+base::Status DummyAuxDataTokenizer::NotifyEndOfStream() {
+  return base::OkStatus();
+}
+
+base::Status DummyAuxDataTokenizer::OnItraceStartRecord(ItraceStartRecord) {
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/aux_data_tokenizer.h b/src/trace_processor/importers/perf/aux_data_tokenizer.h
new file mode 100644
index 0000000..ac9fd80
--- /dev/null
+++ b/src/trace_processor/importers/perf/aux_data_tokenizer.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_AUX_DATA_TOKENIZER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_AUX_DATA_TOKENIZER_H_
+
+#include <cstdint>
+#include <memory>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/common/clock_tracker.h"
+#include "src/trace_processor/importers/perf/perf_session.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+class TraceProcessorContext;
+namespace perf_importer {
+
+struct AuxRecord;
+class AuxStream;
+struct ItraceStartRecord;
+
+class AuxDataTokenizer {
+ public:
+  virtual ~AuxDataTokenizer();
+  virtual void OnDataLoss(uint64_t) = 0;
+  virtual base::Status Parse(AuxRecord record, TraceBlobView data) = 0;
+  virtual base::Status NotifyEndOfStream() = 0;
+  virtual base::Status OnItraceStartRecord(ItraceStartRecord start) = 0;
+};
+
+// Base class for `AuxDataTokenizer` factories.
+// A factory is created upon encountering an AUXTRACE_INFO record. the payload
+// for such messages usually contains trace specific information to setup trace
+// specific parsing. Subclasses are responsible for parsing the payload and
+// storing any data needed to create `AuxDataTokenizer` instances as new data
+// streams are encountered in the trace.
+class AuxDataTokenizerFactory {
+ public:
+  virtual ~AuxDataTokenizerFactory();
+  virtual base::StatusOr<std::unique_ptr<AuxDataTokenizer>> Create(
+      TraceProcessorContext* context,
+      AuxStream* stream) = 0;
+};
+
+// Generic `AuxDataTokenizerFactory` implementation for factories that keep no
+// state.
+template <typename Tokenizer>
+class SimpleAuxDataTokenizerFactory : public AuxDataTokenizerFactory {
+ public:
+  SimpleAuxDataTokenizerFactory() {}
+  base::StatusOr<std::unique_ptr<AuxDataTokenizer>> Create(
+      TraceProcessorContext* context,
+      AuxStream* stream) override {
+    return std::unique_ptr<AuxDataTokenizer>(new Tokenizer(context, stream));
+  }
+};
+
+// Dummy tokenizer that just discard data.
+// Used to skip streams that we do not know how to parse.
+class DummyAuxDataTokenizer : public AuxDataTokenizer {
+ public:
+  DummyAuxDataTokenizer(TraceProcessorContext* context, AuxStream* stream);
+  void OnDataLoss(uint64_t size) override;
+  base::Status Parse(AuxRecord, TraceBlobView data) override;
+  base::Status NotifyEndOfStream() override;
+  base::Status OnItraceStartRecord(ItraceStartRecord start) override;
+
+ private:
+  TraceProcessorContext* const context_;
+};
+
+// Dummy factory that creates tokenizers that just discard data.
+// Used to skip streams that we do not know how to parse.
+using DummyAuxDataTokenizerFactory =
+    SimpleAuxDataTokenizerFactory<DummyAuxDataTokenizer>;
+
+}  // namespace perf_importer
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_AUX_DATA_TOKENIZER_H_
diff --git a/src/trace_processor/importers/perf/aux_record.cc b/src/trace_processor/importers/perf/aux_record.cc
new file mode 100644
index 0000000..73d22e3
--- /dev/null
+++ b/src/trace_processor/importers/perf/aux_record.cc
@@ -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.
+ */
+
+#include "src/trace_processor/importers/perf/aux_record.h"
+#include <cstdint>
+
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/perf/reader.h"
+#include "src/trace_processor/importers/perf/record.h"
+#include "src/trace_processor/importers/perf/sample_id.h"
+#include "src/trace_processor/importers/perf/util.h"
+
+namespace perfetto::trace_processor::perf_importer {
+// static
+base::Status AuxRecord::Parse(const Record& record) {
+  attr = record.attr;
+  Reader reader(record.payload.copy());
+  if (!reader.Read(offset) || !reader.Read(size) || !reader.Read(flags)) {
+    return base::ErrStatus("Failed to parse AUX record");
+  }
+
+  uint64_t unused;
+  if (!SafeAdd(offset, size, &unused)) {
+    return base::ErrStatus("AUX record overflows");
+  }
+
+  if (!record.has_trailing_sample_id()) {
+    sample_id.reset();
+    return base::OkStatus();
+  }
+
+  sample_id.emplace();
+  return sample_id->ParseFromRecord(record);
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/aux_record.h b/src/trace_processor/importers/perf/aux_record.h
new file mode 100644
index 0000000..770a5ae
--- /dev/null
+++ b/src/trace_processor/importers/perf/aux_record.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_AUX_RECORD_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_AUX_RECORD_H_
+
+#include <cstdint>
+#include <optional>
+
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/ref_counted.h"
+#include "src/trace_processor/importers/perf/perf_event_attr.h"
+#include "src/trace_processor/importers/perf/sample_id.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+struct Record;
+struct AuxRecord {
+  base::Status Parse(const Record& record);
+  uint64_t end() const { return offset + size; }
+
+  RefPtr<PerfEventAttr> attr;
+  uint64_t offset;
+  uint64_t size;
+  uint64_t flags;
+  std::optional<SampleId> sample_id;
+};
+
+}  // namespace perfetto::trace_processor::perf_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_AUX_RECORD_H_
diff --git a/src/trace_processor/importers/perf/aux_stream_manager.cc b/src/trace_processor/importers/perf/aux_stream_manager.cc
new file mode 100644
index 0000000..482f17d
--- /dev/null
+++ b/src/trace_processor/importers/perf/aux_stream_manager.cc
@@ -0,0 +1,260 @@
+/*
+ * 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.
+ */
+
+#include "src/trace_processor/importers/perf/aux_stream_manager.h"
+
+#include <cinttypes>
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <utility>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/perf/aux_data_tokenizer.h"
+#include "src/trace_processor/importers/perf/auxtrace_info_record.h"
+#include "src/trace_processor/importers/perf/auxtrace_record.h"
+#include "src/trace_processor/importers/perf/etm_tokenizer.h"
+#include "src/trace_processor/importers/perf/perf_event.h"
+#include "src/trace_processor/importers/perf/record.h"
+#include "src/trace_processor/importers/perf/spe_tokenizer.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+base::StatusOr<std::reference_wrapper<AuxStream>>
+AuxStreamManager::GetOrCreateStreamForSampleId(
+    const std::optional<SampleId>& sample_id) {
+  if (!sample_id.has_value() || !sample_id->cpu().has_value()) {
+    return base::ErrStatus(
+        "Aux data handling only implemented for per cpu data.");
+  }
+  return GetOrCreateStreamForCpu(*sample_id->cpu());
+}
+
+base::Status AuxStreamManager::OnAuxtraceInfoRecord(AuxtraceInfoRecord info) {
+  if (tokenizer_factory_) {
+    return base::ErrStatus("Multiple PERF_RECORD_AUXTRACE_INFO not supported.");
+  }
+
+  switch (info.type) {
+    case PERF_AUXTRACE_CS_ETM: {
+      ASSIGN_OR_RETURN(tokenizer_factory_,
+                       CreateEtmTokenizerFactory(std::move(info.payload)));
+      break;
+    }
+    case PERF_AUXTRACE_ARM_SPE: {
+      tokenizer_factory_ = std::make_unique<SpeTokenizerFactory>();
+      break;
+    }
+    default:
+      context_->storage->IncrementIndexedStats(stats::perf_unknown_aux_data,
+                                               static_cast<int>(info.type));
+
+      tokenizer_factory_ = std::make_unique<DummyAuxDataTokenizerFactory>();
+      break;
+  }
+  return base::OkStatus();
+}
+
+base::Status AuxStreamManager::OnAuxRecord(AuxRecord aux) {
+  if (!tokenizer_factory_) {
+    return base::ErrStatus(
+        "PERF_RECORD_AUX without previous PERF_RECORD_AUXTRACE_INFO.");
+  }
+  ASSIGN_OR_RETURN(AuxStream & stream,
+                   GetOrCreateStreamForSampleId(aux.sample_id));
+  return stream.OnAuxRecord(aux);
+}
+
+base::Status AuxStreamManager::OnAuxtraceRecord(AuxtraceRecord auxtrace,
+                                                TraceBlobView data) {
+  if (!tokenizer_factory_) {
+    return base::ErrStatus(
+        "PERF_RECORD_AUXTRACE without previous PERF_RECORD_AUXTRACE_INFO.");
+  }
+  if (auxtrace.cpu == std::numeric_limits<uint32_t>::max()) {
+    // Aux data can be written by cpu or by tid. An unset cpu will have a value
+    // of UINT32_MAX. Be aware for an unset tid simpleperf uses 0 and perf uses
+    // UINT32_MAX. ¯\_(ツ)_/¯
+    // Deal just with per cpu data for now.
+    return base::ErrStatus(
+        "Aux data handling only implemented for per cpu data.");
+  }
+  ASSIGN_OR_RETURN(AuxStream & stream, GetOrCreateStreamForCpu(auxtrace.cpu));
+  return stream.OnAuxtraceRecord(std::move(auxtrace), std::move(data));
+}
+
+base::Status AuxStreamManager::OnItraceStartRecord(ItraceStartRecord start) {
+  ASSIGN_OR_RETURN(AuxStream & stream,
+                   GetOrCreateStreamForSampleId(start.sample_id));
+  return stream.OnItraceStartRecord(std::move(start));
+}
+
+base::Status AuxStreamManager::FinalizeStreams() {
+  for (auto it = auxdata_streams_by_cpu_.GetIterator(); it; ++it) {
+    RETURN_IF_ERROR(it.value()->NotifyEndOfStream());
+  }
+
+  return base::OkStatus();
+}
+
+base::StatusOr<std::reference_wrapper<AuxStream>>
+AuxStreamManager::GetOrCreateStreamForCpu(uint32_t cpu) {
+  PERFETTO_CHECK(tokenizer_factory_);
+  std::unique_ptr<AuxStream>* stream_ptr = auxdata_streams_by_cpu_.Find(cpu);
+  if (!stream_ptr) {
+    std::unique_ptr<AuxStream> stream(new AuxStream(this));
+    ASSIGN_OR_RETURN(std::unique_ptr<AuxDataTokenizer> tokenizer,
+                     tokenizer_factory_->Create(context_, stream.get()));
+    stream->tokenizer_ = std::move(tokenizer);
+    stream_ptr = auxdata_streams_by_cpu_.Insert(cpu, std::move(stream)).first;
+  }
+
+  return std::ref(**stream_ptr);
+}
+
+AuxStream::AuxStream(AuxStreamManager* manager) : manager_(*manager) {}
+AuxStream::~AuxStream() = default;
+
+base::Status AuxStream::OnAuxRecord(AuxRecord aux) {
+  if (aux.offset < aux_end_) {
+    return base::ErrStatus("Overlapping AuxRecord. Got %" PRIu64
+                           ", expected at least %" PRIu64,
+                           aux.offset, aux_end_);
+  }
+  if (aux.offset > aux_end_) {
+    manager_.context()->storage->IncrementStats(
+        stats::perf_aux_missing, static_cast<int64_t>(aux.offset - aux_end_));
+  }
+  if (aux.flags & PERF_AUX_FLAG_TRUNCATED) {
+    manager_.context()->storage->IncrementStats(stats::perf_aux_truncated);
+  }
+  if (aux.flags & PERF_AUX_FLAG_PARTIAL) {
+    manager_.context()->storage->IncrementStats(stats::perf_aux_partial);
+  }
+  if (aux.flags & PERF_AUX_FLAG_COLLISION) {
+    manager_.context()->storage->IncrementStats(stats::perf_aux_collision);
+  }
+  outstanding_aux_records_.emplace_back(std::move(aux));
+  aux_end_ = aux.end();
+  return MaybeParse();
+}
+
+base::Status AuxStream::OnAuxtraceRecord(AuxtraceRecord auxtrace,
+                                         TraceBlobView data) {
+  PERFETTO_CHECK(auxtrace.size == data.size());
+  if (auxtrace.offset < auxtrace_end_) {
+    return base::ErrStatus("Overlapping AuxtraceData");
+  }
+  if (auxtrace.offset > auxtrace_end_) {
+    manager_.context()->storage->IncrementStats(
+        stats::perf_auxtrace_missing,
+        static_cast<int64_t>(auxtrace.offset - auxtrace_end_));
+  }
+  outstanding_auxtrace_data_.emplace_back(std::move(auxtrace), std::move(data));
+  auxtrace_end_ = outstanding_auxtrace_data_.back().end();
+  return MaybeParse();
+}
+
+base::Status AuxStream::MaybeParse() {
+  while (!outstanding_aux_records_.empty() &&
+         !outstanding_auxtrace_data_.empty()) {
+    const AuxRecord& aux_record = outstanding_aux_records_.front();
+    AuxtraceDataChunk& auxtrace_data = outstanding_auxtrace_data_.front();
+
+    // We need both auxtrace and aux, so we start at the biggest offset.
+    const uint64_t start_offset =
+        std::max(aux_record.offset, auxtrace_data.offset());
+
+    if (tokenizer_offset_ < start_offset) {
+      tokenizer_->OnDataLoss(start_offset - tokenizer_offset_);
+      tokenizer_offset_ = start_offset;
+    }
+
+    // Not enough aux data at front of queue.
+    if (start_offset >= aux_record.end()) {
+      outstanding_aux_records_.pop_front();
+      continue;
+    }
+
+    // Not enough auxtrace data at front of queue.
+    if (start_offset >= auxtrace_data.end()) {
+      outstanding_auxtrace_data_.pop_front();
+      continue;
+    }
+
+    const uint64_t end_offset = std::min(aux_record.end(), auxtrace_data.end());
+    const uint64_t size = end_offset - start_offset;
+
+    PERFETTO_CHECK(tokenizer_offset_ == start_offset);
+    PERFETTO_CHECK(start_offset != end_offset);
+
+    auxtrace_data.DropUntil(start_offset);
+    TraceBlobView data = auxtrace_data.ConsumeFront(size);
+
+    AuxRecord adjusted_aux_record = aux_record;
+    adjusted_aux_record.offset = tokenizer_offset_;
+    adjusted_aux_record.size = size;
+    tokenizer_offset_ += size;
+    RETURN_IF_ERROR(
+        tokenizer_->Parse(std::move(adjusted_aux_record), std::move(data)));
+  }
+  return base::OkStatus();
+}
+
+base::Status AuxStream::NotifyEndOfStream() {
+  if (aux_end_ < auxtrace_end_) {
+    manager_.context()->storage->IncrementStats(
+        stats::perf_aux_missing,
+        static_cast<int64_t>(auxtrace_end_ - aux_end_));
+  } else if (auxtrace_end_ < aux_end_) {
+    manager_.context()->storage->IncrementStats(
+        stats::perf_auxtrace_missing,
+        static_cast<int64_t>(aux_end_ - auxtrace_end_));
+  }
+
+  uint64_t end = std::max(aux_end_, auxtrace_end_);
+  if (tokenizer_offset_ < end) {
+    uint64_t loss = end - tokenizer_offset_;
+    tokenizer_->OnDataLoss(loss);
+    tokenizer_offset_ += loss;
+  }
+  return tokenizer_->NotifyEndOfStream();
+}
+
+void AuxStream::AuxtraceDataChunk::DropUntil(uint64_t offset) {
+  PERFETTO_CHECK(offset >= this->offset() && offset <= end());
+  const uint64_t size = offset - this->offset();
+
+  data_ = data_.slice_off(size, data_.size() - size);
+  auxtrace_.size -= size;
+  auxtrace_.offset += size;
+}
+
+TraceBlobView AuxStream::AuxtraceDataChunk::ConsumeFront(uint64_t size) {
+  PERFETTO_CHECK(size <= data_.size());
+  TraceBlobView res = data_.slice_off(0, size);
+  data_ = data_.slice_off(size, data_.size() - size);
+  auxtrace_.size -= size;
+  auxtrace_.offset += size;
+  return res;
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/aux_stream_manager.h b/src/trace_processor/importers/perf/aux_stream_manager.h
new file mode 100644
index 0000000..2cbde78
--- /dev/null
+++ b/src/trace_processor/importers/perf/aux_stream_manager.h
@@ -0,0 +1,144 @@
+/*
+ * 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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_AUX_STREAM_MANAGER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_AUX_STREAM_MANAGER_H_
+
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <optional>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/circular_queue.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/perf/aux_data_tokenizer.h"
+#include "src/trace_processor/importers/perf/aux_record.h"
+#include "src/trace_processor/importers/perf/auxtrace_record.h"
+#include "src/trace_processor/importers/perf/itrace_start_record.h"
+#include "src/trace_processor/importers/perf/perf_session.h"
+#include "src/trace_processor/importers/perf/time_conv_record.h"
+#include "src/trace_processor/storage/stats.h"
+
+namespace perfetto {
+namespace trace_processor {
+class TraceProcessorContext;
+
+namespace perf_importer {
+
+struct Record;
+class SampleId;
+struct AuxtraceInfoRecord;
+
+class AuxStreamManager;
+
+// Takes care of reconstructing the original data stream out of AUX and AUXTRACE
+// records. Does not parse tha actual data it just forwards it to the associated
+// `AuxDataTokenizer` .
+class AuxStream {
+ public:
+  ~AuxStream();
+  base::Status OnAuxRecord(AuxRecord aux);
+  base::Status OnAuxtraceRecord(AuxtraceRecord auxtrace, TraceBlobView data);
+  base::Status NotifyEndOfStream();
+  base::Status OnItraceStartRecord(ItraceStartRecord start) {
+    return tokenizer_->OnItraceStartRecord(std::move(start));
+  }
+  std::optional<uint64_t> ConvertTscToPerfTime(uint64_t cycles);
+
+ private:
+  class AuxtraceDataChunk {
+   public:
+    AuxtraceDataChunk(AuxtraceRecord auxtrace, TraceBlobView data)
+        : auxtrace_(std::move(auxtrace)), data_(std::move(data)) {}
+
+    TraceBlobView ConsumeFront(uint64_t size);
+    void DropUntil(uint64_t offset);
+
+    uint64_t offset() const { return auxtrace_.offset; }
+    uint64_t end() const { return auxtrace_.offset + data_.size(); }
+    uint64_t size() const { return data_.size(); }
+
+   private:
+    AuxtraceRecord auxtrace_;
+    TraceBlobView data_;
+  };
+
+  friend AuxStreamManager;
+  explicit AuxStream(AuxStreamManager* manager);
+
+  base::Status MaybeParse();
+
+  AuxStreamManager& manager_;
+  std::unique_ptr<AuxDataTokenizer> tokenizer_;
+  base::CircularQueue<AuxRecord> outstanding_aux_records_;
+  uint64_t aux_end_ = 0;
+  base::CircularQueue<AuxtraceDataChunk> outstanding_auxtrace_data_;
+  uint64_t auxtrace_end_ = 0;
+  uint64_t tokenizer_offset_ = 0;
+};
+
+// Keeps track of all aux streams in a perf file.
+class AuxStreamManager {
+ public:
+  explicit AuxStreamManager(TraceProcessorContext* context)
+      : context_(context) {}
+  base::Status OnAuxtraceInfoRecord(AuxtraceInfoRecord info);
+  base::Status OnAuxRecord(AuxRecord aux);
+  base::Status OnAuxtraceRecord(AuxtraceRecord auxtrace, TraceBlobView data);
+  base::Status OnItraceStartRecord(ItraceStartRecord start);
+  base::Status OnTimeConvRecord(TimeConvRecord time_conv) {
+    time_conv_ = std::move(time_conv);
+    return base::OkStatus();
+  }
+
+  base::Status FinalizeStreams();
+
+  TraceProcessorContext* context() const { return context_; }
+
+  std::optional<uint64_t> ConvertTscToPerfTime(uint64_t cycles) {
+    if (!time_conv_) {
+      context_->storage->IncrementStats(stats::perf_no_tsc_data);
+      return std::nullopt;
+    }
+    return time_conv_->ConvertTscToPerfTime(cycles);
+  }
+
+ private:
+  base::StatusOr<std::reference_wrapper<AuxStream>>
+  GetOrCreateStreamForSampleId(const std::optional<SampleId>& sample_id);
+  base::StatusOr<std::reference_wrapper<AuxStream>> GetOrCreateStreamForCpu(
+      uint32_t cpu);
+
+  TraceProcessorContext* const context_;
+  std::unique_ptr<AuxDataTokenizerFactory> tokenizer_factory_;
+  base::FlatHashMap<uint32_t, std::unique_ptr<AuxStream>>
+      auxdata_streams_by_cpu_;
+  std::optional<TimeConvRecord> time_conv_;
+};
+
+inline std::optional<uint64_t> AuxStream::ConvertTscToPerfTime(
+    uint64_t cycles) {
+  return manager_.ConvertTscToPerfTime(cycles);
+}
+
+}  // namespace perf_importer
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_AUX_STREAM_MANAGER_H_
diff --git a/src/trace_processor/importers/perf/aux_stream_manager_unittest.cc b/src/trace_processor/importers/perf/aux_stream_manager_unittest.cc
new file mode 100644
index 0000000..30cff84
--- /dev/null
+++ b/src/trace_processor/importers/perf/aux_stream_manager_unittest.cc
@@ -0,0 +1,285 @@
+/*
+ * 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.
+ */
+
+#include "src/trace_processor/importers/perf/aux_stream_manager.h"
+#include <cstdint>
+#include <memory>
+
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/trace_blob.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/perf/aux_record.h"
+#include "src/trace_processor/importers/perf/auxtrace_info_record.h"
+#include "src/trace_processor/storage/stats.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto::trace_processor::perf_importer {
+namespace {
+
+std::unique_ptr<TraceProcessorContext> CreateTraceProcessorContext() {
+  auto ctx = std::make_unique<TraceProcessorContext>();
+  ctx->storage = std::make_shared<TraceStorage>();
+  return ctx;
+}
+
+AuxtraceInfoRecord CreateAuxtraceInfoRecord() {
+  AuxtraceInfoRecord info;
+  info.type = 0;
+  return info;
+}
+
+AuxRecord CreateAuxRecord(uint64_t offset, uint64_t size, uint32_t cpu) {
+  AuxRecord aux;
+  aux.offset = offset;
+  aux.size = size;
+  aux.flags = 0;
+  aux.sample_id.emplace();
+  aux.sample_id->set_cpu(cpu);
+  return aux;
+}
+
+AuxtraceRecord CreateAuxtraceRecord(uint64_t offset,
+                                    uint64_t size,
+                                    uint32_t cpu) {
+  AuxtraceRecord auxtrace;
+  auxtrace.offset = offset;
+  auxtrace.size = size;
+  auxtrace.cpu = cpu;
+  auxtrace.tid = 0;
+  return auxtrace;
+}
+
+TEST(AuxStreamManagerTest, NoAuxStreamsCanFinalize) {
+  auto ctx = CreateTraceProcessorContext();
+  AuxStreamManager manager(ctx.get());
+  EXPECT_TRUE(manager.FinalizeStreams().ok());
+}
+
+TEST(AuxStreamManagerTest, NoAuxTraceInfoFailsMethods) {
+  auto ctx = CreateTraceProcessorContext();
+  AuxStreamManager manager(ctx.get());
+
+  EXPECT_FALSE(manager
+                   .OnAuxtraceRecord(CreateAuxtraceRecord(0, 10, 0),
+                                     TraceBlobView(TraceBlob::Allocate(10)))
+                   .ok());
+  EXPECT_FALSE(manager.OnAuxRecord(CreateAuxRecord(0, 10, 0)).ok());
+}
+
+TEST(AuxStreamManagerTest, MultipleAuxTraceInfoFails) {
+  auto ctx = CreateTraceProcessorContext();
+  AuxStreamManager manager(ctx.get());
+
+  AuxtraceInfoRecord info_0;
+  info_0.type = 0;
+  EXPECT_TRUE(manager.OnAuxtraceInfoRecord(std::move(info_0)).ok());
+
+  AuxtraceInfoRecord info_1;
+  info_1.type = 1;
+  EXPECT_FALSE(manager.OnAuxtraceInfoRecord(std::move(info_1)).ok());
+}
+
+TEST(AuxStreamManagerTest, ReconstructsStream) {
+  constexpr uint64_t kSize = 10;
+  constexpr uint32_t kCpu = 0;
+  TraceBlobView data(TraceBlob::Allocate(kSize));
+  TraceBlobView double_data(TraceBlob::Allocate(2 * kSize));
+  auto ctx = CreateTraceProcessorContext();
+  AuxStreamManager manager(ctx.get());
+  ASSERT_TRUE(manager.OnAuxtraceInfoRecord(CreateAuxtraceInfoRecord()).ok());
+
+  manager.OnAuxRecord(CreateAuxRecord(0, kSize, kCpu));
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_ignored].value, 0);
+
+  manager.OnAuxRecord(CreateAuxRecord(10, kSize, kCpu));
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_ignored].value, 0);
+
+  manager.OnAuxtraceRecord(CreateAuxtraceRecord(0, 2 * kSize, kCpu),
+                           double_data.copy());
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_ignored].value, 20);
+
+  manager.OnAuxtraceRecord(CreateAuxtraceRecord(20, kSize, kCpu), data.copy());
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_ignored].value, 20);
+
+  manager.OnAuxtraceRecord(CreateAuxtraceRecord(30, kSize, kCpu), data.copy());
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_ignored].value, 20);
+
+  manager.OnAuxRecord(CreateAuxRecord(20, 2 * kSize, kCpu));
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_ignored].value, 40);
+}
+
+TEST(AuxStreamManagerTest, AuxLoss) {
+  constexpr uint64_t kSize = 10;
+  constexpr uint32_t kCpu = 0;
+  TraceBlobView data(TraceBlob::Allocate(kSize));
+  TraceBlobView triple_data(TraceBlob::Allocate(3 * kSize));
+  auto ctx = CreateTraceProcessorContext();
+  AuxStreamManager manager(ctx.get());
+  ASSERT_TRUE(manager.OnAuxtraceInfoRecord(CreateAuxtraceInfoRecord()).ok());
+
+  manager.OnAuxRecord(CreateAuxRecord(10, kSize, kCpu));
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_missing].value, 10);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_auxtrace_missing].value, 0);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_ignored].value, 0);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_lost].value, 0);
+
+  manager.OnAuxtraceRecord(CreateAuxtraceRecord(0, 3 * kSize, kCpu),
+                           triple_data.copy());
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_missing].value, 10);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_auxtrace_missing].value, 0);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_ignored].value, 10);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_lost].value, 10);
+
+  manager.FinalizeStreams();
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_missing].value, 20);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_auxtrace_missing].value, 0);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_ignored].value, 10);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_lost].value, 20);
+}
+
+TEST(AuxStreamManagerTest, AuxtraceLoss) {
+  constexpr uint64_t kSize = 10;
+  constexpr uint32_t kCpu = 0;
+  TraceBlobView data(TraceBlob::Allocate(kSize));
+  auto ctx = CreateTraceProcessorContext();
+  AuxStreamManager manager(ctx.get());
+  ASSERT_TRUE(manager.OnAuxtraceInfoRecord(CreateAuxtraceInfoRecord()).ok());
+
+  manager.OnAuxtraceRecord(CreateAuxtraceRecord(10, kSize, kCpu), data.copy());
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_missing].value, 0);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_auxtrace_missing].value, 10);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_ignored].value, 0);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_lost].value, 0);
+
+  manager.OnAuxRecord(CreateAuxRecord(0, 3 * kSize, kCpu));
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_missing].value, 0);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_auxtrace_missing].value, 10);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_ignored].value, 10);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_lost].value, 10);
+
+  manager.FinalizeStreams();
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_missing].value, 0);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_auxtrace_missing].value, 20);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_ignored].value, 10);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_lost].value, 20);
+}
+
+TEST(AuxStreamManagerTest, ComplexStream) {
+  constexpr uint32_t kCpu = 0;
+  TraceBlobView data_5(TraceBlob::Allocate(5));
+  TraceBlobView data_10(TraceBlob::Allocate(10));
+  TraceBlobView data_15(TraceBlob::Allocate(15));
+
+  auto ctx = CreateTraceProcessorContext();
+  AuxStreamManager manager(ctx.get());
+  ASSERT_TRUE(manager.OnAuxtraceInfoRecord(CreateAuxtraceInfoRecord()).ok());
+
+  uint64_t aux_offset = 0;
+  uint64_t auxtrace_offset = 0;
+
+  auto aux = [&](uint64_t size) {
+    manager.OnAuxRecord(CreateAuxRecord(aux_offset, size, kCpu));
+    aux_offset += size;
+  };
+  auto aux_hole = [&](uint64_t size) { aux_offset += size; };
+
+  auto auxtrace = [&](uint64_t size) {
+    manager.OnAuxtraceRecord(CreateAuxtraceRecord(auxtrace_offset, size, kCpu),
+                             TraceBlobView(TraceBlob::Allocate(size)));
+    auxtrace_offset += size;
+  };
+  auto auxtrace_hole = [&](uint64_t size) { auxtrace_offset += size; };
+
+  //          . . . . . . . . . . . . . . . . . . . . . .
+  //          |105                                      |
+  // Aux      |---|30         |10 |30         |-|20     |
+  // Auxtrace |5|10 |50                 |5|-|5|---|5|---|
+  // Result   |---|60                     |-|5|---|5|---|
+  //          . . . . . . . . . . . . . . . . . . . . . .
+  aux_hole(10);
+  aux(30);
+  aux(10);
+  aux(30);
+  aux_hole(5);
+  aux(20);
+  auxtrace(5);
+  auxtrace(10);
+  auxtrace(50);
+  auxtrace(5);
+  auxtrace_hole(5);
+  auxtrace(5);
+  auxtrace_hole(10);
+  auxtrace(5);
+
+  manager.FinalizeStreams();
+
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_missing].value, 15);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_auxtrace_missing].value, 25);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_ignored].value, 70);
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_lost].value, 35);
+}
+
+TEST(AuxStreamManagerTest, StreamOverlapFails) {
+  constexpr uint64_t kSize = 10;
+  constexpr uint32_t kCpu = 0;
+  TraceBlobView data(TraceBlob::Allocate(kSize));
+  auto ctx = CreateTraceProcessorContext();
+  AuxStreamManager manager(ctx.get());
+  ASSERT_TRUE(manager.OnAuxtraceInfoRecord(CreateAuxtraceInfoRecord()).ok());
+
+  EXPECT_TRUE(manager.OnAuxRecord(CreateAuxRecord(0, kSize, kCpu)).ok());
+  EXPECT_FALSE(manager.OnAuxRecord(CreateAuxRecord(0, kSize, kCpu)).ok());
+
+  EXPECT_TRUE(
+      manager
+          .OnAuxtraceRecord(CreateAuxtraceRecord(0, kSize, kCpu), data.copy())
+          .ok());
+  EXPECT_FALSE(
+      manager
+          .OnAuxtraceRecord(CreateAuxtraceRecord(0, kSize, kCpu), data.copy())
+          .ok());
+}
+
+TEST(AuxStreamManagerTest, MultipleStreams) {
+  constexpr uint64_t kSize = 10;
+  constexpr uint32_t kCpu_0 = 0;
+  constexpr uint32_t kCpu_1 = 1;
+  TraceBlobView data(TraceBlob::Allocate(kSize));
+  auto ctx = CreateTraceProcessorContext();
+  AuxStreamManager manager(ctx.get());
+  ASSERT_TRUE(manager.OnAuxtraceInfoRecord(CreateAuxtraceInfoRecord()).ok());
+
+  EXPECT_TRUE(manager.OnAuxRecord(CreateAuxRecord(0, kSize, kCpu_0)).ok());
+  EXPECT_TRUE(manager.OnAuxRecord(CreateAuxRecord(0, kSize, kCpu_1)).ok());
+
+  EXPECT_TRUE(
+      manager
+          .OnAuxtraceRecord(CreateAuxtraceRecord(0, kSize, kCpu_0), data.copy())
+          .ok());
+  EXPECT_TRUE(
+      manager
+          .OnAuxtraceRecord(CreateAuxtraceRecord(0, kSize, kCpu_1), data.copy())
+          .ok());
+
+  manager.FinalizeStreams();
+
+  EXPECT_EQ(ctx->storage->stats()[stats::perf_aux_ignored].value, 20);
+}
+
+}  // namespace
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/auxtrace_info_record.cc b/src/trace_processor/importers/perf/auxtrace_info_record.cc
new file mode 100644
index 0000000..01616f3
--- /dev/null
+++ b/src/trace_processor/importers/perf/auxtrace_info_record.cc
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf/auxtrace_info_record.h"
+
+#include "perfetto/base/status.h"
+#include "src/trace_processor/importers/perf/reader.h"
+#include "src/trace_processor/importers/perf/record.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+base::Status AuxtraceInfoRecord::Parse(const Record& record) {
+  Reader reader(record.payload.copy());
+
+  if (!reader.Read(type) || !reader.Read(reserved) ||
+      !reader.ReadBlob(payload, static_cast<uint32_t>(reader.size_left()))) {
+    return base::ErrStatus("Failed to parse PERF_RECORD_AUXTRACE_INFO");
+  }
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/auxtrace_info_record.h b/src/trace_processor/importers/perf/auxtrace_info_record.h
new file mode 100644
index 0000000..08e64aa
--- /dev/null
+++ b/src/trace_processor/importers/perf/auxtrace_info_record.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_PERF_AUXTRACE_INFO_RECORD_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_AUXTRACE_INFO_RECORD_H_
+
+#include <cstdint>
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+struct Record;
+
+struct AuxtraceInfoRecord {
+  uint32_t type;
+  uint32_t reserved;  // alignment
+  TraceBlobView payload;
+
+  base::Status Parse(const Record& record);
+};
+
+}  // namespace perfetto::trace_processor::perf_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_AUXTRACE_INFO_RECORD_H_
diff --git a/src/trace_processor/importers/perf/auxtrace_record.cc b/src/trace_processor/importers/perf/auxtrace_record.cc
new file mode 100644
index 0000000..ac9394b
--- /dev/null
+++ b/src/trace_processor/importers/perf/auxtrace_record.cc
@@ -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.
+ */
+
+#include "src/trace_processor/importers/perf/auxtrace_record.h"
+
+#include "perfetto/base/status.h"
+#include "src/trace_processor/importers/perf/reader.h"
+#include "src/trace_processor/importers/perf/record.h"
+#include "src/trace_processor/importers/perf/util.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+base::Status AuxtraceRecord::Parse(const Record& record) {
+  Reader reader(record.payload.copy());
+  if (!reader.Read(*this)) {
+    return base::ErrStatus("Failed to parse PERF_RECORD_AUXTRACE");
+  }
+
+  uint64_t unused;
+  if (!SafeAdd(offset, size, &unused)) {
+    return base::ErrStatus("AUXTRACE record overflows");
+  }
+
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/auxtrace_record.h b/src/trace_processor/importers/perf/auxtrace_record.h
new file mode 100644
index 0000000..2bc2d4f
--- /dev/null
+++ b/src/trace_processor/importers/perf/auxtrace_record.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_AUXTRACE_RECORD_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_AUXTRACE_RECORD_H_
+
+#include <cstdint>
+#include "perfetto/base/status.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+struct Record;
+
+struct AuxtraceRecord {
+  uint64_t end() const { return offset + size; }
+  uint64_t size;
+  uint64_t offset;
+  uint64_t reference;
+  uint32_t idx;
+  uint32_t tid;
+  uint32_t cpu;
+  // Alignment
+  uint32_t reserved;
+
+  base::Status Parse(const Record& record);
+};
+
+}  // namespace perfetto::trace_processor::perf_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_AUXTRACE_RECORD_H_
diff --git a/src/trace_processor/importers/perf/etm_tokenizer.cc b/src/trace_processor/importers/perf/etm_tokenizer.cc
new file mode 100644
index 0000000..f9f0aec
--- /dev/null
+++ b/src/trace_processor/importers/perf/etm_tokenizer.cc
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf/etm_tokenizer.h"
+#include <memory>
+
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/trace_processor/ref_counted.h"
+#include "perfetto/trace_processor/status.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/perf/aux_data_tokenizer.h"
+#include "src/trace_processor/importers/perf/aux_record.h"
+#include "src/trace_processor/importers/perf/perf_session.h"
+#include "src/trace_processor/importers/perf/reader.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto::trace_processor::perf_importer {
+namespace {
+
+struct EtmV4Info {
+  uint64_t cpu;
+  uint64_t nrtrcparams;
+  uint64_t trcconfigr;
+  uint64_t trctraceidr;
+  uint64_t trcidr0;
+  uint64_t trcidr1;
+  uint64_t trcidr2;
+  uint64_t trcidr8;
+  uint64_t trcauthstatus;
+};
+
+struct EteInfo : public EtmV4Info {
+  uint64_t trcdevarch;
+};
+
+struct EtmConfiguration {
+  util::Status Parse(TraceBlobView data);
+  uint64_t version;
+  uint32_t pmu_type;
+  uint64_t snapshot;
+  std::vector<EtmV4Info> etm_v4_infos;
+  std::vector<EteInfo> ete_infos;
+};
+
+util::Status EtmConfiguration::Parse(TraceBlobView data) {
+  static constexpr uint64_t kEtmV4Magic = 0x4040404040404040ULL;
+  static constexpr uint64_t kEteMagic = 0x5050505050505050ULL;
+  Reader reader(std::move(data));
+
+  if (!reader.Read(version)) {
+    return base::ErrStatus("Failed to parse EtmConfiguration.");
+  }
+
+  if (version != 1) {
+    return base::ErrStatus("Invalid version in EtmConfiguration: %" PRIu64,
+                           version);
+  }
+
+  uint32_t nr;
+  if (!reader.Read(nr) || !reader.Read(pmu_type) || !reader.Read(snapshot)) {
+    return base::ErrStatus("Failed to parse EtmConfiguration.");
+  }
+
+  for (; nr != 0; --nr) {
+    uint64_t magic;
+    if (!reader.Read(magic)) {
+      return base::ErrStatus("Failed to parse EtmConfiguration.");
+    }
+    switch (magic) {
+      case kEtmV4Magic:
+        etm_v4_infos.emplace_back();
+        if (!reader.Read(etm_v4_infos.back())) {
+          return base::ErrStatus("Failed to parse EtmV4Info.");
+        }
+        break;
+      case kEteMagic:
+        ete_infos.emplace_back();
+        if (!reader.Read(ete_infos.back())) {
+          return base::ErrStatus("Failed to parse EteInfo.");
+        }
+        break;
+      default:
+        return base::ErrStatus("Unknown magic in EtmConfiguration: %s",
+                               base::Uint64ToHexString(magic).c_str());
+    }
+  }
+
+  return base::OkStatus();
+}
+
+}  // namespace
+
+base::StatusOr<std::unique_ptr<AuxDataTokenizerFactory>>
+CreateEtmTokenizerFactory(TraceBlobView data) {
+  EtmConfiguration config;
+  RETURN_IF_ERROR(config.Parse(std::move(data)));
+  return std::unique_ptr<AuxDataTokenizerFactory>(
+      new DummyAuxDataTokenizerFactory());
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/etm_tokenizer.h b/src/trace_processor/importers/perf/etm_tokenizer.h
new file mode 100644
index 0000000..8559718
--- /dev/null
+++ b/src/trace_processor/importers/perf/etm_tokenizer.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_ETM_TOKENIZER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_ETM_TOKENIZER_H_
+
+#include <memory>
+
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/perf/aux_data_tokenizer.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace perf_importer {
+
+base::StatusOr<std::unique_ptr<AuxDataTokenizerFactory>>
+CreateEtmTokenizerFactory(TraceBlobView info);
+
+}  // namespace perf_importer
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_ETM_TOKENIZER_H_
diff --git a/src/trace_processor/importers/perf/itrace_start_record.cc b/src/trace_processor/importers/perf/itrace_start_record.cc
new file mode 100644
index 0000000..b5923bf
--- /dev/null
+++ b/src/trace_processor/importers/perf/itrace_start_record.cc
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf/itrace_start_record.h"
+
+#include "src/trace_processor/importers/perf/reader.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+base::Status ItraceStartRecord::Parse(const Record& record) {
+  Reader reader(record.payload.copy());
+
+  if (!reader.Read(pid) || !reader.Read(tid)) {
+    return base::ErrStatus("Failed to parse PERF_RECORD_ITRACE_START");
+  }
+
+  if (!record.has_trailing_sample_id()) {
+    sample_id.reset();
+    return base::OkStatus();
+  }
+
+  sample_id.emplace();
+  return sample_id->ParseFromRecord(record);
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/itrace_start_record.h b/src/trace_processor/importers/perf/itrace_start_record.h
new file mode 100644
index 0000000..ce35003
--- /dev/null
+++ b/src/trace_processor/importers/perf/itrace_start_record.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_ITRACE_START_RECORD_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_ITRACE_START_RECORD_H_
+
+#include <cstdint>
+#include <optional>
+
+#include "perfetto/base/status.h"
+#include "src/trace_processor/importers/perf/sample_id.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+struct Record;
+
+struct ItraceStartRecord {
+  uint32_t pid;
+  uint32_t tid;
+  std::optional<SampleId> sample_id;
+
+  base::Status Parse(const Record& record);
+};
+
+}  // namespace perfetto::trace_processor::perf_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_ITRACE_START_RECORD_H_
diff --git a/src/trace_processor/importers/perf/perf_data_tokenizer.cc b/src/trace_processor/importers/perf/perf_data_tokenizer.cc
index 85a7f2f..0c991aa 100644
--- a/src/trace_processor/importers/perf/perf_data_tokenizer.cc
+++ b/src/trace_processor/importers/perf/perf_data_tokenizer.cc
@@ -21,6 +21,8 @@
 #include <cstddef>
 #include <cstdint>
 #include <cstring>
+#include <limits>
+#include <memory>
 #include <optional>
 #include <string>
 #include <tuple>
@@ -34,24 +36,34 @@
 #include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
+#include "protos/perfetto/common/builtin_clock.pbzero.h"
 #include "protos/perfetto/trace/clock_snapshot.pbzero.h"
 #include "protos/third_party/simpleperf/record_file.pbzero.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/perf/attrs_section_reader.h"
+#include "src/trace_processor/importers/perf/aux_data_tokenizer.h"
+#include "src/trace_processor/importers/perf/aux_record.h"
+#include "src/trace_processor/importers/perf/aux_stream_manager.h"
+#include "src/trace_processor/importers/perf/auxtrace_info_record.h"
+#include "src/trace_processor/importers/perf/auxtrace_record.h"
 #include "src/trace_processor/importers/perf/dso_tracker.h"
+#include "src/trace_processor/importers/perf/etm_tokenizer.h"
 #include "src/trace_processor/importers/perf/features.h"
+#include "src/trace_processor/importers/perf/itrace_start_record.h"
 #include "src/trace_processor/importers/perf/perf_event.h"
 #include "src/trace_processor/importers/perf/perf_event_attr.h"
 #include "src/trace_processor/importers/perf/perf_file.h"
 #include "src/trace_processor/importers/perf/perf_session.h"
 #include "src/trace_processor/importers/perf/reader.h"
 #include "src/trace_processor/importers/perf/record.h"
+#include "src/trace_processor/importers/perf/sample_id.h"
 #include "src/trace_processor/importers/proto/perf_sample_tracker.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/util/build_id.h"
 #include "src/trace_processor/util/status_macros.h"
+#include "src/trace_processor/util/trace_blob_view_reader.h"
 
 namespace perfetto::trace_processor::perf_importer {
 namespace {
@@ -108,7 +120,7 @@
 }  // namespace
 
 PerfDataTokenizer::PerfDataTokenizer(TraceProcessorContext* ctx)
-    : context_(ctx) {}
+    : context_(ctx), aux_manager_(ctx) {}
 
 PerfDataTokenizer::~PerfDataTokenizer() = default;
 
@@ -126,8 +138,7 @@
   buffer_.PushBack(std::move(blob));
 
   base::StatusOr<ParsingResult> result = ParsingResult::kSuccess;
-  while (result.ok() && result.value() == ParsingResult::kSuccess &&
-         !buffer_.empty()) {
+  while (result.ok() && result.value() != ParsingResult::kMoreDataNeeded) {
     switch (parsing_state_) {
       case ParsingState::kParseHeader:
         result = ParseHeader();
@@ -145,6 +156,10 @@
         result = ParseRecords();
         break;
 
+      case ParsingState::kParseAuxtraceData:
+        result = ParseAuxtraceData();
+        break;
+
       case ParsingState::kParseFeatures:
         result = ParseFeatures();
         break;
@@ -154,7 +169,10 @@
         break;
 
       case ParsingState::kDone:
-        result = base::ErrStatus("Unexpected data");
+        if (!buffer_.empty()) {
+          return base::ErrStatus("Unexpected data, %zu", buffer_.avail());
+        }
+        return base::OkStatus();
     }
   }
   return result.status();
@@ -224,6 +242,10 @@
   }
 
   ASSIGN_OR_RETURN(perf_session_, builder.Build());
+  if (perf_session_->HasPerfClock()) {
+    context_->clock_tracker->SetTraceTimeClock(
+        protos::pbzero::BUILTIN_CLOCK_PERF);
+  }
   parsing_state_ = ParsingState::kSeekRecords;
   return ParsingResult::kSuccess;
 }
@@ -247,15 +269,47 @@
       return res;
     }
 
-    if (!PushRecord(std::move(record))) {
-      context_->storage->IncrementStats(stats::perf_record_skipped);
+    if (record.header.type == PERF_RECORD_AUXTRACE) {
+      PERFETTO_CHECK(!current_auxtrace_.has_value());
+      current_auxtrace_.emplace();
+      RETURN_IF_ERROR(current_auxtrace_->Parse(record));
+      parsing_state_ = ParsingState::kParseAuxtraceData;
+      return ParsingResult::kSuccess;
     }
+
+    RETURN_IF_ERROR(ProcessRecord(std::move(record)));
   }
 
+  RETURN_IF_ERROR(aux_manager_.FinalizeStreams());
+
   parsing_state_ = ParsingState::kParseFeatureSections;
   return ParsingResult::kSuccess;
 }
 
+base::Status PerfDataTokenizer::ProcessRecord(Record record) {
+  const uint32_t type = record.header.type;
+  switch (type) {
+    case PERF_RECORD_AUXTRACE:
+      PERFETTO_FATAL("Unreachable");
+
+    case PERF_RECORD_AUXTRACE_INFO:
+      return ProcessAuxtraceInfoRecord(std::move(record));
+
+    case PERF_RECORD_AUX:
+      return ProcessAuxRecord(std::move(record));
+
+    case PERF_RECORD_TIME_CONV:
+      return ProcessTimeConvRecord(std::move(record));
+
+    case PERF_RECORD_ITRACE_START:
+      return ProcessItraceStartRecord(std::move(record));
+
+    default:
+      MaybePushRecord(std::move(record));
+      return base::OkStatus();
+  }
+}
+
 base::StatusOr<PerfDataTokenizer::ParsingResult> PerfDataTokenizer::ParseRecord(
     Record& record) {
   record.session = perf_session_;
@@ -290,14 +344,18 @@
   return ParsingResult::kSuccess;
 }
 
-base::StatusOr<int64_t> PerfDataTokenizer::ToTraceTimestamp(
-    std::optional<uint64_t> time) {
+base::StatusOr<int64_t> PerfDataTokenizer::ExtractTraceTimestamp(
+    const Record& record) {
+  std::optional<uint64_t> time;
+  if (!ReadTime(record, time)) {
+    return base::ErrStatus("Failed to read time");
+  }
+
   base::StatusOr<int64_t> trace_ts =
       time.has_value()
-          ? context_->clock_tracker->ToTraceTime(
-                protos::pbzero::ClockSnapshot::Clock::MONOTONIC,
-                static_cast<int64_t>(*time))
-          : std::max(latest_timestamp_, context_->sorter->max_timestamp());
+          ? context_->clock_tracker->ToTraceTime(record.attr->clock_id(),
+                                                 static_cast<int64_t>(*time))
+          : std::min(latest_timestamp_, context_->sorter->max_timestamp());
 
   if (PERFETTO_LIKELY(trace_ts.ok())) {
     latest_timestamp_ = std::max(latest_timestamp_, *trace_ts);
@@ -305,29 +363,14 @@
 
   return trace_ts;
 }
-
-bool PerfDataTokenizer::PushRecord(Record record) {
-  std::optional<uint64_t> time;
-  if (!ReadTime(record, time)) {
-    return false;
-  }
-
-  base::StatusOr<int64_t> trace_ts = ToTraceTimestamp(time);
+void PerfDataTokenizer::MaybePushRecord(Record record) {
+  base::StatusOr<int64_t> trace_ts = ExtractTraceTimestamp(record);
   if (!trace_ts.ok()) {
-    return false;
+    context_->storage->IncrementIndexedStats(
+        stats::perf_record_skipped, static_cast<int>(record.header.type));
+    return;
   }
-
-  switch (record.header.type) {
-    case PERF_RECORD_AUXTRACE_INFO:
-    case PERF_RECORD_AUXTRACE:
-    case PERF_RECORD_AUX:
-      break;
-    default:
-      context_->sorter->PushPerfRecord(*trace_ts, std::move(record));
-      break;
-  }
-
-  return true;
+  context_->sorter->PushPerfRecord(*trace_ts, std::move(record));
 }
 
 base::StatusOr<PerfDataTokenizer::ParsingResult>
@@ -350,6 +393,12 @@
   std::sort(feature_sections_.begin(), feature_sections_.end(),
             [](const std::pair<uint8_t, PerfFile::Section>& lhs,
                const std::pair<uint8_t, PerfFile::Section>& rhs) {
+              if (lhs.second.offset == rhs.second.offset) {
+                // Some sections have 0 length and thus there can be offset
+                // collisions. To make sure we parse sections by increasing
+                // offset parse empty sections first.
+                return lhs.second.size > rhs.second.size;
+              }
               return lhs.second.offset > rhs.second.offset;
             });
 
@@ -442,6 +491,58 @@
   return base::OkStatus();
 }
 
+base::Status PerfDataTokenizer::ProcessAuxtraceInfoRecord(Record record) {
+  AuxtraceInfoRecord auxtrace_info;
+  RETURN_IF_ERROR(auxtrace_info.Parse(record));
+  return aux_manager_.OnAuxtraceInfoRecord(std::move(auxtrace_info));
+}
+
+base::Status PerfDataTokenizer::ProcessAuxRecord(Record record) {
+  AuxRecord aux;
+  RETURN_IF_ERROR(aux.Parse(record));
+  return aux_manager_.OnAuxRecord(std::move(aux));
+}
+
+base::Status PerfDataTokenizer::ProcessTimeConvRecord(Record record) {
+  Reader reader(std::move(record.payload));
+  TimeConvRecord time_conv;
+  if (!reader.Read(time_conv)) {
+    return base::ErrStatus("Failed to parse PERF_RECORD_TIME_CONV");
+  }
+
+  return aux_manager_.OnTimeConvRecord(std::move(time_conv));
+}
+
+base::StatusOr<PerfDataTokenizer::ParsingResult>
+PerfDataTokenizer::ParseAuxtraceData() {
+  PERFETTO_CHECK(current_auxtrace_.has_value());
+  const uint64_t size = current_auxtrace_->size;
+  if (buffer_.avail() < size) {
+    return ParsingResult::kMoreDataNeeded;
+  }
+
+  // TODO(carlscab): We could make this more efficient and avoid the copies by
+  // passing several chunks instead.
+  std::optional<TraceBlobView> data =
+      buffer_.SliceOff(buffer_.start_offset(), size);
+  buffer_.PopFrontBytes(size);
+  PERFETTO_CHECK(data.has_value());
+  base::Status status = aux_manager_.OnAuxtraceRecord(
+      std::move(*current_auxtrace_), std::move(*data));
+  current_auxtrace_.reset();
+  parsing_state_ = ParsingState::kParseRecords;
+  RETURN_IF_ERROR(status);
+  return ParseRecords();
+}
+
+base::Status PerfDataTokenizer::ProcessItraceStartRecord(Record record) {
+  ItraceStartRecord start;
+  RETURN_IF_ERROR(start.Parse(record));
+  aux_manager_.OnItraceStartRecord(std::move(start));
+  MaybePushRecord(std::move(record));
+  return base::OkStatus();
+}
+
 base::Status PerfDataTokenizer::NotifyEndOfFile() {
   if (parsing_state_ != ParsingState::kDone) {
     return base::ErrStatus("Premature end of perf file.");
diff --git a/src/trace_processor/importers/perf/perf_data_tokenizer.h b/src/trace_processor/importers/perf/perf_data_tokenizer.h
index 8856178..2600907 100644
--- a/src/trace_processor/importers/perf/perf_data_tokenizer.h
+++ b/src/trace_processor/importers/perf/perf_data_tokenizer.h
@@ -19,15 +19,23 @@
 
 #include <stdint.h>
 #include <cstdint>
+#include <map>
+#include <memory>
 #include <optional>
 #include <vector>
 
 #include "perfetto/base/flat_set.h"
 #include "perfetto/base/status.h"
+#include "perfetto/ext/base/circular_queue.h"
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/status_or.h"
 #include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/importers/perf/aux_data_tokenizer.h"
+#include "src/trace_processor/importers/perf/aux_stream_manager.h"
+#include "src/trace_processor/importers/perf/auxtrace_info_record.h"
+#include "src/trace_processor/importers/perf/auxtrace_record.h"
 #include "src/trace_processor/importers/perf/perf_file.h"
 #include "src/trace_processor/importers/perf/perf_session.h"
 #include "src/trace_processor/util/trace_blob_view_reader.h"
@@ -38,7 +46,11 @@
 
 namespace perf_importer {
 
+class AuxDataTokenizer;
+class AuxDataTokenizerFactory;
 struct Record;
+class SampleId;
+struct AuxRecord;
 
 class PerfDataTokenizer : public ChunkedTraceReader {
  public:
@@ -57,6 +69,7 @@
     kParseAttrs,
     kSeekRecords,
     kParseRecords,
+    kParseAuxtraceData,
     kParseFeatureSections,
     kParseFeatures,
     kDone,
@@ -67,14 +80,21 @@
   base::StatusOr<ParsingResult> ParseAttrs();
   base::StatusOr<ParsingResult> SeekRecords();
   base::StatusOr<ParsingResult> ParseRecords();
+  base::StatusOr<ParsingResult> ParseAuxtraceData();
   base::StatusOr<ParsingResult> ParseFeatureSections();
   base::StatusOr<ParsingResult> ParseFeatures();
 
   base::StatusOr<PerfDataTokenizer::ParsingResult> ParseRecord(Record& record);
-  bool PushRecord(Record record);
+  void MaybePushRecord(Record record);
   base::Status ParseFeature(uint8_t feature_id, TraceBlobView payload);
 
-  base::StatusOr<int64_t> ToTraceTimestamp(std::optional<uint64_t> time);
+  base::Status ProcessRecord(Record record);
+  base::Status ProcessAuxRecord(Record record);
+  base::Status ProcessAuxtraceInfoRecord(Record record);
+  base::Status ProcessTimeConvRecord(Record record);
+  base::Status ProcessItraceStartRecord(Record record);
+
+  base::StatusOr<int64_t> ExtractTraceTimestamp(const Record& record);
 
   TraceProcessorContext* context_;
 
@@ -93,6 +113,9 @@
   util::TraceBlobViewReader buffer_;
 
   int64_t latest_timestamp_ = 0;
+
+  std::optional<AuxtraceRecord> current_auxtrace_;
+  AuxStreamManager aux_manager_;
 };
 
 }  // namespace perf_importer
diff --git a/src/trace_processor/importers/perf/perf_event.h b/src/trace_processor/importers/perf/perf_event.h
index 58077be..c347b8b 100644
--- a/src/trace_processor/importers/perf/perf_event.h
+++ b/src/trace_processor/importers/perf/perf_event.h
@@ -194,6 +194,7 @@
   PERF_RECORD_USER_TYPE_START = 64,
   PERF_RECORD_AUXTRACE_INFO = 70,
   PERF_RECORD_AUXTRACE = 71,
+  PERF_RECORD_TIME_CONV = 79,
   PERF_RECORD_MAX, /* non-ABI */
 };
 
@@ -263,4 +264,21 @@
   PERF_CONTEXT_MAX = static_cast<uint64_t>(-4095),
 };
 
+enum auxtrace_type {
+  PERF_AUXTRACE_UNKNOWN,
+  PERF_AUXTRACE_INTEL_PT,
+  PERF_AUXTRACE_INTEL_BTS,
+  PERF_AUXTRACE_CS_ETM,
+  PERF_AUXTRACE_ARM_SPE,
+  PERF_AUXTRACE_S390_CPUMSF,
+  PERF_AUXTRACE_HISI_PTT,
+};
+
+enum perf_aux_flag {
+  PERF_AUX_FLAG_TRUNCATED = 1U << 0,
+  PERF_AUX_FLAG_OVERWRITE = 1U << 1,
+  PERF_AUX_FLAG_PARTIAL = 1U << 2,
+  PERF_AUX_FLAG_COLLISION = 1U << 3,
+};
+
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_EVENT_H_
diff --git a/src/trace_processor/importers/perf/perf_event_attr.cc b/src/trace_processor/importers/perf/perf_event_attr.cc
index 86c8d23..7542a25 100644
--- a/src/trace_processor/importers/perf/perf_event_attr.cc
+++ b/src/trace_processor/importers/perf/perf_event_attr.cc
@@ -19,9 +19,11 @@
 #include <cstddef>
 #include <cstdint>
 #include <cstring>
+#include <ctime>
 #include <optional>
 
 #include "perfetto/ext/base/string_view.h"
+#include "protos/perfetto/common/builtin_clock.pbzero.h"
 #include "src/trace_processor/importers/perf/perf_counter.h"
 #include "src/trace_processor/importers/perf/perf_event.h"
 #include "src/trace_processor/storage/trace_storage.h"
@@ -93,18 +95,51 @@
 
   return std::nullopt;
 }
+
+size_t GetSampleIdSize(const perf_event_attr& attr) {
+  constexpr uint64_t kSampleIdFlags = PERF_SAMPLE_TID | PERF_SAMPLE_TIME |
+                                      PERF_SAMPLE_ID | PERF_SAMPLE_STREAM_ID |
+                                      PERF_SAMPLE_CPU | PERF_SAMPLE_IDENTIFIER;
+  return CountSetFlags(attr.sample_type & kSampleIdFlags) * kBytesPerField;
+}
+
+ClockTracker::ClockId ExtractClockId(const perf_event_attr& attr) {
+  if (!attr.use_clockid) {
+    return protos::pbzero::BUILTIN_CLOCK_PERF;
+  }
+  switch (attr.clockid) {
+    // Linux perf uses the values in <time.h> not sure if these are portable
+    // across platforms, so using the actual values here just in case.
+    case 0:  // CLOCK_REALTIME
+      return protos::pbzero::BUILTIN_CLOCK_REALTIME;
+    case 1:  // CLOCK_MONOTONIC
+      return protos::pbzero::BUILTIN_CLOCK_MONOTONIC;
+    case 4:  // CLOCK_MONOTONIC_RAW
+      return protos::pbzero::BUILTIN_CLOCK_MONOTONIC_RAW;
+    case 5:  // CLOCK_REALTIME_COARSE
+      return protos::pbzero::BUILTIN_CLOCK_REALTIME_COARSE;
+    case 6:  // CLOCK_MONOTONIC_COARSE
+      return protos::pbzero::BUILTIN_CLOCK_MONOTONIC_COARSE;
+    case 7:  // CLOCK_BOOTTIME
+      return protos::pbzero::BUILTIN_CLOCK_BOOTTIME;
+    default:
+      return protos::pbzero::BUILTIN_CLOCK_UNKNOWN;
+  }
+}
 }  // namespace
 
 PerfEventAttr::PerfEventAttr(TraceProcessorContext* context,
                              tables::PerfSessionTable::Id perf_session_id,
                              perf_event_attr attr)
     : context_(context),
+      clock_id_(ExtractClockId(attr)),
       perf_session_id_(perf_session_id),
       attr_(attr),
       time_offset_from_start_(TimeOffsetFromStartOfSampleRecord(attr_)),
       time_offset_from_end_(TimeOffsetFromEndOfNonSampleRecord(attr_)),
       id_offset_from_start_(IdOffsetFromStartOfSampleRecord(attr_)),
-      id_offset_from_end_(IdOffsetFromEndOfNonSampleRecord(attr_)) {}
+      id_offset_from_end_(IdOffsetFromEndOfNonSampleRecord(attr_)),
+      sample_id_size_(GetSampleIdSize(attr_)) {}
 
 PerfEventAttr::~PerfEventAttr() = default;
 
diff --git a/src/trace_processor/importers/perf/perf_event_attr.h b/src/trace_processor/importers/perf/perf_event_attr.h
index 716f33a..783c2fd 100644
--- a/src/trace_processor/importers/perf/perf_event_attr.h
+++ b/src/trace_processor/importers/perf/perf_event_attr.h
@@ -26,6 +26,7 @@
 #include <utility>
 
 #include "perfetto/trace_processor/ref_counted.h"
+#include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/perf/perf_counter.h"
 #include "src/trace_processor/importers/perf/perf_event.h"
 #include "src/trace_processor/tables/profiler_tables_py.h"
@@ -92,8 +93,12 @@
     event_name_ = std::move(event_name);
   }
 
+  size_t sample_id_size() const { return sample_id_size_; }
+
   PerfCounter& GetOrCreateCounter(uint32_t cpu);
 
+  ClockTracker::ClockId clock_id() const { return clock_id_; }
+
  private:
   bool is_timebase() const {
     // This is what simpleperf uses for events that are not supposed to sample
@@ -104,12 +109,14 @@
   PerfCounter CreateCounter(uint32_t cpu) const;
 
   TraceProcessorContext* const context_;
+  const ClockTracker::ClockId clock_id_;
   tables::PerfSessionTable::Id perf_session_id_;
   perf_event_attr attr_;
   std::optional<size_t> time_offset_from_start_;
   std::optional<size_t> time_offset_from_end_;
   std::optional<size_t> id_offset_from_start_;
   std::optional<size_t> id_offset_from_end_;
+  size_t sample_id_size_;
   std::unordered_map<uint32_t, PerfCounter> counters_;
   std::string event_name_;
 };
diff --git a/src/trace_processor/importers/perf/perf_session.cc b/src/trace_processor/importers/perf/perf_session.cc
index 6b3ed7f..fb4963a 100644
--- a/src/trace_processor/importers/perf/perf_session.cc
+++ b/src/trace_processor/importers/perf/perf_session.cc
@@ -33,6 +33,7 @@
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
+#include "protos/perfetto/common/builtin_clock.pbzero.h"
 #include "src/trace_processor/importers/perf/perf_event.h"
 #include "src/trace_processor/importers/perf/perf_event_attr.h"
 #include "src/trace_processor/importers/perf/reader.h"
@@ -57,17 +58,20 @@
   auto perf_session_id =
       context_->storage->mutable_perf_session_table()->Insert({}).id;
 
-  PerfEventAttr base_attr(context_, perf_session_id, attr_with_ids_[0].attr);
+  RefPtr<PerfEventAttr> first_attr;
   base::FlatHashMap<uint64_t, RefPtr<PerfEventAttr>> attrs_by_id;
   for (const auto& entry : attr_with_ids_) {
     RefPtr<PerfEventAttr> attr(
         new PerfEventAttr(context_, perf_session_id, entry.attr));
-    if (base_attr.sample_id_all() != attr->sample_id_all()) {
+    if (!first_attr) {
+      first_attr = attr;
+    }
+    if (first_attr->sample_id_all() != attr->sample_id_all()) {
       return base::ErrStatus(
           "perf_event_attr with different sample_id_all values");
     }
 
-    if (!OffsetsMatch(base_attr, *attr)) {
+    if (!OffsetsMatch(*first_attr, *attr)) {
       return base::ErrStatus("perf_event_attr with different id offsets");
     }
 
@@ -79,14 +83,14 @@
     }
   }
   if (attr_with_ids_.size() > 1 &&
-      (!base_attr.id_offset_from_start().has_value() ||
-       (base_attr.sample_id_all() &&
-        !base_attr.id_offset_from_end().has_value()))) {
+      (!first_attr->id_offset_from_start().has_value() ||
+       (first_attr->sample_id_all() &&
+        !first_attr->id_offset_from_end().has_value()))) {
     return base::ErrStatus("No id offsets for multiple perf_event_attr");
   }
-  return RefPtr<PerfSession>(new PerfSession(context_, perf_session_id,
-                                             std::move(attrs_by_id),
-                                             attr_with_ids_.size() == 1));
+  return RefPtr<PerfSession>(
+      new PerfSession(context_, perf_session_id, std::move(first_attr),
+                      std::move(attrs_by_id), attr_with_ids_.size() == 1));
 }
 
 base::StatusOr<RefPtr<PerfEventAttr>> PerfSession::FindAttrForRecord(
@@ -96,13 +100,12 @@
     return RefPtr<PerfEventAttr>();
   }
 
-  RefPtr<PerfEventAttr> first(attrs_by_id_.GetIterator().value().get());
   if (has_single_perf_event_attr_) {
-    return first;
+    return first_attr_;
   }
 
-  if (header.type != PERF_RECORD_SAMPLE && !first->sample_id_all()) {
-    return first;
+  if (header.type != PERF_RECORD_SAMPLE && !first_attr_->sample_id_all()) {
+    return first_attr_;
   }
 
   uint64_t id;
@@ -111,7 +114,7 @@
   }
 
   if (id == 0) {
-    return first;
+    return first_attr_;
   }
 
   auto it = FindAttrForEventId(id);
@@ -190,4 +193,13 @@
           base::StringView(base::Join(args, " "))));
 }
 
+bool PerfSession::HasPerfClock() const {
+  for (auto it = attrs_by_id_.GetIterator(); it; ++it) {
+    if (it.value()->clock_id() == protos::pbzero::BUILTIN_CLOCK_PERF) {
+      return true;
+    }
+  }
+  return false;
+}
+
 }  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/perf_session.h b/src/trace_processor/importers/perf/perf_session.h
index a442b54..2a90ab1 100644
--- a/src/trace_processor/importers/perf/perf_session.h
+++ b/src/trace_processor/importers/perf/perf_session.h
@@ -89,6 +89,8 @@
 
   void SetIsSimpleperf() { is_simpleperf_ = true; }
 
+  bool HasPerfClock() const;
+
  private:
   struct BuildIdMapKey {
     int32_t pid;
@@ -107,10 +109,12 @@
 
   PerfSession(TraceProcessorContext* context,
               tables::PerfSessionTable::Id perf_session_id,
+              RefPtr<PerfEventAttr> first_attr,
               base::FlatHashMap<uint64_t, RefPtr<PerfEventAttr>> attrs_by_id,
               bool has_single_perf_event_attr)
       : context_(context),
         perf_session_id_(perf_session_id),
+        first_attr_(std::move(first_attr)),
         attrs_by_id_(std::move(attrs_by_id)),
         has_single_perf_event_attr_(has_single_perf_event_attr) {}
 
@@ -120,7 +124,9 @@
 
   TraceProcessorContext* const context_;
   tables::PerfSessionTable::Id perf_session_id_;
+  RefPtr<PerfEventAttr> first_attr_;
   base::FlatHashMap<uint64_t, RefPtr<PerfEventAttr>> attrs_by_id_;
+
   // Multiple ids can map to the same perf_event_attr. This member tells us
   // whether there was only one perf_event_attr (with potentially different ids
   // associated). This makes the attr lookup given a record trivial and not
diff --git a/src/trace_processor/importers/perf/record.h b/src/trace_processor/importers/perf/record.h
index c4d2279..1aabc7b 100644
--- a/src/trace_processor/importers/perf/record.h
+++ b/src/trace_processor/importers/perf/record.h
@@ -49,6 +49,14 @@
     }
   }
 
+  bool has_trailing_sample_id() const {
+    if (!attr) {
+      return false;
+    }
+    return attr->sample_id_all() && header.type != PERF_RECORD_SAMPLE &&
+           header.type < PERF_RECORD_USER_TYPE_START;
+  }
+
   bool mmap_has_build_id() const {
     return header.misc & PERF_RECORD_MISC_MMAP_BUILD_ID;
   }
diff --git a/src/trace_processor/importers/perf/record_parser.cc b/src/trace_processor/importers/perf/record_parser.cc
index a63a5bd..273fee3 100644
--- a/src/trace_processor/importers/perf/record_parser.cc
+++ b/src/trace_processor/importers/perf/record_parser.cc
@@ -34,6 +34,7 @@
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/stack_profile_tracker.h"
 #include "src/trace_processor/importers/common/virtual_memory_mapping.h"
+#include "src/trace_processor/importers/perf/itrace_start_record.h"
 #include "src/trace_processor/importers/perf/mmap_record.h"
 #include "src/trace_processor/importers/perf/perf_counter.h"
 #include "src/trace_processor/importers/perf/perf_event.h"
@@ -97,9 +98,8 @@
 
 void RecordParser::ParsePerfRecord(int64_t ts, Record record) {
   if (base::Status status = ParseRecord(ts, std::move(record)); !status.ok()) {
-    context_->storage->IncrementStats(record.header.type == PERF_RECORD_SAMPLE
-                                          ? stats::perf_samples_skipped
-                                          : stats::perf_record_skipped);
+    context_->storage->IncrementIndexedStats(
+        stats::perf_record_skipped, static_cast<int>(record.header.type));
   }
 }
 
@@ -117,6 +117,9 @@
     case PERF_RECORD_MMAP2:
       return ParseMmap2(std::move(record));
 
+    case PERF_RECORD_ITRACE_START:
+      return ParseItraceStart(std::move(record));
+
     case PERF_RECORD_AUX:
     case PERF_RECORD_AUXTRACE:
     case PERF_RECORD_AUXTRACE_INFO:
@@ -225,7 +228,7 @@
       context_->storage->IncrementStats(stats::perf_dummy_mapping_used);
       // Simpleperf will not create mappings for anonymous executable mappings
       // which are used by JITted code (e.g. V8 JavaScript).
-      mapping = mapping_tracker_->GetDummyMapping();
+      mapping = GetDummyMapping(upid);
     }
 
     const FrameId frame_id =
@@ -294,6 +297,13 @@
   return base::OkStatus();
 }
 
+base::Status RecordParser::ParseItraceStart(Record record) {
+  ItraceStartRecord start;
+  RETURN_IF_ERROR(start.Parse(record));
+  context_->process_tracker->UpdateThread(start.tid, start.pid);
+  return base::OkStatus();
+}
+
 UniquePid RecordParser::GetUpid(const CommonMmapRecordFields& fields) const {
   UniqueTid utid =
       context_->process_tracker->UpdateThread(fields.tid, fields.pid);
@@ -346,4 +356,14 @@
   return base::OkStatus();
 }
 
+DummyMemoryMapping* RecordParser::GetDummyMapping(UniquePid upid) {
+  if (auto it = dummy_mappings_.Find(upid); it) {
+    return *it;
+  }
+
+  DummyMemoryMapping* mapping = &mapping_tracker_->CreateDummyMapping("");
+  dummy_mappings_.Insert(upid, mapping);
+  return mapping;
+}
+
 }  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/record_parser.h b/src/trace_processor/importers/perf/record_parser.h
index 76926d1..e226474 100644
--- a/src/trace_processor/importers/perf/record_parser.h
+++ b/src/trace_processor/importers/perf/record_parser.h
@@ -22,6 +22,7 @@
 #include <optional>
 
 #include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/perf/mmap_record.h"
 #include "src/trace_processor/importers/perf/record.h"
@@ -31,6 +32,7 @@
 namespace perfetto {
 namespace trace_processor {
 
+class DummyMemoryMapping;
 class MappingTracker;
 class TraceProcessorContext;
 
@@ -53,6 +55,7 @@
   base::Status ParseComm(Record record);
   base::Status ParseMmap(Record record);
   base::Status ParseMmap2(Record record);
+  base::Status ParseItraceStart(Record record);
 
   base::Status InternSample(Sample sample);
 
@@ -66,8 +69,11 @@
 
   UniquePid GetUpid(const CommonMmapRecordFields& fields) const;
 
-  TraceProcessorContext* const context_ = nullptr;
+  DummyMemoryMapping* GetDummyMapping(UniquePid upid);
+
+  TraceProcessorContext* const context_;
   MappingTracker* const mapping_tracker_;
+  base::FlatHashMap<UniquePid, DummyMemoryMapping*> dummy_mappings_;
 };
 
 }  // namespace perf_importer
diff --git a/src/trace_processor/importers/perf/sample_id.cc b/src/trace_processor/importers/perf/sample_id.cc
new file mode 100644
index 0000000..f9f4240
--- /dev/null
+++ b/src/trace_processor/importers/perf/sample_id.cc
@@ -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.
+ */
+
+#include "src/trace_processor/importers/perf/sample_id.h"
+#include <cstdint>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/importers/perf/perf_event.h"
+#include "src/trace_processor/importers/perf/perf_event_attr.h"
+#include "src/trace_processor/importers/perf/reader.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+base::Status SampleId::ParseFromRecord(const Record& record) {
+  PERFETTO_CHECK(record.header.type != PERF_RECORD_SAMPLE);
+  if (!record.attr || !record.attr->sample_id_all()) {
+    sample_type_ = 0;
+    return base::OkStatus();
+  }
+
+  Reader reader(record.payload.copy());
+
+  size_t size = record.attr->sample_id_size();
+  if (size > record.payload.size()) {
+    return base::ErrStatus(
+        "Record is too small to hold a SampleId. Expected at least %zu bytes, "
+        "but found %zu",
+        size, record.payload.size());
+  }
+
+  PERFETTO_CHECK(reader.Skip(record.payload.size() - size));
+  if (!ReadFrom(*record.attr, reader)) {
+    return base::ErrStatus("Failed to parse SampleId");
+  }
+  return base::OkStatus();
+}
+
+bool SampleId::ReadFrom(const PerfEventAttr& attr, Reader& reader) {
+  sample_type_ = attr.sample_type();
+
+  if (sample_type_ & PERF_SAMPLE_TID) {
+    if (!reader.Read(pid_) || !reader.Read(tid_)) {
+      return false;
+    }
+  }
+  if (sample_type_ & PERF_SAMPLE_TIME) {
+    if (!reader.Read(time_)) {
+      return false;
+    }
+  }
+  if (sample_type_ & PERF_SAMPLE_ID) {
+    if (!reader.Read(id_)) {
+      return false;
+    }
+  }
+  if (sample_type_ & PERF_SAMPLE_STREAM_ID) {
+    if (!reader.Read(stream_id_)) {
+      return false;
+    }
+  }
+  if (sample_type_ & PERF_SAMPLE_CPU) {
+    if (!reader.Read(cpu_) || !reader.Skip(sizeof(uint32_t))) {
+      return false;
+    }
+  }
+  if (sample_type_ & PERF_SAMPLE_IDENTIFIER) {
+    if (!reader.Read(id_)) {
+      return false;
+    }
+  }
+  return true;
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/sample_id.h b/src/trace_processor/importers/perf/sample_id.h
new file mode 100644
index 0000000..fc033fb
--- /dev/null
+++ b/src/trace_processor/importers/perf/sample_id.h
@@ -0,0 +1,87 @@
+/*
+ * 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_PERF_SAMPLE_ID_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_SAMPLE_ID_H_
+
+#include <cstdint>
+#include <optional>
+
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/perf/perf_event.h"
+#include "src/trace_processor/importers/perf/record.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+class PerfEventAttr;
+class Reader;
+
+class SampleId {
+ public:
+  base::Status ParseFromRecord(const Record& record);
+  bool ReadFrom(const PerfEventAttr& attr, Reader& reader);
+
+  SampleId() : sample_type_(0) {}
+
+  std::optional<uint32_t> tid() const {
+    return sample_type_ & PERF_SAMPLE_TID ? std::make_optional(tid_)
+                                          : std::nullopt;
+  }
+  std::optional<uint32_t> pid() const {
+    return sample_type_ & PERF_SAMPLE_TID ? std::make_optional(pid_)
+                                          : std::nullopt;
+  }
+  std::optional<uint64_t> time() const {
+    return sample_type_ & PERF_SAMPLE_TIME ? std::make_optional(time_)
+                                           : std::nullopt;
+  }
+  std::optional<uint64_t> id() const {
+    return sample_type_ & (PERF_SAMPLE_ID | PERF_SAMPLE_IDENTIFIER)
+               ? std::make_optional(id_)
+               : std::nullopt;
+  }
+  std::optional<uint64_t> stream_id() const {
+    return sample_type_ & PERF_SAMPLE_STREAM_ID ? std::make_optional(stream_id_)
+                                                : std::nullopt;
+  }
+  std::optional<uint32_t> cpu() const {
+    return sample_type_ & PERF_SAMPLE_CPU ? std::make_optional(cpu_)
+                                          : std::nullopt;
+  }
+
+  void set_cpu(std::optional<uint32_t> cpu) {
+    if (cpu.has_value()) {
+      sample_type_ |= PERF_SAMPLE_CPU;
+      cpu_ = *cpu;
+    } else {
+      sample_type_ &= ~static_cast<uint64_t>(PERF_SAMPLE_CPU);
+    }
+  }
+
+ private:
+  uint32_t tid_;
+  uint32_t pid_;
+  uint64_t time_;
+  uint64_t id_;
+  uint64_t stream_id_;
+  uint32_t cpu_;
+  uint64_t sample_type_;
+};
+
+}  // namespace perfetto::trace_processor::perf_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_SAMPLE_ID_H_
diff --git a/src/trace_processor/importers/perf/spe.h b/src/trace_processor/importers/perf/spe.h
new file mode 100644
index 0000000..327e708
--- /dev/null
+++ b/src/trace_processor/importers/perf/spe.h
@@ -0,0 +1,405 @@
+/*
+ * 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.
+ */
+
+// Collection of constant and utilities to parse SPE data.
+// SPE packet spec can be found here:
+// Arm Architecture Reference Manual for A-profile architecture
+// https://developer.arm.com/documentation/ddi0487/latest/
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_SPE_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_SPE_H_
+
+#include <cstddef>
+#include <cstdint>
+#include "perfetto/base/logging.h"
+#include "perfetto/public/compiler.h"
+
+namespace perfetto::trace_processor::perf_importer::spe {
+
+// Test whether a given bit is set. e.g.
+// IsBitSet<1>(0b0010) == true
+// IsBitSet<0>(0b0010) == false
+template <int bit, typename T>
+inline constexpr bool IsBitSet(T value) {
+  static_assert(std::is_unsigned_v<T>);
+  static_assert(bit < sizeof(T) * 8);
+  return value & (T(1) << bit);
+}
+
+// Index value in Address packets
+enum class AddressIndex : uint8_t {
+  kInstruction,
+  kBranchTarget,
+  kDataVirtual,
+  kDataPhysical,
+  kPrevBranchTarget,
+  kUnknown,
+  kMax = kUnknown,
+};
+
+// Index value in Counter packets
+enum class CounterIndex : uint8_t {
+  kTotalLatency,
+  kIssueLatency,
+  kTranslationLatency,
+  kUnknown,
+  kMax = kUnknown,
+};
+
+enum class ContextIndex : uint8_t {
+  kEl1,
+  kEl2,
+  kUnknown,
+  kMax = kUnknown,
+};
+
+// Operation class for OperationType packets
+enum class OperationClass : uint8_t {
+  kOther,
+  kLoadOrStoreOrAtomic,
+  kBranchOrExceptionReturn,
+  kUnknown,
+  kMax = kUnknown,
+};
+
+// Data source types for a payload of a DataSource packet
+enum class DataSource : uint8_t {
+  kL1D,
+  kL2,
+  kPeerCore,
+  kLocalCluster,
+  kSysCache,
+  kPeerCluster,
+  kRemote,
+  kDram,
+  kUnknown,
+  kMax = kUnknown,
+};
+
+// Exception levels instructions can execute in.
+enum class ExceptionLevel { kEl0, kEl1, kEl2, kEl3, kMax = kEl3 };
+
+// Common constants to both short and extended headers
+constexpr uint8_t COMMON_HEADER_MASK = 0b1111'1000;
+constexpr uint8_t COMMON_HEADER_ADDRESS_PACKET = 0b1011'0000;
+constexpr uint8_t COMMON_HEADER_COUNTER_PACKET = 0b1001'1000;
+
+constexpr uint8_t COMMON_HEADER_SIZE_MASK = 0b0011'0000;
+constexpr uint8_t COMMON_HEADER_SIZE_MASK_RSHIFT = 4;
+
+constexpr uint8_t COMMON_HEADER_NO_PAYLOAD_MASK = 0b1110'0000;
+constexpr uint8_t COMMON_HEADER_NO_PAYLOAD = 0b0000'0000;
+
+// Constants for short headers
+constexpr uint8_t SHORT_HEADER_PADDING = 0b0000'0000;
+constexpr uint8_t SHORT_HEADER_END_PACKET = 0b0000'0001;
+constexpr uint8_t SHORT_HEADER_TIMESTAMP_PACKET = 0b0111'0001;
+
+constexpr uint8_t SHORT_HEADER_MASK_1 = 0b1100'1111;
+constexpr uint8_t SHORT_HEADER_EVENTS_PACKET = 0b0100'0010;
+constexpr uint8_t SHORT_HEADER_DATA_SOURCE_PACKET = 0b0100'0011;
+
+constexpr uint8_t SHORT_HEADER_MASK_2 = 0b1111'1100;
+constexpr uint8_t SHORT_HEADER_CONTEXT_PACKET = 0b0110'0100;
+constexpr uint8_t SHORT_HEADER_OPERATION_TYPE_PACKET = 0b0100'1000;
+
+constexpr uint8_t SHORT_HEADER_INDEX_MASK = 0b0000'0111;
+
+// Constants for extended headers
+constexpr uint8_t EXTENDED_HEADER_MASK = 0b1110'0000;
+constexpr uint8_t EXTENDED_HEADER = 0b0010'0000;
+
+constexpr uint8_t EXTENDED_HEADER_INDEX_MASK = 0b0000'0011;
+constexpr uint8_t EXTENDED_HEADER_INDEX_LSHIFT = 3;
+
+// OperationType packet constants
+constexpr uint8_t PKT_OP_TYPE_HEADER_CLASS_MASK = 0b0000'0011;
+constexpr uint8_t PKT_OP_TYPE_HEADER_CLASS_OTHER = 0b0000'0000;
+constexpr uint8_t PKT_OP_TYPE_HEADER_CLASS_LD_ST_ATOMIC = 0b0000'0001;
+constexpr uint8_t PKT_OP_TYPE_HEADER_CLASS_BR_ERET = 0b0000'0010;
+
+constexpr uint8_t PKT_OP_TYPE_PAYLOAD_SUBCLASS_OTHER_MASK = 0b1111'1110;
+constexpr uint8_t PKT_OP_TYPE_PAYLOAD_SUBCLASS_OTHER = 0b0000'0000;
+
+constexpr uint8_t PKT_OP_TYPE_PAYLOAD_SUBCLASS_SVE_OTHER_MASK = 0b1000'1001;
+constexpr uint8_t PKT_OP_TYPE_PAYLOAD_SUBCLASS_SVE_OTHER = 0b0000'1000;
+
+// DataSource packet constants
+constexpr uint16_t PKT_DATA_SOURCE_PAYLOAD_L1D = 0b0000'0000;
+constexpr uint16_t PKT_DATA_SOURCE_PAYLOAD_L2 = 0b0000'1000;
+constexpr uint16_t PKT_DATA_SOURCE_PAYLOAD_PEER_CORE = 0b0000'1001;
+constexpr uint16_t PKT_DATA_SOURCE_PAYLOAD_LOCAL_CLUSTER = 0b0000'1010;
+constexpr uint16_t PKT_DATA_SOURCE_PAYLOAD_SYS_CACHE = 0b0000'1011;
+constexpr uint16_t PKT_DATA_SOURCE_PAYLOAD_PEER_CLUSTER = 0b0000'1100;
+constexpr uint16_t PKT_DATA_SOURCE_PAYLOAD_REMOTE = 0b0000'1101;
+constexpr uint16_t PKT_DATA_SOURCE_PAYLOAD_DRAM = 0b0000'1110;
+
+// Helper to cast a value into a typed enum. Takes care of invalid inputs by
+// returning the `kUnknown` value.
+template <typename T>
+T ToEnum(uint8_t val) {
+  if (PERFETTO_LIKELY(val < static_cast<uint8_t>(T::kMax))) {
+    return static_cast<T>(val);
+  }
+  return T::kUnknown;
+}
+
+// An SPE record is a collection of packets. An End or Timestamp packet signals
+// the end of a record. Each record consists of a 1 or 2 byte header followed by
+// 0 - 4 bytes of payload. The `ShortHeader`, and `ExtendedHeader` hide all the
+// low level bit fiddling details of handling packets. When parsing a stream of
+// SPE records you can just check the first byte in the stream to determine if
+// it belongs to a short or extended header and then use the appropiate class to
+// determine packet type, payload length and packet details. There are other
+// helper classes to parse payloads for the different packets.
+
+// Checks if a header bytes is a padding packet. (no payload)
+inline bool IsPadding(uint8_t byte) {
+  return byte == SHORT_HEADER_PADDING;
+}
+
+// Checks if a header byte corresponds to an extended header.
+inline bool IsExtendedHeader(uint8_t byte) {
+  return (byte & EXTENDED_HEADER_MASK) == EXTENDED_HEADER;
+}
+
+class ShortHeader {
+ public:
+  explicit ShortHeader(uint8_t byte) : byte_0_(byte) {
+    PERFETTO_DCHECK(!IsExtendedHeader(byte));
+  }
+
+  inline bool IsPadding() { return byte_0_ == SHORT_HEADER_PADDING; }
+
+  inline bool IsEndPacket() { return byte_0_ == SHORT_HEADER_END_PACKET; }
+
+  inline bool IsTimestampPacket() {
+    return byte_0_ == SHORT_HEADER_TIMESTAMP_PACKET;
+  }
+
+  bool IsAddressPacket() const {
+    return (byte_0_ & COMMON_HEADER_MASK) == COMMON_HEADER_ADDRESS_PACKET;
+  }
+
+  AddressIndex GetAddressIndex() const {
+    PERFETTO_DCHECK(IsAddressPacket());
+    return ToEnum<AddressIndex>(index());
+  }
+
+  bool IsCounterPacket() const {
+    return (byte_0_ & COMMON_HEADER_MASK) == COMMON_HEADER_COUNTER_PACKET;
+  }
+
+  CounterIndex GetCounterIndex() const {
+    PERFETTO_DCHECK(IsCounterPacket());
+    return ToEnum<CounterIndex>(index());
+  }
+
+  bool IsEventsPacket() const {
+    return (byte_0_ & SHORT_HEADER_MASK_1) == SHORT_HEADER_EVENTS_PACKET;
+  }
+
+  bool IsContextPacket() const {
+    return (byte_0_ & SHORT_HEADER_MASK_2) == SHORT_HEADER_CONTEXT_PACKET;
+  }
+
+  ContextIndex GetContextIndex() const { return ToEnum<ContextIndex>(index()); }
+
+  bool IsDataSourcePacket() const {
+    return (byte_0_ & SHORT_HEADER_MASK_1) == SHORT_HEADER_DATA_SOURCE_PACKET;
+  }
+
+  DataSource GetDataSource(uint64_t payload) {
+    PERFETTO_DCHECK(IsDataSourcePacket());
+    switch (payload) {
+      case PKT_DATA_SOURCE_PAYLOAD_L1D:
+        return DataSource::kL1D;
+      case PKT_DATA_SOURCE_PAYLOAD_L2:
+        return DataSource::kL2;
+      case PKT_DATA_SOURCE_PAYLOAD_PEER_CORE:
+        return DataSource::kPeerCore;
+      case PKT_DATA_SOURCE_PAYLOAD_LOCAL_CLUSTER:
+        return DataSource::kLocalCluster;
+      case PKT_DATA_SOURCE_PAYLOAD_SYS_CACHE:
+        return DataSource::kSysCache;
+      case PKT_DATA_SOURCE_PAYLOAD_PEER_CLUSTER:
+        return DataSource::kPeerCluster;
+      case PKT_DATA_SOURCE_PAYLOAD_REMOTE:
+        return DataSource::kRemote;
+      case PKT_DATA_SOURCE_PAYLOAD_DRAM:
+        return DataSource::kDram;
+      default:
+        break;
+    }
+    return DataSource::kUnknown;
+  }
+
+  bool IsOperationTypePacket() const {
+    return (byte_0_ & SHORT_HEADER_MASK_2) ==
+           SHORT_HEADER_OPERATION_TYPE_PACKET;
+  }
+
+  OperationClass GetOperationClass() const {
+    PERFETTO_DCHECK(IsOperationTypePacket());
+    switch (byte_0_ & PKT_OP_TYPE_HEADER_CLASS_MASK) {
+      case PKT_OP_TYPE_HEADER_CLASS_OTHER:
+        return OperationClass::kOther;
+
+      case PKT_OP_TYPE_HEADER_CLASS_LD_ST_ATOMIC:
+        return OperationClass::kLoadOrStoreOrAtomic;
+
+      case PKT_OP_TYPE_HEADER_CLASS_BR_ERET:
+        return OperationClass::kBranchOrExceptionReturn;
+
+      default:
+        break;
+    }
+    return OperationClass::kUnknown;
+  }
+
+  bool HasPayload() const {
+    return (byte_0_ & COMMON_HEADER_NO_PAYLOAD_MASK) !=
+           COMMON_HEADER_NO_PAYLOAD;
+  }
+
+  uint8_t GetPayloadSize() const {
+    PERFETTO_DCHECK(!IsExtendedHeader(byte_0_));
+    if (!HasPayload()) {
+      return 0;
+    }
+    return static_cast<uint8_t>(1 << ((byte_0_ & COMMON_HEADER_SIZE_MASK) >>
+                                      COMMON_HEADER_SIZE_MASK_RSHIFT));
+  }
+
+ private:
+  friend class ExtendedHeader;
+
+  uint8_t index() const { return byte_0_ & SHORT_HEADER_INDEX_MASK; }
+
+  uint8_t byte_0_;
+};
+
+class ExtendedHeader {
+ public:
+  ExtendedHeader(uint8_t byte_0, uint8_t byte_1)
+      : byte_0_(byte_0), short_header_(byte_1) {
+    PERFETTO_DCHECK(IsExtendedHeader(byte_0));
+  }
+
+  bool IsAddressPacket() const { return short_header_.IsAddressPacket(); }
+
+  AddressIndex GetAddressIndex() const { return ToEnum<AddressIndex>(index()); }
+
+  bool IsCounterPacket() const { return short_header_.IsCounterPacket(); }
+
+  CounterIndex GetCounterIndex() const { return ToEnum<CounterIndex>(index()); }
+
+  inline uint8_t GetPayloadSize() { return short_header_.GetPayloadSize(); }
+
+ private:
+  uint8_t byte_1() const { return short_header_.byte_0_; }
+
+  uint8_t index() const {
+    return static_cast<uint8_t>((byte_0_ & EXTENDED_HEADER_INDEX_MASK)
+                                << EXTENDED_HEADER_INDEX_LSHIFT) +
+           short_header_.index();
+  }
+
+  uint8_t byte_0_;
+  ShortHeader short_header_;
+};
+
+enum class OperationOtherSubclass : uint8_t {
+  kOther,
+  kSveVecOp,
+  kUnknown,
+  kMax = kUnknown
+};
+class OperationTypeOtherPayload {
+ public:
+  explicit OperationTypeOtherPayload(uint8_t payload) : payload_(payload) {}
+
+  OperationOtherSubclass subclass() const {
+    if ((payload_ & PKT_OP_TYPE_PAYLOAD_SUBCLASS_OTHER_MASK) ==
+        PKT_OP_TYPE_PAYLOAD_SUBCLASS_OTHER) {
+      return OperationOtherSubclass::kOther;
+    }
+    if ((payload_ & PKT_OP_TYPE_PAYLOAD_SUBCLASS_SVE_OTHER_MASK) ==
+        PKT_OP_TYPE_PAYLOAD_SUBCLASS_SVE_OTHER) {
+      return OperationOtherSubclass::kSveVecOp;
+    }
+    return OperationOtherSubclass::kUnknown;
+  }
+
+ private:
+  uint8_t payload_;
+};
+
+class OperationTypeLdStAtPayload {
+ public:
+  explicit OperationTypeLdStAtPayload(uint8_t payload) : payload_(payload) {}
+
+  bool IsStore() const { return IsBitSet<0>(payload_); }
+
+ private:
+  uint8_t payload_;
+};
+
+namespace internal {
+inline uint64_t GetPacketAddressAddress(uint64_t payload) {
+  return payload & 0x0FFFFFFFFFFFFFFF;
+}
+
+inline bool GetPacketAddressNs(uint64_t payload) {
+  return IsBitSet<63>(payload);
+}
+
+inline ExceptionLevel GetPacketAddressEl(uint64_t payload) {
+  return static_cast<ExceptionLevel>((payload >> 61) & 0x03);
+}
+
+inline bool GetPacketAddressNse(uint64_t payload) {
+  return IsBitSet<60>(payload);
+}
+}  // namespace internal
+
+struct InstructionVirtualAddress {
+  explicit InstructionVirtualAddress(uint64_t payload)
+      : address(internal::GetPacketAddressAddress(payload)),
+        el(internal::GetPacketAddressEl(payload)),
+        ns(internal::GetPacketAddressNs(payload)),
+        nse(internal::GetPacketAddressNse(payload)) {}
+  uint64_t address;
+  ExceptionLevel el;
+  bool ns;
+  bool nse;
+};
+
+struct DataVirtualAddress {
+  explicit DataVirtualAddress(uint64_t payload)
+      : address(internal::GetPacketAddressAddress(payload)) {}
+  uint64_t address;
+};
+
+struct DataPhysicalAddress {
+  explicit DataPhysicalAddress(uint64_t payload)
+      : address(internal::GetPacketAddressAddress(payload)) {}
+  uint64_t address;
+};
+
+}  // namespace perfetto::trace_processor::perf_importer::spe
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_SPE_H_
diff --git a/src/trace_processor/importers/perf/spe_record_parser.cc b/src/trace_processor/importers/perf/spe_record_parser.cc
new file mode 100644
index 0000000..6dc4c6a
--- /dev/null
+++ b/src/trace_processor/importers/perf/spe_record_parser.cc
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf/spe_record_parser.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/common/mapping_tracker.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
+#include "src/trace_processor/importers/perf/reader.h"
+#include "src/trace_processor/importers/perf/spe.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
+#include "src/trace_processor/tables/perf_tables_py.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+// static
+const char* SpeRecordParserImpl::ToString(spe::DataSource ds) {
+  switch (ds) {
+    case spe::DataSource::kUnknown:
+      return "UNKNOWN";
+    case spe::DataSource::kL1D:
+      return "L1D";
+    case spe::DataSource::kL2:
+      return "L2";
+    case spe::DataSource::kPeerCore:
+      return "PEER_CORE";
+    case spe::DataSource::kLocalCluster:
+      return "LOCAL_CLUSTER";
+    case spe::DataSource::kSysCache:
+      return "SYS_CACHE";
+    case spe::DataSource::kPeerCluster:
+      return "PEER_CLUSTER";
+    case spe::DataSource::kRemote:
+      return "REMOTE";
+    case spe::DataSource::kDram:
+      return "DRAM";
+  }
+  PERFETTO_FATAL("For GCC");
+}
+
+// static
+const char* SpeRecordParserImpl::ToString(spe::ExceptionLevel el) {
+  switch (el) {
+    case spe::ExceptionLevel::kEl0:
+      return "EL0";
+    case spe::ExceptionLevel::kEl1:
+      return "EL1";
+    case spe::ExceptionLevel::kEl2:
+      return "EL2";
+    case spe::ExceptionLevel::kEl3:
+      return "EL3";
+  }
+  PERFETTO_FATAL("For GCC");
+}
+
+// static
+const char* SpeRecordParserImpl::ToString(OperationName name) {
+  switch (name) {
+    case OperationName::kOther:
+      return "OTHER";
+    case OperationName::kSveVecOp:
+      return "SVE_VEC_OP";
+    case OperationName::kLoad:
+      return "LOAD";
+    case OperationName::kStore:
+      return "STORE";
+    case OperationName::kBranch:
+      return "BRANCH";
+    case OperationName::kUnknown:
+      return "UNKNOWN";
+  }
+  PERFETTO_FATAL("For GCC");
+}
+
+StringId SpeRecordParserImpl::ToStringId(OperationName name) {
+  if (operation_name_strings_[name] == kNullStringId) {
+    operation_name_strings_[name] =
+        context_->storage->InternString(ToString(name));
+  }
+  return operation_name_strings_[name];
+}
+
+StringId SpeRecordParserImpl::ToStringId(spe::ExceptionLevel el) {
+  if (exception_level_strings_[el] == kNullStringId) {
+    exception_level_strings_[el] =
+        context_->storage->InternString(ToString(el));
+  }
+  return exception_level_strings_[el];
+}
+
+StringId SpeRecordParserImpl::ToStringId(spe::DataSource ds) {
+  if (data_source_strings_[ds] == kNullStringId) {
+    data_source_strings_[ds] = context_->storage->InternString(ToString(ds));
+  }
+  return data_source_strings_[ds];
+}
+
+SpeRecordParserImpl::SpeRecordParserImpl(TraceProcessorContext* context)
+    : context_(context), reader_(TraceBlobView()) {}
+
+void SpeRecordParserImpl::ParseSpeRecord(int64_t ts, TraceBlobView data) {
+  reader_ = Reader(std::move(data));
+  inflight_row_ = {};
+  inflight_row_.ts = ts;
+  inflight_record_ = {};
+
+  // No need to check that there is enough data as this has been validated by
+  // the tokenization step.
+  while (reader_.size_left() != 0) {
+    uint8_t byte_0;
+    reader_.Read(byte_0);
+
+    if (spe::IsExtendedHeader(byte_0)) {
+      uint8_t byte_1;
+      reader_.Read(byte_1);
+      spe::ExtendedHeader extended_header(byte_0, byte_1);
+      ReadExtendedPacket(extended_header);
+    } else {
+      ReadShortPacket(spe::ShortHeader(byte_0));
+    }
+  }
+  if (!inflight_record_.instruction_address) {
+    context_->storage->mutable_spe_record_table()->Insert(inflight_row_);
+    return;
+  }
+
+  const auto& inst = *inflight_record_.instruction_address;
+
+  inflight_row_.exception_level = ToStringId(inst.el);
+
+  if (inst.el == spe::ExceptionLevel::kEl0 && inflight_row_.utid) {
+    const auto upid =
+        *context_->storage->thread_table()
+             .FindById(tables::ThreadTable::Id(*inflight_row_.utid))
+             ->upid();
+
+    VirtualMemoryMapping* mapping =
+        context_->mapping_tracker->FindUserMappingForAddress(upid,
+                                                             inst.address);
+    if (mapping) {
+      inflight_row_.instruction_frame_id =
+          mapping->InternFrame(mapping->ToRelativePc(inst.address), "");
+    }
+  } else if (inst.el == spe::ExceptionLevel::kEl1) {
+    VirtualMemoryMapping* mapping =
+        context_->mapping_tracker->FindKernelMappingForAddress(inst.address);
+    if (mapping) {
+      inflight_row_.instruction_frame_id =
+          mapping->InternFrame(mapping->ToRelativePc(inst.address), "");
+    }
+  }
+
+  if (!inflight_row_.instruction_frame_id.has_value()) {
+    inflight_row_.instruction_frame_id = GetDummyMapping()->InternFrame(
+        GetDummyMapping()->ToRelativePc(inst.address), "");
+  }
+
+  context_->storage->mutable_spe_record_table()->Insert(inflight_row_);
+}
+
+void SpeRecordParserImpl::ReadShortPacket(spe::ShortHeader short_header) {
+  if (short_header.IsAddressPacket()) {
+    ReadAddressPacket(short_header.GetAddressIndex());
+
+  } else if (short_header.IsCounterPacket()) {
+    ReadCounterPacket(short_header.GetCounterIndex());
+
+  } else if (short_header.IsEventsPacket()) {
+    ReadEventsPacket(short_header);
+
+  } else if (short_header.IsContextPacket()) {
+    ReadContextPacket(short_header);
+
+  } else if (short_header.IsOperationTypePacket()) {
+    ReadOperationTypePacket(short_header);
+
+  } else if (short_header.IsDataSourcePacket()) {
+    ReadDataSourcePacket(short_header);
+
+  } else {
+    reader_.Skip(short_header.GetPayloadSize());
+  }
+}
+
+void SpeRecordParserImpl::ReadExtendedPacket(
+    spe::ExtendedHeader extended_header) {
+  if (extended_header.IsAddressPacket()) {
+    ReadAddressPacket(extended_header.GetAddressIndex());
+
+  } else if (extended_header.IsCounterPacket()) {
+    ReadCounterPacket(extended_header.GetCounterIndex());
+
+  } else {
+    reader_.Skip(extended_header.GetPayloadSize());
+  }
+}
+
+void SpeRecordParserImpl::ReadAddressPacket(spe::AddressIndex index) {
+  uint64_t payload;
+  reader_.Read(payload);
+
+  switch (index) {
+    case spe::AddressIndex::kInstruction:
+      inflight_record_.instruction_address =
+          spe::InstructionVirtualAddress(payload);
+      break;
+
+    case spe::AddressIndex::kDataVirtual:
+      inflight_row_.data_virtual_address =
+          static_cast<int64_t>(spe::DataVirtualAddress(payload).address);
+      break;
+
+    case spe::AddressIndex::kDataPhysical:
+      inflight_row_.data_physical_address =
+          static_cast<int64_t>(spe::DataPhysicalAddress(payload).address);
+      break;
+
+    case spe::AddressIndex::kBranchTarget:
+    case spe::AddressIndex::kPrevBranchTarget:
+    case spe::AddressIndex::kUnknown:
+      break;
+  }
+}
+
+void SpeRecordParserImpl::ReadCounterPacket(spe::CounterIndex index) {
+  uint16_t value;
+  reader_.Read(value);
+  switch (index) {
+    case spe::CounterIndex::kTotalLatency:
+      inflight_row_.total_latency = value;
+      break;
+
+    case spe::CounterIndex::kIssueLatency:
+      inflight_row_.issue_latency = value;
+      break;
+
+    case spe::CounterIndex::kTranslationLatency:
+      inflight_row_.translation_latency = value;
+      break;
+
+    case spe::CounterIndex::kUnknown:
+      break;
+  }
+}
+
+void SpeRecordParserImpl::ReadEventsPacket(spe::ShortHeader short_header) {
+  inflight_row_.events_bitmask =
+      static_cast<int64_t>(ReadPayload(short_header));
+}
+
+void SpeRecordParserImpl::ReadContextPacket(spe::ShortHeader short_header) {
+  uint32_t tid;
+  reader_.Read(tid);
+  inflight_row_.utid = context_->process_tracker->GetOrCreateThread(tid);
+  switch (short_header.GetContextIndex()) {
+    case spe::ContextIndex::kEl1:
+    case spe::ContextIndex::kEl2:
+    case spe::ContextIndex::kUnknown:
+      break;
+  }
+}
+
+void SpeRecordParserImpl::ReadOperationTypePacket(
+    spe::ShortHeader short_header) {
+  uint8_t payload;
+  reader_.Read(payload);
+  inflight_row_.operation = ToStringId(GetOperationName(short_header, payload));
+}
+
+SpeRecordParserImpl::OperationName SpeRecordParserImpl::GetOperationName(
+    spe::ShortHeader short_header,
+    uint8_t payload) const {
+  switch (short_header.GetOperationClass()) {
+    case spe::OperationClass::kOther:
+      switch (spe::OperationTypeOtherPayload(payload).subclass()) {
+        case spe::OperationOtherSubclass::kOther:
+          return OperationName::kOther;
+        case spe::OperationOtherSubclass::kSveVecOp:
+          return OperationName::kSveVecOp;
+        case spe::OperationOtherSubclass::kUnknown:
+          return OperationName::kUnknown;
+      }
+      PERFETTO_FATAL("For GCC");
+
+    case spe::OperationClass::kLoadOrStoreOrAtomic:
+      if (spe::OperationTypeLdStAtPayload(payload).IsStore()) {
+        return OperationName::kStore;
+      }
+      return OperationName::kLoad;
+
+    case spe::OperationClass::kBranchOrExceptionReturn:
+      return OperationName::kBranch;
+
+    case spe::OperationClass::kUnknown:
+      return OperationName::kUnknown;
+  }
+  PERFETTO_FATAL("For GCC");
+}
+
+VirtualMemoryMapping* SpeRecordParserImpl::GetDummyMapping() {
+  if (!dummy_mapping_) {
+    dummy_mapping_ =
+        &context_->mapping_tracker->CreateDummyMapping("spe_dummy");
+  }
+  return dummy_mapping_;
+}
+
+void SpeRecordParserImpl::ReadDataSourcePacket(spe::ShortHeader short_header) {
+  inflight_row_.data_source =
+      ToStringId(short_header.GetDataSource(ReadPayload(short_header)));
+}
+
+uint64_t SpeRecordParserImpl::ReadPayload(spe::ShortHeader short_header) {
+  switch (short_header.GetPayloadSize()) {
+    case 1: {
+      uint8_t data;
+      reader_.Read(data);
+      return data;
+    }
+    case 2: {
+      uint16_t data;
+      reader_.Read(data);
+      return data;
+    }
+    case 4: {
+      uint32_t data;
+      reader_.Read(data);
+      return data;
+    }
+    case 8: {
+      uint64_t data;
+      reader_.Read(data);
+      return data;
+    }
+    default:
+      break;
+  }
+  PERFETTO_FATAL("Unreachable");
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/spe_record_parser.h b/src/trace_processor/importers/perf/spe_record_parser.h
new file mode 100644
index 0000000..da48962
--- /dev/null
+++ b/src/trace_processor/importers/perf/spe_record_parser.h
@@ -0,0 +1,109 @@
+/*
+ * 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_PERF_SPE_RECORD_PARSER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_SPE_RECORD_PARSER_H_
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/common/trace_parser.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
+#include "src/trace_processor/importers/perf/reader.h"
+#include "src/trace_processor/importers/perf/spe.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/perf_tables_py.h"
+
+namespace perfetto::trace_processor {
+class TraceProcessorContext;
+namespace perf_importer {
+
+class SpeRecordParserImpl : public SpeRecordParser {
+ public:
+  explicit SpeRecordParserImpl(TraceProcessorContext* context);
+
+  void ParseSpeRecord(int64_t, TraceBlobView) override;
+
+ private:
+  template <typename Enum>
+  class CachedStringIdArray {
+   public:
+    static constexpr size_t size = static_cast<size_t>(Enum::kMax) + 1;
+    explicit CachedStringIdArray() { cache_.fill(kNullStringId); }
+    StringId& operator[](Enum e) { return cache_[static_cast<size_t>(e)]; }
+
+   private:
+    std::array<StringId, size> cache_;
+  };
+
+  struct InflightSpeRecord {
+    std::optional<spe::InstructionVirtualAddress> instruction_address;
+  };
+
+  enum class OperationName {
+    kOther,
+    kSveVecOp,
+    kLoad,
+    kStore,
+    kBranch,
+    kUnknown,
+    kMax = kUnknown
+  };
+
+  static const char* ToString(OperationName name);
+  static const char* ToString(spe::ExceptionLevel el);
+  static const char* ToString(spe::DataSource ds);
+
+  StringId ToStringId(OperationName name);
+  StringId ToStringId(spe::ExceptionLevel el);
+  StringId ToStringId(spe::DataSource ds);
+
+  void ReadShortPacket(spe::ShortHeader short_header);
+  void ReadExtendedPacket(spe::ExtendedHeader extended_header);
+
+  void ReadAddressPacket(spe::AddressIndex index);
+  void ReadCounterPacket(spe::CounterIndex index);
+
+  void ReadEventsPacket(spe::ShortHeader short_header);
+  void ReadContextPacket(spe::ShortHeader short_header);
+  void ReadOperationTypePacket(spe::ShortHeader short_header);
+  void ReadDataSourcePacket(spe::ShortHeader short_header);
+
+  uint64_t ReadPayload(spe::ShortHeader short_header);
+
+  OperationName GetOperationName(spe::ShortHeader short_header,
+                                 uint8_t payload) const;
+
+  VirtualMemoryMapping* GetDummyMapping();
+
+  TraceProcessorContext* const context_;
+  CachedStringIdArray<OperationName> operation_name_strings_;
+  CachedStringIdArray<spe::DataSource> data_source_strings_;
+  CachedStringIdArray<spe::ExceptionLevel> exception_level_strings_;
+
+  Reader reader_;
+  tables::SpeRecordTable::Row inflight_row_;
+  InflightSpeRecord inflight_record_;
+
+  VirtualMemoryMapping* dummy_mapping_ = nullptr;
+};
+
+}  // namespace perf_importer
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_SPE_RECORD_PARSER_H_
diff --git a/src/trace_processor/importers/perf/spe_tokenizer.cc b/src/trace_processor/importers/perf/spe_tokenizer.cc
new file mode 100644
index 0000000..6c637b6
--- /dev/null
+++ b/src/trace_processor/importers/perf/spe_tokenizer.cc
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf/spe_tokenizer.h"
+
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <optional>
+#include <utility>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/common/clock_tracker.h"
+#include "src/trace_processor/importers/perf/aux_data_tokenizer.h"
+#include "src/trace_processor/importers/perf/aux_record.h"
+#include "src/trace_processor/importers/perf/itrace_start_record.h"
+#include "src/trace_processor/importers/perf/spe.h"
+#include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/storage/stats.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+void SpeTokenizer::OnDataLoss(uint64_t) {
+  // Clear any inflight parsing.
+  buffer_.PopFrontUntil(buffer_.end_offset());
+}
+
+base::Status SpeTokenizer::OnItraceStartRecord(ItraceStartRecord) {
+  // Clear any inflight parsing.
+  buffer_.PopFrontUntil(buffer_.end_offset());
+  return base::OkStatus();
+}
+
+base::Status SpeTokenizer::Parse(AuxRecord aux, TraceBlobView data) {
+  last_aux_record_ = std::move(aux);
+  buffer_.PushBack(std::move(data));
+  while (ProcessRecord()) {
+  }
+  return base::OkStatus();
+}
+
+bool SpeTokenizer::ProcessRecord() {
+  for (auto it = buffer_.GetIterator(); it;) {
+    uint8_t byte_0 = *it;
+    // Must be true (we passed the for loop condition).
+    it.MaybeAdvance(1);
+
+    if (spe::IsExtendedHeader(byte_0)) {
+      if (!it) {
+        return false;
+      }
+      uint8_t byte_1 = *it;
+      uint8_t payload_size =
+          spe::ExtendedHeader(byte_0, byte_1).GetPayloadSize();
+      if (!it.MaybeAdvance(payload_size + 1)) {
+        return false;
+      }
+      continue;
+    }
+
+    spe::ShortHeader short_header(byte_0);
+    uint8_t payload_size = short_header.GetPayloadSize();
+    if (!it.MaybeAdvance(payload_size)) {
+      return false;
+    }
+
+    if (short_header.IsEndPacket()) {
+      size_t record_len = it.file_offset() - buffer_.start_offset();
+      TraceBlobView record =
+          *buffer_.SliceOff(buffer_.start_offset(), record_len);
+      buffer_.PopFrontUntil(it.file_offset());
+      Emit(std::move(record), std::nullopt);
+      return true;
+    }
+
+    if (short_header.IsTimestampPacket()) {
+      size_t record_len = it.file_offset() - buffer_.start_offset();
+      TraceBlobView record =
+          *buffer_.SliceOff(buffer_.start_offset(), record_len);
+      buffer_.PopFrontUntil(it.file_offset());
+      Emit(std::move(record), ReadTimestamp(record));
+      return true;
+    }
+  }
+  return false;
+}
+
+uint64_t SpeTokenizer::ReadTimestamp(const TraceBlobView& record) {
+  PERFETTO_CHECK(record.size() >= 8);
+  uint64_t timestamp;
+  memcpy(&timestamp, record.data() + record.size() - 8, 8);
+  return timestamp;
+}
+
+base::Status SpeTokenizer::NotifyEndOfStream() {
+  return base::OkStatus();
+}
+
+void SpeTokenizer::Emit(TraceBlobView record, std::optional<uint64_t> cycles) {
+  PERFETTO_CHECK(last_aux_record_);
+
+  std::optional<uint64_t> perf_time;
+
+  if (cycles.has_value()) {
+    perf_time = stream_.ConvertTscToPerfTime(*cycles);
+  } else {
+    context_->storage->IncrementStats(stats::spe_no_timestamp);
+  }
+
+  if (!perf_time && last_aux_record_->sample_id.has_value()) {
+    perf_time = last_aux_record_->sample_id->time();
+  }
+
+  if (!perf_time) {
+    context_->sorter->PushSpeRecord(context_->sorter->max_timestamp(),
+                                    std::move(record));
+    return;
+  }
+
+  base::StatusOr<int64_t> trace_time = context_->clock_tracker->ToTraceTime(
+      last_aux_record_->attr->clock_id(), static_cast<int64_t>(*perf_time));
+  if (!trace_time.ok()) {
+    context_->storage->IncrementStats(stats::spe_record_droped);
+    return;
+  }
+  context_->sorter->PushSpeRecord(*trace_time, std::move(record));
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/spe_tokenizer.h b/src/trace_processor/importers/perf/spe_tokenizer.h
new file mode 100644
index 0000000..6af3349
--- /dev/null
+++ b/src/trace_processor/importers/perf/spe_tokenizer.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_IMPORTERS_PERF_SPE_TOKENIZER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_SPE_TOKENIZER_H_
+
+#include <cstdint>
+#include <optional>
+
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/perf/aux_data_tokenizer.h"
+#include "src/trace_processor/importers/perf/aux_record.h"
+#include "src/trace_processor/importers/perf/aux_stream_manager.h"
+#include "src/trace_processor/importers/perf/perf_session.h"
+#include "src/trace_processor/util/trace_blob_view_reader.h"
+
+namespace perfetto ::trace_processor {
+class TraceProcessorContext;
+namespace perf_importer {
+
+class SpeTokenizer : public AuxDataTokenizer {
+ public:
+  explicit SpeTokenizer(TraceProcessorContext* context, AuxStream* stream)
+      : context_(context), stream_(*stream) {}
+  void OnDataLoss(uint64_t) override;
+  base::Status Parse(AuxRecord record, TraceBlobView data) override;
+  base::Status NotifyEndOfStream() override;
+  base::Status OnItraceStartRecord(ItraceStartRecord) override;
+
+ private:
+  // A SPE trace is just a stream of SPE records which in turn are a collection
+  // of packets. An End or Timestamp packet signals the end of the current
+  // record. This method will read the stream until an end of record condition,
+  // emit the record to the sorter, consume the bytes from the buffer, and
+  // finally return true. If not enough data is available to parse a full record
+  // it returns false and the internal buffer is not modified.
+  bool ProcessRecord();
+  uint64_t ReadTimestamp(const TraceBlobView& record);
+
+  // Emits a record to the sorter. You can optionally pass the cycles value
+  // contained in the timestamp packet which will be used to determine the trace
+  // timestamp.
+  void Emit(TraceBlobView data, std::optional<uint64_t> cycles);
+  TraceProcessorContext* const context_;
+  AuxStream& stream_;
+  util::TraceBlobViewReader buffer_;
+  std::optional<AuxRecord> last_aux_record_;
+};
+
+using SpeTokenizerFactory = SimpleAuxDataTokenizerFactory<SpeTokenizer>;
+
+}  // namespace perf_importer
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_SPE_TOKENIZER_H_
diff --git a/src/trace_processor/importers/perf/time_conv_record.h b/src/trace_processor/importers/perf/time_conv_record.h
new file mode 100644
index 0000000..e96be15
--- /dev/null
+++ b/src/trace_processor/importers/perf/time_conv_record.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_TIME_CONV_RECORD_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_TIME_CONV_RECORD_H_
+
+#include <cstdint>
+
+namespace perfetto::trace_processor::perf_importer {
+
+struct TimeConvRecord {
+  uint64_t time_shift;
+  uint64_t time_mult;
+  uint64_t time_zero;
+  uint64_t time_cycles;
+  uint64_t time_mask;
+  uint8_t cap_user_time_zero;
+  uint8_t cap_user_time_short;
+  uint8_t reserved[6];  // alignment
+
+  uint64_t ConvertTscToPerfTime(uint64_t cycles) const {
+    uint64_t quot, rem;
+
+    if (cap_user_time_short) {
+      cycles = time_cycles + ((cycles - time_cycles) & time_mask);
+    }
+
+    quot = cycles >> time_shift;
+    rem = cycles & ((uint64_t(1) << time_shift) - 1);
+    return time_zero + quot * time_mult + ((rem * time_mult) >> time_shift);
+  }
+};
+
+}  // namespace perfetto::trace_processor::perf_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_TIME_CONV_RECORD_H_
diff --git a/src/trace_processor/importers/perf/util.h b/src/trace_processor/importers/perf/util.h
new file mode 100644
index 0000000..e182e2b
--- /dev/null
+++ b/src/trace_processor/importers/perf/util.h
@@ -0,0 +1,29 @@
+/*
+ * 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_PERF_UTIL_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_UTIL_H_
+
+#include <cstdint>
+namespace perfetto::trace_processor::perf_importer {
+
+inline bool SafeAdd(uint64_t a, uint64_t b, uint64_t* result) {
+  return !__builtin_add_overflow(a, b, result);
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_UTIL_H_
diff --git a/src/trace_processor/importers/perf_text/BUILD.gn b/src/trace_processor/importers/perf_text/BUILD.gn
new file mode 100644
index 0000000..0596c03
--- /dev/null
+++ b/src/trace_processor/importers/perf_text/BUILD.gn
@@ -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("../../../../gn/test.gni")
+
+source_set("perf_text") {
+  sources = [
+    "perf_text_trace_parser_impl.cc",
+    "perf_text_trace_parser_impl.h",
+    "perf_text_trace_tokenizer.cc",
+    "perf_text_trace_tokenizer.h",
+  ]
+  deps = [
+    ":perf_text_event",
+    ":perf_text_sample_line_parser",
+    "../../../../gn:default_deps",
+    "../../sorter",
+    "../../storage",
+    "../../tables",
+    "../../types",
+    "../../util:trace_blob_view_reader",
+    "../common",
+  ]
+}
+
+source_set("perf_text_event") {
+  sources = [ "perf_text_event.h" ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../../containers",
+    "../../tables",
+  ]
+}
+
+source_set("perf_text_sample_line_parser") {
+  sources = [
+    "perf_text_sample_line_parser.cc",
+    "perf_text_sample_line_parser.h",
+  ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../../../base",
+    "../../containers",
+  ]
+}
diff --git a/src/trace_processor/importers/perf_text/perf_text_event.h b/src/trace_processor/importers/perf_text/perf_text_event.h
new file mode 100644
index 0000000..ed37bb7
--- /dev/null
+++ b/src/trace_processor/importers/perf_text/perf_text_event.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_PERF_TEXT_PERF_TEXT_EVENT_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_TEXT_PERF_TEXT_EVENT_H_
+
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+
+#include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/tables/profiler_tables_py.h"
+
+namespace perfetto::trace_processor::perf_text_importer {
+
+struct alignas(8) PerfTextEvent {
+  std::optional<StringPool::Id> comm;
+  uint32_t tid;
+  std::optional<uint32_t> pid;
+  tables::StackProfileCallsiteTable::Id callsite_id;
+};
+
+}  // namespace perfetto::trace_processor::perf_text_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_TEXT_PERF_TEXT_EVENT_H_
diff --git a/src/trace_processor/importers/perf_text/perf_text_sample_line_parser.cc b/src/trace_processor/importers/perf_text/perf_text_sample_line_parser.cc
new file mode 100644
index 0000000..526060f
--- /dev/null
+++ b/src/trace_processor/importers/perf_text/perf_text_sample_line_parser.cc
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf_text/perf_text_sample_line_parser.h"
+
+#include <cctype>
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "perfetto/ext/base/string_utils.h"
+
+namespace perfetto::trace_processor::perf_text_importer {
+
+namespace {
+
+std::string_view FindTsAtEnd(std::string_view line) {
+  // We need to have 8 characters to have a valid timestamp with decimal
+  // and 6 trailing digits.
+  if (line.size() < 8) {
+    return {};
+  }
+  // All of the 6 trailing digits should be digits.
+  for (char c : line.substr(line.size() - 6)) {
+    if (!isdigit(c)) {
+      return {};
+    }
+  }
+  // 7 digits from the end should be a '.'.
+  if (line[line.size() - 7] != '.') {
+    return {};
+  }
+
+  // A space before the timestamp dot should exist.
+  std::string_view until_dot = line.substr(0, line.size() - 7);
+  size_t c = until_dot.rfind(' ');
+  if (c == std::string_view::npos) {
+    return {};
+  }
+
+  // All the characters between the last space and the colon should also
+  // be the digits.
+  for (char x : until_dot.substr(c + 1, until_dot.size() - c - 1)) {
+    if (!isdigit(x)) {
+      return {};
+    }
+  }
+  return line.substr(c + 1);
+}
+
+}  // namespace
+
+std::optional<SampleLine> ParseSampleLine(std::string_view line) {
+  // Example of what we're parsing here:
+  // trace_processor 3962131 303057.417513:          1 cpu_atom/cycles/Pu:
+  //
+  // Find colons and look backwards to find something which looks like a
+  // timestamp. Anything before that is metadata of the sample we may be able
+  // to parse out.
+  for (size_t s = 0, cln = line.find(':', s); cln != std::string_view::npos;
+       s = cln + 1, cln = line.find(':', s)) {
+    std::string_view raw_ts = FindTsAtEnd(line.substr(0, cln));
+    if (raw_ts.empty()) {
+      continue;
+    }
+    std::optional<double> ts = base::StringToDouble(std::string(raw_ts));
+    if (!ts) {
+      continue;
+    }
+    std::string before_ts(line.data(),
+                          static_cast<size_t>(raw_ts.data() - line.data()));
+
+    // simpleperf puts tabs after the comm while perf puts spaces. Make it
+    // consistent and just use spaces.
+    before_ts = base::ReplaceAll(before_ts, "\t", "  ");
+
+    std::vector<std::string> pieces = base::SplitString(before_ts, " ");
+    if (pieces.empty()) {
+      continue;
+    }
+
+    size_t pos = pieces.size() - 1;
+
+    // Try to parse out the CPU in the form: '[cpu]' (e.g. '[3]').
+    std::optional<uint32_t> cpu;
+    if (base::StartsWith(pieces[pos], "[") &&
+        base::EndsWith(pieces[pos], "]")) {
+      cpu = base::StringToUInt32(pieces[pos].substr(1, pieces[pos].size() - 2));
+      if (!cpu) {
+        continue;
+      }
+      --pos;
+    }
+
+    // Try to parse out the tid and pid in the form 'pid/tid' (e.g.
+    // '1024/1025'). If there's no '/' then just try to parse it as a tid.
+    std::vector<std::string> pid_and_tid = base::SplitString(pieces[pos], "/");
+    if (pid_and_tid.size() == 0 || pid_and_tid.size() > 2) {
+      continue;
+    }
+
+    uint32_t tid_idx = pid_and_tid.size() == 1 ? 0 : 1;
+    auto opt_tid = base::StringToUInt32(pid_and_tid[tid_idx]);
+    if (!opt_tid) {
+      continue;
+    }
+    uint32_t tid = *opt_tid;
+
+    std::optional<uint32_t> pid;
+    if (pid_and_tid.size() == 2) {
+      pid = base::StringToUInt32(pid_and_tid[0]);
+      if (!pid) {
+        continue;
+      }
+    }
+
+    // All the remaining pieces are the comm which needs to be joined together
+    // with ' '.
+    pieces.resize(pos);
+    std::string comm = base::Join(pieces, " ");
+    return SampleLine{
+        comm, pid, tid, cpu, static_cast<int64_t>(*ts * 1000 * 1000 * 1000),
+    };
+  }
+  return std::nullopt;
+}
+
+bool IsPerfTextFormatTrace(const uint8_t* ptr, size_t size) {
+  std::string_view str(reinterpret_cast<const char*>(ptr), size);
+  size_t nl = str.find('\n');
+  if (nl == std::string_view::npos) {
+    return false;
+  }
+  return ParseSampleLine(str.substr(0, nl)).has_value();
+}
+
+}  // namespace perfetto::trace_processor::perf_text_importer
diff --git a/src/trace_processor/importers/perf_text/perf_text_sample_line_parser.h b/src/trace_processor/importers/perf_text/perf_text_sample_line_parser.h
new file mode 100644
index 0000000..f64f6af
--- /dev/null
+++ b/src/trace_processor/importers/perf_text/perf_text_sample_line_parser.h
@@ -0,0 +1,46 @@
+/*
+ * 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_PERF_TEXT_PERF_TEXT_SAMPLE_LINE_PARSER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_TEXT_PERF_TEXT_SAMPLE_LINE_PARSER_H_
+
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <string_view>
+
+namespace perfetto::trace_processor::perf_text_importer {
+
+struct SampleLine {
+  std::string comm;
+  std::optional<uint32_t> pid;
+  uint32_t tid;
+  std::optional<uint32_t> cpu;
+  int64_t ts;
+};
+
+// Given a single line of a perf text sample, parses it into its components and
+// returns the result. If parsing was not possible, returns std::nullopt.
+std::optional<SampleLine> ParseSampleLine(std::string_view line);
+
+// Given a chunk of a trace file (starting at `ptr` and containing `size`
+// bytes), returns whether the file is a perf text format trace.
+bool IsPerfTextFormatTrace(const uint8_t* ptr, size_t size);
+
+}  // namespace perfetto::trace_processor::perf_text_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_TEXT_PERF_TEXT_SAMPLE_LINE_PARSER_H_
diff --git a/src/trace_processor/importers/perf_text/perf_text_trace_parser_impl.cc b/src/trace_processor/importers/perf_text/perf_text_trace_parser_impl.cc
new file mode 100644
index 0000000..e81c4d3
--- /dev/null
+++ b/src/trace_processor/importers/perf_text/perf_text_trace_parser_impl.cc
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf_text/perf_text_trace_parser_impl.h"
+
+#include <cstdint>
+
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/importers/perf_text/perf_text_event.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/profiler_tables_py.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::perf_text_importer {
+
+PerfTextTraceParserImpl::PerfTextTraceParserImpl(TraceProcessorContext* context)
+    : context_(context) {}
+
+PerfTextTraceParserImpl::~PerfTextTraceParserImpl() = default;
+
+void PerfTextTraceParserImpl::ParsePerfTextEvent(int64_t ts,
+                                                 PerfTextEvent evt) {
+  auto* ss = context_->storage->mutable_cpu_profile_stack_sample_table();
+  tables::CpuProfileStackSampleTable::Row row;
+  row.ts = ts;
+  row.callsite_id = evt.callsite_id;
+  row.utid = evt.pid
+                 ? context_->process_tracker->UpdateThread(evt.tid, *evt.pid)
+                 : context_->process_tracker->GetOrCreateThread(evt.tid);
+  if (evt.comm) {
+    context_->process_tracker->UpdateThreadNameAndMaybeProcessName(
+        evt.tid, *evt.comm, ThreadNamePriority::kOther);
+  }
+  ss->Insert(row);
+}
+
+}  // namespace perfetto::trace_processor::perf_text_importer
diff --git a/src/trace_processor/importers/perf_text/perf_text_trace_parser_impl.h b/src/trace_processor/importers/perf_text/perf_text_trace_parser_impl.h
new file mode 100644
index 0000000..d586091
--- /dev/null
+++ b/src/trace_processor/importers/perf_text/perf_text_trace_parser_impl.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_TEXT_PERF_TEXT_TRACE_PARSER_IMPL_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_TEXT_PERF_TEXT_TRACE_PARSER_IMPL_H_
+
+#include <cstdint>
+
+#include "src/trace_processor/importers/common/trace_parser.h"
+#include "src/trace_processor/importers/perf_text/perf_text_event.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::perf_text_importer {
+
+class PerfTextTraceParserImpl : public PerfTextTraceParser {
+ public:
+  explicit PerfTextTraceParserImpl(TraceProcessorContext*);
+  ~PerfTextTraceParserImpl() override;
+
+  void ParsePerfTextEvent(int64_t ts, PerfTextEvent) override;
+
+ private:
+  TraceProcessorContext* const context_;
+};
+
+}  // namespace perfetto::trace_processor::perf_text_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_TEXT_PERF_TEXT_TRACE_PARSER_IMPL_H_
diff --git a/src/trace_processor/importers/perf_text/perf_text_trace_tokenizer.cc b/src/trace_processor/importers/perf_text/perf_text_trace_tokenizer.cc
new file mode 100644
index 0000000..fb99c35
--- /dev/null
+++ b/src/trace_processor/importers/perf_text/perf_text_trace_tokenizer.cc
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf_text/perf_text_trace_tokenizer.h"
+
+#include <cctype>
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/common/mapping_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
+#include "src/trace_processor/importers/perf_text/perf_text_event.h"
+#include "src/trace_processor/importers/perf_text/perf_text_sample_line_parser.h"
+#include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/trace_blob_view_reader.h"
+
+namespace perfetto::trace_processor::perf_text_importer {
+
+namespace {
+
+std::string_view ToStringView(const TraceBlobView& tbv) {
+  return {reinterpret_cast<const char*>(tbv.data()), tbv.size()};
+}
+
+std::string Slice(const std::string& str, size_t start, size_t end) {
+  return str.substr(start, end - start);
+}
+
+}  // namespace
+
+PerfTextTraceTokenizer::PerfTextTraceTokenizer(TraceProcessorContext* ctx)
+    : context_(ctx) {}
+PerfTextTraceTokenizer::~PerfTextTraceTokenizer() = default;
+
+base::Status PerfTextTraceTokenizer::Parse(TraceBlobView blob) {
+  reader_.PushBack(std::move(blob));
+  std::vector<FrameId> frames;
+  // Loop over each sample.
+  for (;;) {
+    auto it = reader_.GetIterator();
+    auto r = it.MaybeFindAndRead('\n');
+    if (!r) {
+      return base::OkStatus();
+    }
+    // The start line of a sample. An example:
+    // trace_processor 3962131 303057.417513:          1 cpu_atom/cycles/Pu:
+    //
+    // Note that perf script output is fully configurable so we have to be
+    // parse all the optionality carefully.
+    std::string_view first_line = ToStringView(*r);
+    std::optional<SampleLine> sample = ParseSampleLine(first_line);
+    if (!sample) {
+      return base::ErrStatus(
+          "Perf text parser: unable to parse sample line (context: '%s')",
+          std::string(first_line).c_str());
+    }
+
+    // Loop over the frames in the sample.
+    for (;;) {
+      auto raw_frame = it.MaybeFindAndRead('\n');
+      // If we don't manage to parse the full stack, we should bail out.
+      if (!raw_frame) {
+        return base::OkStatus();
+      }
+      // An empty line indicates that we have reached the end of this sample.
+      std::string frame =
+          base::TrimWhitespace(std::string(ToStringView(*raw_frame)));
+      if (frame.size() == 0) {
+        break;
+      }
+
+      size_t symbol_end = frame.find(' ');
+      if (symbol_end == std::string::npos) {
+        return base::ErrStatus(
+            "Perf text parser: unable to find symbol in frame (context: '%s')",
+            frame.c_str());
+      }
+
+      size_t mapping_start = frame.rfind('(');
+      if (mapping_start == std::string::npos || frame.back() != ')') {
+        return base::ErrStatus(
+            "Perf text parser: unable to find mapping in frame (context: '%s')",
+            frame.c_str());
+      }
+
+      std::string mapping_name =
+          Slice(frame, mapping_start + 1, frame.size() - 1);
+      DummyMemoryMapping* mapping;
+      if (DummyMemoryMapping** mapping_ptr = mappings_.Find(mapping_name);
+          mapping_ptr) {
+        mapping = *mapping_ptr;
+      } else {
+        mapping = &context_->mapping_tracker->CreateDummyMapping(mapping_name);
+        PERFETTO_CHECK(mappings_.Insert(mapping_name, mapping).second);
+      }
+
+      std::string symbol_name_with_offset =
+          base::TrimWhitespace(Slice(frame, symbol_end, mapping_start));
+      size_t offset = symbol_name_with_offset.rfind('+');
+      base::StringView symbol_name(symbol_name_with_offset);
+      if (offset != std::string::npos) {
+        symbol_name = symbol_name.substr(0, offset);
+      }
+      frames.emplace_back(
+          mapping->InternDummyFrame(symbol_name, base::StringView()));
+    }
+    if (frames.empty()) {
+      return base::ErrStatus(
+          "Perf text parser: no frames in sample (context: '%s')",
+          std::string(first_line).c_str());
+    }
+
+    std::optional<CallsiteId> parent_callsite;
+    uint32_t depth = 0;
+    for (auto rit = frames.rbegin(); rit != frames.rend(); ++rit) {
+      parent_callsite = context_->stack_profile_tracker->InternCallsite(
+          parent_callsite, *rit, ++depth);
+    }
+    frames.clear();
+
+    PerfTextEvent evt;
+    if (!sample->comm.empty()) {
+      evt.comm = context_->storage->InternString(
+          base::StringView(sample->comm.data(), sample->comm.size()));
+    }
+    evt.tid = sample->tid;
+    evt.pid = sample->pid;
+    evt.callsite_id = *parent_callsite;
+
+    context_->sorter->PushPerfTextEvent(sample->ts, evt);
+    reader_.PopFrontUntil(it.file_offset());
+  }
+}
+
+base::Status PerfTextTraceTokenizer::NotifyEndOfFile() {
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor::perf_text_importer
diff --git a/src/trace_processor/importers/perf_text/perf_text_trace_tokenizer.h b/src/trace_processor/importers/perf_text/perf_text_trace_tokenizer.h
new file mode 100644
index 0000000..42330db
--- /dev/null
+++ b/src/trace_processor/importers/perf_text/perf_text_trace_tokenizer.h
@@ -0,0 +1,46 @@
+/*
+ * 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_PERF_TEXT_PERF_TEXT_TRACE_TOKENIZER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_TEXT_PERF_TEXT_TRACE_TOKENIZER_H_
+
+#include <string>
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/trace_blob_view_reader.h"
+
+namespace perfetto::trace_processor::perf_text_importer {
+
+class PerfTextTraceTokenizer : public ChunkedTraceReader {
+ public:
+  explicit PerfTextTraceTokenizer(TraceProcessorContext*);
+  ~PerfTextTraceTokenizer() override;
+
+  base::Status Parse(TraceBlobView) override;
+  base::Status NotifyEndOfFile() override;
+
+ private:
+  TraceProcessorContext* const context_;
+  util::TraceBlobViewReader reader_;
+  base::FlatHashMap<std::string, DummyMemoryMapping*> mappings_;
+};
+
+}  // namespace perfetto::trace_processor::perf_text_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_TEXT_PERF_TEXT_TRACE_TOKENIZER_H_
diff --git a/src/trace_processor/importers/proto/BUILD.gn b/src/trace_processor/importers/proto/BUILD.gn
index f7ebbe7..fe8ebcc 100644
--- a/src/trace_processor/importers/proto/BUILD.gn
+++ b/src/trace_processor/importers/proto/BUILD.gn
@@ -186,7 +186,9 @@
     "../../../../protos/perfetto/trace/system_info:zero",
     "../../../../protos/perfetto/trace/translation:zero",
     "../../../base",
+    "../../../kernel_utils:syscall_table",
     "../../../protozero",
+    "../../containers",
     "../../sorter",
     "../../storage",
     "../../tables",
diff --git a/src/trace_processor/importers/proto/android_probes_module.cc b/src/trace_processor/importers/proto/android_probes_module.cc
index 31f51dc..01a65fc 100644
--- a/src/trace_processor/importers/proto/android_probes_module.cc
+++ b/src/trace_processor/importers/proto/android_probes_module.cc
@@ -155,7 +155,7 @@
     }
     StringId counter_name_id =
         context_->storage->InternString(counter_name.string_view());
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kPower, counter_name_id,
         [this, &desc](ArgsTracker::BoundInserter& inserter) {
           StringId raw_name = context_->storage->InternString(desc.rail_name());
diff --git a/src/trace_processor/importers/proto/android_probes_parser.cc b/src/trace_processor/importers/proto/android_probes_parser.cc
index a6cd9b6..2da16c9 100644
--- a/src/trace_processor/importers/proto/android_probes_parser.cc
+++ b/src/trace_processor/importers/proto/android_probes_parser.cc
@@ -16,21 +16,34 @@
 
 #include "src/trace_processor/importers/proto/android_probes_parser.h"
 
+#include <atomic>
+#include <cinttypes>
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
 #include <optional>
+#include <string>
 
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/status_or.h"
 #include "perfetto/ext/base/string_utils.h"
-#include "perfetto/ext/traced/sys_stats_counters.h"
+#include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/async_track_set_tracker.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/metadata_tracker.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
-#include "src/trace_processor/importers/syscalls/syscall_tracker.h"
-#include "src/trace_processor/types/tcp_state.h"
+#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/android_probes_tracker.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/types/trace_processor_context.h"
+#include "src/trace_processor/types/variadic.h"
 
-#include "protos/perfetto/common/android_energy_consumer_descriptor.pbzero.h"
 #include "protos/perfetto/common/android_log_constants.pbzero.h"
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
 #include "protos/perfetto/config/trace_config.pbzero.h"
@@ -42,15 +55,8 @@
 #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/ps/process_stats.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/system_info.pbzero.h"
 
-#include "src/trace_processor/importers/proto/android_probes_tracker.h"
-
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 AndroidProbesParser::AndroidProbesParser(TraceProcessorContext* context)
     : context_(context),
@@ -60,11 +66,16 @@
       batt_current_avg_id_(
           context->storage->InternString("batt.current.avg_ua")),
       batt_voltage_id_(context->storage->InternString("batt.voltage_uv")),
+      batt_power_id_(context->storage->InternString("batt.power_mw")),
       screen_state_id_(context->storage->InternString("ScreenState")),
       device_state_id_(context->storage->InternString("DeviceStateChanged")),
       battery_status_id_(context->storage->InternString("BatteryStatus")),
       plug_type_id_(context->storage->InternString("PlugType")),
-      rail_packet_timestamp_id_(context->storage->InternString("packet_ts")) {}
+      rail_packet_timestamp_id_(context->storage->InternString("packet_ts")),
+      energy_consumer_id_(
+          context_->storage->InternString("energy_consumer_id")),
+      consumer_type_id_(context_->storage->InternString("consumer_type")),
+      ordinal_id_(context_->storage->InternString("ordinal")) {}
 
 void AndroidProbesParser::ParseBatteryCounters(int64_t ts, ConstBytes blob) {
   protos::pbzero::BatteryCounters::Decoder evt(blob.data, blob.size);
@@ -73,6 +84,7 @@
   StringId batt_current_id = batt_current_id_;
   StringId batt_current_avg_id = batt_current_avg_id_;
   StringId batt_voltage_id = batt_voltage_id_;
+  StringId batt_power_id = batt_power_id_;
   if (evt.has_name()) {
     std::string batt_name = evt.name().ToStdString();
     batt_charge_id = context_->storage->InternString(base::StringView(
@@ -85,15 +97,17 @@
         std::string("batt.").append(batt_name).append(".current.avg_ua")));
     batt_voltage_id = context_->storage->InternString(base::StringView(
         std::string("batt.").append(batt_name).append(".voltage_uv")));
+    batt_power_id = context_->storage->InternString(base::StringView(
+        std::string("batt.").append(batt_name).append(".power_mw")));
   }
   if (evt.has_charge_counter_uah()) {
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kPower, batt_charge_id);
     context_->event_tracker->PushCounter(
         ts, static_cast<double>(evt.charge_counter_uah()), track);
   } else if (evt.has_energy_counter_uwh() && evt.has_voltage_uv()) {
     // Calculate charge counter from energy counter and voltage.
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kPower, batt_charge_id);
     auto energy = evt.energy_counter_uwh();
     auto voltage = evt.voltage_uv();
@@ -104,29 +118,38 @@
   }
 
   if (evt.has_capacity_percent()) {
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kPower, batt_capacity_id);
     context_->event_tracker->PushCounter(
         ts, static_cast<double>(evt.capacity_percent()), track);
   }
   if (evt.has_current_ua()) {
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kPower, batt_current_id);
     context_->event_tracker->PushCounter(
         ts, static_cast<double>(evt.current_ua()), track);
   }
   if (evt.has_current_avg_ua()) {
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kPower, batt_current_avg_id);
     context_->event_tracker->PushCounter(
         ts, static_cast<double>(evt.current_avg_ua()), track);
   }
   if (evt.has_voltage_uv()) {
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kPower, batt_voltage_id);
     context_->event_tracker->PushCounter(
         ts, static_cast<double>(evt.voltage_uv()), track);
   }
+  if (evt.has_current_ua() && evt.has_voltage_uv()) {
+    // Calculate power from current and voltage.
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
+        TrackTracker::Group::kPower, batt_power_id);
+    auto current = evt.current_ua();
+    auto voltage = evt.voltage_uv();
+    context_->event_tracker->PushCounter(
+        ts, static_cast<double>(current * voltage / 1000000000), track);
+  }
 }
 
 void AndroidProbesParser::ParsePowerRails(int64_t ts,
@@ -189,8 +212,14 @@
   auto consumer_type = energy_consumer_specs->type;
   auto ordinal = energy_consumer_specs->ordinal;
 
-  TrackId energy_track = context_->track_tracker->InternEnergyCounterTrack(
-      consumer_name, consumer_id, consumer_type, ordinal);
+  TrackId energy_track = context_->track_tracker->InternSingleDimensionTrack(
+      tracks::android_energy_estimation_breakdown, energy_consumer_id_,
+      Variadic::Integer(consumer_id),
+      TrackTracker::LegacyStringIdName{consumer_name},
+      [&](ArgsTracker::BoundInserter& inserter) {
+        inserter.AddArg(consumer_type_id_, Variadic::String(consumer_type));
+        inserter.AddArg(ordinal_id_, Variadic::Integer(ordinal));
+      });
   context_->event_tracker->PushCounter(ts, total_energy, energy_track);
 
   // Consumers providing per-uid energy breakdown
@@ -204,9 +233,14 @@
       continue;
     }
 
-    TrackId energy_uid_track =
-        context_->track_tracker->InternEnergyPerUidCounterTrack(
-            consumer_name, consumer_id, breakdown.uid());
+    auto builder = context_->track_tracker->CreateDimensionsBuilder();
+    builder.AppendUid(breakdown.uid());
+    builder.AppendDimension(energy_consumer_id_,
+                            Variadic::Integer(consumer_id));
+    TrackId energy_uid_track = context_->track_tracker->InternTrack(
+        tracks::android_energy_estimation_breakdown_per_uid,
+        std::move(builder).Build(),
+        TrackTracker::LegacyStringIdName{consumer_name});
     context_->event_tracker->PushCounter(
         ts, static_cast<double>(breakdown.energy_uws()), energy_uid_track);
   }
@@ -235,7 +269,7 @@
       return;
     }
 
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kPower, entity_state->overall_name);
     context_->event_tracker->PushCounter(
         ts, double(residency.total_time_in_state_ms()), track);
@@ -413,7 +447,7 @@
                                                    ConstBytes blob) {
   protos::pbzero::InitialDisplayState::Decoder state(blob.data, blob.size);
 
-  TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+  TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
       TrackTracker::Group::kDeviceState, screen_state_id_);
   context_->event_tracker->PushCounter(ts, state.display_state(), track);
 }
@@ -438,13 +472,14 @@
           context_->async_track_set_tracker->Scoped(track_set_id, ts, 0);
       context_->slice_tracker->Scoped(ts, track_id, kNullStringId, state_id, 0);
     } else if (name.StartsWith("debug.tracing.battery_stats.") ||
-               name == "debug.tracing.mcc" || name == "debug.tracing.mnc") {
+               name == "debug.tracing.mcc" || name == "debug.tracing.mnc" ||
+               name == "debug.tracing.desktop_mode_visible_tasks") {
       StringId name_id = context_->storage->InternString(
           name.substr(strlen("debug.tracing.")));
       std::optional<int32_t> state =
           base::StringToInt32(kv.value().ToStdString());
       if (state) {
-        TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+        TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
             TrackTracker::Group::kNetwork, name_id);
         context_->event_tracker->PushCounter(ts, *state, track);
       }
@@ -459,7 +494,7 @@
       std::optional<int32_t> state =
           base::StringToInt32(kv.value().ToStdString());
       if (state) {
-        TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+        TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
             TrackTracker::Group::kDeviceState, *mapped_name_id);
         context_->event_tracker->PushCounter(ts, *state, track);
       }
@@ -467,5 +502,4 @@
   }
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // 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 725063f..04d8a24 100644
--- a/src/trace_processor/importers/proto/android_probes_parser.h
+++ b/src/trace_processor/importers/proto/android_probes_parser.h
@@ -17,13 +17,12 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_ANDROID_PROBES_PARSER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_ANDROID_PROBES_PARSER_H_
 
-#include <vector>
+#include <cstdint>
 
 #include "perfetto/protozero/field.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class TraceProcessorContext;
 
@@ -53,13 +52,16 @@
   const StringId batt_current_id_;
   const StringId batt_current_avg_id_;
   const StringId batt_voltage_id_;
+  const StringId batt_power_id_;
   const StringId screen_state_id_;
   const StringId device_state_id_;
   const StringId battery_status_id_;
   const StringId plug_type_id_;
   const StringId rail_packet_timestamp_id_;
+  const StringId energy_consumer_id_;
+  const StringId consumer_type_id_;
+  const StringId ordinal_id_;
 };
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_ANDROID_PROBES_PARSER_H_
diff --git a/src/trace_processor/importers/proto/atoms.descriptor b/src/trace_processor/importers/proto/atoms.descriptor
index 923f583..b318814 100644
--- a/src/trace_processor/importers/proto/atoms.descriptor
+++ b/src/trace_processor/importers/proto/atoms.descriptor
Binary files differ
diff --git a/src/trace_processor/importers/proto/chrome_system_probes_parser.cc b/src/trace_processor/importers/proto/chrome_system_probes_parser.cc
index e3c95bd..aadf3f5 100644
--- a/src/trace_processor/importers/proto/chrome_system_probes_parser.cc
+++ b/src/trace_processor/importers/proto/chrome_system_probes_parser.cc
@@ -74,7 +74,7 @@
         continue;
       UniquePid upid = context_->process_tracker->GetOrCreateProcess(pid);
       TrackId track =
-          context_->track_tracker->InternProcessCounterTrack(name, upid);
+          context_->track_tracker->LegacyInternProcessCounterTrack(name, upid);
       int64_t value = fld.as_int64() * 1024;
       context_->event_tracker->PushCounter(ts, static_cast<double>(value),
                                            track);
diff --git a/src/trace_processor/importers/proto/gpu_event_parser.cc b/src/trace_processor/importers/proto/gpu_event_parser.cc
index 49f7dbd..d5875d8 100644
--- a/src/trace_processor/importers/proto/gpu_event_parser.cc
+++ b/src/trace_processor/importers/proto/gpu_event_parser.cc
@@ -182,7 +182,7 @@
 
       auto name_id = context_->storage->InternString(name);
       auto desc_id = context_->storage->InternString(desc);
-      auto track_id = context_->track_tracker->CreateGpuCounterTrack(
+      auto track_id = context_->track_tracker->LegacyCreateGpuCounterTrack(
           name_id, 0 /* gpu_id */, desc_id, unit_id);
       gpu_counter_track_ids_.emplace(counter_id, track_id);
       if (spec.has_groups()) {
@@ -265,12 +265,12 @@
     track.description = context_->storage->InternString(hw_queue.description());
     if (gpu_hw_queue_counter_ >= gpu_hw_queue_ids_.size()) {
       gpu_hw_queue_ids_.emplace_back(
-          context_->track_tracker->InternGpuTrack(track));
+          context_->track_tracker->LegacyInternGpuTrack(track));
     } else {
       // If a gpu_render_stage_event is received before the specification, it is
       // possible that the slot has already been allocated.
       gpu_hw_queue_ids_[gpu_hw_queue_counter_] =
-          context_->track_tracker->InternGpuTrack(track);
+          context_->track_tracker->LegacyInternGpuTrack(track);
     }
   } else {
     // If a gpu_render_stage_event is received before the specification, a track
@@ -429,7 +429,7 @@
       track.scope = gpu_render_stage_scope_id_;
       track.description =
           context_->storage->InternString(decoder->description());
-      track_id = context_->track_tracker->InternGpuTrack(track);
+      track_id = context_->track_tracker->LegacyInternGpuTrack(track);
     } else {
       uint32_t id = static_cast<uint32_t>(event.hw_queue_id());
       if (id < gpu_hw_queue_ids_.size() && gpu_hw_queue_ids_[id].has_value()) {
@@ -454,7 +454,7 @@
             context_->storage->InternString(writer.GetStringView());
         tables::GpuTrackTable::Row track(track_name);
         track.scope = gpu_render_stage_scope_id_;
-        track_id = context_->track_tracker->InternGpuTrack(track);
+        track_id = context_->track_tracker->LegacyInternGpuTrack(track);
         gpu_hw_queue_ids_.resize(id + 1);
         gpu_hw_queue_ids_[id] = track_id;
       }
@@ -534,8 +534,8 @@
       }
       track_str_id = vulkan_memory_tracker_.FindAllocationScopeCounterString(
           allocation_scope);
-      track = context_->track_tracker->InternProcessCounterTrack(track_str_id,
-                                                                 upid);
+      track = context_->track_tracker->LegacyInternProcessCounterTrack(
+          track_str_id, upid);
       context_->event_tracker->PushCounter(
           event.timestamp(),
           static_cast<double>(vulkan_driver_memory_counters_[allocation_scope]),
@@ -562,8 +562,8 @@
       track_str_id = vulkan_memory_tracker_.FindMemoryTypeCounterString(
           memory_type,
           VulkanMemoryTracker::DeviceCounterType::kAllocationCounter);
-      track = context_->track_tracker->InternProcessCounterTrack(track_str_id,
-                                                                 upid);
+      track = context_->track_tracker->LegacyInternProcessCounterTrack(
+          track_str_id, upid);
       context_->event_tracker->PushCounter(
           event.timestamp(),
           static_cast<double>(
@@ -591,8 +591,8 @@
       }
       track_str_id = vulkan_memory_tracker_.FindMemoryTypeCounterString(
           memory_type, VulkanMemoryTracker::DeviceCounterType::kBindCounter);
-      track = context_->track_tracker->InternProcessCounterTrack(track_str_id,
-                                                                 upid);
+      track = context_->track_tracker->LegacyInternProcessCounterTrack(
+          track_str_id, upid);
       context_->event_tracker->PushCounter(
           event.timestamp(),
           static_cast<double>(vulkan_device_memory_counters_bind_[memory_type]),
@@ -690,7 +690,7 @@
 
   tables::GpuTrackTable::Row track(gpu_log_track_name_id_);
   track.scope = gpu_log_scope_id_;
-  TrackId track_id = context_->track_tracker->InternGpuTrack(track);
+  TrackId track_id = context_->track_tracker->LegacyInternGpuTrack(track);
 
   auto args_callback = [this, &event](ArgsTracker::BoundInserter* inserter) {
     if (event.has_tag()) {
@@ -736,7 +736,7 @@
     // track so that they can appear close to the render stage slices.
     tables::GpuTrackTable::Row track(vk_event_track_id_);
     track.scope = vk_event_scope_id_;
-    TrackId track_id = context_->track_tracker->InternGpuTrack(track);
+    TrackId track_id = context_->track_tracker->LegacyInternGpuTrack(track);
     tables::GpuSliceTable::Row row;
     row.ts = ts;
     row.dur = static_cast<int64_t>(event.duration_ns());
@@ -764,14 +764,14 @@
   const uint32_t pid = gpu_mem_total.pid();
   if (pid == 0) {
     // Pid 0 is used to indicate the global total
-    track = context_->track_tracker->InternGlobalCounterTrack(
+    track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kMemory, gpu_mem_total_name_id_, {},
         gpu_mem_total_unit_id_, gpu_mem_total_global_desc_id_);
   } else {
     // Process emitting the packet can be different from the pid in the event.
     UniqueTid utid = context_->process_tracker->UpdateThread(pid, pid);
     UniquePid upid = context_->storage->thread_table()[utid].upid().value_or(0);
-    track = context_->track_tracker->InternProcessCounterTrack(
+    track = context_->track_tracker->LegacyInternProcessCounterTrack(
         gpu_mem_total_name_id_, upid, gpu_mem_total_unit_id_,
         gpu_mem_total_proc_desc_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 d75c617..55075ee 100644
--- a/src/trace_processor/importers/proto/graphics_frame_event_parser.cc
+++ b/src/trace_processor/importers/proto/graphics_frame_event_parser.cc
@@ -132,7 +132,7 @@
 
   tables::GpuTrackTable::Row track(track_name_id);
   track.scope = graphics_event_scope_id_;
-  TrackId track_id = context_->track_tracker->InternGpuTrack(track);
+  TrackId track_id = context_->track_tracker->LegacyInternGpuTrack(track);
 
   auto* graphics_frame_slice_table =
       context_->storage->mutable_graphics_frame_slice_table();
@@ -247,7 +247,7 @@
           context_->storage->InternString(track_name.GetStringView());
       tables::GpuTrackTable::Row app_track(track_name_id);
       app_track.scope = graphics_event_scope_id_;
-      track_id = context_->track_tracker->InternGpuTrack(app_track);
+      track_id = context_->track_tracker->LegacyInternGpuTrack(app_track);
 
       // Error handling
       auto dequeue_time = dequeue_map_.find(event_key);
@@ -301,7 +301,7 @@
           context_->storage->InternString(track_name.GetStringView());
       tables::GpuTrackTable::Row gpu_track(track_name_id);
       gpu_track.scope = graphics_event_scope_id_;
-      track_id = context_->track_tracker->InternGpuTrack(gpu_track);
+      track_id = context_->track_tracker->LegacyInternGpuTrack(gpu_track);
       queue_map_[event_key] = track_id;
       break;
     }
@@ -332,7 +332,7 @@
           context_->storage->InternString(track_name.GetStringView());
       tables::GpuTrackTable::Row sf_track(track_name_id);
       sf_track.scope = graphics_event_scope_id_;
-      track_id = context_->track_tracker->InternGpuTrack(sf_track);
+      track_id = context_->track_tracker->LegacyInternGpuTrack(sf_track);
       latch_map_[event_key] = track_id;
       break;
     }
@@ -356,7 +356,7 @@
           context_->storage->InternString(track_name.GetStringView());
       tables::GpuTrackTable::Row display_track(track_name_id);
       display_track.scope = graphics_event_scope_id_;
-      track_id = context_->track_tracker->InternGpuTrack(display_track);
+      track_id = context_->track_tracker->LegacyInternGpuTrack(display_track);
       display_map_[layer_name_id] = track_id;
       break;
     }
diff --git a/src/trace_processor/importers/proto/memory_tracker_snapshot_parser.cc b/src/trace_processor/importers/proto/memory_tracker_snapshot_parser.cc
index dc96491..27296ba 100644
--- a/src/trace_processor/importers/proto/memory_tracker_snapshot_parser.cc
+++ b/src/trace_processor/importers/proto/memory_tracker_snapshot_parser.cc
@@ -20,6 +20,7 @@
 #include "protos/perfetto/trace/memory_graph.pbzero.h"
 #include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
+#include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/tables/memory_tables_py.h"
 
 namespace perfetto {
@@ -169,8 +170,13 @@
 
   // For now, we use the existing global instant event track for chrome events,
   // since memory dumps are global.
-  TrackId track_id =
-      context_->track_tracker->GetOrCreateLegacyChromeGlobalInstantTrack();
+  TrackId track_id = context_->track_tracker->InternGlobalTrack(
+      tracks::legacy_chrome_global_instants, TrackTracker::AutoName(),
+      [this](ArgsTracker::BoundInserter& inserter) {
+        inserter.AddArg(
+            context_->storage->InternString("source"),
+            Variadic::String(context_->storage->InternString("chrome")));
+      });
 
   tables::MemorySnapshotTable::Row snapshot_row(
       ts, track_id, level_of_detail_ids_[static_cast<size_t>(level_of_detail)]);
diff --git a/src/trace_processor/importers/proto/metadata_module.cc b/src/trace_processor/importers/proto/metadata_module.cc
index 96bd017..c82b01f 100644
--- a/src/trace_processor/importers/proto/metadata_module.cc
+++ b/src/trace_processor/importers/proto/metadata_module.cc
@@ -104,7 +104,8 @@
 void MetadataModule::ParseTrigger(int64_t ts, ConstBytes blob) {
   protos::pbzero::Trigger::Decoder trigger(blob.data, blob.size);
   StringId cat_id = kNullStringId;
-  TrackId track_id = context_->track_tracker->GetOrCreateTriggerTrack();
+  TrackId track_id = context_->track_tracker->InternGlobalTrack(
+      tracks::triggers, TrackTracker::LegacyCharArrayName("Trace Triggers"));
   StringId name_id = context_->storage->InternString(trigger.trigger_name());
   context_->slice_tracker->Scoped(
       ts, track_id, cat_id, name_id,
@@ -126,7 +127,8 @@
 void MetadataModule::ParseChromeTrigger(int64_t ts, ConstBytes blob) {
   protos::pbzero::ChromeTrigger::Decoder trigger(blob.data, blob.size);
   StringId cat_id = kNullStringId;
-  TrackId track_id = context_->track_tracker->GetOrCreateTriggerTrack();
+  TrackId track_id =
+      context_->track_tracker->InternGlobalTrack(tracks::triggers);
   StringId name_id;
   if (trigger.has_trigger_name()) {
     name_id = context_->storage->InternString(trigger.trigger_name());
diff --git a/src/trace_processor/importers/proto/metadata_module.h b/src/trace_processor/importers/proto/metadata_module.h
index 0014936..ca9b97e 100644
--- a/src/trace_processor/importers/proto/metadata_module.h
+++ b/src/trace_processor/importers/proto/metadata_module.h
@@ -52,8 +52,9 @@
   void ParseTraceUuid(ConstBytes);
 
   TraceProcessorContext* context_;
-  StringId producer_name_key_id_ = kNullStringId;
-  StringId trusted_producer_uid_key_id_ = kNullStringId;
+
+  const StringId producer_name_key_id_;
+  const StringId trusted_producer_uid_key_id_;
 };
 
 }  // namespace 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 36ede9a..9331c18 100644
--- a/src/trace_processor/importers/proto/network_trace_module_unittest.cc
+++ b/src/trace_processor/importers/proto/network_trace_module_unittest.cc
@@ -136,15 +136,15 @@
   ASSERT_EQ(slices.row_count(), 1u);
   EXPECT_EQ(slices[0].ts(), 123);
 
-  EXPECT_TRUE(HasArg(1u, "packet_length", Variadic::Integer(72)));
-  EXPECT_TRUE(HasArg(1u, "socket_uid", Variadic::Integer(1010)));
-  EXPECT_TRUE(HasArg(1u, "local_port", Variadic::Integer(5100)));
-  EXPECT_TRUE(HasArg(1u, "remote_port", Variadic::Integer(443)));
-  EXPECT_TRUE(HasArg(1u, "packet_transport",
+  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",
                      Variadic::String(storage_->InternString("IPPROTO_TCP"))));
-  EXPECT_TRUE(HasArg(1u, "socket_tag",
+  EXPECT_TRUE(HasArg(2u, "socket_tag",
                      Variadic::String(storage_->InternString("0x407"))));
-  EXPECT_TRUE(HasArg(1u, "packet_tcp_flags",
+  EXPECT_TRUE(HasArg(2u, "packet_tcp_flags",
                      Variadic::String(storage_->InternString(".s..a..."))));
 }
 
@@ -176,8 +176,8 @@
   EXPECT_EQ(slices[0].ts(), 123);
   EXPECT_EQ(slices[1].ts(), 133);
 
-  EXPECT_TRUE(HasArg(1u, "packet_length", Variadic::Integer(72)));
-  EXPECT_TRUE(HasArg(2u, "packet_length", Variadic::Integer(100)));
+  EXPECT_TRUE(HasArg(2u, "packet_length", Variadic::Integer(72)));
+  EXPECT_TRUE(HasArg(3u, "packet_length", Variadic::Integer(100)));
 }
 
 TEST_F(NetworkTraceModuleTest, TokenizeAndParseAggregateBundle) {
@@ -201,8 +201,8 @@
   EXPECT_EQ(slices[0].ts(), 123);
   EXPECT_EQ(slices[0].dur(), 10);
 
-  EXPECT_TRUE(HasArg(1u, "packet_length", Variadic::Integer(172)));
-  EXPECT_TRUE(HasArg(1u, "packet_count", Variadic::Integer(2)));
+  EXPECT_TRUE(HasArg(2u, "packet_length", Variadic::Integer(172)));
+  EXPECT_TRUE(HasArg(2u, "packet_count", Variadic::Integer(2)));
 }
 
 }  // namespace
diff --git a/src/trace_processor/importers/proto/perf_sample_tracker.cc b/src/trace_processor/importers/proto/perf_sample_tracker.cc
index 52ff115..4db6cc7 100644
--- a/src/trace_processor/importers/proto/perf_sample_tracker.cc
+++ b/src/trace_processor/importers/proto/perf_sample_tracker.cc
@@ -86,25 +86,28 @@
   return "unknown";
 }
 
-StringId InternTimebaseCounterName(
+template <typename T>
+StringId InternCounterName(
     const protos::pbzero::PerfSampleDefaults::Decoder& perf_defaults,
-    TraceProcessorContext* context) {
+    TraceProcessorContext* context,
+    const T& desc_decoder) {
   using namespace protos::pbzero;
   PerfEvents::Timebase::Decoder timebase(perf_defaults.timebase());
 
-  auto config_given_name = timebase.name();
+  auto config_given_name = desc_decoder.name();
   if (config_given_name.size > 0) {
     return context->storage->InternString(config_given_name);
   }
-  if (timebase.has_counter()) {
-    return context->storage->InternString(StringifyCounter(timebase.counter()));
+  if (desc_decoder.has_counter()) {
+    return context->storage->InternString(
+        StringifyCounter(desc_decoder.counter()));
   }
-  if (timebase.has_tracepoint()) {
-    PerfEvents::Tracepoint::Decoder tracepoint(timebase.tracepoint());
+  if (desc_decoder.has_tracepoint()) {
+    PerfEvents::Tracepoint::Decoder tracepoint(desc_decoder.tracepoint());
     return context->storage->InternString(tracepoint.name());
   }
-  if (timebase.has_raw_event()) {
-    PerfEvents::RawEvent::Decoder raw(timebase.raw_event());
+  if (desc_decoder.has_raw_event()) {
+    PerfEvents::RawEvent::Decoder raw(desc_decoder.raw_event());
     // This doesn't follow any pre-existing naming scheme, but aims to be a
     // short-enough default that is distinguishable.
     base::StackString<128> name(
@@ -113,9 +116,32 @@
     return context->storage->InternString(name.string_view());
   }
 
-  PERFETTO_DLOG("Could not name the perf timebase counter");
+  PERFETTO_DLOG("Could not name the perf counter");
   return context->storage->InternString("unknown");
 }
+
+StringId InternTimebaseCounterName(
+    const protos::pbzero::PerfSampleDefaults::Decoder& perf_defaults,
+    TraceProcessorContext* context) {
+  using namespace protos::pbzero;
+  PerfEvents::Timebase::Decoder timebase(perf_defaults.timebase());
+  return InternCounterName(perf_defaults, context, timebase);
+}
+
+std::vector<StringId> InternFollowersCounterName(
+    const protos::pbzero::PerfSampleDefaults::Decoder& perf_defaults,
+    TraceProcessorContext* context) {
+  using namespace protos::pbzero;
+
+  std::vector<StringId> string_ids;
+
+  for (auto it = perf_defaults.followers(); it; ++it) {
+    FollowerEvent::Decoder followers(*it);
+    string_ids.push_back(InternCounterName(perf_defaults, context, followers));
+  }
+
+  return string_ids;
+}
 }  // namespace
 
 PerfSampleTracker::SamplingStreamInfo PerfSampleTracker::GetSamplingStreamInfo(
@@ -133,7 +159,8 @@
 
   auto cpu_it = seq_state->per_cpu.find(cpu);
   if (cpu_it != seq_state->per_cpu.end())
-    return {seq_state->perf_session_id, cpu_it->second.timebase_track_id};
+    return {seq_state->perf_session_id, cpu_it->second.timebase_track_id,
+            cpu_it->second.follower_track_ids};
 
   std::optional<PerfSampleDefaults::Decoder> perf_defaults;
   if (nullable_defaults && nullable_defaults->has_perf_sample_defaults()) {
@@ -150,10 +177,23 @@
         StringifyCounter(protos::pbzero::PerfEvents::SW_CPU_CLOCK));
   }
 
-  TrackId timebase_track_id = context_->track_tracker->CreatePerfCounterTrack(
-      name_id, session_id, cpu, /*is_timebase=*/true);
+  TrackId timebase_track_id =
+      context_->track_tracker->LegacyCreatePerfCounterTrack(
+          name_id, session_id, cpu, /*is_timebase=*/true);
 
-  seq_state->per_cpu.emplace(cpu, timebase_track_id);
+  std::vector<TrackId> follower_track_ids;
+  if (perf_defaults.has_value()) {
+    auto name_ids = InternFollowersCounterName(perf_defaults.value(), context_);
+    follower_track_ids.reserve(name_ids.size());
+    for (const auto& follower_name_id : name_ids) {
+      follower_track_ids.push_back(
+          context_->track_tracker->LegacyCreatePerfCounterTrack(
+              follower_name_id, session_id, cpu, /*is_timebase=*/true));
+    }
+  }
+
+  seq_state->per_cpu.emplace(
+      cpu, CpuSequenceState{timebase_track_id, follower_track_ids});
 
   // If the config requested process sharding, record in the stats table which
   // shard was chosen for the trace. It should be the same choice for all data
@@ -169,7 +209,7 @@
         static_cast<int64_t>(perf_defaults->chosen_process_shard()));
   }
 
-  return {session_id, timebase_track_id};
+  return {session_id, timebase_track_id, std::move(follower_track_ids)};
 }
 
 tables::PerfSessionTable::Id PerfSampleTracker::CreatePerfSession() {
diff --git a/src/trace_processor/importers/proto/perf_sample_tracker.h b/src/trace_processor/importers/proto/perf_sample_tracker.h
index 178683d..f702417 100644
--- a/src/trace_processor/importers/proto/perf_sample_tracker.h
+++ b/src/trace_processor/importers/proto/perf_sample_tracker.h
@@ -39,11 +39,14 @@
   struct SamplingStreamInfo {
     tables::PerfSessionTable::Id perf_session_id;
     TrackId timebase_track_id = kInvalidTrackId;
+    std::vector<TrackId> follower_track_ids;
 
     SamplingStreamInfo(tables::PerfSessionTable::Id _perf_session_id,
-                       TrackId _timebase_track_id)
+                       TrackId _timebase_track_id,
+                       std::vector<TrackId> _follower_track_ids)
         : perf_session_id(_perf_session_id),
-          timebase_track_id(_timebase_track_id) {}
+          timebase_track_id(_timebase_track_id),
+          follower_track_ids(std::move(_follower_track_ids)) {}
   };
 
   explicit PerfSampleTracker(TraceProcessorContext* context)
@@ -57,9 +60,12 @@
  private:
   struct CpuSequenceState {
     TrackId timebase_track_id = kInvalidTrackId;
+    std::vector<TrackId> follower_track_ids;
 
-    CpuSequenceState(TrackId _timebase_track_id)
-        : timebase_track_id(_timebase_track_id) {}
+    CpuSequenceState(TrackId _timebase_track_id,
+                     std::vector<TrackId> _follower_track_ids)
+        : timebase_track_id(_timebase_track_id),
+          follower_track_ids(std::move(_follower_track_ids)) {}
   };
 
   struct SequenceState {
diff --git a/src/trace_processor/importers/proto/perf_sample_tracker_unittest.cc b/src/trace_processor/importers/proto/perf_sample_tracker_unittest.cc
index 86336a7..0f2dfbc 100644
--- a/src/trace_processor/importers/proto/perf_sample_tracker_unittest.cc
+++ b/src/trace_processor/importers/proto/perf_sample_tracker_unittest.cc
@@ -16,29 +16,34 @@
 
 #include "src/trace_processor/importers/proto/perf_sample_tracker.h"
 #include <cstdint>
+#include <memory>
 #include <string>
 
-#include "perfetto/base/logging.h"
+#include "src/trace_processor/importers/common/cpu_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/track_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "test/gtest_and_gmock.h"
 
 #include "protos/perfetto/common/perf_events.gen.h"
 #include "protos/perfetto/trace/profiling/profile_packet.gen.h"
-#include "protos/perfetto/trace/profiling/profile_packet.pbzero.h"
 #include "protos/perfetto/trace/trace_packet_defaults.gen.h"
 #include "protos/perfetto/trace/trace_packet_defaults.pbzero.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 namespace {
 
 class PerfSampleTrackerTest : public ::testing::Test {
  public:
   PerfSampleTrackerTest() {
-    context.storage.reset(new TraceStorage());
-    context.track_tracker.reset(new TrackTracker(&context));
-    context.perf_sample_tracker.reset(new PerfSampleTracker(&context));
+    context.storage = std::make_shared<TraceStorage>();
+    context.machine_tracker = std::make_unique<MachineTracker>(&context, 0);
+    context.cpu_tracker = std::make_unique<CpuTracker>(&context);
+    context.global_args_tracker =
+        std::make_unique<GlobalArgsTracker>(context.storage.get());
+    context.track_tracker = std::make_unique<TrackTracker>(&context);
+    context.perf_sample_tracker = std::make_unique<PerfSampleTracker>(&context);
   }
 
  protected:
@@ -183,6 +188,67 @@
   ASSERT_EQ(track_name, "test-name");
 }
 
+// Validate that associated counters in the description create related tracks.
+TEST_F(PerfSampleTrackerTest, FollowersTracks) {
+  uint32_t seq_id = 42;
+  uint32_t cpu_id = 0;
+
+  protos::gen::TracePacketDefaults defaults;
+  auto* perf_defaults = defaults.mutable_perf_sample_defaults();
+  perf_defaults->mutable_timebase()->set_name("leader");
+
+  // Associate a raw event.
+  auto* raw_follower = perf_defaults->add_followers();
+  raw_follower->set_name("raw");
+  auto* raw_event = raw_follower->mutable_raw_event();
+  raw_event->set_type(8);
+  raw_event->set_config(18);
+
+  // Associate a tracepoint.
+  auto* tracepoint_follower = perf_defaults->add_followers();
+  tracepoint_follower->set_name("tracepoint");
+  tracepoint_follower->mutable_tracepoint()->set_name("sched:sched_switch");
+
+  // Associate a HW counter.
+  auto* counter_follower = perf_defaults->add_followers();
+  counter_follower->set_name("pmu");
+  counter_follower->set_counter(protos::gen::PerfEvents::HW_CACHE_MISSES);
+
+  // Serialize the packet.
+  auto defaults_pb = defaults.SerializeAsString();
+  protos::pbzero::TracePacketDefaults::Decoder defaults_decoder(defaults_pb);
+
+  auto stream = context.perf_sample_tracker->GetSamplingStreamInfo(
+      seq_id, cpu_id, &defaults_decoder);
+
+  ASSERT_EQ(stream.follower_track_ids.size(), 3u);
+
+  std::vector<TrackId> track_ids;
+  track_ids.push_back(stream.timebase_track_id);
+  track_ids.insert(track_ids.end(), stream.follower_track_ids.begin(),
+                   stream.follower_track_ids.end());
+  std::vector<std::string> track_names = {"leader", "raw", "tracepoint", "pmu"};
+
+  ASSERT_EQ(track_ids.size(), track_names.size());
+
+  for (size_t i = 0; i < track_ids.size(); ++i) {
+    TrackId track_id = track_ids[i];
+    const auto& track_table = context.storage->perf_counter_track_table();
+    auto row_id = track_table.id().IndexOf(track_id);
+
+    // Check the track exists and looks sensible.
+    ASSERT_TRUE(row_id.has_value());
+    EXPECT_EQ(track_table.perf_session_id()[*row_id], stream.perf_session_id);
+    EXPECT_EQ(track_table.cpu()[*row_id], cpu_id);
+    EXPECT_TRUE(track_table.is_timebase()[*row_id]);
+
+    // Using the config-supplied name for the track.
+    std::string track_name =
+        context.storage->GetString(track_table.name()[*row_id]).ToStdString();
+    ASSERT_EQ(track_name, track_names[i]);
+  }
+}
+
 TEST_F(PerfSampleTrackerTest, ProcessShardingStatsEntries) {
   uint32_t cpu0 = 0;
   uint32_t cpu1 = 1;
@@ -238,5 +304,4 @@
 }
 
 }  // namespace
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/pigweed_detokenizer.cc b/src/trace_processor/importers/proto/pigweed_detokenizer.cc
index b3a97b8..9e3ed8d 100644
--- a/src/trace_processor/importers/proto/pigweed_detokenizer.cc
+++ b/src/trace_processor/importers/proto/pigweed_detokenizer.cc
@@ -159,26 +159,28 @@
       formatted_size =
           perfetto::base::SprintfTrunc(buffer, kFormatBufferSize, fmt, value);
     } else {
-      uint64_t value;
+      uint64_t raw;
       auto old_ptr = ptr;
       ptr = protozero::proto_utils::ParseVarInt(ptr, bytes.data + bytes.size,
-                                                &value);
+                                                &raw);
       if (old_ptr == ptr) {
         return base::ErrStatus("Truncated Pigweed varint");
       }
+      // All Pigweed integers (including unsigned) are zigzag encoded.
+      int64_t value = ::protozero::proto_utils::ZigZagDecode(raw);
       if (arg.type == kSignedInt) {
-        int64_t value_signed;
-        memcpy(&value_signed, &value, sizeof(value_signed));
-        args.push_back(value_signed);
-        formatted_size = perfetto::base::SprintfTrunc(buffer, kFormatBufferSize,
-                                                      fmt, value_signed);
-      } else {
-        if (arg.type == kUnsigned32) {
-          value &= 0xFFFFFFFFu;
-        }
         args.push_back(value);
         formatted_size =
             perfetto::base::SprintfTrunc(buffer, kFormatBufferSize, fmt, value);
+      } else {
+        uint64_t value_unsigned;
+        memcpy(&value_unsigned, &value, sizeof(value_unsigned));
+        if (arg.type == kUnsigned32) {
+          value_unsigned &= 0xFFFFFFFFu;
+        }
+        args.push_back(value_unsigned);
+        formatted_size = perfetto::base::SprintfTrunc(buffer, kFormatBufferSize,
+                                                      fmt, value_unsigned);
       }
     }
     if (formatted_size == kFormatBufferSize - 1) {
diff --git a/src/trace_processor/importers/proto/pixel_modem_module.cc b/src/trace_processor/importers/proto/pixel_modem_module.cc
index 1d150a1..d0deb80 100644
--- a/src/trace_processor/importers/proto/pixel_modem_module.cc
+++ b/src/trace_processor/importers/proto/pixel_modem_module.cc
@@ -83,6 +83,10 @@
   for (auto it = evt.events(); it && ts_it; ++it, ++ts_it) {
     protozero::ConstBytes event_bytes = *it;
     ts += *ts_it;
+    if (ts < 0) {
+      context_->storage->IncrementStats(stats::pixel_modem_negative_timestamp);
+      continue;
+    }
 
     protozero::HeapBuffered<protos::pbzero::TracePacket> data_packet;
     // Keep the original timestamp to later extract as an arg; the sorter does
diff --git a/src/trace_processor/importers/proto/pixel_modem_parser.cc b/src/trace_processor/importers/proto/pixel_modem_parser.cc
index 56e08b3..0119f54 100644
--- a/src/trace_processor/importers/proto/pixel_modem_parser.cc
+++ b/src/trace_processor/importers/proto/pixel_modem_parser.cc
@@ -64,6 +64,7 @@
       detokenizer_(pigweed::CreateNullDetokenizer()),
       template_id_(context->storage->InternString("raw_template")),
       token_id_(context->storage->InternString("token_id")),
+      token_id_hex_(context->storage->InternString("token_id_hex")),
       packet_timestamp_id_(context->storage->InternString("packet_ts")) {}
 
 PixelModemParser::~PixelModemParser() = default;
@@ -103,13 +104,17 @@
         inserter->AddArg(template_id_,
                          Variadic::String(context_->storage->InternString(
                              detokenized_str.template_str().c_str())));
-        inserter->AddArg(token_id_, Variadic::Integer(detokenized_str.token()));
+        uint32_t token = detokenized_str.token();
+        inserter->AddArg(token_id_, Variadic::Integer(token));
+        inserter->AddArg(token_id_hex_,
+                         Variadic::String(context_->storage->InternString(
+                             base::IntToHexString(token).c_str())));
         inserter->AddArg(packet_timestamp_id_,
                          Variadic::UnsignedInteger(trace_packet_ts));
         auto pw_args = detokenized_str.args();
         for (size_t i = 0; i < pw_args.size(); i++) {
           StringId arg_name = context_->storage->InternString(
-              ("pw_token_" + std::to_string(detokenized_str.token()) + ".arg_" +
+              ("pw_token_" + std::to_string(token) + ".arg_" +
                std::to_string(i))
                   .c_str());
           auto arg = pw_args[i];
diff --git a/src/trace_processor/importers/proto/pixel_modem_parser.h b/src/trace_processor/importers/proto/pixel_modem_parser.h
index a505a0a..bdb6d18 100644
--- a/src/trace_processor/importers/proto/pixel_modem_parser.h
+++ b/src/trace_processor/importers/proto/pixel_modem_parser.h
@@ -41,6 +41,7 @@
 
   const StringId template_id_;
   const StringId token_id_;
+  const StringId token_id_hex_;
   const StringId packet_timestamp_id_;
 };
 
diff --git a/src/trace_processor/importers/proto/profile_module.cc b/src/trace_processor/importers/proto/profile_module.cc
index 813fa09..3e281ed 100644
--- a/src/trace_processor/importers/proto/profile_module.cc
+++ b/src/trace_processor/importers/proto/profile_module.cc
@@ -251,6 +251,16 @@
       ts, static_cast<double>(sample.timebase_count()),
       sampling_stream.timebase_track_id);
 
+  if (sample.has_follower_counts()) {
+    auto track_it = sampling_stream.follower_track_ids.begin();
+    auto track_end = sampling_stream.follower_track_ids.end();
+    for (auto it = sample.follower_counts(); it && track_it != track_end;
+         ++it, ++track_it) {
+      context_->event_tracker->PushCounter(ts, static_cast<double>(*it),
+                                           *track_it);
+    }
+  }
+
   const UniqueTid utid =
       context_->process_tracker->UpdateThread(sample.tid(), sample.pid());
   const UniquePid upid =
diff --git a/src/trace_processor/importers/proto/profile_packet_sequence_state_unittest.cc b/src/trace_processor/importers/proto/profile_packet_sequence_state_unittest.cc
index 2022952..88d492a 100644
--- a/src/trace_processor/importers/proto/profile_packet_sequence_state_unittest.cc
+++ b/src/trace_processor/importers/proto/profile_packet_sequence_state_unittest.cc
@@ -16,10 +16,15 @@
 
 #include "src/trace_processor/importers/proto/profile_packet_sequence_state.h"
 
+#include <cstddef>
 #include <cstdint>
 #include <memory>
 #include <optional>
+#include <string>
 
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/ext/base/utils.h"
+#include "perfetto/trace_processor/ref_counted.h"
 #include "src/trace_processor/importers/common/mapping_tracker.h"
 #include "src/trace_processor/importers/common/stack_profile_tracker.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
@@ -131,7 +136,7 @@
   profile_packet_sequence_state().FinalizeProfile();
 
   EXPECT_THAT(context.storage->stack_profile_mapping_table()[0].build_id(),
-              context.storage->InternString({kBuildIDHexName}));
+              context.storage->InternString(kBuildIDHexName));
   EXPECT_THAT(context.storage->stack_profile_mapping_table()[0].exact_offset(),
               kMappingExactOffset);
   EXPECT_THAT(context.storage->stack_profile_mapping_table()[0].start_offset(),
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 8a11d5d..f4b6fca 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
@@ -16,21 +16,24 @@
 
 #include "src/trace_processor/importers/proto/proto_trace_parser_impl.h"
 
-#include <string.h>
-
-#include <cinttypes>
+#include <cstdint>
+#include <cstring>
 #include <string>
+#include <utility>
+#include <vector>
 
 #include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/metatrace_events.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/uuid.h"
-
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/containers/null_term_string_view.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/cpu_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
+#include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
@@ -39,8 +42,8 @@
 #include "src/trace_processor/importers/etw/etw_module.h"
 #include "src/trace_processor/importers/ftrace/ftrace_module.h"
 #include "src/trace_processor/importers/proto/track_event_module.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/types/trace_processor_context.h"
 #include "src/trace_processor/types/variadic.h"
 
@@ -49,8 +52,7 @@
 #include "protos/perfetto/trace/perfetto/perfetto_metatrace.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 ProtoTraceParserImpl::ProtoTraceParserImpl(TraceProcessorContext* context)
     : context_(context),
@@ -109,8 +111,8 @@
 }
 
 void ProtoTraceParserImpl::ParseEtwEvent(uint32_t cpu,
-                                     int64_t ts,
-                                     TracePacketData data) {
+                                         int64_t ts,
+                                         TracePacketData data) {
   PERFETTO_DCHECK(context_->etw_module);
   context_->etw_module->ParseEtwEventData(cpu, ts, data);
 
@@ -121,8 +123,8 @@
 }
 
 void ProtoTraceParserImpl::ParseFtraceEvent(uint32_t cpu,
-                                        int64_t ts,
-                                        TracePacketData data) {
+                                            int64_t ts,
+                                            TracePacketData data) {
   PERFETTO_DCHECK(context_->ftrace_module);
   context_->ftrace_module->ParseFtraceEventData(cpu, ts, data);
 
@@ -133,8 +135,8 @@
 }
 
 void ProtoTraceParserImpl::ParseInlineSchedSwitch(uint32_t cpu,
-                                              int64_t ts,
-                                              InlineSchedSwitch data) {
+                                                  int64_t ts,
+                                                  InlineSchedSwitch data) {
   PERFETTO_DCHECK(context_->ftrace_module);
   context_->ftrace_module->ParseInlineSchedSwitch(cpu, ts, data);
 
@@ -145,8 +147,8 @@
 }
 
 void ProtoTraceParserImpl::ParseInlineSchedWaking(uint32_t cpu,
-                                              int64_t ts,
-                                              InlineSchedWaking data) {
+                                                  int64_t ts,
+                                                  InlineSchedWaking data) {
   PERFETTO_DCHECK(context_->ftrace_module);
   context_->ftrace_module->ParseInlineSchedWaking(cpu, ts, data);
 
@@ -353,7 +355,7 @@
       name_id = context_->storage->InternString(event.counter_name());
     }
     TrackId track =
-        context_->track_tracker->InternThreadCounterTrack(name_id, utid);
+        context_->track_tracker->LegacyInternThreadCounterTrack(name_id, utid);
     auto opt_id =
         context_->event_tracker->PushCounter(ts, event.counter_value(), track);
     if (opt_id) {
@@ -373,5 +375,4 @@
   return *maybe_id;
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_impl.h b/src/trace_processor/importers/proto/proto_trace_parser_impl.h
index 2c4dc07..0c1db93 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_impl.h
+++ b/src/trace_processor/importers/proto/proto_trace_parser_impl.h
@@ -17,11 +17,9 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PROTO_TRACE_PARSER_IMPL_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PROTO_TRACE_PARSER_IMPL_H_
 
-#include <stdint.h>
+#include <cstdint>
 
-#include <array>
-#include <memory>
-
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/protozero/field.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
@@ -29,11 +27,9 @@
 
 namespace perfetto {
 
-namespace protos {
-namespace pbzero {
+namespace protos::pbzero {
 class TracePacket_Decoder;
-}  // namespace pbzero
-}  // namespace protos
+}  // namespace protos::pbzero
 
 namespace trace_processor {
 
@@ -65,12 +61,12 @@
                               int64_t /*ts*/,
                               InlineSchedWaking data) override;
 
-  void ParseChromeEvents(int64_t ts, ConstBytes);
-  void ParseMetatraceEvent(int64_t ts, ConstBytes);
-
  private:
   StringId GetMetatraceInternedString(uint64_t iid);
 
+  void ParseChromeEvents(int64_t ts, ConstBytes);
+  void ParseMetatraceEvent(int64_t ts, ConstBytes);
+
   TraceProcessorContext* context_;
 
   const StringId metatrace_id_;
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 cbff477..229a658 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
@@ -63,6 +63,8 @@
 #include "test/gtest_and_gmock.h"
 
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
+#include "protos/perfetto/common/perf_events.gen.h"
+#include "protos/perfetto/common/perf_events.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"
@@ -94,6 +96,7 @@
 #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"
+#include "src/trace_processor/importers/proto/perf_sample_tracker.h"
 
 namespace perfetto::trace_processor {
 namespace {
@@ -122,7 +125,7 @@
   // a NAN must return false.
   double d_exp = exp;
   double d_arg = arg;
-  if (isnan(d_exp) || isnan(d_arg))
+  if (std::isnan(d_exp) || std::isnan(d_arg))
     return false;
   return fabs(d_arg - d_exp) < 1e-128;
 }
@@ -304,6 +307,8 @@
         &context_, TraceSorter::SortingMode::kFullSort);
     context_.descriptor_pool_ = std::make_unique<DescriptorPool>();
 
+    context_.perf_sample_tracker.reset(new PerfSampleTracker(&context_));
+
     RegisterDefaultModules(&context_);
     RegisterAdditionalModules(&context_);
   }
@@ -691,6 +696,20 @@
   EXPECT_EQ(context_.storage->track_table().row_count(), 1u);
 }
 
+TEST_F(ProtoTraceParserTest, LoadGpuFreqStats) {
+  auto* packet = trace_->add_packet();
+  uint64_t ts = 1000;
+  packet->set_timestamp(ts);
+  auto* bundle = packet->set_sys_stats();
+  bundle->add_gpufreq_mhz(300);
+  EXPECT_CALL(*event_, PushCounter(static_cast<int64_t>(ts),
+                                   static_cast<double>(300), TrackId{1u}));
+  Tokenize();
+  context_.sorter->ExtractEventsForced();
+
+  EXPECT_EQ(context_.storage->track_table().row_count(), 2u);
+}
+
 TEST_F(ProtoTraceParserTest, LoadMemInfo) {
   auto* packet = trace_->add_packet();
   uint64_t ts = 1000;
@@ -2446,31 +2465,29 @@
   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(), 1u);
+  EXPECT_EQ(raw_table[0].arg_set_id(), 3u);
 
-  EXPECT_GE(storage_->arg_table().row_count(), 10u);
-
-  EXPECT_TRUE(HasArg(1u, storage_->InternString("legacy_event.category"),
+  EXPECT_TRUE(HasArg(3u, storage_->InternString("legacy_event.category"),
                      Variadic::String(cat_1)));
-  EXPECT_TRUE(HasArg(1u, storage_->InternString("legacy_event.name"),
+  EXPECT_TRUE(HasArg(3u, storage_->InternString("legacy_event.name"),
                      Variadic::String(ev_1)));
-  EXPECT_TRUE(HasArg(1u, storage_->InternString("legacy_event.phase"),
+  EXPECT_TRUE(HasArg(3u, storage_->InternString("legacy_event.phase"),
                      Variadic::String(question)));
-  EXPECT_TRUE(HasArg(1u, storage_->InternString("legacy_event.duration_ns"),
+  EXPECT_TRUE(HasArg(3u, storage_->InternString("legacy_event.duration_ns"),
                      Variadic::Integer(23000)));
-  EXPECT_TRUE(HasArg(1u,
+  EXPECT_TRUE(HasArg(3u,
                      storage_->InternString("legacy_event.thread_timestamp_ns"),
                      Variadic::Integer(2005000)));
-  EXPECT_TRUE(HasArg(1u,
+  EXPECT_TRUE(HasArg(3u,
                      storage_->InternString("legacy_event.thread_duration_ns"),
                      Variadic::Integer(15000)));
-  EXPECT_TRUE(HasArg(1u, storage_->InternString("legacy_event.use_async_tts"),
+  EXPECT_TRUE(HasArg(3u, storage_->InternString("legacy_event.use_async_tts"),
                      Variadic::Boolean(true)));
-  EXPECT_TRUE(HasArg(1u, storage_->InternString("legacy_event.global_id"),
+  EXPECT_TRUE(HasArg(3u, storage_->InternString("legacy_event.global_id"),
                      Variadic::UnsignedInteger(99u)));
-  EXPECT_TRUE(HasArg(1u, storage_->InternString("legacy_event.id_scope"),
+  EXPECT_TRUE(HasArg(3u, storage_->InternString("legacy_event.id_scope"),
                      Variadic::String(scope_1)));
-  EXPECT_TRUE(HasArg(1u, debug_an_1, Variadic::UnsignedInteger(10u)));
+  EXPECT_TRUE(HasArg(3u, debug_an_1, Variadic::UnsignedInteger(10u)));
 }
 
 TEST_F(ProtoTraceParserTest, TrackEventLegacyTimestampsWithClockSnapshot) {
@@ -3029,5 +3046,67 @@
   EXPECT_THAT(value.string_value, HasSubstr("size_kb: 42"));
 }
 
+TEST_F(ProtoTraceParserTest, PerfEventWithMultipleCounter) {
+  {
+    auto* packet = trace_->add_packet();
+    packet->set_trusted_packet_sequence_id(1);
+    packet->set_incremental_state_cleared(true);
+    packet->set_timestamp(3000);
+    auto* perf_sample_default =
+        packet->set_trace_packet_defaults()->set_perf_sample_defaults();
+
+    // leader description:
+    auto* timebase = perf_sample_default->set_timebase();
+    timebase->set_name("leader");
+    timebase->set_counter(
+        protos::pbzero::PerfEvents::Counter::SW_CONTEXT_SWITCHES);
+    timebase->set_frequency(1000);
+
+    // followers description:
+    auto* follower = perf_sample_default->add_followers();
+    follower->set_counter(protos::pbzero::PerfEvents::Counter::HW_CPU_CYCLES);
+    follower->set_name("cycle-follower");
+
+    follower = perf_sample_default->add_followers();
+    follower->set_counter(protos::pbzero::PerfEvents::Counter::HW_CACHE_MISSES);
+    follower->set_name("cache-follower");
+  }
+  {
+    auto* packet = trace_->add_packet();
+    packet->set_trusted_packet_sequence_id(1);
+    packet->set_timestamp(3000);
+    auto* perf_sample = packet->set_perf_sample();
+    perf_sample->set_cpu(0);
+    perf_sample->set_pid(1);
+    perf_sample->set_tid(42);
+    perf_sample->set_cpu_mode(
+        ::perfetto::protos::pbzero::Profiling_CpuMode::MODE_USER);
+    perf_sample->set_timebase_count(512);
+    perf_sample->add_follower_counts(1024);
+    perf_sample->add_follower_counts(2048);
+  }
+
+  EXPECT_CALL(*event_, PushCounter(3000, testing::DoubleEq(512), TrackId{0u}));
+  EXPECT_CALL(*event_, PushCounter(3000, testing::DoubleEq(1024), TrackId{1u}));
+  EXPECT_CALL(*event_, PushCounter(3000, testing::DoubleEq(2048), TrackId{2u}));
+
+  Tokenize();
+  context_.sorter->ExtractEventsForced();
+
+  EXPECT_EQ(storage_->track_table().row_count(), 3u);
+
+  // Validate the perf counter track table.
+  EXPECT_EQ(storage_->perf_counter_track_table().row_count(), 3u);
+
+  const auto& perf_counter_names = storage_->perf_counter_track_table().name();
+  EXPECT_EQ(perf_counter_names[0], storage_->InternString("leader"));
+  EXPECT_EQ(perf_counter_names[1], storage_->InternString("cycle-follower"));
+  EXPECT_EQ(perf_counter_names[2], storage_->InternString("cache-follower"));
+  const auto& cpu_ids = storage_->perf_counter_track_table().cpu();
+  EXPECT_EQ(cpu_ids[0], 0u);
+  EXPECT_EQ(cpu_ids[1], 0u);
+  EXPECT_EQ(cpu_ids[2], 0u);
+}
+
 }  // namespace
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.cc b/src/trace_processor/importers/proto/proto_trace_reader.cc
index 54bfdba..6776fbd 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.cc
+++ b/src/trace_processor/importers/proto/proto_trace_reader.cc
@@ -16,33 +16,38 @@
 
 #include "src/trace_processor/importers/proto/proto_trace_reader.h"
 
+#include <algorithm>
+#include <cinttypes>
+#include <cstddef>
+#include <cstdint>
+#include <map>
 #include <numeric>
 #include <optional>
 #include <string>
+#include <tuple>
+#include <utility>
 #include <vector>
 
-#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/status_or.h"
 #include "perfetto/ext/base/string_view.h"
-#include "perfetto/ext/base/utils.h"
+#include "perfetto/protozero/field.h"
 #include "perfetto/protozero/proto_decoder.h"
-#include "perfetto/protozero/proto_utils.h"
 #include "perfetto/public/compiler.h"
-#include "perfetto/trace_processor/status.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/machine_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
-#include "src/trace_processor/importers/common/track_tracker.h"
-#include "src/trace_processor/importers/ftrace/ftrace_module.h"
 #include "src/trace_processor/importers/proto/packet_analyzer.h"
+#include "src/trace_processor/importers/proto/proto_importer_module.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 "src/trace_processor/util/gzip_utils.h"
 
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
 #include "protos/perfetto/common/trace_stats.pbzero.h"
@@ -50,13 +55,11 @@
 #include "protos/perfetto/trace/clock_snapshot.pbzero.h"
 #include "protos/perfetto/trace/extension_descriptor.pbzero.h"
 #include "protos/perfetto/trace/perfetto/tracing_service_event.pbzero.h"
-#include "protos/perfetto/trace/profiling/profile_common.pbzero.h"
 #include "protos/perfetto/trace/remote_clock_sync.pbzero.h"
 #include "protos/perfetto/trace/trace.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 ProtoTraceReader::ProtoTraceReader(TraceProcessorContext* ctx)
     : context_(ctx),
@@ -65,13 +68,13 @@
           ctx->storage->InternString("invalid_incremental_state")) {}
 ProtoTraceReader::~ProtoTraceReader() = default;
 
-util::Status ProtoTraceReader::Parse(TraceBlobView blob) {
+base::Status ProtoTraceReader::Parse(TraceBlobView blob) {
   return tokenizer_.Tokenize(std::move(blob), [this](TraceBlobView packet) {
     return ParsePacket(std::move(packet));
   });
 }
 
-util::Status ProtoTraceReader::ParseExtensionDescriptor(ConstBytes descriptor) {
+base::Status ProtoTraceReader::ParseExtensionDescriptor(ConstBytes descriptor) {
   protos::pbzero::ExtensionDescriptor::Decoder decoder(descriptor.data,
                                                        descriptor.size);
 
@@ -82,10 +85,10 @@
       /*merge_existing_messages=*/true);
 }
 
-util::Status ProtoTraceReader::ParsePacket(TraceBlobView packet) {
+base::Status ProtoTraceReader::ParsePacket(TraceBlobView packet) {
   protos::pbzero::TracePacket::Decoder decoder(packet.data(), packet.length());
   if (PERFETTO_UNLIKELY(decoder.bytes_left())) {
-    return util::ErrStatus(
+    return base::ErrStatus(
         "Failed to parse proto packet fully; the trace is probably corrupt.");
   }
 
@@ -107,15 +110,19 @@
   // Assert that the packet is parsed using the right instance of reader.
   PERFETTO_DCHECK(decoder.has_machine_id() == !!context_->machine_id());
 
-  const uint32_t seq_id = decoder.trusted_packet_sequence_id();
-  auto* state = GetIncrementalStateForPacketSequence(seq_id);
+  uint32_t seq_id = decoder.trusted_packet_sequence_id();
+  auto [scoped_state, inserted] = sequence_state_.Insert(seq_id, {});
+  if (decoder.has_trusted_packet_sequence_id()) {
+    if (!inserted && decoder.previous_packet_dropped()) {
+      ++scoped_state->previous_packet_dropped_count;
+    }
+  }
 
   if (decoder.first_packet_on_sequence()) {
     HandleFirstPacketOnSequence(seq_id);
   }
 
   uint32_t sequence_flags = decoder.sequence_flags();
-
   if (decoder.incremental_state_cleared() ||
       sequence_flags &
           protos::pbzero::TracePacket::SEQ_INCREMENTAL_STATE_CLEARED) {
@@ -124,16 +131,6 @@
     HandlePreviousPacketDropped(decoder);
   }
 
-  uint32_t sequence_id = decoder.trusted_packet_sequence_id();
-  if (sequence_id) {
-    auto [data_loss, inserted] =
-        packet_sequence_data_loss_.Insert(sequence_id, 0);
-
-    if (!inserted && decoder.previous_packet_dropped()) {
-      *data_loss += 1;
-    }
-  }
-
   // It is important that we parse defaults before parsing other fields such as
   // the timestamp, since the defaults could affect them.
   if (decoder.has_trace_packet_defaults()) {
@@ -147,7 +144,7 @@
   }
 
   if (decoder.has_clock_snapshot()) {
-    return ParseClockSnapshot(decoder.clock_snapshot(), sequence_id);
+    return ParseClockSnapshot(decoder.clock_snapshot(), seq_id);
   }
 
   if (decoder.has_trace_stats()) {
@@ -169,26 +166,29 @@
     return ParseExtensionDescriptor(decoder.extension_descriptor());
   }
 
+  auto* state = GetIncrementalStateForPacketSequence(seq_id);
   if (decoder.sequence_flags() &
       protos::pbzero::TracePacket::SEQ_NEEDS_INCREMENTAL_STATE) {
     if (!seq_id) {
-      return util::ErrStatus(
+      return base::ErrStatus(
           "TracePacket specified SEQ_NEEDS_INCREMENTAL_STATE but the "
           "TraceWriter's sequence_id is zero (the service is "
           "probably too old)");
     }
+    scoped_state->needs_incremental_state_total++;
 
     if (!state->IsIncrementalStateValid()) {
       if (context_->content_analyzer) {
         // Account for the skipped packet for trace proto content analysis,
         // with a special annotation.
         PacketAnalyzer::SampleAnnotation annotation;
-        annotation.push_back(
-            {skipped_packet_key_id_, invalid_incremental_state_key_id_});
+        annotation.emplace_back(skipped_packet_key_id_,
+                                invalid_incremental_state_key_id_);
         PacketAnalyzer::Get(context_)->ProcessPacket(packet, annotation);
       }
+      scoped_state->needs_incremental_state_skipped++;
       context_->storage->IncrementStats(stats::tokenizer_skipped_packets);
-      return util::OkStatus();
+      return base::OkStatus();
     }
   }
 
@@ -223,7 +223,7 @@
       ClockTracker::ClockId converted_clock_id = timestamp_clock_id;
       if (ClockTracker::IsSequenceClock(converted_clock_id)) {
         if (!seq_id) {
-          return util::ErrStatus(
+          return base::ErrStatus(
               "TracePacket specified a sequence-local clock id (%" PRIu32
               ") but the TraceWriter's sequence_id is zero (the service is "
               "probably too old)",
@@ -239,7 +239,7 @@
         // We don't return an error here as it will cause the trace to stop
         // parsing. Instead, we rely on the stat increment in ToTraceTime() to
         // inform the user about the error.
-        return util::OkStatus();
+        return base::OkStatus();
       }
       timestamp = trace_ts.value();
     }
@@ -280,17 +280,24 @@
   context_->sorter->PushTracePacket(timestamp, state->current_generation(),
                                     std::move(packet), context_->machine_id());
 
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 void ProtoTraceReader::ParseTraceConfig(protozero::ConstBytes blob) {
-  protos::pbzero::TraceConfig::Decoder trace_config(blob);
-  if (trace_config.write_into_file() && !trace_config.flush_period_ms()) {
-    PERFETTO_ELOG(
-        "It is strongly recommended to have flush_period_ms set when "
-        "write_into_file is turned on. This trace will be loaded fully "
-        "into memory before sorting which increases the likelihood of "
-        "OOMs.");
+  using Config = protos::pbzero::TraceConfig;
+  Config::Decoder trace_config(blob);
+  if (trace_config.write_into_file()) {
+    if (!trace_config.flush_period_ms()) {
+      context_->storage->IncrementStats(stats::config_write_into_file_no_flush);
+    }
+    int i = 0;
+    for (auto it = trace_config.buffers(); it; ++it, ++i) {
+      Config::BufferConfig::Decoder buf(*it);
+      if (buf.fill_policy() == Config::BufferConfig::FillPolicy::DISCARD) {
+        context_->storage->IncrementIndexedStats(
+            stats::config_write_into_file_discard, i);
+      }
+    }
   }
 }
 
@@ -373,7 +380,7 @@
   }
 }
 
-util::Status ProtoTraceReader::ParseClockSnapshot(ConstBytes blob,
+base::Status ProtoTraceReader::ParseClockSnapshot(ConstBytes blob,
                                                   uint32_t seq_id) {
   std::vector<ClockTracker::ClockTimestamp> clock_timestamps;
   protos::pbzero::ClockSnapshot::Decoder evt(blob.data, blob.size);
@@ -386,7 +393,7 @@
     ClockTracker::ClockId clock_id = clk.clock_id();
     if (ClockTracker::IsSequenceClock(clk.clock_id())) {
       if (!seq_id) {
-        return util::ErrStatus(
+        return base::ErrStatus(
             "ClockSnapshot packet is specifying a sequence-scoped clock id "
             "(%" PRIu64 ") but the TracePacket sequence_id is zero",
             clock_id);
@@ -450,10 +457,10 @@
 
     context_->storage->mutable_clock_snapshot_table()->Insert(row);
   }
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
-util::Status ProtoTraceReader::ParseRemoteClockSync(ConstBytes blob) {
+base::Status ProtoTraceReader::ParseRemoteClockSync(ConstBytes blob) {
   protos::pbzero::RemoteClockSync::Decoder evt(blob.data, blob.size);
 
   std::vector<SyncClockSnapshots> sync_clock_snapshots;
@@ -496,7 +503,7 @@
     context_->clock_tracker->SetClockOffset(it.key(), it.value());
   }
 
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 base::FlatHashMap<int64_t /*Clock Id*/, int64_t /*Offset*/>
@@ -593,7 +600,7 @@
   }
 }
 
-util::Status ProtoTraceReader::ParseServiceEvent(int64_t ts, ConstBytes blob) {
+base::Status ProtoTraceReader::ParseServiceEvent(int64_t ts, ConstBytes blob) {
   protos::pbzero::TracingServiceEvent::Decoder tse(blob);
   if (tse.tracing_started()) {
     context_->metadata_tracker->SetMetadata(metadata::tracing_started_ns,
@@ -615,7 +622,21 @@
   if (tse.read_tracing_buffers_completed()) {
     context_->sorter->NotifyReadBufferEvent();
   }
-  return util::OkStatus();
+  if (tse.has_slow_starting_data_sources()) {
+    protos::pbzero::TracingServiceEvent::DataSources::Decoder msg(
+        tse.slow_starting_data_sources());
+    for (auto it = msg.data_source(); it; it++) {
+      protos::pbzero::TracingServiceEvent::DataSources::DataSource::Decoder
+          data_source(*it);
+      std::string formatted = data_source.producer_name().ToStdString() + " " +
+                              data_source.data_source_name().ToStdString();
+      context_->metadata_tracker->AppendMetadata(
+          metadata::slow_start_data_source,
+          Variadic::String(
+              context_->storage->InternString(base::StringView(formatted))));
+    }
+  }
+  return base::OkStatus();
 }
 
 void ProtoTraceReader::ParseTraceStats(ConstBytes blob) {
@@ -719,21 +740,30 @@
         static_cast<int64_t>(buf.trace_writer_packet_loss()));
   }
 
-  base::FlatHashMap<int32_t, int64_t> data_loss_per_buffer;
-
+  struct BufStats {
+    uint32_t packet_loss = 0;
+    uint32_t incremental_sequences_dropped = 0;
+  };
+  base::FlatHashMap<int32_t, BufStats> stats_per_buffer;
   for (auto it = evt.writer_stats(); it; ++it) {
-    protos::pbzero::TraceStats::WriterStats::Decoder writer(*it);
-    auto* data_loss = packet_sequence_data_loss_.Find(
-        static_cast<uint32_t>(writer.sequence_id()));
-    if (data_loss) {
-      data_loss_per_buffer[static_cast<int32_t>(writer.buffer())] +=
-          static_cast<int64_t>(*data_loss);
+    protos::pbzero::TraceStats::WriterStats::Decoder w(*it);
+    auto seq_id = static_cast<uint32_t>(w.sequence_id());
+    if (auto* s = sequence_state_.Find(seq_id)) {
+      auto& stats = stats_per_buffer[static_cast<int32_t>(w.buffer())];
+      stats.packet_loss += s->previous_packet_dropped_count;
+      stats.incremental_sequences_dropped +=
+          s->needs_incremental_state_skipped > 0 &&
+          s->needs_incremental_state_skipped ==
+              s->needs_incremental_state_total;
     }
   }
 
-  for (auto it = data_loss_per_buffer.GetIterator(); it; ++it) {
+  for (auto it = stats_per_buffer.GetIterator(); it; ++it) {
+    auto& v = it.value();
     storage->SetIndexedStats(stats::traced_buf_sequence_packet_loss, it.key(),
-                             it.value());
+                             v.packet_loss);
+    storage->SetIndexedStats(stats::traced_buf_incremental_sequences_dropped,
+                             it.key(), v.incremental_sequences_dropped);
   }
 }
 
@@ -741,5 +771,4 @@
   return base::OkStatus();
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.h b/src/trace_processor/importers/proto/proto_trace_reader.h
index 45c0c9f..fa9986e 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.h
+++ b/src/trace_processor/importers/proto/proto_trace_reader.h
@@ -17,11 +17,13 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PROTO_TRACE_READER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PROTO_TRACE_READER_H_
 
-#include <stdint.h>
-
-#include <tuple>
+#include <cstddef>
+#include <cstdint>
+#include <optional>
 #include <utility>
+#include <vector>
 
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/flat_hash_map.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
 #include "src/trace_processor/importers/proto/multi_machine_trace_manager.h"
@@ -35,12 +37,10 @@
 
 namespace perfetto {
 
-namespace protos {
-namespace pbzero {
+namespace protos::pbzero {
 class TracePacket_Decoder;
 class TraceConfig_Decoder;
-}  // namespace pbzero
-}  // namespace protos
+}  // namespace protos::pbzero
 
 namespace trace_processor {
 
@@ -61,13 +61,13 @@
   ~ProtoTraceReader() override;
 
   // ChunkedTraceReader implementation.
-  util::Status Parse(TraceBlobView) override;
+  base::Status Parse(TraceBlobView) override;
   base::Status NotifyEndOfFile() override;
 
   using SyncClockSnapshots = base::FlatHashMap<
       int64_t,
       std::pair</*host ts*/ uint64_t, /*client ts*/ uint64_t>>;
-  base::FlatHashMap<int64_t /*Clock Id*/, int64_t /*Offset*/>
+  static base::FlatHashMap<int64_t /*Clock Id*/, int64_t /*Offset*/>
   CalculateClockOffsetsForTesting(
       std::vector<SyncClockSnapshots>& sync_clock_snapshots) {
     return CalculateClockOffsets(sync_clock_snapshots);
@@ -76,11 +76,18 @@
   std::optional<StringId> GetBuiltinClockNameOrNull(int64_t clock_id);
 
  private:
+  struct SequenceScopedState {
+    std::optional<PacketSequenceStateBuilder> sequence_state_builder;
+    uint32_t previous_packet_dropped_count = 0;
+    uint32_t needs_incremental_state_total = 0;
+    uint32_t needs_incremental_state_skipped = 0;
+  };
+
   using ConstBytes = protozero::ConstBytes;
-  util::Status ParsePacket(TraceBlobView);
-  util::Status ParseServiceEvent(int64_t ts, ConstBytes);
-  util::Status ParseClockSnapshot(ConstBytes blob, uint32_t seq_id);
-  util::Status ParseRemoteClockSync(ConstBytes blob);
+  base::Status ParsePacket(TraceBlobView);
+  base::Status ParseServiceEvent(int64_t ts, ConstBytes);
+  base::Status ParseClockSnapshot(ConstBytes blob, uint32_t seq_id);
+  base::Status ParseRemoteClockSync(ConstBytes blob);
   void HandleIncrementalStateCleared(
       const protos::pbzero::TracePacket_Decoder&);
   void HandleFirstPacketOnSequence(uint32_t packet_sequence_id);
@@ -92,20 +99,18 @@
   void ParseTraceConfig(ConstBytes);
   void ParseTraceStats(ConstBytes);
 
-  base::FlatHashMap<int64_t /*Clock Id*/, int64_t /*Offset*/>
+  static base::FlatHashMap<int64_t /*Clock Id*/, int64_t /*Offset*/>
   CalculateClockOffsets(std::vector<SyncClockSnapshots>&);
 
   PacketSequenceStateBuilder* GetIncrementalStateForPacketSequence(
       uint32_t sequence_id) {
-    auto* builder = packet_sequence_state_builders_.Find(sequence_id);
-    if (builder == nullptr) {
-      builder = packet_sequence_state_builders_
-                    .Insert(sequence_id, PacketSequenceStateBuilder(context_))
-                    .first;
+    auto& builder = sequence_state_.Find(sequence_id)->sequence_state_builder;
+    if (!builder) {
+      builder = PacketSequenceStateBuilder(context_);
     }
-    return builder;
+    return &*builder;
   }
-  util::Status ParseExtensionDescriptor(ConstBytes descriptor);
+  base::Status ParseExtensionDescriptor(ConstBytes descriptor);
 
   TraceProcessorContext* context_;
 
@@ -115,11 +120,7 @@
   // timestamp given is latest_timestamp_.
   int64_t latest_timestamp_ = 0;
 
-  base::FlatHashMap<uint32_t, PacketSequenceStateBuilder>
-      packet_sequence_state_builders_;
-
-  base::FlatHashMap<uint32_t, size_t> packet_sequence_data_loss_;
-
+  base::FlatHashMap<uint32_t, SequenceScopedState> sequence_state_;
   StringId skipped_packet_key_id_;
   StringId invalid_incremental_state_key_id_;
 };
diff --git a/src/trace_processor/importers/proto/system_probes_parser.cc b/src/trace_processor/importers/proto/system_probes_parser.cc
index 162aa1b..08212a8 100644
--- a/src/trace_processor/importers/proto/system_probes_parser.cc
+++ b/src/trace_processor/importers/proto/system_probes_parser.cc
@@ -16,22 +16,40 @@
 
 #include "src/trace_processor/importers/proto/system_probes_parser.h"
 
+#include <algorithm>
+#include <array>
+#include <cstddef>
 #include <cstdint>
 #include <optional>
+#include <string>
+#include <utility>
+#include <variant>
+#include <vector>
 
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/status_or.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/ext/traced/sys_stats_counters.h"
+#include "perfetto/protozero/field.h"
 #include "perfetto/protozero/proto_decoder.h"
+#include "perfetto/public/compiler.h"
+#include "protos/perfetto/trace/sys_stats/sys_stats.pbzero.h"
+#include "src/kernel_utils/syscall_table.h"
+#include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/cpu_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/system_info_tracker.h"
+#include "src/trace_processor/importers/common/track_tracker.h"
+#include "src/trace_processor/importers/common/tracks.h"
 #include "src/trace_processor/importers/syscalls/syscall_tracker.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/trace_processor_context.h"
 
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
@@ -39,6 +57,7 @@
 #include "protos/perfetto/trace/ps/process_tree.pbzero.h"
 #include "protos/perfetto/trace/system_info.pbzero.h"
 #include "protos/perfetto/trace/system_info/cpu_info.pbzero.h"
+#include "src/trace_processor/types/variadic.h"
 
 namespace {
 
@@ -48,8 +67,7 @@
 
 }  // namespace
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 namespace {
 
@@ -107,11 +125,21 @@
   return VersionStringToSdkVersion(version);
 }
 
+struct ArmCpuIdentifier {
+  uint32_t implementer;
+  uint32_t architecture;
+  uint32_t variant;
+  uint32_t part;
+  uint32_t revision;
+};
+
 struct CpuInfo {
   uint32_t cpu = 0;
   std::optional<uint32_t> capacity;
   std::vector<uint32_t> frequencies;
   protozero::ConstChars processor;
+  // Extend the variant to support additional identifiers
+  std::variant<std::nullopt_t, ArmCpuIdentifier> identifier = std::nullopt;
 };
 
 struct CpuMaxFrequency {
@@ -128,28 +156,23 @@
       bytes_unit_id_(context->storage->InternString("bytes")),
       available_chunks_unit_id_(
           context->storage->InternString("available chunks")),
+      irq_id_(context->storage->InternString("irq")),
       num_forks_name_id_(context->storage->InternString("num_forks")),
       num_irq_total_name_id_(context->storage->InternString("num_irq_total")),
       num_softirq_total_name_id_(
           context->storage->InternString("num_softirq_total")),
-      num_irq_name_id_(context->storage->InternString("num_irq")),
-      num_softirq_name_id_(context->storage->InternString("num_softirq")),
-      cpu_times_user_ns_id_(
-          context->storage->InternString("cpu.times.user_ns")),
-      cpu_times_user_nice_ns_id_(
-          context->storage->InternString("cpu.times.user_nice_ns")),
-      cpu_times_system_mode_ns_id_(
-          context->storage->InternString("cpu.times.system_mode_ns")),
-      cpu_times_idle_ns_id_(
-          context->storage->InternString("cpu.times.idle_ns")),
-      cpu_times_io_wait_ns_id_(
-          context->storage->InternString("cpu.times.io_wait_ns")),
-      cpu_times_irq_ns_id_(context->storage->InternString("cpu.times.irq_ns")),
-      cpu_times_softirq_ns_id_(
-          context->storage->InternString("cpu.times.softirq_ns")),
       oom_score_adj_id_(context->storage->InternString("oom_score_adj")),
-      cpu_freq_id_(context_->storage->InternString("cpufreq")),
-      thermal_unit_id_(context->storage->InternString("C")) {
+      thermal_unit_id_(context->storage->InternString("C")),
+      gpufreq_id(context->storage->InternString("gpufreq")),
+      gpufreq_unit_id(context->storage->InternString("MHz")),
+      cpu_stat_counter_name_id_(context->storage->InternString("counter_name")),
+      arm_cpu_implementer(
+          context->storage->InternString("arm_cpu_implementer")),
+      arm_cpu_architecture(
+          context->storage->InternString("arm_cpu_architecture")),
+      arm_cpu_variant(context->storage->InternString("arm_cpu_variant")),
+      arm_cpu_part(context->storage->InternString("arm_cpu_part")),
+      arm_cpu_revision(context->storage->InternString("arm_cpu_revision")) {
   for (const auto& name : BuildMeminfoCounterNames()) {
     meminfo_strs_id_.emplace_back(context->storage->InternString(name));
   }
@@ -227,7 +250,7 @@
     base::StackString<512> track_name("%s.%s", tag_prefix.c_str(),
                                       counter_name);
     StringId string_id = context_->storage->InternString(track_name.c_str());
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kIo, string_id);
     context_->event_tracker->PushCounter(ts, value, track);
   };
@@ -302,7 +325,7 @@
       continue;
     }
     // /proc/meminfo counters are in kB, convert to bytes
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kMemory, meminfo_strs_id_[key], {},
         bytes_unit_id_);
     context_->event_tracker->PushCounter(
@@ -319,7 +342,7 @@
         "%.*s %.*s", int(key.size()), key.data(), int(devfreq_subtitle.size()),
         devfreq_subtitle.data());
     StringId name = context_->storage->InternString(counter_name.string_view());
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kClockFrequency, name);
     context_->event_tracker->PushCounter(ts, static_cast<double>(vm.value()),
                                          track);
@@ -327,8 +350,8 @@
 
   uint32_t c = 0;
   for (auto it = sys_stats.cpufreq_khz(); it; ++it, ++c) {
-    TrackId track =
-        context_->track_tracker->InternCpuCounterTrack(cpu_freq_id_, c);
+    TrackId track = context_->track_tracker->InternCpuCounterTrack(
+        tracks::cpu_frequency, c, TrackTracker::LegacyCharArrayName{"cpufreq"});
     context_->event_tracker->PushCounter(ts, static_cast<double>(*it), track);
   }
 
@@ -340,7 +363,7 @@
       context_->storage->IncrementStats(stats::vmstat_unknown_keys);
       continue;
     }
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kMemory, vmstat_strs_id_[key]);
     context_->event_tracker->PushCounter(ts, static_cast<double>(vm.value()),
                                          track);
@@ -354,76 +377,78 @@
       continue;
     }
 
-    TrackId track = context_->track_tracker->InternCpuCounterTrack(
-        cpu_times_user_ns_id_, ct.cpu_id());
-    context_->event_tracker->PushCounter(ts, static_cast<double>(ct.user_ns()),
-                                         track);
-
-    track = context_->track_tracker->InternCpuCounterTrack(
-        cpu_times_user_nice_ns_id_, ct.cpu_id());
+    auto ucpu = context_->cpu_tracker->GetOrCreateCpu(ct.cpu_id());
+    auto intern_track =
+        [&, this](TrackTracker::LegacyCharArrayName name) -> TrackId {
+      auto builder = context_->track_tracker->CreateDimensionsBuilder();
+      builder.AppendDimension(
+          cpu_stat_counter_name_id_,
+          Variadic::String(context_->storage->InternString(name.name)));
+      builder.AppendUcpu(ucpu);
+      return context_->track_tracker->InternCounterTrack(
+          tracks::cpu_stat, std::move(builder).Build(), name);
+    };
     context_->event_tracker->PushCounter(
-        ts, static_cast<double>(ct.user_nice_ns()), track);
-
-    track = context_->track_tracker->InternCpuCounterTrack(
-        cpu_times_system_mode_ns_id_, ct.cpu_id());
+        ts, static_cast<double>(ct.user_ns()),
+        intern_track(TrackTracker::LegacyCharArrayName{"cpu.times.user_ns"}));
     context_->event_tracker->PushCounter(
-        ts, static_cast<double>(ct.system_mode_ns()), track);
-
-    track = context_->track_tracker->InternCpuCounterTrack(
-        cpu_times_idle_ns_id_, ct.cpu_id());
-    context_->event_tracker->PushCounter(ts, static_cast<double>(ct.idle_ns()),
-                                         track);
-
-    track = context_->track_tracker->InternCpuCounterTrack(
-        cpu_times_io_wait_ns_id_, ct.cpu_id());
+        ts, static_cast<double>(ct.user_nice_ns()),
+        intern_track(
+            TrackTracker::LegacyCharArrayName{"cpu.times.user_nice_ns"}));
     context_->event_tracker->PushCounter(
-        ts, static_cast<double>(ct.io_wait_ns()), track);
-
-    track = context_->track_tracker->InternCpuCounterTrack(cpu_times_irq_ns_id_,
-                                                           ct.cpu_id());
-    context_->event_tracker->PushCounter(ts, static_cast<double>(ct.irq_ns()),
-                                         track);
-
-    track = context_->track_tracker->InternCpuCounterTrack(
-        cpu_times_softirq_ns_id_, ct.cpu_id());
+        ts, static_cast<double>(ct.system_mode_ns()),
+        intern_track(
+            TrackTracker::LegacyCharArrayName{"cpu.times.system_mode_ns"}));
     context_->event_tracker->PushCounter(
-        ts, static_cast<double>(ct.softirq_ns()), track);
+        ts, static_cast<double>(ct.idle_ns()),
+        intern_track(TrackTracker::LegacyCharArrayName{"cpu.times.idle_ns"}));
+    context_->event_tracker->PushCounter(
+        ts, static_cast<double>(ct.io_wait_ns()),
+        intern_track(
+            TrackTracker::LegacyCharArrayName{"cpu.times.io_wait_ns"}));
+    context_->event_tracker->PushCounter(
+        ts, static_cast<double>(ct.irq_ns()),
+        intern_track(TrackTracker::LegacyCharArrayName{"cpu.times.irq_ns"}));
+    context_->event_tracker->PushCounter(
+        ts, static_cast<double>(ct.softirq_ns()),
+        intern_track(
+            TrackTracker::LegacyCharArrayName{"cpu.times.softirq_ns"}));
   }
 
   for (auto it = sys_stats.num_irq(); it; ++it) {
     protos::pbzero::SysStats::InterruptCount::Decoder ic(*it);
-
-    TrackId track = context_->track_tracker->InternIrqCounterTrack(
-        num_irq_name_id_, ic.irq());
+    TrackId track = context_->track_tracker->InternSingleDimensionTrack(
+        tracks::irq_counter, irq_id_, Variadic::Integer(ic.irq()),
+        TrackTracker::LegacyCharArrayName{"num_irq"});
     context_->event_tracker->PushCounter(ts, static_cast<double>(ic.count()),
                                          track);
   }
 
   for (auto it = sys_stats.num_softirq(); it; ++it) {
     protos::pbzero::SysStats::InterruptCount::Decoder ic(*it);
-
-    TrackId track = context_->track_tracker->InternSoftirqCounterTrack(
-        num_softirq_name_id_, ic.irq());
+    TrackId track = context_->track_tracker->InternSingleDimensionTrack(
+        tracks::softirq_counter, irq_id_, Variadic::Integer(ic.irq()),
+        TrackTracker::LegacyCharArrayName{"num_softirq"});
     context_->event_tracker->PushCounter(ts, static_cast<double>(ic.count()),
                                          track);
   }
 
   if (sys_stats.has_num_forks()) {
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kDeviceState, num_forks_name_id_);
     context_->event_tracker->PushCounter(
         ts, static_cast<double>(sys_stats.num_forks()), track);
   }
 
   if (sys_stats.has_num_irq_total()) {
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kDeviceState, num_irq_total_name_id_);
     context_->event_tracker->PushCounter(
         ts, static_cast<double>(sys_stats.num_irq_total()), track);
   }
 
   if (sys_stats.has_num_softirq_total()) {
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kDeviceState, num_softirq_total_name_id_);
     context_->event_tracker->PushCounter(
         ts, static_cast<double>(sys_stats.num_softirq_total()), track);
@@ -444,7 +469,7 @@
                                           chunk_size_kb);
       StringId name =
           context_->storage->InternString(counter_name.string_view());
-      TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+      TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
           TrackTracker::Group::kMemory, name, {}, available_chunks_unit_id_);
       context_->event_tracker->PushCounter(ts, static_cast<double>(*order_it),
                                            track);
@@ -471,7 +496,7 @@
     // Unit = total blocked time on this resource in nanoseconds.
     // TODO(b/315152880): Consider moving psi entries for cpu/io/memory into
     // groups specific to that resource (e.g., `Group::kMemory`).
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kDeviceState,
         sys_stats_psi_resource_names_[resource], {}, ns_unit_id_);
     context_->event_tracker->PushCounter(
@@ -481,7 +506,7 @@
   for (auto it = sys_stats.thermal_zone(); it; ++it) {
     protos::pbzero::SysStats::ThermalZone::Decoder thermal(*it);
     StringId track_name = context_->storage->InternString(thermal.type());
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kThermals, track_name, {}, thermal_unit_id_);
     context_->event_tracker->PushCounter(
         ts, static_cast<double>(thermal.temp()), track);
@@ -490,6 +515,12 @@
   for (auto it = sys_stats.cpuidle_state(); it; ++it) {
     ParseCpuIdleStats(ts, *it);
   }
+
+  for (auto it = sys_stats.gpufreq_mhz(); it; ++it, ++c) {
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
+        TrackTracker::Group::kPower, gpufreq_id, {}, gpufreq_unit_id);
+    context_->event_tracker->PushCounter(ts, static_cast<double>(*it), track);
+  }
 }
 
 void SystemProbesParser::ParseCpuIdleStats(int64_t ts, ConstBytes blob) {
@@ -498,14 +529,11 @@
   for (auto cpuidle_field = cpuidle_state.cpuidle_state_entry(); cpuidle_field;
        ++cpuidle_field) {
     protos::pbzero::SysStats::CpuIdleStateEntry::Decoder idle(*cpuidle_field);
-    std::string state = idle.state().ToStdString();
-    uint64_t time = idle.duration_us();
 
-    std::string track_name = "cpuidle." + state;
-    StringId string_id = context_->storage->InternString(track_name.c_str());
-    TrackId track =
-        context_->track_tracker->InternCpuCounterTrack(string_id, cpu_id);
-    context_->event_tracker->PushCounter(ts, static_cast<double>(time), track);
+    TrackId track = context_->track_tracker->LegacyInternCpuIdleStateTrack(
+        cpu_id, context_->storage->InternString(idle.state()));
+    context_->event_tracker->PushCounter(
+        ts, static_cast<double>(idle.duration_us()), track);
   }
 }
 
@@ -561,7 +589,7 @@
     // single cmdline element. This will be wrong for binaries that have spaces
     // in their path and are invoked without additional arguments, but those are
     // very rare. The full cmdline will still be correct either way.
-    if (bool(++proc.cmdline()) == false) {
+    if (!static_cast<bool>(++proc.cmdline())) {
       size_t delim_pos = argv0.find(' ');
       if (delim_pos != base::StringView::npos) {
         argv0 = argv0.substr(0, delim_pos);
@@ -683,7 +711,7 @@
       const StringId& name = proc_stats_process_names_[field_id];
       UniquePid upid = context_->process_tracker->GetOrCreateProcess(pid);
       TrackId track =
-          context_->track_tracker->InternProcessCounterTrack(name, upid);
+          context_->track_tracker->LegacyInternProcessCounterTrack(name, upid);
       int64_t value = counter_values[field_id];
       context_->event_tracker->PushCounter(ts, static_cast<double>(value),
                                            track);
@@ -770,6 +798,13 @@
             packet.android_build_fingerprint())));
   }
 
+  if (packet.has_android_device_manufacturer()) {
+    context_->metadata_tracker->SetMetadata(
+        metadata::android_device_manufacturer,
+        Variadic::String(context_->storage->InternString(
+            packet.android_device_manufacturer())));
+  }
+
   // If we have the SDK version in the trace directly just use that.
   // Otherwise, try and parse it from the fingerprint.
   std::optional<int64_t> opt_sdk_version;
@@ -792,6 +827,13 @@
             context_->storage->InternString(packet.android_soc_model())));
   }
 
+  if (packet.has_android_guest_soc_model()) {
+    context_->metadata_tracker->SetMetadata(
+        metadata::android_guest_soc_model,
+        Variadic::String(
+            context_->storage->InternString(packet.android_guest_soc_model())));
+  }
+
   if (packet.has_android_hardware_revision()) {
     context_->metadata_tracker->SetMetadata(
         metadata::android_hardware_revision,
@@ -799,6 +841,20 @@
             packet.android_hardware_revision())));
   }
 
+  if (packet.has_android_storage_model()) {
+    context_->metadata_tracker->SetMetadata(
+        metadata::android_storage_model,
+        Variadic::String(
+            context_->storage->InternString(packet.android_storage_model())));
+  }
+
+  if (packet.has_android_ram_model()) {
+    context_->metadata_tracker->SetMetadata(
+        metadata::android_ram_model,
+        Variadic::String(
+            context_->storage->InternString(packet.android_ram_model())));
+  }
+
   page_size_ = packet.page_size();
   if (!page_size_) {
     page_size_ = 4096;
@@ -817,9 +873,11 @@
   uint32_t cpu_id = 0;
   for (auto it = packet.cpus(); it; it++, cpu_id++) {
     protos::pbzero::CpuInfo::Cpu::Decoder cpu(*it);
+
     CpuInfo current_cpu_info;
     current_cpu_info.cpu = cpu_id;
     current_cpu_info.processor = cpu.processor();
+
     for (auto freq_it = cpu.frequencies(); freq_it; freq_it++) {
       uint32_t current_cpu_frequency = *freq_it;
       current_cpu_info.frequencies.push_back(current_cpu_frequency);
@@ -827,6 +885,18 @@
     if (cpu.has_capacity()) {
       current_cpu_info.capacity = cpu.capacity();
     }
+
+    if (cpu.has_arm_identifier()) {
+      protos::pbzero::CpuInfo::ArmCpuIdentifier::Decoder identifier(
+          cpu.arm_identifier());
+
+      current_cpu_info.identifier = ArmCpuIdentifier{
+          identifier.implementer(), identifier.architecture(),
+          identifier.variant(),     identifier.part(),
+          identifier.revision(),
+      };
+    }
+
     cpu_infos.push_back(current_cpu_info);
   }
 
@@ -837,13 +907,13 @@
 
   // Capacities are defined as existing on all CPUs if present and so we set
   // them as invalid if any is missing
-  bool valid_capacities =
-      std::all_of(cpu_infos.begin(), cpu_infos.end(),
-                  [](CpuInfo info) { return info.capacity.has_value(); });
+  bool valid_capacities = std::all_of(
+      cpu_infos.begin(), cpu_infos.end(),
+      [](const CpuInfo& info) { return info.capacity.has_value(); });
 
-  bool valid_frequencies =
-      std::all_of(cpu_infos.begin(), cpu_infos.end(),
-                  [](CpuInfo info) { return !info.frequencies.empty(); });
+  bool valid_frequencies = std::all_of(
+      cpu_infos.begin(), cpu_infos.end(),
+      [](const CpuInfo& info) { return !info.frequencies.empty(); });
 
   std::vector<uint32_t> cluster_ids(cpu_infos.size());
   uint32_t cluster_id = 0;
@@ -896,8 +966,18 @@
       cpu_freq_row.freq = frequency;
       context_->storage->mutable_cpu_freq_table()->Insert(cpu_freq_row);
     }
+
+    if (auto* id = std::get_if<ArmCpuIdentifier>(&cpu_info.identifier)) {
+      context_->args_tracker->AddArgsTo(ucpu)
+          .AddArg(arm_cpu_implementer,
+                  Variadic::UnsignedInteger(id->implementer))
+          .AddArg(arm_cpu_architecture,
+                  Variadic::UnsignedInteger(id->architecture))
+          .AddArg(arm_cpu_variant, Variadic::UnsignedInteger(id->variant))
+          .AddArg(arm_cpu_part, Variadic::UnsignedInteger(id->part))
+          .AddArg(arm_cpu_revision, Variadic::UnsignedInteger(id->revision));
+    }
   }
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/system_probes_parser.h b/src/trace_processor/importers/proto/system_probes_parser.h
index c888400..87b5734 100644
--- a/src/trace_processor/importers/proto/system_probes_parser.h
+++ b/src/trace_processor/importers/proto/system_probes_parser.h
@@ -18,14 +18,15 @@
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_SYSTEM_PROBES_PARSER_H_
 
 #include <array>
+#include <cstddef>
+#include <cstdint>
 #include <vector>
 
 #include "perfetto/protozero/field.h"
 #include "protos/perfetto/trace/sys_stats/sys_stats.pbzero.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class TraceProcessorContext;
 
@@ -37,7 +38,7 @@
   explicit SystemProbesParser(TraceProcessorContext*);
 
   void ParseProcessTree(ConstBytes);
-  void ParseProcessStats(int64_t timestamp, ConstBytes);
+  void ParseProcessStats(int64_t ts, ConstBytes);
   void ParseSysStats(int64_t ts, ConstBytes);
   void ParseSystemInfo(ConstBytes);
   void ParseCpuInfo(ConstBytes);
@@ -54,22 +55,25 @@
   const StringId ns_unit_id_;
   const StringId bytes_unit_id_;
   const StringId available_chunks_unit_id_;
+  const StringId irq_id_;
 
   const StringId num_forks_name_id_;
   const StringId num_irq_total_name_id_;
   const StringId num_softirq_total_name_id_;
-  const StringId num_irq_name_id_;
-  const StringId num_softirq_name_id_;
-  const StringId cpu_times_user_ns_id_;
-  const StringId cpu_times_user_nice_ns_id_;
-  const StringId cpu_times_system_mode_ns_id_;
-  const StringId cpu_times_idle_ns_id_;
-  const StringId cpu_times_io_wait_ns_id_;
-  const StringId cpu_times_irq_ns_id_;
-  const StringId cpu_times_softirq_ns_id_;
   const StringId oom_score_adj_id_;
-  const StringId cpu_freq_id_;
   const StringId thermal_unit_id_;
+  const StringId gpufreq_id;
+  const StringId gpufreq_unit_id;
+
+  const StringId cpu_stat_counter_name_id_;
+
+  // Arm CPU identifier string IDs
+  const StringId arm_cpu_implementer;
+  const StringId arm_cpu_architecture;
+  const StringId arm_cpu_variant;
+  const StringId arm_cpu_part;
+  const StringId arm_cpu_revision;
+
   std::vector<StringId> meminfo_strs_id_;
   std::vector<StringId> vmstat_strs_id_;
 
@@ -96,7 +100,6 @@
   int64_t prev_flush_time = -1;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_SYSTEM_PROBES_PARSER_H_
diff --git a/src/trace_processor/importers/proto/track_event_module.cc b/src/trace_processor/importers/proto/track_event_module.cc
index aee720c..0ae00f2 100644
--- a/src/trace_processor/importers/proto/track_event_module.cc
+++ b/src/trace_processor/importers/proto/track_event_module.cc
@@ -15,20 +15,21 @@
  */
 #include "src/trace_processor/importers/proto/track_event_module.h"
 
-#include "perfetto/base/build_config.h"
+#include <cstdint>
+#include <utility>
+
 #include "perfetto/base/logging.h"
-#include "perfetto/ext/base/string_utils.h"
-#include "src/trace_processor/importers/common/track_tracker.h"
+#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/packet_sequence_state_generation.h"
+#include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/importers/proto/track_event_tracker.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
-#include "protos/perfetto/config/data_source_config.pbzero.h"
-#include "protos/perfetto/config/trace_config.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 using perfetto::protos::pbzero::TracePacket;
 
@@ -59,9 +60,8 @@
       return tokenizer_.TokenizeTrackDescriptorPacket(std::move(state), decoder,
                                                       packet_timestamp);
     case TracePacket::kTrackEventFieldNumber:
-      tokenizer_.TokenizeTrackEventPacket(std::move(state), decoder, packet,
-                                          packet_timestamp);
-      return ModuleResult::Handled();
+      return tokenizer_.TokenizeTrackEventPacket(std::move(state), decoder,
+                                                 packet, packet_timestamp);
     case TracePacket::kThreadDescriptorFieldNumber:
       // TODO(eseckler): Remove once Chrome has switched to TrackDescriptors.
       return tokenizer_.TokenizeThreadDescriptorPacket(std::move(state),
@@ -70,13 +70,6 @@
   return ModuleResult::Ignored();
 }
 
-void TrackEventModule::ParseTrackEventData(const TracePacket::Decoder& decoder,
-                                           int64_t ts,
-                                           const TrackEventData& data) {
-  parser_.ParseTrackEvent(ts, &data, decoder.track_event(),
-                          decoder.trusted_packet_sequence_id());
-}
-
 void TrackEventModule::ParseTracePacketData(const TracePacket::Decoder& decoder,
                                             int64_t ts,
                                             const TracePacketData&,
@@ -107,9 +100,15 @@
   track_event_tracker_->OnFirstPacketOnSequence(packet_sequence_id);
 }
 
+void TrackEventModule::ParseTrackEventData(const TracePacket::Decoder& decoder,
+                                           int64_t ts,
+                                           const TrackEventData& data) {
+  parser_.ParseTrackEvent(ts, &data, decoder.track_event(),
+                          decoder.trusted_packet_sequence_id());
+}
+
 void TrackEventModule::NotifyEndOfFile() {
   parser_.NotifyEndOfFile();
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/track_event_module.h b/src/trace_processor/importers/proto/track_event_module.h
index 4a7ae69..756a1fc 100644
--- a/src/trace_processor/importers/proto/track_event_module.h
+++ b/src/trace_processor/importers/proto/track_event_module.h
@@ -17,7 +17,11 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_MODULE_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_MODULE_H_
 
+#include <cstdint>
+#include <memory>
+
 #include "perfetto/trace_processor/ref_counted.h"
+#include "src/trace_processor/importers/common/parser_types.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_parser.h"
@@ -25,8 +29,7 @@
 
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class TrackEventModule : public ProtoImporterModule {
  public:
@@ -41,6 +44,11 @@
       RefPtr<PacketSequenceStateGeneration> state,
       uint32_t field_id) override;
 
+  void ParseTracePacketData(const protos::pbzero::TracePacket::Decoder& decoder,
+                            int64_t ts,
+                            const TracePacketData& data,
+                            uint32_t field_id) override;
+
   void OnIncrementalStateCleared(uint32_t) override;
 
   void OnFirstPacketOnSequence(uint32_t) override;
@@ -49,11 +57,6 @@
                            int64_t ts,
                            const TrackEventData& data);
 
-  void ParseTracePacketData(const protos::pbzero::TracePacket::Decoder& decoder,
-                            int64_t ts,
-                            const TracePacketData& data,
-                            uint32_t field_id) override;
-
   void NotifyEndOfFile() override;
 
  private:
@@ -62,7 +65,6 @@
   TrackEventParser parser_;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_MODULE_H_
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index 01b5313..8a241df 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -31,7 +31,6 @@
 #include "perfetto/protozero/proto_decoder.h"
 #include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/basic_types.h"
-#include "perfetto/trace_processor/status.h"
 #include "src/trace_processor/containers/null_term_string_view.h"
 #include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
@@ -209,21 +208,14 @@
 
     if (context_->content_analyzer) {
       PacketAnalyzer::SampleAnnotation annotation;
-      annotation.push_back({parser_->event_category_key_id_, category_id_});
-      annotation.push_back({parser_->event_name_key_id_, name_id_});
+      annotation.emplace_back(parser_->event_category_key_id_, category_id_);
+      annotation.emplace_back(parser_->event_name_key_id_, name_id_);
       PacketAnalyzer::Get(context_)->ProcessPacket(
           event_data_->trace_packet_data.packet, annotation);
     }
 
     RETURN_IF_ERROR(ParseTrackAssociation());
 
-    // Counter-type events don't support arguments (those are on the
-    // CounterDescriptor instead). All they have is a |{double_,}counter_value|.
-    if (event_.type() == TrackEvent::TYPE_COUNTER) {
-      ParseCounterEvent();
-      return base::OkStatus();
-    }
-
     // If we have legacy thread time / instruction count fields, also parse them
     // into the counters tables.
     ParseLegacyThreadTimeAndInstructionsAsCounters();
@@ -233,6 +225,12 @@
     // these counter values and also parse them as slice attributes / arguments.
     ParseExtraCounterValues();
 
+    // Non-legacy counters are treated differently. Legacy counters do not have
+    // a track_id_ and should instead go through the switch below.
+    if (event_.type() == TrackEvent::TYPE_COUNTER) {
+      return ParseCounterEvent();
+    }
+
     // TODO(eseckler): Replace phase with type and remove handling of
     // legacy_event_.phase() once it is no longer used by producers.
     char phase = static_cast<char>(ParsePhaseOrType());
@@ -373,9 +371,10 @@
           track_event_tracker_->GetDescriptorTrack(track_uuid_, name_id_,
                                                    packet_sequence_id_);
       if (!opt_track_id) {
-        track_event_tracker_->ReserveDescriptorChildTrack(track_uuid_,
-                                                          /*parent_uuid=*/0,
-                                                          name_id_);
+        TrackEventTracker::DescriptorTrackReservation r;
+        r.parent_uuid = 0;
+        r.name = name_id_;
+        track_event_tracker_->ReserveDescriptorTrack(track_uuid_, r);
         opt_track_id = track_event_tracker_->GetDescriptorTrack(
             track_uuid_, name_id_, packet_sequence_id_);
       }
@@ -496,7 +495,7 @@
           id_scope = storage_->InternString(base::StringView(concat));
         }
 
-        track_id_ = context_->track_tracker->InternLegacyChromeAsyncTrack(
+        track_id_ = context_->track_tracker->LegacyInternLegacyChromeAsyncTrack(
             name_id_, upid_.value_or(0), source_id, source_id_is_process_scoped,
             id_scope);
         legacy_passthrough_utid_ = utid_;
@@ -516,8 +515,14 @@
             }
             break;
           case LegacyEvent::SCOPE_GLOBAL:
-            track_id_ = context_->track_tracker
-                            ->GetOrCreateLegacyChromeGlobalInstantTrack();
+            track_id_ = context_->track_tracker->InternGlobalTrack(
+                tracks::legacy_chrome_global_instants, TrackTracker::AutoName(),
+                [this](ArgsTracker::BoundInserter& inserter) {
+                  inserter.AddArg(
+                      context_->storage->InternString("source"),
+                      Variadic::String(
+                          context_->storage->InternString("chrome")));
+                });
             legacy_passthrough_utid_ = utid_;
             utid_ = std::nullopt;
             break;
@@ -527,9 +532,11 @@
                   "Process-scoped instant event without process association");
             }
 
-            track_id_ =
-                context_->track_tracker->InternLegacyChromeProcessInstantTrack(
-                    *upid_);
+            track_id_ = context_->track_tracker->InternProcessTrack(
+                tracks::chrome_process_instant, *upid_);
+            context_->args_tracker->AddArgsTo(track_id_).AddArg(
+                context_->storage->InternString("source"),
+                Variadic::String(context_->storage->InternString("chrome")));
             legacy_passthrough_utid_ = utid_;
             utid_ = std::nullopt;
             break;
@@ -560,7 +567,7 @@
     }
   }
 
-  void ParseCounterEvent() {
+  base::Status ParseCounterEvent() {
     // Tokenizer ensures that TYPE_COUNTER events are associated with counter
     // tracks and have values.
     PERFETTO_DCHECK(storage_->counter_track_table().FindById(track_id_));
@@ -568,7 +575,9 @@
                     event_.has_double_counter_value());
 
     context_->event_tracker->PushCounter(
-        ts_, static_cast<double>(event_data_->counter_value), track_id_);
+        ts_, static_cast<double>(event_data_->counter_value), track_id_,
+        [this](BoundInserter* inserter) { ParseTrackEventArgs(inserter); });
+    return base::OkStatus();
   }
 
   void ParseLegacyThreadTimeAndInstructionsAsCounters() {
@@ -581,14 +590,16 @@
     // EventTracker expects counters to be pushed in order of their timestamps.
     // One more reason to switch to split begin/end events.
     if (thread_timestamp_) {
-      TrackId track_id = context_->track_tracker->InternThreadCounterTrack(
-          parser_->counter_name_thread_time_id_, *utid_);
+      TrackId track_id =
+          context_->track_tracker->LegacyInternThreadCounterTrack(
+              parser_->counter_name_thread_time_id_, *utid_);
       context_->event_tracker->PushCounter(
           ts_, static_cast<double>(*thread_timestamp_), track_id);
     }
     if (thread_instruction_count_) {
-      TrackId track_id = context_->track_tracker->InternThreadCounterTrack(
-          parser_->counter_name_thread_instruction_count_id_, *utid_);
+      TrackId track_id =
+          context_->track_tracker->LegacyInternThreadCounterTrack(
+              parser_->counter_name_thread_instruction_count_id_, *utid_);
       context_->event_tracker->PushCounter(
           ts_, static_cast<double>(*thread_instruction_count_), track_id);
     }
@@ -1341,6 +1352,7 @@
   std::optional<UniqueTid> upid_;
   std::optional<int64_t> thread_timestamp_;
   std::optional<int64_t> thread_instruction_count_;
+
   // All events in legacy JSON require a thread ID, but for some types of
   // events (e.g. async events or process/global-scoped instants), we don't
   // store it in the slice/track model. To pass the utid through to the json
@@ -1534,10 +1546,17 @@
   }
 
   // Override the name with the most recent name seen (after sorting by ts).
-  if (decoder.has_name() || decoder.has_static_name()) {
+  ::protozero::ConstChars name = {nullptr, 0};
+  if (decoder.has_name()) {
+    name = decoder.name();
+  } else if (decoder.has_static_name()) {
+    name = decoder.static_name();
+  } else if (decoder.has_atrace_name()) {
+    name = decoder.atrace_name();
+  }
+  if (name.data != nullptr) {
     auto* tracks = context_->storage->mutable_track_table();
-    const StringId raw_name_id = context_->storage->InternString(
-        decoder.has_name() ? decoder.name() : decoder.static_name());
+    const StringId raw_name_id = context_->storage->InternString(name);
     const StringId name_id =
         context_->process_track_translation_table->TranslateName(raw_name_id);
     tracks->FindById(track_id)->set_name(name_id);
diff --git a/src/trace_processor/importers/proto/track_event_parser.h b/src/trace_processor/importers/proto/track_event_parser.h
index 6198dd5..7843a53 100644
--- a/src/trace_processor/importers/proto/track_event_parser.h
+++ b/src/trace_processor/importers/proto/track_event_parser.h
@@ -18,11 +18,11 @@
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_PARSER_H_
 
 #include <array>
-#include <map>
+#include <cstdint>
+#include <optional>
+#include <vector>
 
-#include "perfetto/base/build_config.h"
 #include "perfetto/protozero/field.h"
-#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/trace_parser.h"
@@ -31,14 +31,11 @@
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/util/proto_to_args_parser.h"
 
-#include "protos/perfetto/trace/track_event/track_event.pbzero.h"
-
 namespace Json {
 class Value;
 }
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 // Field numbers to be added to args table automatically via reflection
 //
@@ -135,7 +132,6 @@
   ActiveChromeProcessesTracker active_chrome_processes_tracker_;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_PARSER_H_
diff --git a/src/trace_processor/importers/proto/track_event_sequence_state.cc b/src/trace_processor/importers/proto/track_event_sequence_state.cc
index 99bc93e..ee138b5 100644
--- a/src/trace_processor/importers/proto/track_event_sequence_state.cc
+++ b/src/trace_processor/importers/proto/track_event_sequence_state.cc
@@ -18,8 +18,7 @@
 
 #include "protos/perfetto/trace/track_event/thread_descriptor.pbzero.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 void TrackEventSequenceState::SetThreadDescriptor(
     const protos::pbzero::ThreadDescriptor::Decoder& decoder) {
@@ -33,5 +32,4 @@
   thread_instruction_count_ = decoder.reference_thread_instruction_count();
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/track_event_tokenizer.cc b/src/trace_processor/importers/proto/track_event_tokenizer.cc
index 238e247..86c402f 100644
--- a/src/trace_processor/importers/proto/track_event_tokenizer.cc
+++ b/src/trace_processor/importers/proto/track_event_tokenizer.cc
@@ -16,34 +16,51 @@
 
 #include "src/trace_processor/importers/proto/track_event_tokenizer.h"
 
+#include <cinttypes>
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <utility>
+
+#include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/protozero/proto_decoder.h"
+#include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
+#include "protos/perfetto/trace/interned_data/interned_data.pbzero.h"
+#include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
-#include "src/trace_processor/importers/common/machine_tracker.h"
+#include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
+#include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
-#include "src/trace_processor/importers/common/track_tracker.h"
+#include "src/trace_processor/importers/json/json_utils.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/proto_trace_reader.h"
 #include "src/trace_processor/importers/proto/track_event_tracker.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 "protos/perfetto/common/builtin_clock.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.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/process_descriptor.pbzero.h"
 #include "protos/perfetto/trace/track_event/range_of_interest.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"
+#include "src/trace_processor/types/variadic.h"
+#include "src/trace_processor/util/status_macros.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 namespace {
 using protos::pbzero::CounterDescriptor;
@@ -79,9 +96,13 @@
     RefPtr<PacketSequenceStateGeneration> state,
     const protos::pbzero::TracePacket::Decoder& packet,
     int64_t packet_timestamp) {
+  using TrackDescriptorProto = protos::pbzero::TrackDescriptor;
+  using Reservation = TrackEventTracker::DescriptorTrackReservation;
   auto track_descriptor_field = packet.track_descriptor();
-  protos::pbzero::TrackDescriptor::Decoder track(track_descriptor_field.data,
-                                                 track_descriptor_field.size);
+  TrackDescriptorProto::Decoder track(track_descriptor_field.data,
+                                      track_descriptor_field.size);
+
+  Reservation reservation;
 
   if (!track.has_uuid()) {
     PERFETTO_ELOG("TrackDescriptor packet without uuid");
@@ -89,11 +110,37 @@
     return ModuleResult::Handled();
   }
 
-  StringId name_id = kNullStringId;
+  if (track.has_parent_uuid()) {
+    reservation.parent_uuid = track.parent_uuid();
+  }
+
+  if (track.has_child_ordering()) {
+    switch (track.child_ordering()) {
+      case TrackDescriptorProto::ChildTracksOrdering::UNKNOWN:
+        reservation.ordering = Reservation::ChildTracksOrdering::kUnknown;
+        break;
+      case TrackDescriptorProto::ChildTracksOrdering::CHRONOLOGICAL:
+        reservation.ordering = Reservation::ChildTracksOrdering::kChronological;
+        break;
+      case TrackDescriptorProto::ChildTracksOrdering::LEXICOGRAPHIC:
+        reservation.ordering = Reservation::ChildTracksOrdering::kLexicographic;
+        break;
+      case TrackDescriptorProto::ChildTracksOrdering::EXPLICIT:
+        reservation.ordering = Reservation::ChildTracksOrdering::kExplicit;
+        break;
+      default:
+        PERFETTO_FATAL("Unsupported ChildTracksOrdering");
+    }
+  }
+
+  if (track.has_sibling_order_rank()) {
+    reservation.sibling_order_rank = track.sibling_order_rank();
+  }
+
   if (track.has_name())
-    name_id = context_->storage->InternString(track.name());
+    reservation.name = context_->storage->InternString(track.name());
   else if (track.has_static_name())
-    name_id = context_->storage->InternString(track.static_name());
+    reservation.name = context_->storage->InternString(track.static_name());
 
   if (packet.has_trusted_pid()) {
     context_->process_tracker->UpdateTrustedPid(
@@ -115,12 +162,18 @@
       TokenizeThreadDescriptor(*state, thread);
     }
 
-    track_event_tracker_->ReserveDescriptorThreadTrack(
-        track.uuid(), track.parent_uuid(), name_id,
-        static_cast<uint32_t>(thread.pid()),
-        static_cast<uint32_t>(thread.tid()), packet_timestamp,
-        track.disallow_merging_with_system_tracks());
-  } else if (track.has_process()) {
+    reservation.min_timestamp = packet_timestamp;
+    reservation.pid = static_cast<uint32_t>(thread.pid());
+    reservation.tid = static_cast<uint32_t>(thread.tid());
+    reservation.use_separate_track =
+        track.disallow_merging_with_system_tracks();
+
+    track_event_tracker_->ReserveDescriptorTrack(track.uuid(), reservation);
+
+    return ModuleResult::Ignored();
+  }
+
+  if (track.has_process()) {
     protos::pbzero::ProcessDescriptor::Decoder process(track.process());
 
     if (!process.has_pid()) {
@@ -130,10 +183,13 @@
       return ModuleResult::Handled();
     }
 
-    track_event_tracker_->ReserveDescriptorProcessTrack(
-        track.uuid(), name_id, static_cast<uint32_t>(process.pid()),
-        packet_timestamp);
-  } else if (track.has_counter()) {
+    reservation.pid = static_cast<uint32_t>(process.pid());
+    reservation.min_timestamp = packet_timestamp;
+    track_event_tracker_->ReserveDescriptorTrack(track.uuid(), reservation);
+
+    return ModuleResult::Ignored();
+  }
+  if (track.has_counter()) {
     protos::pbzero::CounterDescriptor::Decoder counter(track.counter());
 
     StringId category_id = kNullStringId;
@@ -157,31 +213,42 @@
     // threads, in which case it has to use absolute values on a different
     // track_uuid. Right now these absolute values are imported onto a separate
     // counter track than the other thread's regular thread time values.)
-    if (name_id.is_null()) {
+    if (reservation.name.is_null()) {
       switch (counter.type()) {
         case CounterDescriptor::COUNTER_UNSPECIFIED:
           break;
         case CounterDescriptor::COUNTER_THREAD_TIME_NS:
-          name_id = counter_name_thread_time_id_;
+          reservation.name = counter_name_thread_time_id_;
           break;
         case CounterDescriptor::COUNTER_THREAD_INSTRUCTION_COUNT:
-          name_id = counter_name_thread_instruction_count_id_;
+          reservation.name = counter_name_thread_instruction_count_id_;
           break;
       }
     }
 
-    track_event_tracker_->ReserveDescriptorCounterTrack(
-        track.uuid(), track.parent_uuid(), name_id, category_id,
-        counter.unit_multiplier(), counter.is_incremental(),
-        packet.trusted_packet_sequence_id());
-  } else {
-    track_event_tracker_->ReserveDescriptorChildTrack(
-        track.uuid(), track.parent_uuid(), name_id);
+    reservation.is_counter = true;
+
+    TrackEventTracker::DescriptorTrackReservation::CounterDetails
+        counter_details;
+    counter_details.category = category_id;
+    counter_details.is_incremental = counter.is_incremental();
+    counter_details.unit_multiplier = counter.unit_multiplier();
+
+    // Incrementally encoded counters are only valid on a single sequence.
+    if (counter.is_incremental())
+      counter_details.packet_sequence_id = packet.trusted_packet_sequence_id();
+
+    reservation.counter_details = std::move(counter_details);
+    track_event_tracker_->ReserveDescriptorTrack(track.uuid(), reservation);
+
+    return ModuleResult::Ignored();
   }
 
+  track_event_tracker_->ReserveDescriptorTrack(track.uuid(), reservation);
+
   // Let ProtoTraceReader forward the packet to the parser.
   return ModuleResult::Ignored();
-}
+}  // namespace perfetto::trace_processor
 
 ModuleResult TrackEventTokenizer::TokenizeThreadDescriptorPacket(
     RefPtr<PacketSequenceStateGeneration> state,
@@ -217,7 +284,7 @@
   state.SetThreadDescriptor(thread);
 }
 
-void TrackEventTokenizer::TokenizeTrackEventPacket(
+ModuleResult TrackEventTokenizer::TokenizeTrackEventPacket(
     RefPtr<PacketSequenceStateGeneration> state,
     const protos::pbzero::TracePacket::Decoder& packet,
     TraceBlobView* packet_blob,
@@ -225,12 +292,10 @@
   if (PERFETTO_UNLIKELY(!packet.has_trusted_packet_sequence_id())) {
     PERFETTO_ELOG("TrackEvent packet without trusted_packet_sequence_id");
     context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-    return;
+    return ModuleResult::Handled();
   }
 
-  auto field = packet.track_event();
-  protos::pbzero::TrackEvent::Decoder event(field.data, field.size);
-
+  protos::pbzero::TrackEvent::Decoder event(packet.track_event());
   protos::pbzero::TrackEventDefaults::Decoder* defaults =
       state->GetTrackEventDefaults();
 
@@ -246,7 +311,7 @@
     // packet loss.
     if (!state->track_event_timestamps_valid()) {
       context_->storage->IncrementStats(stats::tokenizer_skipped_packets);
-      return;
+      return ModuleResult::Handled();
     }
     timestamp = state->IncrementAndGetTrackEventTimeNs(
         event.timestamp_delta_us() * 1000);
@@ -272,7 +337,16 @@
   } else {
     PERFETTO_ELOG("TrackEvent without valid timestamp");
     context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-    return;
+    return ModuleResult::Handled();
+  }
+
+  // Handle legacy sample events which might have timestamps embedded inside.
+  if (PERFETTO_UNLIKELY(event.has_legacy_event())) {
+    protos::pbzero::TrackEvent::LegacyEvent::Decoder leg(event.legacy_event());
+    if (PERFETTO_UNLIKELY(leg.phase() == 'P')) {
+      RETURN_IF_ERROR(TokenizeLegacySampleEvent(
+          event, leg, *data.trace_packet_data.sequence_state));
+    }
   }
 
   if (event.has_thread_time_delta_us()) {
@@ -280,7 +354,7 @@
     // packet loss.
     if (!state->track_event_timestamps_valid()) {
       context_->storage->IncrementStats(stats::tokenizer_skipped_packets);
-      return;
+      return ModuleResult::Handled();
     }
     data.thread_timestamp = state->IncrementAndGetTrackEventThreadTimeNs(
         event.thread_time_delta_us() * 1000);
@@ -294,7 +368,7 @@
     // packet loss.
     if (!state->track_event_timestamps_valid()) {
       context_->storage->IncrementStats(stats::tokenizer_skipped_packets);
-      return;
+      return ModuleResult::Handled();
     }
     data.thread_instruction_count =
         state->IncrementAndGetTrackEventThreadInstructionCount(
@@ -315,7 +389,7 @@
       PERFETTO_DLOG(
           "Ignoring TrackEvent with counter_value but without track_uuid");
       context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-      return;
+      return ModuleResult::Handled();
     }
 
     if (!event.has_counter_value() && !event.has_double_counter_value()) {
@@ -325,7 +399,7 @@
           "track_uuid %" PRIu64,
           track_uuid);
       context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-      return;
+      return ModuleResult::Handled();
     }
 
     std::optional<double> value;
@@ -343,7 +417,7 @@
       PERFETTO_DLOG("Ignoring TrackEvent with invalid track_uuid %" PRIu64,
                     track_uuid);
       context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-      return;
+      return ModuleResult::Handled();
     }
 
     data.counter_value = *value;
@@ -358,7 +432,7 @@
   if (!result.ok()) {
     PERFETTO_DLOG("%s", result.c_message());
     context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-    return;
+    return ModuleResult::Handled();
   }
   result = AddExtraCounterValues(
       data, index, packet.trusted_packet_sequence_id(),
@@ -368,11 +442,12 @@
   if (!result.ok()) {
     PERFETTO_DLOG("%s", result.c_message());
     context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-    return;
+    return ModuleResult::Handled();
   }
 
   context_->sorter->PushTrackEventPacket(timestamp, std::move(data),
                                          context_->machine_id());
+  return ModuleResult::Handled();
 }
 
 template <typename T>
@@ -394,19 +469,19 @@
   } else if (default_track_uuid_it) {
     track_uuid_it = default_track_uuid_it;
   } else {
-    return base::Status(
+    return base::ErrStatus(
         "Ignoring TrackEvent with extra_{double_,}counter_values but without "
         "extra_{double_,}counter_track_uuids");
   }
 
   for (; value_it; ++value_it, ++track_uuid_it, ++index) {
     if (!*track_uuid_it) {
-      return base::Status(
+      return base::ErrStatus(
           "Ignoring TrackEvent with more extra_{double_,}counter_values than "
           "extra_{double_,}counter_track_uuids");
     }
     if (index >= TrackEventData::kMaxNumExtraCounters) {
-      return base::Status(
+      return base::ErrStatus(
           "Ignoring TrackEvent with more extra_{double_,}counter_values than "
           "TrackEventData::kMaxNumExtraCounters");
     }
@@ -415,7 +490,7 @@
             *track_uuid_it, trusted_packet_sequence_id,
             static_cast<double>(*value_it));
     if (!abs_value) {
-      return base::Status(
+      return base::ErrStatus(
           "Ignoring TrackEvent with invalid extra counter track");
     }
     data.extra_counter_values[index] = *abs_value;
@@ -423,5 +498,79 @@
   return base::OkStatus();
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+base::Status TrackEventTokenizer::TokenizeLegacySampleEvent(
+    const protos::pbzero::TrackEvent::Decoder& event,
+    const protos::pbzero::TrackEvent::LegacyEvent::Decoder& legacy,
+    PacketSequenceStateGeneration& state) {
+  // We are just trying to parse out the V8 profiling events into the cpu
+  // sampling tables: if we don't have JSON enabled, just don't do this.
+  if (!context_->json_trace_parser) {
+    return base::OkStatus();
+  }
+#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
+  for (auto it = event.debug_annotations(); it; ++it) {
+    protos::pbzero::DebugAnnotation::Decoder da(*it);
+    auto* interned_name = state.LookupInternedMessage<
+        protos::pbzero::InternedData::kDebugAnnotationNamesFieldNumber,
+        protos::pbzero::DebugAnnotationName>(da.name_iid());
+    base::StringView name(interned_name->name());
+    if (name != "data" || !da.has_legacy_json_value()) {
+      continue;
+    }
+    auto opt_val = json::ParseJsonString(da.legacy_json_value());
+    if (!opt_val) {
+      continue;
+    }
+    const auto& val = *opt_val;
+    if (val.isMember("startTime")) {
+      ASSIGN_OR_RETURN(int64_t ts, context_->clock_tracker->ToTraceTime(
+                                       protos::pbzero::BUILTIN_CLOCK_MONOTONIC,
+                                       val["startTime"].asInt64() * 1000));
+      context_->legacy_v8_cpu_profile_tracker->SetStartTsForSessionAndPid(
+          legacy.unscoped_id(), static_cast<uint32_t>(state.pid()), ts);
+      continue;
+    }
+    const auto& profile = val["cpuProfile"];
+    for (const auto& n : profile["nodes"]) {
+      uint32_t node_id = n["id"].asUInt();
+      std::optional<uint32_t> parent_node_id =
+          n.isMember("parent") ? std::make_optional(n["parent"].asUInt())
+                               : std::nullopt;
+      const auto& frame = n["callFrame"];
+      base::StringView url =
+          frame.isMember("url") ? frame["url"].asCString() : base::StringView();
+      base::StringView function_name = frame["functionName"].asCString();
+      base::Status status =
+          context_->legacy_v8_cpu_profile_tracker->AddCallsite(
+              legacy.unscoped_id(), static_cast<uint32_t>(state.pid()), node_id,
+              parent_node_id, url, function_name);
+      if (!status.ok()) {
+        context_->storage->IncrementStats(
+            stats::legacy_v8_cpu_profile_invalid_callsite);
+        continue;
+      }
+    }
+    const auto& samples = profile["samples"];
+    const auto& deltas = val["timeDeltas"];
+    if (samples.size() != deltas.size()) {
+      return base::ErrStatus(
+          "v8 legacy profile: samples and timestamps do not have same size");
+    }
+    for (uint32_t i = 0; i < samples.size(); ++i) {
+      ASSIGN_OR_RETURN(
+          int64_t ts,
+          context_->legacy_v8_cpu_profile_tracker->AddDeltaAndGetTs(
+              legacy.unscoped_id(), static_cast<uint32_t>(state.pid()),
+              deltas[i].asInt64() * 1000));
+      context_->sorter->PushLegacyV8CpuProfileEvent(
+          ts, legacy.unscoped_id(), static_cast<uint32_t>(state.pid()),
+          static_cast<uint32_t>(state.tid()), samples[i].asUInt());
+    }
+  }
+#else
+  base::ignore_result(event, legacy, state);
+#endif
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/track_event_tokenizer.h b/src/trace_processor/importers/proto/track_event_tokenizer.h
index 6ce3347..0627562 100644
--- a/src/trace_processor/importers/proto/track_event_tokenizer.h
+++ b/src/trace_processor/importers/proto/track_event_tokenizer.h
@@ -17,23 +17,26 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_TOKENIZER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_TOKENIZER_H_
 
-#include <stdint.h>
+#include <cstddef>
+#include <cstdint>
 
+#include "perfetto/base/status.h"
 #include "perfetto/protozero/proto_decoder.h"
+#include "perfetto/trace_processor/ref_counted.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"
 
 namespace perfetto {
 
-namespace protos {
-namespace pbzero {
+namespace protos::pbzero {
 class ChromeThreadDescriptor_Decoder;
 class ProcessDescriptor_Decoder;
 class ThreadDescriptor_Decoder;
 class TracePacket_Decoder;
-}  // namespace pbzero
-}  // namespace protos
+class TrackEvent_Decoder;
+class TrackEvent_LegacyEvent_Decoder;
+}  // namespace protos::pbzero
 
 namespace trace_processor {
 
@@ -57,10 +60,11 @@
   ModuleResult TokenizeThreadDescriptorPacket(
       RefPtr<PacketSequenceStateGeneration> state,
       const protos::pbzero::TracePacket_Decoder&);
-  void TokenizeTrackEventPacket(RefPtr<PacketSequenceStateGeneration> state,
-                                const protos::pbzero::TracePacket_Decoder&,
-                                TraceBlobView* packet,
-                                int64_t packet_timestamp);
+  ModuleResult TokenizeTrackEventPacket(
+      RefPtr<PacketSequenceStateGeneration> state,
+      const protos::pbzero::TracePacket_Decoder&,
+      TraceBlobView* packet,
+      int64_t packet_timestamp);
 
  private:
   void TokenizeThreadDescriptor(
@@ -74,6 +78,10 @@
       protozero::RepeatedFieldIterator<T> value_it,
       protozero::RepeatedFieldIterator<uint64_t> packet_track_uuid_it,
       protozero::RepeatedFieldIterator<uint64_t> default_track_uuid_it);
+  base::Status TokenizeLegacySampleEvent(
+      const protos::pbzero::TrackEvent_Decoder&,
+      const protos::pbzero::TrackEvent_LegacyEvent_Decoder&,
+      PacketSequenceStateGeneration& state);
 
   TraceProcessorContext* context_;
   TrackEventTracker* track_event_tracker_;
diff --git a/src/trace_processor/importers/proto/track_event_tracker.cc b/src/trace_processor/importers/proto/track_event_tracker.cc
index a336b60..8762006 100644
--- a/src/trace_processor/importers/proto/track_event_tracker.cc
+++ b/src/trace_processor/importers/proto/track_event_tracker.cc
@@ -32,9 +32,9 @@
 #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/track_tracker.h"
+#include "src/trace_processor/importers/common/tracks.h"
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.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"
 
@@ -47,20 +47,20 @@
       category_key_(context->storage->InternString("category")),
       has_first_packet_on_sequence_key_id_(
           context->storage->InternString("has_first_packet_on_sequence")),
+      child_ordering_key_(context->storage->InternString("child_ordering")),
+      explicit_id_(context->storage->InternString("explicit")),
+      lexicographic_id_(context->storage->InternString("lexicographic")),
+      chronological_id_(context->storage->InternString("chronological")),
+      sibling_order_rank_key_(
+          context->storage->InternString("sibling_order_rank")),
       descriptor_source_(context->storage->InternString("descriptor")),
       default_descriptor_track_name_(
           context->storage->InternString("Default Track")),
       context_(context) {}
 
-void TrackEventTracker::ReserveDescriptorProcessTrack(uint64_t uuid,
-                                                      StringId name,
-                                                      uint32_t pid,
-                                                      int64_t timestamp) {
-  DescriptorTrackReservation reservation;
-  reservation.min_timestamp = timestamp;
-  reservation.pid = pid;
-  reservation.name = name;
-
+void TrackEventTracker::ReserveDescriptorTrack(
+    uint64_t uuid,
+    const DescriptorTrackReservation& reservation) {
   std::map<uint64_t, DescriptorTrackReservation>::iterator it;
   bool inserted;
   std::tie(it, inserted) =
@@ -70,125 +70,14 @@
     return;
 
   if (!it->second.IsForSameTrack(reservation)) {
-    // Process tracks should not be reassigned to a different pid later (neither
-    // should the type of the track change).
     PERFETTO_DLOG("New track reservation for process 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, timestamp);
-}
-
-void TrackEventTracker::ReserveDescriptorThreadTrack(uint64_t uuid,
-                                                     uint64_t parent_uuid,
-                                                     StringId name,
-                                                     uint32_t pid,
-                                                     uint32_t tid,
-                                                     int64_t timestamp,
-                                                     bool use_separate_track) {
-  DescriptorTrackReservation reservation;
-  reservation.min_timestamp = timestamp;
-  reservation.parent_uuid = parent_uuid;
-  reservation.pid = pid;
-  reservation.tid = tid;
-  reservation.name = name;
-  reservation.use_separate_track = use_separate_track;
-
-  std::map<uint64_t, DescriptorTrackReservation>::iterator it;
-  bool inserted;
-  std::tie(it, inserted) =
-      reserved_descriptor_tracks_.insert(std::make_pair<>(uuid, reservation));
-
-  if (inserted)
-    return;
-
-  if (!it->second.IsForSameTrack(reservation)) {
-    // Thread tracks should not be reassigned to a different pid/tid later
-    // (neither should the type of the track change).
-    PERFETTO_DLOG("New track reservation for thread 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, timestamp);
-}
-
-void TrackEventTracker::ReserveDescriptorCounterTrack(
-    uint64_t uuid,
-    uint64_t parent_uuid,
-    StringId name,
-    StringId category,
-    int64_t unit_multiplier,
-    bool is_incremental,
-    uint32_t packet_sequence_id) {
-  DescriptorTrackReservation reservation;
-  reservation.parent_uuid = parent_uuid;
-  reservation.is_counter = true;
-  reservation.name = name;
-  reservation.category = category;
-  reservation.unit_multiplier = unit_multiplier;
-  reservation.is_incremental = is_incremental;
-  // Incrementally encoded counters are only valid on a single sequence.
-  if (is_incremental)
-    reservation.packet_sequence_id = packet_sequence_id;
-
-  std::map<uint64_t, DescriptorTrackReservation>::iterator it;
-  bool inserted;
-  std::tie(it, inserted) =
-      reserved_descriptor_tracks_.insert(std::make_pair<>(uuid, reservation));
-
-  if (inserted || it->second.IsForSameTrack(reservation))
-    return;
-
-  // Counter tracks should not be reassigned to a different parent track later
-  // (neither should the type of the track change).
-  PERFETTO_DLOG("New track reservation for counter track with uuid %" PRIu64
-                " doesn't match earlier one",
-                uuid);
-  context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-}
-
-void TrackEventTracker::ReserveDescriptorChildTrack(uint64_t uuid,
-                                                    uint64_t parent_uuid,
-                                                    StringId name) {
-  DescriptorTrackReservation reservation;
-  reservation.parent_uuid = parent_uuid;
-  reservation.name = name;
-
-  std::map<uint64_t, DescriptorTrackReservation>::iterator it;
-  bool inserted;
-  std::tie(it, inserted) =
-      reserved_descriptor_tracks_.insert(std::make_pair<>(uuid, reservation));
-
-  if (inserted || it->second.IsForSameTrack(reservation))
-    return;
-
-  // Child tracks should not be reassigned to a different parent track later
-  // (neither should the type of the track change).
-  PERFETTO_DLOG("New track reservation for child track with uuid %" PRIu64
-                " doesn't match earlier one",
-                uuid);
-  context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-}
-
-TrackId TrackEventTracker::InsertThreadTrack(UniqueTid utid) {
-  tables::ThreadTrackTable::Row row;
-  row.utid = utid;
-  row.machine_id = context_->machine_id();
-  auto* thread_tracks = context_->storage->mutable_thread_track_table();
-  return thread_tracks->Insert(row).id;
-}
-
-TrackId TrackEventTracker::InternThreadTrack(UniqueTid utid) {
-  auto it = thread_tracks_.find(utid);
-  if (it != thread_tracks_.end()) {
-    return it->second;
-  }
-  return thread_tracks_[utid] = InsertThreadTrack(utid);
+  it->second.min_timestamp =
+      std::min(it->second.min_timestamp, reservation.min_timestamp);
 }
 
 std::optional<TrackId> TrackEventTracker::GetDescriptorTrack(
@@ -202,8 +91,7 @@
 
   // 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* tracks = context_->storage->mutable_track_table();
-  auto rr = *tracks->FindById(*track_id);
+  auto rr = *context_->storage->mutable_track_table()->FindById(*track_id);
   if (!rr.name().is_null()) {
     return track_id;
   }
@@ -255,16 +143,36 @@
       .AddArg(source_id_key_, Variadic::Integer(static_cast<int64_t>(uuid)))
       .AddArg(is_root_in_scope_key_,
               Variadic::Boolean(resolved_track->is_root_in_scope()));
-  if (!reservation.category.is_null())
-    args.AddArg(category_key_, Variadic::String(reservation.category));
+  if (reservation.counter_details &&
+      !reservation.counter_details->category.is_null())
+    args.AddArg(category_key_,
+                Variadic::String(reservation.counter_details->category));
   if (packet_sequence_id &&
       sequences_with_first_packet_.find(*packet_sequence_id) !=
           sequences_with_first_packet_.end()) {
     args.AddArg(has_first_packet_on_sequence_key_id_, Variadic::Boolean(true));
   }
 
-  auto* tracks = context_->storage->mutable_track_table();
-  auto row_ref = *tracks->FindById(track_id);
+  switch (reservation.ordering) {
+    case DescriptorTrackReservation::ChildTracksOrdering::kLexicographic:
+      args.AddArg(child_ordering_key_, Variadic::String(lexicographic_id_));
+      break;
+    case DescriptorTrackReservation::ChildTracksOrdering::kChronological:
+      args.AddArg(child_ordering_key_, Variadic::String(chronological_id_));
+      break;
+    case DescriptorTrackReservation::ChildTracksOrdering::kExplicit:
+      args.AddArg(child_ordering_key_, Variadic::String(explicit_id_));
+      break;
+    case DescriptorTrackReservation::ChildTracksOrdering::kUnknown:
+      break;
+  }
+
+  if (reservation.sibling_order_rank) {
+    args.AddArg(sibling_order_rank_key_,
+                Variadic::Integer(*reservation.sibling_order_rank));
+  }
+
+  auto row_ref = *context_->storage->mutable_track_table()->FindById(track_id);
   if (parent_id) {
     row_ref.set_parent_id(*parent_id);
   }
@@ -284,59 +192,54 @@
     switch (track.scope()) {
       case ResolvedDescriptorTrack::Scope::kThread: {
         if (track.use_separate_track()) {
-          return InternThreadTrack(track.utid());
+          auto it = thread_tracks_.find(track.utid());
+          if (it != thread_tracks_.end()) {
+            return it->second;
+          }
+          TrackId id = context_->track_tracker->CreateThreadTrack(
+              tracks::track_event, track.utid(), TrackTracker::AutoName());
+          thread_tracks_[track.utid()] = id;
+          return id;
         }
         return context_->track_tracker->InternThreadTrack(track.utid());
       }
       case ResolvedDescriptorTrack::Scope::kProcess:
-        return context_->track_tracker->InternProcessTrack(track.upid());
+        return context_->track_tracker->InternProcessTrack(tracks::track_event,
+                                                           track.upid());
       case ResolvedDescriptorTrack::Scope::kGlobal:
         // Will be handled below.
         break;
     }
   }
 
+  if (track.is_counter()) {
+    switch (track.scope()) {
+      case ResolvedDescriptorTrack::Scope::kThread:
+        return context_->track_tracker->CreateThreadCounterTrack(
+            tracks::track_event, track.utid(), TrackTracker::AutoName());
+      case ResolvedDescriptorTrack::Scope::kProcess:
+        return context_->track_tracker->CreateProcessCounterTrack(
+            tracks::track_event, track.upid(), std::nullopt,
+            TrackTracker::AutoName());
+      case ResolvedDescriptorTrack::Scope::kGlobal:
+        return context_->track_tracker->CreateCounterTrack(
+            tracks::track_event, std::nullopt, TrackTracker::AutoName());
+    }
+  }
+
   switch (track.scope()) {
     case ResolvedDescriptorTrack::Scope::kThread: {
-      if (track.is_counter()) {
-        tables::ThreadCounterTrackTable::Row row;
-        row.utid = track.utid();
-        row.machine_id = context_->machine_id();
-
-        auto* thread_counter_tracks =
-            context_->storage->mutable_thread_counter_track_table();
-        return thread_counter_tracks->Insert(row).id;
-      }
-
-      return InsertThreadTrack(track.utid());
+      return context_->track_tracker->CreateThreadTrack(
+          tracks::track_event, track.utid(), TrackTracker::AutoName());
     }
     case ResolvedDescriptorTrack::Scope::kProcess: {
-      if (track.is_counter()) {
-        tables::ProcessCounterTrackTable::Row row;
-        row.upid = track.upid();
-        row.machine_id = context_->machine_id();
-
-        auto* process_counter_tracks =
-            context_->storage->mutable_process_counter_track_table();
-        return process_counter_tracks->Insert(row).id;
-      }
-
-      tables::ProcessTrackTable::Row row;
-      row.upid = track.upid();
-      row.machine_id = context_->machine_id();
-
-      auto* process_tracks = context_->storage->mutable_process_track_table();
-      return process_tracks->Insert(row).id;
+      return context_->track_tracker->CreateProcessTrack(
+          tracks::track_event, track.upid(), std::nullopt,
+          TrackTracker::AutoName());
     }
     case ResolvedDescriptorTrack::Scope::kGlobal: {
-      if (track.is_counter()) {
-        tables::CounterTrackTable::Row row;
-        row.machine_id = context_->machine_id();
-        return context_->storage->mutable_counter_track_table()->Insert(row).id;
-      }
-      tables::TrackTable::Row row;
-      row.machine_id = context_->machine_id();
-      return context_->storage->mutable_track_table()->Insert(row).id;
+      return context_->track_tracker->CreateTrack(
+          tracks::track_event, std::nullopt, TrackTracker::AutoName());
     }
   }
   PERFETTO_FATAL("For GCC");
@@ -545,8 +448,10 @@
     return *track_id;
 
   // Otherwise reserve a new track and resolve it.
-  ReserveDescriptorChildTrack(kDefaultDescriptorTrackUuid, /*parent_uuid=*/0,
-                              default_descriptor_track_name_);
+  DescriptorTrackReservation r;
+  r.parent_uuid = 0;
+  r.name = default_descriptor_track_name_;
+  ReserveDescriptorTrack(kDefaultDescriptorTrackUuid, r);
   return *GetDescriptorTrack(kDefaultDescriptorTrackUuid);
 }
 
@@ -567,23 +472,27 @@
                   counter_track_uuid);
     return std::nullopt;
   }
+  if (!reservation.counter_details) {
+    PERFETTO_FATAL("Counter tracks require `counter_details`.");
+  }
+  DescriptorTrackReservation::CounterDetails& c_details =
+      *reservation.counter_details;
 
-  if (reservation.unit_multiplier > 0)
-    value *= static_cast<double>(reservation.unit_multiplier);
+  if (c_details.unit_multiplier > 0)
+    value *= static_cast<double>(c_details.unit_multiplier);
 
-  if (reservation.is_incremental) {
-    if (reservation.packet_sequence_id != packet_sequence_id) {
+  if (c_details.is_incremental) {
+    if (c_details.packet_sequence_id != packet_sequence_id) {
       PERFETTO_DLOG(
           "Incremental counter track with uuid %" PRIu64
           " was updated from the wrong packet sequence (expected: %" PRIu32
           " got:%" PRIu32 ")",
-          counter_track_uuid, reservation.packet_sequence_id,
-          packet_sequence_id);
+          counter_track_uuid, c_details.packet_sequence_id, packet_sequence_id);
       return std::nullopt;
     }
 
-    reservation.latest_value += value;
-    value = reservation.latest_value;
+    c_details.latest_value += value;
+    value = c_details.latest_value;
   }
 
   return value;
@@ -597,12 +506,13 @@
   for (auto& entry : reserved_descriptor_tracks_) {
     DescriptorTrackReservation& reservation = entry.second;
     // Only consider incremental counter tracks for current sequence.
-    if (!reservation.is_counter || !reservation.is_incremental ||
-        reservation.packet_sequence_id != packet_sequence_id) {
+    if (!reservation.is_counter || !reservation.counter_details ||
+        !reservation.counter_details->is_incremental ||
+        reservation.counter_details->packet_sequence_id != packet_sequence_id) {
       continue;
     }
     // Reset their value to 0, see CounterDescriptor's |is_incremental|.
-    reservation.latest_value = 0;
+    reservation.counter_details->latest_value = 0;
   }
 }
 
diff --git a/src/trace_processor/importers/proto/track_event_tracker.h b/src/trace_processor/importers/proto/track_event_tracker.h
index 2eb7092..b801a74 100644
--- a/src/trace_processor/importers/proto/track_event_tracker.h
+++ b/src/trace_processor/importers/proto/track_event_tracker.h
@@ -17,85 +17,86 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_TRACKER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_TRACKER_H_
 
+#include <cstdint>
+#include <map>
+#include <optional>
+#include <tuple>
 #include <unordered_set>
+#include <vector>
 
-#include "src/trace_processor/importers/common/args_tracker.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/status_or.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 // Tracks and stores tracks based on track types, ids and scopes.
 class TrackEventTracker {
  public:
+  // Data from TrackDescriptor proto used to reserve a track before interning it
+  // with |TrackTracker|.
+  struct DescriptorTrackReservation {
+    // Maps to TrackDescriptor::ChildTracksOrdering proto values
+    enum class ChildTracksOrdering {
+      kUnknown = 0,
+      kLexicographic = 1,
+      kChronological = 2,
+      kExplicit = 3,
+    };
+    struct CounterDetails {
+      StringId category;
+      int64_t unit_multiplier = 1;
+      bool is_incremental = false;
+      uint32_t packet_sequence_id = 0;
+      double latest_value = 0;
+
+      bool operator==(const CounterDetails& o) const {
+        return std::tie(category, unit_multiplier, is_incremental,
+                        packet_sequence_id, latest_value) ==
+               std::tie(o.category, o.unit_multiplier, o.is_incremental,
+                        o.packet_sequence_id, o.latest_value);
+      }
+    };
+
+    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.
+    StringId name = kNullStringId;
+    bool use_separate_track = false;
+    bool is_counter = false;
+
+    // For counter tracks.
+    std::optional<CounterDetails> counter_details;
+
+    // For UI visualisation
+    ChildTracksOrdering ordering = ChildTracksOrdering::kUnknown;
+    std::optional<int32_t> sibling_order_rank;
+
+    // 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);
+    }
+  };
   explicit TrackEventTracker(TraceProcessorContext*);
 
   // Associate a TrackDescriptor track identified by the given |uuid| with a
-  // process's |pid|. This is called during tokenization. If a reservation for
-  // the same |uuid| already exists, verifies that the present reservation
-  // matches the new one.
-  //
-  // The track will be resolved to the process track (see InternProcessTrack())
-  // upon the first call to GetDescriptorTrack() with the same |uuid|. At this
-  // time, |pid| will also be resolved to a |upid|.
-  void ReserveDescriptorProcessTrack(uint64_t uuid,
-                                     StringId name,
-                                     uint32_t pid,
-                                     int64_t timestamp);
-
-  // Associate a TrackDescriptor track identified by the given |uuid| with a
-  // thread's |pid| and |tid|. This is called during tokenization. If a
+  // given track description. This is called during tokenization. If a
   // reservation for the same |uuid| already exists, verifies that the present
   // reservation matches the new one.
   //
-  // The track will be resolved to the thread track (see InternThreadTrack())
+  // The track will be resolved to the track (see TrackTracker::InternTrack())
   // upon the first call to GetDescriptorTrack() with the same |uuid|. At this
-  // time, |pid| will also be resolved to a |upid|.
-  void ReserveDescriptorThreadTrack(uint64_t uuid,
-                                    uint64_t parent_uuid,
-                                    StringId name,
-                                    uint32_t pid,
-                                    uint32_t tid,
-                                    int64_t timestamp,
-                                    bool use_separate_track);
-
-  // Associate a TrackDescriptor track identified by the given |uuid| with a
-  // parent track (usually a process- or thread-associated track). This is
-  // called during tokenization. If a reservation for the same |uuid| already
-  // exists, will attempt to update it.
-  //
-  // The track will be created upon the first call to GetDescriptorTrack() with
-  // the same |uuid|. If |parent_uuid| is 0, the track will become a global
-  // track. Otherwise, it will become a new track of the same type as its parent
-  // track.
-  void ReserveDescriptorChildTrack(uint64_t uuid,
-                                   uint64_t parent_uuid,
-                                   StringId name);
-
-  // Associate a counter-type TrackDescriptor track identified by the given
-  // |uuid| with a parent track (usually a process or thread track). This is
-  // called during tokenization. If a reservation for the same |uuid| already
-  // exists, will attempt to update it. The provided |category| will be stored
-  // into the track's args.
-  //
-  // If |is_incremental| is true, the counter will only be valid on the packet
-  // sequence identified by |packet_sequence_id|. |unit_multiplier| is an
-  // optional multiplication factor applied to counter values. Values for the
-  // counter will be translated during tokenization via
-  // ConvertToAbsoluteCounterValue().
-  //
-  // The track will be created upon the first call to GetDescriptorTrack() with
-  // the same |uuid|. If |parent_uuid| is 0, the track will become a global
-  // track. Otherwise, it will become a new counter track for the same
-  // process/thread as its parent track.
-  void ReserveDescriptorCounterTrack(uint64_t uuid,
-                                     uint64_t parent_uuid,
-                                     StringId name,
-                                     StringId category,
-                                     int64_t unit_multiplier,
-                                     bool is_incremental,
-                                     uint32_t packet_sequence_id);
+  // time, |pid| will be resolved to a |upid| and |tid| to |utid|.
+  void ReserveDescriptorTrack(uint64_t uuid, const DescriptorTrackReservation&);
 
   // Returns the ID of the track for the TrackDescriptor with the given |uuid|.
   // This is called during parsing. The first call to GetDescriptorTrack() for
@@ -126,13 +127,6 @@
   // GetDescriptorTrack is moved back.
   TrackId GetOrCreateDefaultDescriptorTrack();
 
-  // Track events timestamps in Chrome have microsecond resolution, while
-  // system events use nanoseconds. It results in broken event nesting when
-  // track events and system events share a track.
-  // So TrackEventTracker needs to support its own tracks, separate from the
-  // ones in the TrackTracker.
-  TrackId InternThreadTrack(UniqueTid utid);
-
   // 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|.
@@ -149,34 +143,6 @@
   }
 
  private:
-  struct DescriptorTrackReservation {
-    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.
-    StringId name = kNullStringId;
-    bool use_separate_track = false;
-
-    // For counter tracks.
-    bool is_counter = false;
-    StringId category = kNullStringId;
-    int64_t unit_multiplier = 1;
-    bool is_incremental = false;
-    uint32_t packet_sequence_id = 0;
-    double latest_value = 0;
-
-    // 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, category,
-                      unit_multiplier, is_incremental, packet_sequence_id) ==
-             std::tie(other.parent_uuid, pid, tid, is_counter, category,
-                      unit_multiplier, is_incremental, packet_sequence_id);
-    }
-  };
-
   class ResolvedDescriptorTrack {
    public:
     enum class Scope {
@@ -231,7 +197,6 @@
       uint64_t uuid,
       const DescriptorTrackReservation&,
       std::vector<uint64_t>* descendent_uuids);
-  TrackId InsertThreadTrack(UniqueTid utid);
 
   static constexpr uint64_t kDefaultDescriptorTrackUuid = 0u;
 
@@ -251,22 +216,26 @@
 
   std::unordered_set<uint32_t> sequences_with_first_packet_;
 
-  const StringId source_key_ = kNullStringId;
-  const StringId source_id_key_ = kNullStringId;
-  const StringId is_root_in_scope_key_ = kNullStringId;
-  const StringId category_key_ = kNullStringId;
-  const StringId has_first_packet_on_sequence_key_id_ = kNullStringId;
+  const StringId source_key_;
+  const StringId source_id_key_;
+  const StringId is_root_in_scope_key_;
+  const StringId category_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_ = kNullStringId;
+  const StringId descriptor_source_;
 
-  const StringId default_descriptor_track_name_ = kNullStringId;
+  const StringId default_descriptor_track_name_;
 
   std::optional<int64_t> range_of_interest_start_us_;
 
   TraceProcessorContext* const context_;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_TRACKER_H_
diff --git a/src/trace_processor/importers/proto/winscope/BUILD.gn b/src/trace_processor/importers/proto/winscope/BUILD.gn
index ab09809..ba4452c 100644
--- a/src/trace_processor/importers/proto/winscope/BUILD.gn
+++ b/src/trace_processor/importers/proto/winscope/BUILD.gn
@@ -20,8 +20,6 @@
     "android_input_event_parser.h",
     "protolog_message_decoder.cc",
     "protolog_message_decoder.h",
-    "protolog_messages_tracker.cc",
-    "protolog_messages_tracker.h",
     "protolog_parser.cc",
     "protolog_parser.h",
     "shell_transitions_parser.cc",
@@ -57,6 +55,7 @@
     "../../common:parser_types",
     "../../proto:minimal",
     "../../proto:packet_sequence_state_generation_hdr",
+    "../../../util:winscope_proto_mapping"
   ]
 }
 
diff --git a/src/trace_processor/importers/proto/winscope/android_input_event_parser.cc b/src/trace_processor/importers/proto/winscope/android_input_event_parser.cc
index 03bf5ad..e3b2553 100644
--- a/src/trace_processor/importers/proto/winscope/android_input_event_parser.cc
+++ b/src/trace_processor/importers/proto/winscope/android_input_event_parser.cc
@@ -16,13 +16,14 @@
 
 #include "src/trace_processor/importers/proto/winscope/android_input_event_parser.h"
 
+#include "perfetto/ext/base/base64.h"
 #include "protos/perfetto/trace/android/android_input_event.pbzero.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/proto/args_parser.h"
-#include "src/trace_processor/importers/proto/winscope/winscope.descriptor.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/tables/android_tables_py.h"
 #include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/winscope_proto_mapping.h"
 
 namespace perfetto::trace_processor {
 
@@ -33,10 +34,7 @@
 using perfetto::protos::pbzero::TracePacket;
 
 AndroidInputEventParser::AndroidInputEventParser(TraceProcessorContext* context)
-    : context_(*context), args_parser_{pool_} {
-  pool_.AddFromFileDescriptorSet(kWinscopeDescriptor.data(),
-                                 kWinscopeDescriptor.size());
-}
+    : context_(*context), args_parser_{*context->descriptor_pool_} {}
 
 void AndroidInputEventParser::ParseAndroidInputEvent(
     int64_t packet_ts,
@@ -80,6 +78,10 @@
   tables::AndroidMotionEventsTable::Row event_row;
   event_row.event_id = event_proto.event_id();
   event_row.ts = packet_ts;
+  event_row.base64_proto =
+      context_.storage->mutable_string_pool()->InternString(
+          base::StringView(base::Base64Encode(bytes.data, bytes.size)));
+  event_row.base64_proto_id = event_row.base64_proto.raw_id();
 
   auto event_row_id = context_.storage->mutable_android_motion_events_table()
                           ->Insert(event_row)
@@ -88,7 +90,9 @@
   ArgsParser writer{packet_ts, inserter, *context_.storage};
 
   base::Status status =
-      args_parser_.ParseMessage(bytes, ".perfetto.protos.AndroidMotionEvent",
+      args_parser_.ParseMessage(bytes,
+                                *util::winscope_proto_mapping::GetProtoName(
+                                    tables::AndroidMotionEventsTable::Name()),
                                 nullptr /*parse all fields*/, writer);
   if (!status.ok())
     context_.storage->IncrementStats(stats::android_input_event_parse_errors);
@@ -101,6 +105,10 @@
   tables::AndroidKeyEventsTable::Row event_row;
   event_row.event_id = event_proto.event_id();
   event_row.ts = packet_ts;
+  event_row.base64_proto =
+      context_.storage->mutable_string_pool()->InternString(
+          base::StringView(base::Base64Encode(bytes.data, bytes.size)));
+  event_row.base64_proto_id = event_row.base64_proto.raw_id();
 
   auto event_row_id = context_.storage->mutable_android_key_events_table()
                           ->Insert(event_row)
@@ -109,7 +117,9 @@
   ArgsParser writer{packet_ts, inserter, *context_.storage};
 
   base::Status status =
-      args_parser_.ParseMessage(bytes, ".perfetto.protos.AndroidKeyEvent",
+      args_parser_.ParseMessage(bytes,
+                                *util::winscope_proto_mapping::GetProtoName(
+                                    tables::AndroidKeyEventsTable::Name()),
                                 nullptr /*parse all fields*/, writer);
   if (!status.ok())
     context_.storage->IncrementStats(stats::android_input_event_parse_errors);
@@ -123,16 +133,23 @@
   event_row.event_id = event_proto.event_id();
   event_row.vsync_id = event_proto.vsync_id();
   event_row.window_id = event_proto.window_id();
+  event_row.base64_proto =
+      context_.storage->mutable_string_pool()->InternString(
+          base::StringView(base::Base64Encode(bytes.data, bytes.size)));
+  event_row.base64_proto_id = event_row.base64_proto.raw_id();
 
   auto event_row_id =
       context_.storage->mutable_android_input_event_dispatch_table()
           ->Insert(event_row)
           .id;
+
   auto inserter = context_.args_tracker->AddArgsTo(event_row_id);
   ArgsParser writer{packet_ts, inserter, *context_.storage};
 
   base::Status status = args_parser_.ParseMessage(
-      bytes, ".perfetto.protos.AndroidWindowInputDispatchEvent",
+      bytes,
+      *util::winscope_proto_mapping::GetProtoName(
+          tables::AndroidInputEventDispatchTable::Name()),
       nullptr /*parse all fields*/, writer);
   if (!status.ok())
     context_.storage->IncrementStats(stats::android_input_event_parse_errors);
diff --git a/src/trace_processor/importers/proto/winscope/android_input_event_parser.h b/src/trace_processor/importers/proto/winscope/android_input_event_parser.h
index 96f523b..12ce63b 100644
--- a/src/trace_processor/importers/proto/winscope/android_input_event_parser.h
+++ b/src/trace_processor/importers/proto/winscope/android_input_event_parser.h
@@ -36,7 +36,6 @@
 
  private:
   TraceProcessorContext& context_;
-  DescriptorPool pool_;
   util::ProtoToArgsParser args_parser_;
 
   void ParseMotionEvent(int64_t packet_ts, const protozero::ConstBytes& bytes);
diff --git a/src/trace_processor/importers/proto/winscope/protolog_message_decoder.cc b/src/trace_processor/importers/proto/winscope/protolog_message_decoder.cc
index de82a0e..71bca76 100644
--- a/src/trace_processor/importers/proto/winscope/protolog_message_decoder.cc
+++ b/src/trace_processor/importers/proto/winscope/protolog_message_decoder.cc
@@ -26,7 +26,8 @@
 
 namespace perfetto::trace_processor {
 
-ProtoLogMessageDecoder::ProtoLogMessageDecoder() = default;
+ProtoLogMessageDecoder::ProtoLogMessageDecoder(TraceProcessorContext* context)
+    : context_(context) {}
 ProtoLogMessageDecoder::~ProtoLogMessageDecoder() = default;
 
 std::optional<DecodedMessage> ProtoLogMessageDecoder::Decode(
@@ -123,6 +124,11 @@
 }
 
 void ProtoLogMessageDecoder::TrackGroup(uint32_t id, const std::string& tag) {
+  auto tracked_group = tracked_groups_.Find(id);
+  if (tracked_group != nullptr && tracked_group->tag != tag) {
+    context_->storage->IncrementStats(
+            stats::winscope_protolog_view_config_collision);
+  }
   tracked_groups_.Insert(id, TrackedGroup{tag});
 }
 
@@ -132,6 +138,11 @@
     uint32_t group_id,
     const std::string& message,
     const std::optional<std::string>& location) {
+  auto tracked_message = tracked_messages_.Find(message_id);
+  if (tracked_message != nullptr && tracked_message->message != message) {
+    context_->storage->IncrementStats(
+            stats::winscope_protolog_view_config_collision);
+  }
   tracked_messages_.Insert(message_id,
                            TrackedMessage{level, group_id, message, location});
 }
diff --git a/src/trace_processor/importers/proto/winscope/protolog_message_decoder.h b/src/trace_processor/importers/proto/winscope/protolog_message_decoder.h
index 8955c01..c99dd43 100644
--- a/src/trace_processor/importers/proto/winscope/protolog_message_decoder.h
+++ b/src/trace_processor/importers/proto/winscope/protolog_message_decoder.h
@@ -59,12 +59,12 @@
 
 class ProtoLogMessageDecoder : public Destructible {
  public:
-  explicit ProtoLogMessageDecoder();
+  explicit ProtoLogMessageDecoder(TraceProcessorContext* context);
   virtual ~ProtoLogMessageDecoder() override;
 
   static ProtoLogMessageDecoder* GetOrCreate(TraceProcessorContext* context) {
     if (!context->protolog_message_decoder) {
-      context->protolog_message_decoder.reset(new ProtoLogMessageDecoder());
+      context->protolog_message_decoder.reset(new ProtoLogMessageDecoder(context));
     }
     return static_cast<ProtoLogMessageDecoder*>(
         context->protolog_message_decoder.get());
@@ -86,6 +86,7 @@
                     const std::optional<std::string>& location);
 
  private:
+  TraceProcessorContext* const context_;
   base::FlatHashMap<uint64_t, TrackedGroup> tracked_groups_;
   base::FlatHashMap<uint64_t, TrackedMessage> tracked_messages_;
 };
diff --git a/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.cc b/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.cc
deleted file mode 100644
index 2cf095a..0000000
--- a/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.cc
+++ /dev/null
@@ -1,51 +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.
- */
-
-#include "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h"
-
-#include <cstdint>
-#include <optional>
-#include <vector>
-
-namespace perfetto::trace_processor {
-
-ProtoLogMessagesTracker::ProtoLogMessagesTracker() = default;
-ProtoLogMessagesTracker::~ProtoLogMessagesTracker() = default;
-
-void ProtoLogMessagesTracker::TrackMessage(
-    TrackedProtoLogMessage tracked_protolog_message) {
-  tracked_protolog_messages
-      .Insert(tracked_protolog_message.message_id,
-              std::vector<TrackedProtoLogMessage>())
-      .first->emplace_back(tracked_protolog_message);
-}
-
-std::optional<std::vector<ProtoLogMessagesTracker::TrackedProtoLogMessage>*>
-ProtoLogMessagesTracker::GetTrackedMessagesByMessageId(uint64_t message_id) {
-  auto* tracked_messages = tracked_protolog_messages.Find(message_id);
-  if (tracked_messages == nullptr) {
-    // No tracked messages found for this id
-    return std::nullopt;
-  }
-  return tracked_messages;
-}
-
-void ProtoLogMessagesTracker::ClearTrackedMessagesForMessageId(
-    uint64_t message_id) {
-  tracked_protolog_messages.Erase(message_id);
-}
-
-}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h b/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h
deleted file mode 100644
index 3a34ffb..0000000
--- a/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h
+++ /dev/null
@@ -1,69 +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.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_WINSCOPE_PROTOLOG_MESSAGES_TRACKER_H_
-#define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_WINSCOPE_PROTOLOG_MESSAGES_TRACKER_H_
-
-#include <cstdint>
-#include <optional>
-#include <string>
-#include <vector>
-
-#include "perfetto/ext/base/flat_hash_map.h"
-#include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/tables/winscope_tables_py.h"
-#include "src/trace_processor/types/destructible.h"
-#include "src/trace_processor/types/trace_processor_context.h"
-
-namespace perfetto::trace_processor {
-
-class ProtoLogMessagesTracker : public Destructible {
- public:
-  explicit ProtoLogMessagesTracker();
-  virtual ~ProtoLogMessagesTracker() override;
-
-  struct TrackedProtoLogMessage {
-    uint64_t message_id;
-    std::vector<int64_t> sint64_params;
-    std::vector<double> double_params;
-    std::vector<bool> boolean_params;
-    std::vector<std::string> string_params;
-    std::optional<StringId> stacktrace;
-    tables::ProtoLogTable::Id table_row_id;
-    int64_t timestamp;
-  };
-
-  static ProtoLogMessagesTracker* GetOrCreate(TraceProcessorContext* context) {
-    if (!context->protolog_messages_tracker) {
-      context->protolog_messages_tracker.reset(new ProtoLogMessagesTracker());
-    }
-    return static_cast<ProtoLogMessagesTracker*>(
-        context->protolog_messages_tracker.get());
-  }
-
-  void TrackMessage(TrackedProtoLogMessage tracked_protolog_message);
-  std::optional<std::vector<ProtoLogMessagesTracker::TrackedProtoLogMessage>*>
-  GetTrackedMessagesByMessageId(uint64_t message_id);
-  void ClearTrackedMessagesForMessageId(uint64_t message_id);
-
- private:
-  base::FlatHashMap<uint64_t, std::vector<TrackedProtoLogMessage>>
-      tracked_protolog_messages;
-};
-
-}  // namespace perfetto::trace_processor
-
-#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_WINSCOPE_PROTOLOG_MESSAGES_TRACKER_H_
diff --git a/src/trace_processor/importers/proto/winscope/protolog_parser.cc b/src/trace_processor/importers/proto/winscope/protolog_parser.cc
index 0df2967..e9e0c83 100644
--- a/src/trace_processor/importers/proto/winscope/protolog_parser.cc
+++ b/src/trace_processor/importers/proto/winscope/protolog_parser.cc
@@ -35,8 +35,6 @@
 #include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/importers/proto/winscope/protolog_message_decoder.h"
-#include "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h"
-#include "src/trace_processor/importers/proto/winscope/winscope.descriptor.h"
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/tables/winscope_tables_py.h"
@@ -46,7 +44,7 @@
 
 ProtoLogParser::ProtoLogParser(TraceProcessorContext* context)
     : context_(context),
-      args_parser_{pool_},
+      args_parser_{*context_->descriptor_pool_},
       log_level_debug_string_id_(context->storage->InternString("DEBUG")),
       log_level_verbose_string_id_(context->storage->InternString("VERBOSE")),
       log_level_info_string_id_(context->storage->InternString("INFO")),
@@ -54,8 +52,6 @@
       log_level_error_string_id_(context->storage->InternString("ERROR")),
       log_level_wtf_string_id_(context->storage->InternString("WTF")),
       log_level_unknown_string_id_(context_->storage->InternString("UNKNOWN")) {
-  pool_.AddFromFileDescriptorSet(kWinscopeDescriptor.data(),
-                                 kWinscopeDescriptor.size());
 }
 
 void ProtoLogParser::ParseProtoLogMessage(
@@ -66,7 +62,7 @@
 
   std::vector<int64_t> sint64_params;
   for (auto it = protolog_message.sint64_params(); it; ++it) {
-    sint64_params.emplace_back(*it);
+    sint64_params.emplace_back(it->as_sint64());
   }
 
   std::vector<double> double_params;
@@ -134,33 +130,19 @@
         row_id, decoded_message.log_level, decoded_message.group_tag,
         decoded_message.message, stacktrace, location);
   } else {
-    // Viewer config used to decode messages not yet processed for this message.
-    // Delaying decoding...
-    auto* protolog_message_tracker =
-        ProtoLogMessagesTracker::GetOrCreate(context_);
-
-    protolog_message_tracker->TrackMessage(
-        ProtoLogMessagesTracker::TrackedProtoLogMessage{
-            protolog_message.message_id(), std::move(sint64_params),
-            std::move(double_params), std::move(boolean_params),
-            std::move(string_params), stacktrace, row_id, timestamp});
+    // Failed to fully decode the message.
+    // This shouldn't happen since we should have processed all viewer config
+    // messages in the tokenization state, and process the protolog messages
+    // only in the parsing state.
+    context_->storage->IncrementStats(
+        stats::winscope_protolog_message_decoding_failed);
   }
 }
 
-void ProtoLogParser::ParseProtoLogViewerConfig(protozero::ConstBytes blob) {
+void ProtoLogParser::ParseAndAddViewerConfigToMessageDecoder(
+    protozero::ConstBytes blob) {
   protos::pbzero::ProtoLogViewerConfig::Decoder protolog_viewer_config(blob);
 
-  AddViewerConfigToMessageDecoder(protolog_viewer_config);
-
-  for (auto it = protolog_viewer_config.messages(); it; ++it) {
-    protos::pbzero::ProtoLogViewerConfig::MessageData::Decoder message_data(
-        *it);
-    ProcessPendingMessagesWithId(message_data.message_id());
-  }
-}
-
-void ProtoLogParser::AddViewerConfigToMessageDecoder(
-    protos::pbzero::ProtoLogViewerConfig::Decoder& protolog_viewer_config) {
   auto* protolog_message_decoder =
       ProtoLogMessageDecoder::GetOrCreate(context_);
 
@@ -186,38 +168,6 @@
   }
 }
 
-void ProtoLogParser::ProcessPendingMessagesWithId(uint64_t message_id) {
-  auto* protolog_message_decoder =
-      ProtoLogMessageDecoder::GetOrCreate(context_);
-  auto* protolog_message_tracker =
-      ProtoLogMessagesTracker::GetOrCreate(context_);
-
-  auto tracked_messages_opt =
-      protolog_message_tracker->GetTrackedMessagesByMessageId(message_id);
-
-  if (tracked_messages_opt.has_value()) {
-    // There are undecoded messages that can now be docoded to populate the
-    // table.
-    for (const auto& tracked_message : *tracked_messages_opt.value()) {
-      auto message = protolog_message_decoder
-                         ->Decode(tracked_message.message_id,
-                                  tracked_message.sint64_params,
-                                  tracked_message.double_params,
-                                  tracked_message.boolean_params,
-                                  tracked_message.string_params)
-                         .value();
-
-      std::optional<std::string> location = message.location;
-      PopulateReservedRowWithMessage(
-          tracked_message.table_row_id, message.log_level, message.group_tag,
-          message.message, tracked_message.stacktrace, location);
-    }
-
-    // Clear to avoid decoding again
-    protolog_message_tracker->ClearTrackedMessagesForMessageId(message_id);
-  }
-}
-
 void ProtoLogParser::PopulateReservedRowWithMessage(
     tables::ProtoLogTable::Id table_row_id,
     ProtoLogLevel log_level,
diff --git a/src/trace_processor/importers/proto/winscope/protolog_parser.h b/src/trace_processor/importers/proto/winscope/protolog_parser.h
index b7e0e5f..98b71c8 100644
--- a/src/trace_processor/importers/proto/winscope/protolog_parser.h
+++ b/src/trace_processor/importers/proto/winscope/protolog_parser.h
@@ -23,7 +23,6 @@
 
 #include "protos/perfetto/trace/android/protolog.pbzero.h"
 #include "src/trace_processor/importers/proto/winscope/protolog_message_decoder.h"
-#include "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/util/descriptors.h"
 #include "src/trace_processor/util/proto_to_args_parser.h"
@@ -38,12 +37,9 @@
   void ParseProtoLogMessage(PacketSequenceStateGeneration* sequence_state,
                             protozero::ConstBytes,
                             int64_t timestamp);
-  void ParseProtoLogViewerConfig(protozero::ConstBytes);
+  void ParseAndAddViewerConfigToMessageDecoder(protozero::ConstBytes);
 
  private:
-  void AddViewerConfigToMessageDecoder(
-      protos::pbzero::ProtoLogViewerConfig::Decoder& protolog_viewer_config);
-  void ProcessPendingMessagesWithId(uint64_t message_id);
   void PopulateReservedRowWithMessage(tables::ProtoLogTable::Id table_row_id,
                                       ProtoLogLevel level,
                                       std::string& group_tag,
@@ -52,7 +48,6 @@
                                       std::optional<std::string>& location);
 
   TraceProcessorContext* const context_;
-  DescriptorPool pool_;
   util::ProtoToArgsParser args_parser_;
 
   const StringId log_level_debug_string_id_;
diff --git a/src/trace_processor/importers/proto/winscope/shell_transitions_parser.cc b/src/trace_processor/importers/proto/winscope/shell_transitions_parser.cc
index 12618ac..07fef5e 100644
--- a/src/trace_processor/importers/proto/winscope/shell_transitions_parser.cc
+++ b/src/trace_processor/importers/proto/winscope/shell_transitions_parser.cc
@@ -17,21 +17,19 @@
 #include "src/trace_processor/importers/proto/winscope/shell_transitions_parser.h"
 #include "src/trace_processor/importers/proto/winscope/shell_transitions_tracker.h"
 
+#include "perfetto/ext/base/base64.h"
 #include "protos/perfetto/trace/android/shell_transition.pbzero.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/proto/args_parser.h"
-#include "src/trace_processor/importers/proto/winscope/winscope.descriptor.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/winscope_proto_mapping.h"
 
 namespace perfetto {
 namespace trace_processor {
 
 ShellTransitionsParser::ShellTransitionsParser(TraceProcessorContext* context)
-    : context_(context), args_parser_{pool_} {
-  pool_.AddFromFileDescriptorSet(kWinscopeDescriptor.data(),
-                                 kWinscopeDescriptor.size());
-}
+    : context_(context), args_parser_{*context->descriptor_pool_} {}
 
 void ShellTransitionsParser::ParseTransition(protozero::ConstBytes blob) {
   protos::pbzero::ShellTransition::Decoder transition(blob);
@@ -48,10 +46,17 @@
     row.set_ts(transition.dispatch_time_ns());
   }
 
+  auto base64_proto = context_->storage->mutable_string_pool()->InternString(
+      base::StringView(base::Base64Encode(blob.data, blob.size)));
+  row.set_base64_proto(base64_proto);
+  row.set_base64_proto_id(base64_proto.raw_id());
   auto inserter = context_->args_tracker->AddArgsTo(row_id);
   ArgsParser writer(/*timestamp=*/0, inserter, *context_->storage.get());
   base::Status status = args_parser_.ParseMessage(
-      blob, kShellTransitionsProtoName, nullptr /* parse all fields */, writer);
+      blob,
+      *util::winscope_proto_mapping::GetProtoName(
+          tables::WindowManagerShellTransitionsTable::Name()),
+      nullptr /* parse all fields */, writer);
 
   if (!status.ok()) {
     context_->storage->IncrementStats(
@@ -72,6 +77,9 @@
     row.handler_id = mapping.id();
     row.handler_name = context_->storage->InternString(
         base::StringView(mapping.name().ToStdString()));
+    row.base64_proto = context_->storage->mutable_string_pool()->InternString(
+        base::StringView(base::Base64Encode(blob.data, blob.size)));
+    row.base64_proto_id = row.base64_proto.raw_id();
     shell_handlers_table->Insert(row);
   }
 }
diff --git a/src/trace_processor/importers/proto/winscope/shell_transitions_parser.h b/src/trace_processor/importers/proto/winscope/shell_transitions_parser.h
index 44b86d1..3dfe529 100644
--- a/src/trace_processor/importers/proto/winscope/shell_transitions_parser.h
+++ b/src/trace_processor/importers/proto/winscope/shell_transitions_parser.h
@@ -33,11 +33,7 @@
   void ParseHandlerMappings(protozero::ConstBytes);
 
  private:
-  static constexpr auto* kShellTransitionsProtoName =
-      ".perfetto.protos.ShellTransition";
-
   TraceProcessorContext* const context_;
-  DescriptorPool pool_;
   util::ProtoToArgsParser args_parser_;
 };
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/winscope/shell_transitions_tracker.cc b/src/trace_processor/importers/proto/winscope/shell_transitions_tracker.cc
index 6025d91..3526750 100644
--- a/src/trace_processor/importers/proto/winscope/shell_transitions_tracker.cc
+++ b/src/trace_processor/importers/proto/winscope/shell_transitions_tracker.cc
@@ -19,6 +19,7 @@
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/storage/metadata.h"
 #include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/winscope_proto_mapping.h"
 
 namespace perfetto {
 namespace trace_processor {
diff --git a/src/trace_processor/importers/proto/winscope/shell_transitions_tracker.h b/src/trace_processor/importers/proto/winscope/shell_transitions_tracker.h
index 07ef736..5218a7b 100644
--- a/src/trace_processor/importers/proto/winscope/shell_transitions_tracker.h
+++ b/src/trace_processor/importers/proto/winscope/shell_transitions_tracker.h
@@ -20,6 +20,7 @@
 #include "perfetto/trace_processor/basic_types.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/winscope_proto_mapping.h"
 
 namespace perfetto {
 namespace trace_processor {
diff --git a/src/trace_processor/importers/proto/winscope/surfaceflinger_layers_parser.cc b/src/trace_processor/importers/proto/winscope/surfaceflinger_layers_parser.cc
index 20687e4..1975f5d 100644
--- a/src/trace_processor/importers/proto/winscope/surfaceflinger_layers_parser.cc
+++ b/src/trace_processor/importers/proto/winscope/surfaceflinger_layers_parser.cc
@@ -16,21 +16,19 @@
 
 #include "src/trace_processor/importers/proto/winscope/surfaceflinger_layers_parser.h"
 
+#include "perfetto/ext/base/base64.h"
 #include "protos/perfetto/trace/android/surfaceflinger_layers.pbzero.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/proto/args_parser.h"
-#include "src/trace_processor/importers/proto/winscope/winscope.descriptor.h"
 #include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/winscope_proto_mapping.h"
 
 namespace perfetto {
 namespace trace_processor {
 
 SurfaceFlingerLayersParser::SurfaceFlingerLayersParser(
     TraceProcessorContext* context)
-    : context_{context}, args_parser_{pool_} {
-  pool_.AddFromFileDescriptorSet(kWinscopeDescriptor.data(),
-                                 kWinscopeDescriptor.size());
-}
+    : context_{context}, args_parser_{*context->descriptor_pool_} {}
 
 void SurfaceFlingerLayersParser::Parse(int64_t timestamp,
                                        protozero::ConstBytes blob) {
@@ -38,6 +36,10 @@
                                                                 blob.size);
   tables::SurfaceFlingerLayersSnapshotTable::Row snapshot;
   snapshot.ts = timestamp;
+  snapshot.base64_proto =
+      context_->storage->mutable_string_pool()->InternString(
+          base::StringView(base::Base64Encode(blob.data, blob.size)));
+  snapshot.base64_proto_id = snapshot.base64_proto.raw_id();
   auto snapshot_id =
       context_->storage->mutable_surfaceflinger_layers_snapshot_table()
           ->Insert(snapshot)
@@ -45,9 +47,12 @@
 
   auto inserter = context_->args_tracker->AddArgsTo(snapshot_id);
   ArgsParser writer(timestamp, inserter, *context_->storage);
-  base::Status status =
-      args_parser_.ParseMessage(blob, kLayersSnapshotProtoName,
-                                &kLayersSnapshotFieldsToArgsParse, writer);
+  const auto table_name = tables::SurfaceFlingerLayersSnapshotTable::Name();
+  auto allowed_fields =
+      util::winscope_proto_mapping::GetAllowedFields(table_name);
+  base::Status status = args_parser_.ParseMessage(
+      blob, *util::winscope_proto_mapping::GetProtoName(table_name),
+      &allowed_fields.value(), writer);
   if (!status.ok()) {
     context_->storage->IncrementStats(stats::winscope_sf_layers_parse_errors);
   }
@@ -65,14 +70,20 @@
     tables::SurfaceFlingerLayersSnapshotTable::Id snapshot_id) {
   tables::SurfaceFlingerLayerTable::Row layer;
   layer.snapshot_id = snapshot_id;
+  layer.base64_proto = context_->storage->mutable_string_pool()->InternString(
+      base::StringView(base::Base64Encode(blob.data, blob.size)));
+  layer.base64_proto_id = layer.base64_proto.raw_id();
   auto layerId =
       context_->storage->mutable_surfaceflinger_layer_table()->Insert(layer).id;
 
   ArgsTracker tracker(context_);
   auto inserter = tracker.AddArgsTo(layerId);
   ArgsParser writer(timestamp, inserter, *context_->storage);
-  base::Status status = args_parser_.ParseMessage(
-      blob, kLayerProtoName, nullptr /* parse all fields */, writer);
+  base::Status status =
+      args_parser_.ParseMessage(blob,
+                                *util::winscope_proto_mapping::GetProtoName(
+                                    tables::SurfaceFlingerLayerTable::Name()),
+                                nullptr /* parse all fields */, writer);
   if (!status.ok()) {
     context_->storage->IncrementStats(stats::winscope_sf_layers_parse_errors);
   }
diff --git a/src/trace_processor/importers/proto/winscope/surfaceflinger_layers_parser.h b/src/trace_processor/importers/proto/winscope/surfaceflinger_layers_parser.h
index f615a8e..987eb81 100644
--- a/src/trace_processor/importers/proto/winscope/surfaceflinger_layers_parser.h
+++ b/src/trace_processor/importers/proto/winscope/surfaceflinger_layers_parser.h
@@ -33,18 +33,11 @@
   void Parse(int64_t timestamp, protozero::ConstBytes);
 
  private:
-  const std::vector<std::uint32_t> kLayersSnapshotFieldsToArgsParse{1, 2, 4, 5,
-                                                                    6, 7, 8};
-  static constexpr auto* kLayersSnapshotProtoName =
-      ".perfetto.protos.LayersSnapshotProto";
-  static constexpr auto* kLayerProtoName = ".perfetto.protos.LayerProto";
-
   void ParseLayer(int64_t timestamp,
                   protozero::ConstBytes blob,
                   tables::SurfaceFlingerLayersSnapshotTable::Id);
 
   TraceProcessorContext* const context_;
-  DescriptorPool pool_;
   util::ProtoToArgsParser args_parser_;
 };
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/winscope/surfaceflinger_transactions_parser.cc b/src/trace_processor/importers/proto/winscope/surfaceflinger_transactions_parser.cc
index c2c5638..2fa14b6 100644
--- a/src/trace_processor/importers/proto/winscope/surfaceflinger_transactions_parser.cc
+++ b/src/trace_processor/importers/proto/winscope/surfaceflinger_transactions_parser.cc
@@ -16,27 +16,28 @@
 
 #include "src/trace_processor/importers/proto/winscope/surfaceflinger_transactions_parser.h"
 
+#include "perfetto/ext/base/base64.h"
 #include "protos/perfetto/trace/android/surfaceflinger_transactions.pbzero.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/proto/args_parser.h"
-#include "src/trace_processor/importers/proto/winscope/winscope.descriptor.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/winscope_proto_mapping.h"
 
 namespace perfetto {
 namespace trace_processor {
 
 SurfaceFlingerTransactionsParser::SurfaceFlingerTransactionsParser(
     TraceProcessorContext* context)
-    : context_{context}, args_parser_{pool_} {
-  pool_.AddFromFileDescriptorSet(kWinscopeDescriptor.data(),
-                                 kWinscopeDescriptor.size());
-}
+    : context_{context}, args_parser_{*context->descriptor_pool_} {}
 
 void SurfaceFlingerTransactionsParser::Parse(int64_t timestamp,
                                              protozero::ConstBytes blob) {
   tables::SurfaceFlingerTransactionsTable::Row row;
   row.ts = timestamp;
+  row.base64_proto = context_->storage->mutable_string_pool()->InternString(
+      base::StringView(base::Base64Encode(blob.data, blob.size)));
+  row.base64_proto_id = row.base64_proto.raw_id();
   auto rowId = context_->storage->mutable_surfaceflinger_transactions_table()
                    ->Insert(row)
                    .id;
@@ -44,9 +45,11 @@
   ArgsTracker tracker(context_);
   auto inserter = tracker.AddArgsTo(rowId);
   ArgsParser writer(timestamp, inserter, *context_->storage.get());
-  base::Status status =
-      args_parser_.ParseMessage(blob, kTransactionTraceEntryProtoName,
-                                nullptr /* parse all fields */, writer);
+  base::Status status = args_parser_.ParseMessage(
+      blob,
+      *util::winscope_proto_mapping::GetProtoName(
+          tables::SurfaceFlingerTransactionsTable::Name()),
+      nullptr /* parse all fields */, writer);
   if (!status.ok()) {
     context_->storage->IncrementStats(
         stats::winscope_sf_transactions_parse_errors);
diff --git a/src/trace_processor/importers/proto/winscope/surfaceflinger_transactions_parser.h b/src/trace_processor/importers/proto/winscope/surfaceflinger_transactions_parser.h
index f9d45cc..b696408 100644
--- a/src/trace_processor/importers/proto/winscope/surfaceflinger_transactions_parser.h
+++ b/src/trace_processor/importers/proto/winscope/surfaceflinger_transactions_parser.h
@@ -32,11 +32,7 @@
   void Parse(int64_t timestamp, protozero::ConstBytes);
 
  private:
-  static constexpr auto* kTransactionTraceEntryProtoName =
-      ".perfetto.protos.TransactionTraceEntry";
-
   TraceProcessorContext* const context_;
-  DescriptorPool pool_;
   util::ProtoToArgsParser args_parser_;
 };
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/winscope/winscope_module.cc b/src/trace_processor/importers/proto/winscope/winscope_module.cc
index 9946a23..a25a05d 100644
--- a/src/trace_processor/importers/proto/winscope/winscope_module.cc
+++ b/src/trace_processor/importers/proto/winscope/winscope_module.cc
@@ -15,11 +15,13 @@
  */
 
 #include "src/trace_processor/importers/proto/winscope/winscope_module.h"
+#include "perfetto/ext/base/base64.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/proto/args_parser.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/util/winscope_proto_mapping.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -29,12 +31,14 @@
 
 WinscopeModule::WinscopeModule(TraceProcessorContext* context)
     : context_{context},
-      args_parser_{pool_},
+      args_parser_{*context->descriptor_pool_.get()},
       surfaceflinger_layers_parser_(context),
       surfaceflinger_transactions_parser_(context),
       shell_transitions_parser_(context),
       protolog_parser_(context),
       android_input_event_parser_(context) {
+  context->descriptor_pool_->AddFromFileDescriptorSet(
+      kWinscopeDescriptor.data(), kWinscopeDescriptor.size());
   RegisterForField(TracePacket::kSurfaceflingerLayersSnapshotFieldNumber,
                    context);
   RegisterForField(TracePacket::kSurfaceflingerTransactionsFieldNumber,
@@ -44,9 +48,23 @@
   RegisterForField(TracePacket::kProtologMessageFieldNumber, context);
   RegisterForField(TracePacket::kProtologViewerConfigFieldNumber, context);
   RegisterForField(TracePacket::kWinscopeExtensionsFieldNumber, context);
+}
 
-  pool_.AddFromFileDescriptorSet(kWinscopeDescriptor.data(),
-                                 kWinscopeDescriptor.size());
+ModuleResult WinscopeModule::TokenizePacket(
+    const protos::pbzero::TracePacket::Decoder& decoder,
+    TraceBlobView* /*packet*/,
+    int64_t /*packet_timestamp*/,
+    RefPtr<PacketSequenceStateGeneration> /*state*/,
+    uint32_t field_id) {
+
+  switch (field_id) {
+    case TracePacket::kProtologViewerConfigFieldNumber:
+      protolog_parser_.ParseAndAddViewerConfigToMessageDecoder(
+          decoder.protolog_viewer_config());
+      return ModuleResult::Handled();
+  }
+
+  return ModuleResult::Ignored();
 }
 
 void WinscopeModule::ParseTracePacketData(const TracePacket::Decoder& decoder,
@@ -73,10 +91,6 @@
       protolog_parser_.ParseProtoLogMessage(
           data.sequence_state.get(), decoder.protolog_message(), timestamp);
       return;
-    case TracePacket::kProtologViewerConfigFieldNumber:
-      protolog_parser_.ParseProtoLogViewerConfig(
-          decoder.protolog_viewer_config());
-      return;
     case TracePacket::kWinscopeExtensionsFieldNumber:
       ParseWinscopeExtensionsData(decoder.winscope_extensions(), timestamp,
                                   data);
@@ -122,6 +136,9 @@
                                                  protozero::ConstBytes blob) {
   tables::InputMethodClientsTable::Row row;
   row.ts = timestamp;
+  row.base64_proto = context_->storage->mutable_string_pool()->InternString(
+      base::StringView(base::Base64Encode(blob.data, blob.size)));
+  row.base64_proto_id = row.base64_proto.raw_id();
   auto rowId =
       context_->storage->mutable_inputmethod_clients_table()->Insert(row).id;
 
@@ -129,7 +146,9 @@
   auto inserter = tracker.AddArgsTo(rowId);
   ArgsParser writer(timestamp, inserter, *context_->storage.get());
   base::Status status =
-      args_parser_.ParseMessage(blob, kInputMethodClientsProtoName,
+      args_parser_.ParseMessage(blob,
+                                *util::winscope_proto_mapping::GetProtoName(
+                                    tables::InputMethodClientsTable::Name()),
                                 nullptr /* parse all fields */, writer);
   if (!status.ok()) {
     context_->storage->IncrementStats(
@@ -142,6 +161,9 @@
     protozero::ConstBytes blob) {
   tables::InputMethodManagerServiceTable::Row row;
   row.ts = timestamp;
+  row.base64_proto = context_->storage->mutable_string_pool()->InternString(
+      base::StringView(base::Base64Encode(blob.data, blob.size)));
+  row.base64_proto_id = row.base64_proto.raw_id();
   auto rowId = context_->storage->mutable_inputmethod_manager_service_table()
                    ->Insert(row)
                    .id;
@@ -149,9 +171,11 @@
   ArgsTracker tracker(context_);
   auto inserter = tracker.AddArgsTo(rowId);
   ArgsParser writer(timestamp, inserter, *context_->storage.get());
-  base::Status status =
-      args_parser_.ParseMessage(blob, kInputMethodManagerServiceProtoName,
-                                nullptr /* parse all fields */, writer);
+  base::Status status = args_parser_.ParseMessage(
+      blob,
+      *util::winscope_proto_mapping::GetProtoName(
+          tables::InputMethodManagerServiceTable::Name()),
+      nullptr /* parse all fields */, writer);
   if (!status.ok()) {
     context_->storage->IncrementStats(
         stats::winscope_inputmethod_manager_service_parse_errors);
@@ -162,6 +186,9 @@
                                                  protozero::ConstBytes blob) {
   tables::InputMethodServiceTable::Row row;
   row.ts = timestamp;
+  row.base64_proto = context_->storage->mutable_string_pool()->InternString(
+      base::StringView(base::Base64Encode(blob.data, blob.size)));
+  row.base64_proto_id = row.base64_proto.raw_id();
   auto rowId =
       context_->storage->mutable_inputmethod_service_table()->Insert(row).id;
 
@@ -169,7 +196,9 @@
   auto inserter = tracker.AddArgsTo(rowId);
   ArgsParser writer(timestamp, inserter, *context_->storage.get());
   base::Status status =
-      args_parser_.ParseMessage(blob, kInputMethodServiceProtoName,
+      args_parser_.ParseMessage(blob,
+                                *util::winscope_proto_mapping::GetProtoName(
+                                    tables::InputMethodServiceTable::Name()),
                                 nullptr /* parse all fields */, writer);
   if (!status.ok()) {
     context_->storage->IncrementStats(
@@ -183,14 +212,20 @@
     PacketSequenceStateGeneration* sequence_state) {
   tables::ViewCaptureTable::Row row;
   row.ts = timestamp;
+  row.base64_proto = context_->storage->mutable_string_pool()->InternString(
+      base::StringView(base::Base64Encode(blob.data, blob.size)));
+  row.base64_proto_id = row.base64_proto.raw_id();
   auto rowId = context_->storage->mutable_viewcapture_table()->Insert(row).id;
 
   ArgsTracker tracker(context_);
   auto inserter = tracker.AddArgsTo(rowId);
   ViewCaptureArgsParser writer(timestamp, inserter, *context_->storage.get(),
                                sequence_state);
-  base::Status status = args_parser_.ParseMessage(
-      blob, kViewCaptureProtoName, nullptr /* parse all fields */, writer);
+  base::Status status =
+      args_parser_.ParseMessage(blob,
+                                *util::winscope_proto_mapping::GetProtoName(
+                                    tables::ViewCaptureTable::Name()),
+                                nullptr /* parse all fields */, writer);
   if (!status.ok()) {
     context_->storage->IncrementStats(stats::winscope_viewcapture_parse_errors);
   }
@@ -200,13 +235,19 @@
                                             protozero::ConstBytes blob) {
   tables::WindowManagerTable::Row row;
   row.ts = timestamp;
+  row.base64_proto = context_->storage->mutable_string_pool()->InternString(
+      base::StringView(base::Base64Encode(blob.data, blob.size)));
+  row.base64_proto_id = row.base64_proto.raw_id();
   auto rowId = context_->storage->mutable_windowmanager_table()->Insert(row).id;
 
   ArgsTracker tracker(context_);
   auto inserter = tracker.AddArgsTo(rowId);
   ArgsParser writer(timestamp, inserter, *context_->storage.get());
-  base::Status status = args_parser_.ParseMessage(
-      blob, kWindowManagerProtoName, nullptr /* parse all fields */, writer);
+  base::Status status =
+      args_parser_.ParseMessage(blob,
+                                *util::winscope_proto_mapping::GetProtoName(
+                                    tables::WindowManagerTable::Name()),
+                                nullptr /* parse all fields */, writer);
   if (!status.ok()) {
     context_->storage->IncrementStats(
         stats::winscope_windowmanager_parse_errors);
diff --git a/src/trace_processor/importers/proto/winscope/winscope_module.h b/src/trace_processor/importers/proto/winscope/winscope_module.h
index 649e329..b6e876f 100644
--- a/src/trace_processor/importers/proto/winscope/winscope_module.h
+++ b/src/trace_processor/importers/proto/winscope/winscope_module.h
@@ -36,6 +36,13 @@
  public:
   explicit WinscopeModule(TraceProcessorContext* context);
 
+  ModuleResult TokenizePacket(
+    const protos::pbzero::TracePacket::Decoder& decoder,
+    TraceBlobView* packet,
+    int64_t packet_timestamp,
+    RefPtr<PacketSequenceStateGeneration> state,
+    uint32_t field_id) override;
+
   void ParseTracePacketData(const protos::pbzero::TracePacket::Decoder&,
                             int64_t ts,
                             const TracePacketData&,
@@ -56,18 +63,7 @@
                             PacketSequenceStateGeneration* sequence_state);
   void ParseWindowManagerData(int64_t timestamp, protozero::ConstBytes blob);
 
-  static constexpr auto* kInputMethodClientsProtoName =
-      ".perfetto.protos.InputMethodClientsTraceProto";
-  static constexpr auto* kInputMethodManagerServiceProtoName =
-      ".perfetto.protos.InputMethodManagerServiceTraceProto";
-  static constexpr auto* kInputMethodServiceProtoName =
-      ".perfetto.protos.InputMethodServiceTraceProto";
-  static constexpr auto* kViewCaptureProtoName = ".perfetto.protos.ViewCapture";
-  static constexpr auto* kWindowManagerProtoName =
-      ".perfetto.protos.WindowManagerTraceEntry";
-
   TraceProcessorContext* const context_;
-  DescriptorPool pool_;
   util::ProtoToArgsParser args_parser_;
 
   SurfaceFlingerLayersParser surfaceflinger_layers_parser_;
diff --git a/src/trace_processor/importers/syscalls/syscall_tracker_unittest.cc b/src/trace_processor/importers/syscalls/syscall_tracker_unittest.cc
index 260d60d..9a3ac01 100644
--- a/src/trace_processor/importers/syscalls/syscall_tracker_unittest.cc
+++ b/src/trace_processor/importers/syscalls/syscall_tracker_unittest.cc
@@ -16,6 +16,7 @@
 
 #include "src/trace_processor/importers/syscalls/syscall_tracker.h"
 
+#include "src/trace_processor/importers/common/global_args_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "test/gtest_and_gmock.h"
 
@@ -64,6 +65,8 @@
  public:
   SyscallTrackerTest() {
     context.storage.reset(new TraceStorage());
+    context.global_args_tracker.reset(
+        new GlobalArgsTracker(context.storage.get()));
     track_tracker = new TrackTracker(&context);
     context.track_tracker.reset(track_tracker);
     slice_tracker = new MockSliceTracker(&context);
diff --git a/src/trace_processor/importers/systrace/systrace_line.h b/src/trace_processor/importers/systrace/systrace_line.h
index d125d0e..a5aa427 100644
--- a/src/trace_processor/importers/systrace/systrace_line.h
+++ b/src/trace_processor/importers/systrace/systrace_line.h
@@ -17,11 +17,10 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_SYSTRACE_SYSTRACE_LINE_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_SYSTRACE_SYSTRACE_LINE_H_
 
-#include <cinttypes>
+#include <cstdint>
 #include <string>
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 struct alignas(8) SystraceLine {
   int64_t ts;
@@ -35,7 +34,6 @@
   std::string args_str;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_SYSTRACE_SYSTRACE_LINE_H_
diff --git a/src/trace_processor/importers/systrace/systrace_line_parser.cc b/src/trace_processor/importers/systrace/systrace_line_parser.cc
index 7bfb844..2c06136 100644
--- a/src/trace_processor/importers/systrace/systrace_line_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_line_parser.cc
@@ -42,8 +42,6 @@
       rss_stat_tracker_(context_),
       sched_wakeup_name_id_(ctx->storage->InternString("sched_wakeup")),
       sched_waking_name_id_(ctx->storage->InternString("sched_waking")),
-      cpufreq_name_id_(ctx->storage->InternString("cpufreq")),
-      cpuidle_name_id_(ctx->storage->InternString("cpuidle")),
       workqueue_name_id_(ctx->storage->InternString("workqueue")),
       sched_blocked_reason_id_(
           ctx->storage->InternString("sched_blocked_reason")),
@@ -139,7 +137,8 @@
     }
 
     TrackId track = context_->track_tracker->InternCpuCounterTrack(
-        cpufreq_name_id_, event_cpu.value());
+        tracks::cpu_frequency, event_cpu.value(),
+        TrackTracker::LegacyCharArrayName{"cpufreq"});
     context_->event_tracker->PushCounter(line.ts, new_state.value(), track);
   } else if (line.event_name == "cpu_idle") {
     std::optional<uint32_t> event_cpu = base::StringToUInt32(args["cpu_id"]);
@@ -152,7 +151,7 @@
     }
 
     TrackId track = context_->track_tracker->InternCpuCounterTrack(
-        cpuidle_name_id_, event_cpu.value());
+        tracks::cpu_idle, event_cpu.value());
     context_->event_tracker->PushCounter(line.ts, new_state.value(), track);
   } else if (line.event_name == "binder_transaction") {
     auto id = base::StringToInt32(args["transaction"]);
@@ -230,7 +229,7 @@
     std::string clock_name_str = args["name"] + subtitle;
     StringId clock_name =
         context_->storage->InternString(base::StringView(clock_name_str));
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kClockFrequency, clock_name);
     context_->event_tracker->PushCounter(line.ts, rate.value(), track);
   } else if (line.event_name == "workqueue_execute_start") {
@@ -246,7 +245,7 @@
     std::string thermal_zone = args["thermal_zone"] + " Temperature";
     StringId track_name =
         context_->storage->InternString(base::StringView(thermal_zone));
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kThermals, track_name);
     auto temp = base::StringToInt32(args["temp"]);
     if (!temp.has_value()) {
@@ -257,7 +256,7 @@
     std::string type = args["type"] + " Cooling Device";
     StringId track_name =
         context_->storage->InternString(base::StringView(type));
-    TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+    TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
         TrackTracker::Group::kThermals, track_name);
     auto target = base::StringToDouble(args["target"]);
     if (!target.has_value()) {
diff --git a/src/trace_processor/importers/systrace/systrace_line_parser.h b/src/trace_processor/importers/systrace/systrace_line_parser.h
index 017faba..4ff2b83 100644
--- a/src/trace_processor/importers/systrace/systrace_line_parser.h
+++ b/src/trace_processor/importers/systrace/systrace_line_parser.h
@@ -39,8 +39,6 @@
 
   const StringId sched_wakeup_name_id_ = kNullStringId;
   const StringId sched_waking_name_id_ = kNullStringId;
-  const StringId cpufreq_name_id_ = kNullStringId;
-  const StringId cpuidle_name_id_ = kNullStringId;
   const StringId workqueue_name_id_ = kNullStringId;
   const StringId sched_blocked_reason_id_ = kNullStringId;
   const StringId io_wait_id_ = kNullStringId;
diff --git a/src/trace_processor/importers/systrace/systrace_parser.cc b/src/trace_processor/importers/systrace/systrace_parser.cc
index e00322b..c88f4c5 100644
--- a/src/trace_processor/importers/systrace/systrace_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_parser.cc
@@ -24,6 +24,7 @@
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
+#include "src/trace_processor/importers/common/tracks.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
 namespace perfetto {
@@ -270,15 +271,15 @@
         if (killed_pid != 0) {
           UniquePid killed_upid =
               context_->process_tracker->GetOrCreateProcess(killed_pid);
-          TrackId track =
-              context_->track_tracker->InternProcessTrack(killed_upid);
+          TrackId track = context_->track_tracker->InternProcessTrack(
+              tracks::android_lmk, killed_upid);
           context_->slice_tracker->Scoped(ts, track, kNullStringId, lmk_id_, 0);
         }
         // TODO(lalitm): we should not add LMK events to the counters table
         // once the UI has support for displaying instants.
       } else if (point.name == "ScreenState") {
         // Promote ScreenState to its own top level counter.
-        TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+        TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
             TrackTracker::Group::kDeviceState, screen_state_id_);
         context_->event_tracker->PushCounter(
             ts, static_cast<double>(point.int_value), track);
@@ -286,7 +287,7 @@
       } else if (point.name.StartsWith("battery_stats.")) {
         // Promote battery_stats conters to global tracks.
         StringId name_id = context_->storage->InternString(point.name);
-        TrackId track = context_->track_tracker->InternGlobalCounterTrack(
+        TrackId track = context_->track_tracker->LegacyInternGlobalCounterTrack(
             TrackTracker::Group::kPower, name_id);
         context_->event_tracker->PushCounter(
             ts, static_cast<double>(point.int_value), track);
@@ -300,7 +301,8 @@
       UniquePid upid =
           context_->process_tracker->GetOrCreateProcess(point.tgid);
       TrackId track_id =
-          context_->track_tracker->InternProcessCounterTrack(name_id, upid);
+          context_->track_tracker->LegacyInternProcessCounterTrack(name_id,
+                                                                   upid);
       context_->event_tracker->PushCounter(
           ts, static_cast<double>(point.int_value), track_id);
     }
@@ -326,12 +328,14 @@
     UniquePid killed_upid =
         context_->process_tracker->GetOrCreateProcess(*killed_pid);
     // Add the oom score entry
-    TrackId counter_track = context_->track_tracker->InternProcessCounterTrack(
-        oom_score_adj_id_, killed_upid);
+    TrackId counter_track =
+        context_->track_tracker->LegacyInternProcessCounterTrack(
+            oom_score_adj_id_, killed_upid);
     context_->event_tracker->PushCounter(ts, *oom_score_adj, counter_track);
 
     // Add mem.lmk instant event for consistency with other methods.
-    TrackId track = context_->track_tracker->InternProcessTrack(killed_upid);
+    TrackId track = context_->track_tracker->InternProcessTrack(
+        tracks::android_lmk, killed_upid);
     context_->slice_tracker->Scoped(ts, track, kNullStringId, lmk_id_, 0);
   }
 }
diff --git a/src/trace_processor/importers/zip/BUILD.gn b/src/trace_processor/importers/zip/BUILD.gn
deleted file mode 100644
index 0262e02..0000000
--- a/src/trace_processor/importers/zip/BUILD.gn
+++ /dev/null
@@ -1,32 +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.
-
-source_set("full") {
-  sources = [
-    "zip_trace_reader.cc",
-    "zip_trace_reader.h",
-  ]
-  deps = [
-    "../../../../gn:default_deps",
-    "../../../../include/perfetto/ext/base:base",
-    "../../../trace_processor:storage_minimal",
-    "../../types",
-    "../../util:trace_type",
-    "../../util:util",
-    "../../util:zip_reader",
-    "../android_bugreport",
-    "../common",
-    "../proto:minimal",
-  ]
-}
diff --git a/src/trace_processor/importers/zip/zip_trace_reader.cc b/src/trace_processor/importers/zip/zip_trace_reader.cc
deleted file mode 100644
index 732559d..0000000
--- a/src/trace_processor/importers/zip/zip_trace_reader.cc
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/importers/zip/zip_trace_reader.h"
-
-#include <algorithm>
-#include <cstdint>
-#include <cstring>
-#include <memory>
-#include <string>
-#include <tuple>
-#include <utility>
-#include <vector>
-
-#include "perfetto/base/logging.h"
-#include "perfetto/base/status.h"
-#include "perfetto/ext/base/status_or.h"
-#include "perfetto/trace_processor/trace_blob.h"
-#include "perfetto/trace_processor/trace_blob_view.h"
-#include "src/trace_processor/forwarding_trace_parser.h"
-#include "src/trace_processor/importers/android_bugreport/android_bugreport_reader.h"
-#include "src/trace_processor/importers/common/trace_file_tracker.h"
-#include "src/trace_processor/types/trace_processor_context.h"
-#include "src/trace_processor/util/status_macros.h"
-#include "src/trace_processor/util/trace_type.h"
-#include "src/trace_processor/util/zip_reader.h"
-
-namespace perfetto::trace_processor {
-
-ZipTraceReader::ZipTraceReader(TraceProcessorContext* context)
-    : context_(context) {}
-ZipTraceReader::~ZipTraceReader() = default;
-
-bool ZipTraceReader::Entry::operator<(const Entry& rhs) const {
-  // Traces with symbols should be the last ones to be read.
-  // TODO(carlscab): Proto traces with just ModuleSymbols packets should be an
-  // exception. We actually need those are the very end (once whe have all the
-  // Frames). Alternatively we could build a map address -> symbol during
-  // tokenization and use this during parsing to resolve symbols.
-  if (trace_type == kSymbolsTraceType) {
-    return false;
-  }
-  if (rhs.trace_type == kSymbolsTraceType) {
-    return true;
-  }
-
-  // Proto traces should always parsed first as they might contains clock sync
-  // data needed to correctly parse other traces.
-  if (rhs.trace_type == TraceType::kProtoTraceType) {
-    return false;
-  }
-  if (trace_type == TraceType::kProtoTraceType) {
-    return true;
-  }
-
-  if (rhs.trace_type == TraceType::kGzipTraceType) {
-    return false;
-  }
-  if (trace_type == TraceType::kGzipTraceType) {
-    return true;
-  }
-
-  return std::tie(name, index) < std::tie(rhs.name, rhs.index);
-}
-
-base::Status ZipTraceReader::Parse(TraceBlobView blob) {
-  return zip_reader_.Parse(std::move(blob));
-}
-
-base::Status ZipTraceReader::NotifyEndOfFile() {
-  std::vector<util::ZipFile> files = zip_reader_.TakeFiles();
-
-  // Android bug reports are ZIP files and its files do not get handled
-  // separately.
-  if (AndroidBugreportReader::IsAndroidBugReport(files)) {
-    return AndroidBugreportReader::Parse(context_, std::move(files));
-  }
-
-  ASSIGN_OR_RETURN(std::vector<Entry> entries,
-                   ExtractEntries(std::move(files)));
-  std::sort(entries.begin(), entries.end());
-
-  for (Entry& e : entries) {
-    ScopedActiveTraceFile trace_file =
-        context_->trace_file_tracker->StartNewFile(e.name, e.trace_type,
-                                                   e.uncompressed_data.size());
-
-    auto chunk_reader = std::make_unique<ForwardingTraceParser>(context_);
-    auto& parser = *chunk_reader;
-    context_->chunk_readers.push_back(std::move(chunk_reader));
-
-    RETURN_IF_ERROR(parser.Parse(std::move(e.uncompressed_data)));
-    RETURN_IF_ERROR(parser.NotifyEndOfFile());
-
-    // Make sure the ForwardingTraceParser determined the same trace type as we
-    // did.
-    PERFETTO_CHECK(parser.trace_type() == e.trace_type);
-  }
-  return base::OkStatus();
-}
-
-base::StatusOr<std::vector<ZipTraceReader::Entry>>
-ZipTraceReader::ExtractEntries(std::vector<util::ZipFile> files) {
-  // TODO(carlsacab): There is a lot of unnecessary copying going on here.
-  // ZipTraceReader can directly parse the ZIP file and given that we know the
-  // decompressed size we could directly decompress into TraceBlob chunks and
-  // send them to the tokenizer.
-  std::vector<Entry> entries;
-  std::vector<uint8_t> buffer;
-  for (size_t i = 0; i < files.size(); ++i) {
-    const util::ZipFile& zip_file = files[i];
-    Entry entry;
-    entry.name = zip_file.name();
-    entry.index = i;
-    RETURN_IF_ERROR(files[i].Decompress(&buffer));
-    entry.uncompressed_data =
-        TraceBlobView(TraceBlob::CopyFrom(buffer.data(), buffer.size()));
-    entry.trace_type = GuessTraceType(entry.uncompressed_data.data(),
-                                      entry.uncompressed_data.size());
-    entries.push_back(std::move(entry));
-  }
-  return std::move(entries);
-}
-
-}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/zip/zip_trace_reader.h b/src/trace_processor/importers/zip/zip_trace_reader.h
deleted file mode 100644
index b6620ae..0000000
--- a/src/trace_processor/importers/zip/zip_trace_reader.h
+++ /dev/null
@@ -1,76 +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.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_ZIP_ZIP_TRACE_READER_H_
-#define SRC_TRACE_PROCESSOR_IMPORTERS_ZIP_ZIP_TRACE_READER_H_
-
-#include <cstddef>
-#include <memory>
-#include <string>
-#include <vector>
-
-#include "perfetto/base/status.h"
-#include "perfetto/ext/base/status_or.h"
-#include "perfetto/trace_processor/trace_blob_view.h"
-#include "src/trace_processor/importers/common/chunked_trace_reader.h"
-#include "src/trace_processor/util/trace_type.h"
-#include "src/trace_processor/util/zip_reader.h"
-
-namespace perfetto::trace_processor {
-
-class ForwardingTraceParser;
-class TraceProcessorContext;
-
-// Forwards files contained in a ZIP to the appropiate ChunkedTraceReader. It is
-// guaranteed that proto traces will be parsed first.
-class ZipTraceReader : public ChunkedTraceReader {
- public:
-  explicit ZipTraceReader(TraceProcessorContext* context);
-  ~ZipTraceReader() override;
-
-  // ChunkedTraceReader implementation
-  base::Status Parse(TraceBlobView) override;
-  base::Status NotifyEndOfFile() override;
-
- private:
-  // Represents a file in the ZIP file. Used to sort them before sending the
-  // files one by one to a `ForwardingTraceParser` instance.
-  struct Entry {
-    // File name. Used to break ties.
-    std::string name;
-    // Position in the zip file. Used to break ties.
-    size_t index;
-    // Trace type. This is the main attribute traces are ordered by. Proto
-    // traces are always parsed first as they might contains clock sync
-    // data needed to correctly parse other traces.
-    TraceType trace_type;
-    TraceBlobView uncompressed_data;
-    // Comparator used to determine the order in which files in the ZIP will be
-    // read.
-    bool operator<(const Entry& rhs) const;
-  };
-
-  static base::StatusOr<std::vector<Entry>> ExtractEntries(
-      std::vector<util::ZipFile> files);
-  base::Status ParseEntry(Entry entry);
-
-  TraceProcessorContext* const context_;
-  util::ZipReader zip_reader_;
-};
-
-}  // namespace perfetto::trace_processor
-
-#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_ZIP_ZIP_TRACE_READER_H_
diff --git a/src/trace_processor/metrics/metrics_unittest.cc b/src/trace_processor/metrics/metrics_unittest.cc
index 4779251..7b5a050 100644
--- a/src/trace_processor/metrics/metrics_unittest.cc
+++ b/src/trace_processor/metrics/metrics_unittest.cc
@@ -88,9 +88,9 @@
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
                              ProtoDescriptor::Type::kMessage, std::nullopt);
-  descriptor.AddField(FieldDescriptor("int_value", 1,
-                                      FieldDescriptorProto::TYPE_INT64, "",
-                                      std::vector<uint8_t>(), false, false));
+  descriptor.AddField(
+      FieldDescriptor("int_value", 1, FieldDescriptorProto::TYPE_INT64, "",
+                      std::vector<uint8_t>(), std::nullopt, false, false));
 
   ProtoBuilder builder(&pool, &descriptor);
   ASSERT_OK(builder.AppendSqlValue("int_value", SqlValue::Long(12345)));
@@ -112,9 +112,9 @@
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
                              ProtoDescriptor::Type::kMessage, std::nullopt);
-  descriptor.AddField(FieldDescriptor("double_value", 1,
-                                      FieldDescriptorProto::TYPE_DOUBLE, "",
-                                      std::vector<uint8_t>(), false, false));
+  descriptor.AddField(
+      FieldDescriptor("double_value", 1, FieldDescriptorProto::TYPE_DOUBLE, "",
+                      std::vector<uint8_t>(), std::nullopt, false, false));
 
   ProtoBuilder builder(&pool, &descriptor);
   ASSERT_OK(builder.AppendSqlValue("double_value", SqlValue::Double(1.2345)));
@@ -136,9 +136,9 @@
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
                              ProtoDescriptor::Type::kMessage, std::nullopt);
-  descriptor.AddField(FieldDescriptor("string_value", 1,
-                                      FieldDescriptorProto::TYPE_STRING, "",
-                                      std::vector<uint8_t>(), false, false));
+  descriptor.AddField(
+      FieldDescriptor("string_value", 1, FieldDescriptorProto::TYPE_STRING, "",
+                      std::vector<uint8_t>(), std::nullopt, false, false));
 
   ProtoBuilder builder(&pool, &descriptor);
   ASSERT_OK(
@@ -164,9 +164,9 @@
   ProtoDescriptor nested("file.proto", ".perfetto.protos",
                          ".perfetto.protos.TestProto.NestedProto",
                          ProtoDescriptor::Type::kMessage, std::nullopt);
-  nested.AddField(FieldDescriptor("nested_int_value", 1,
-                                  FieldDescriptorProto::TYPE_INT64, "",
-                                  std::vector<uint8_t>(), false, false));
+  nested.AddField(
+      FieldDescriptor("nested_int_value", 1, FieldDescriptorProto::TYPE_INT64,
+                      "", std::vector<uint8_t>(), std::nullopt, false, false));
 
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
@@ -174,7 +174,7 @@
   auto field =
       FieldDescriptor("nested_value", 1, FieldDescriptorProto::TYPE_MESSAGE,
                       ".perfetto.protos.TestProto.NestedProto",
-                      std::vector<uint8_t>(), false, false);
+                      std::vector<uint8_t>(), std::nullopt, false, false);
   field.set_resolved_type_name(".perfetto.protos.TestProto.NestedProto");
   descriptor.AddField(field);
 
@@ -214,9 +214,9 @@
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
                              ProtoDescriptor::Type::kMessage, std::nullopt);
-  descriptor.AddField(FieldDescriptor("rep_int_value", 1,
-                                      FieldDescriptorProto::TYPE_INT64, "",
-                                      std::vector<uint8_t>(), true, false));
+  descriptor.AddField(
+      FieldDescriptor("rep_int_value", 1, FieldDescriptorProto::TYPE_INT64, "",
+                      std::vector<uint8_t>(), std::nullopt, true, false));
 
   ASSERT_THAT(RepeatedFieldBuilder().SerializeToProtoBuilderResult(),
               IsEmpty());
@@ -241,9 +241,9 @@
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
                              ProtoDescriptor::Type::kMessage, std::nullopt);
-  descriptor.AddField(FieldDescriptor("rep_int_value", 1,
-                                      FieldDescriptorProto::TYPE_INT64, "",
-                                      std::vector<uint8_t>(), true, false));
+  descriptor.AddField(
+      FieldDescriptor("rep_int_value", 1, FieldDescriptorProto::TYPE_INT64, "",
+                      std::vector<uint8_t>(), std::nullopt, true, false));
 
   RepeatedFieldBuilder rep_builder;
   rep_builder.AddSqlValue(SqlValue::Long(1234));
@@ -289,7 +289,8 @@
                              ProtoDescriptor::Type::kMessage, std::nullopt);
   FieldDescriptor enum_field("enum_value", 1, FieldDescriptorProto::TYPE_ENUM,
                              ".perfetto.protos.TestEnum",
-                             std::vector<uint8_t>(), false, false);
+                             std::vector<uint8_t>(), std::nullopt, false,
+                             false);
   enum_field.set_resolved_type_name(".perfetto.protos.TestEnum");
   descriptor.AddField(enum_field);
   pool.AddProtoDescriptorForTesting(descriptor);
diff --git a/src/trace_processor/metrics/sql/android/BUILD.gn b/src/trace_processor/metrics/sql/android/BUILD.gn
index 4cacdbf..ae6fb83 100644
--- a/src/trace_processor/metrics/sql/android/BUILD.gn
+++ b/src/trace_processor/metrics/sql/android/BUILD.gn
@@ -59,7 +59,6 @@
     "android_multiuser_populator.sql",
     "android_netperf.sql",
     "android_oom_adjuster.sql",
-    "android_other_traces.sql",
     "android_package_list.sql",
     "android_powrails.sql",
     "android_proxy_power.sql",
@@ -70,7 +69,6 @@
     "android_sysui_notifications_blocking_calls_metric.sql",
     "android_task_names.sql",
     "android_trace_quality.sql",
-    "android_trusty_workqueues.sql",
     "codec_metrics.sql",
     "composer_execution.sql",
     "composition_layers.sql",
@@ -86,7 +84,6 @@
     "jank/cujs_boundaries.sql",
     "jank/frames.sql",
     "jank/internal/counters.sql",
-    "jank/internal/derived_events.sql",
     "jank/internal/query_base.sql",
     "jank/internal/query_frame_slice.sql",
     "jank/params.sql",
@@ -137,13 +134,16 @@
     "startup/mcycles_per_launch.sql",
     "startup/slice_functions.sql",
     "startup/slow_start_reasons.sql",
+    "startup/slow_start_thresholds.sql",
     "startup/system_state.sql",
     "startup/thread_state_breakdown.sql",
     "sysui_notif_shade_list_builder_metric.sql",
     "sysui_notif_shade_list_builder_slices.sql",
     "sysui_update_notif_on_ui_mode_changed_metric.sql",
     "unsymbolized_frames.sql",
-    "wattson_app_startup.sql",
+    "wattson_app_startup_rails.sql",
+    "wattson_markers_rails.sql",
+    "wattson_markers_threads.sql",
     "wattson_rail_relations.sql",
     "wattson_tasks_attribution.sql",
     "wattson_trace_rails.sql",
diff --git a/src/trace_processor/metrics/sql/android/android_batt.sql b/src/trace_processor/metrics/sql/android/android_batt.sql
index a6cc552..0e12c1d 100644
--- a/src/trace_processor/metrics/sql/android/android_batt.sql
+++ b/src/trace_processor/metrics/sql/android/android_batt.sql
@@ -71,6 +71,43 @@
 CREATE VIRTUAL TABLE screen_state_span_with_suspend
 USING span_join(screen_state_span, suspend_slice_);
 
+DROP TABLE IF EXISTS power_mw_intervals;
+CREATE PERFETTO TABLE power_mw_intervals AS
+WITH power_mw_counter AS (
+  SELECT counter.id, ts, track_id, value
+  FROM counter
+  JOIN counter_track ON counter_track.id = counter.track_id
+  WHERE name = 'batt.power_mw'
+)
+SELECT * FROM counter_leading_intervals!(power_mw_counter);
+
+DROP TABLE IF EXISTS energy_usage_estimate;
+CREATE PERFETTO TABLE energy_usage_estimate AS
+with energy_counters as (
+select
+  ts,
+  CASE
+    WHEN energy_counter_uwh IS NOT NULL THEN energy_counter_uwh
+    ELSE charge_uah *  voltage_uv / 1e12 END as energy
+ from android_battery_charge
+), start_energy as (
+  select
+  min(ts),
+  energy
+  from energy_counters
+), end_energy as (
+  select
+  max(ts),
+  energy
+  from energy_counters
+)
+select
+  -- If the battery is discharging, the start value will be greater than the end
+  -- and the estimate will report a positive value.
+  -- Battery energy is in watt hours, so multiply by 3600 to convert to joules.
+  (s.energy - e.energy) * 3600 as estimate
+from start_energy s, end_energy e;
+
 DROP VIEW IF EXISTS android_batt_output;
 CREATE PERFETTO VIEW android_batt_output AS
 SELECT AndroidBatteryMetric(
@@ -103,7 +140,11 @@
       'sleep_screen_doze_ns',
       SUM(CASE WHEN state = 3.0 AND tbl = 'sleep' THEN dur ELSE 0 END),
       'total_wakelock_ns',
-      (SELECT SUM(ts_end - ts) FROM android_batt_wakelocks_merged)
+      (SELECT SUM(ts_end - ts) FROM android_batt_wakelocks_merged),
+      'avg_power_mw',
+      (SELECT SUM(value * dur) / SUM(dur) FROM power_mw_intervals),
+      'energy_usage_estimate',
+      (select estimate FROM energy_usage_estimate)
       ))
     FROM (
       SELECT dur, value AS state, 'total' AS tbl
diff --git a/src/trace_processor/metrics/sql/android/android_camera.sql b/src/trace_processor/metrics/sql/android/android_camera.sql
index a893cf6..1d54753 100644
--- a/src/trace_processor/metrics/sql/android/android_camera.sql
+++ b/src/trace_processor/metrics/sql/android/android_camera.sql
@@ -101,15 +101,6 @@
   SUM(rss_and_dma_val * dur / 1e3) / SUM(dur / 1e3) AS avg_value
 FROM rss_and_dma_all_camera_span;
 
-DROP VIEW IF EXISTS android_camera_event;
-CREATE PERFETTO VIEW android_camera_event AS
-SELECT
-  'counter' AS track_type,
-  'Camera Memory' AS track_name,
-  ts,
-  rss_and_dma_val AS value
-FROM rss_and_dma_all_camera_span;
-
 DROP VIEW IF EXISTS android_camera_output;
 CREATE PERFETTO VIEW android_camera_output AS
 SELECT
diff --git a/src/trace_processor/metrics/sql/android/android_fastrpc.sql b/src/trace_processor/metrics/sql/android/android_fastrpc.sql
index 6d174de..94523ec 100644
--- a/src/trace_processor/metrics/sql/android/android_fastrpc.sql
+++ b/src/trace_processor/metrics/sql/android/android_fastrpc.sql
@@ -43,20 +43,6 @@
 FROM fastrpc_raw_allocs
 GROUP BY 1;
 
--- We need to group by ts here as we can have two events from
--- different processes occurring at the same timestamp. We take the
--- max as this will take both allocations into account at that
--- timestamp.
-DROP VIEW IF EXISTS android_fastrpc_event;
-CREATE PERFETTO VIEW android_fastrpc_event AS
-SELECT
-  'counter' AS track_type,
-  printf('fastrpc allocations (subsystem: %s)', subsystem_name) AS track_name,
-  ts,
-  MAX(value) AS value
-FROM fastrpc_raw_allocs
-GROUP BY 1, 2, 3;
-
 DROP VIEW IF EXISTS android_fastrpc_output;
 CREATE PERFETTO VIEW android_fastrpc_output AS
 SELECT AndroidFastrpcMetric(
diff --git a/src/trace_processor/metrics/sql/android/android_ion.sql b/src/trace_processor/metrics/sql/android/android_ion.sql
index 1071eef..37ccbf3 100644
--- a/src/trace_processor/metrics/sql/android/android_ion.sql
+++ b/src/trace_processor/metrics/sql/android/android_ion.sql
@@ -65,20 +65,6 @@
 FROM ion_raw_allocs
 GROUP BY 1;
 
--- We need to group by ts here as we can have two ion events from
--- different processes occurring at the same timestamp. We take the
--- max as this will take both allocations into account at that
--- timestamp.
-DROP VIEW IF EXISTS android_ion_event;
-CREATE PERFETTO VIEW android_ion_event AS
-SELECT
-  'counter' AS track_type,
-  printf('ION allocations (heap: %s)', heap_name) AS track_name,
-  ts,
-  MAX(value) AS value
-FROM ion_raw_allocs
-GROUP BY 1, 2, 3;
-
 DROP VIEW IF EXISTS android_ion_output;
 CREATE PERFETTO VIEW android_ion_output AS
 SELECT AndroidIonMetric(
diff --git a/src/trace_processor/metrics/sql/android/android_jank_cuj.sql b/src/trace_processor/metrics/sql/android/android_jank_cuj.sql
index 39baf04..09b84ce 100644
--- a/src/trace_processor/metrics/sql/android/android_jank_cuj.sql
+++ b/src/trace_processor/metrics/sql/android/android_jank_cuj.sql
@@ -75,13 +75,6 @@
 -- The same numbers are also reported by FrameTracker to statsd.
 SELECT RUN_METRIC('android/jank/internal/counters.sql');
 
--- Creates derived events to visualize a few of the created tables.
--- Used only for debugging so by default not used and not displayed in the UI.
--- See https://perfetto.dev/docs/contributing/common-tasks#adding-new-derived-events
--- for instructions on how to add these events to the UI.
-SELECT RUN_METRIC('android/jank/internal/derived_events.sql');
-
-
 DROP VIEW IF EXISTS android_jank_cuj_output;
 CREATE PERFETTO VIEW android_jank_cuj_output AS
 SELECT
diff --git a/src/trace_processor/metrics/sql/android/android_lmk.sql b/src/trace_processor/metrics/sql/android/android_lmk.sql
index ae49a0a..3cd9efd 100644
--- a/src/trace_processor/metrics/sql/android/android_lmk.sql
+++ b/src/trace_processor/metrics/sql/android/android_lmk.sql
@@ -37,46 +37,6 @@
       AND raw_events.ts < oom_scores.ts + oom_scores.dur)
 ORDER BY 1;
 
-DROP VIEW IF EXISTS android_lmk_event;
-CREATE PERFETTO VIEW android_lmk_event AS
-WITH raw_events AS (
-  SELECT
-    ts,
-    LEAD(ts) OVER (ORDER BY ts) - ts AS dur,
-    CAST(value AS INTEGER) AS pid
-  FROM counter c
-  JOIN counter_track t ON t.id = c.track_id
-  WHERE t.name = 'kill_one_process'
-  UNION ALL
-  SELECT
-    slice.ts,
-    slice.dur,
-    CAST(STR_SPLIT(slice.name, ",", 1) AS INTEGER) AS pid
-  FROM slice
-  WHERE slice.name GLOB 'lmk,*'
-),
-lmks_with_proc_name AS (
-  SELECT
-    *,
-    process.name AS process_name
-  FROM raw_events
-  LEFT JOIN process ON
-    process.pid = raw_events.pid
-    AND (raw_events.ts >= process.start_ts OR process.start_ts IS NULL)
-    AND (raw_events.ts < process.end_ts OR process.end_ts IS NULL)
-  WHERE raw_events.pid != 0
-)
-SELECT
-  'slice' AS track_type,
-  'Low Memory Kills (LMKs)' AS track_name,
-  ts,
-  dur,
-  CASE
-    WHEN process_name IS NULL THEN printf('Process %d', lmk.pid)
-    ELSE printf('%s (pid: %d)', process_name, lmk.pid)
-  END AS slice_name
-FROM lmks_with_proc_name AS lmk;
-
 DROP VIEW IF EXISTS android_lmk_output;
 CREATE PERFETTO VIEW android_lmk_output AS
 WITH lmk_counts AS (
diff --git a/src/trace_processor/metrics/sql/android/android_other_traces.sql b/src/trace_processor/metrics/sql/android/android_other_traces.sql
deleted file mode 100644
index 367de23..0000000
--- a/src/trace_processor/metrics/sql/android/android_other_traces.sql
+++ /dev/null
@@ -1,47 +0,0 @@
---
--- Copyright 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
---
---     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.
---
-
-DROP VIEW IF EXISTS android_other_traces_view;
-CREATE PERFETTO VIEW android_other_traces_view AS
-SELECT
-  ts,
-  dur,
-  SUBSTR(slice.name, 15) AS uuid,
-  'Finalize' AS event_type
-FROM slice
-JOIN track
-  ON track.name = 'OtherTraces' AND slice.track_id = track.id
-WHERE
-  slice.name GLOB 'finalize-uuid-*';
-
-DROP VIEW IF EXISTS android_other_traces_event;
-CREATE PERFETTO VIEW android_other_traces_event AS
-SELECT
-  'slice' AS track_type,
-  'Other Traces' AS track_name,
-  ts,
-  dur,
-  event_type || ' ' || uuid AS slice_name
-FROM android_other_traces_view;
-
-DROP VIEW IF EXISTS android_other_traces_output;
-CREATE PERFETTO VIEW android_other_traces_output AS
-SELECT AndroidOtherTracesMetric(
-    'finalized_traces_uuid', (
-      SELECT RepeatedField(uuid)
-      FROM android_other_traces_view
-      WHERE event_type = 'Finalize')
-  );
diff --git a/src/trace_processor/metrics/sql/android/android_startup.sql b/src/trace_processor/metrics/sql/android/android_startup.sql
index 2e2e48e..7deb13e 100644
--- a/src/trace_processor/metrics/sql/android/android_startup.sql
+++ b/src/trace_processor/metrics/sql/android/android_startup.sql
@@ -120,6 +120,9 @@
       WHERE lp.startup_id =launches.startup_id
       LIMIT 1
     ),
+    'cpu_count', (
+      SELECT COUNT(DISTINCT cpu) from sched
+    ),
     'package_name', launches.package,
     'process_name', (
       SELECT p.name
@@ -238,6 +241,8 @@
       dur_sum_slice_proto_for_launch(launches.startup_id, 'inflate'),
       'time_get_resources',
       dur_sum_slice_proto_for_launch(launches.startup_id, 'ResourcesManager#getResources'),
+      'time_class_initialization',
+      dur_sum_slice_proto_for_launch(launches.startup_id, 'L*/*;'),
       'time_dex_open',
       dur_sum_slice_proto_for_launch(launches.startup_id, 'OpenDexFilesFromOat*'),
       'time_verify_class',
@@ -290,6 +295,10 @@
         FROM ANDROID_SLICES_FOR_STARTUP_AND_SLICE_NAME(launches.startup_id, 'JIT compiling*')
         WHERE thread_name = 'Jit thread pool'
       ),
+      'class_initialization_count', (
+        SELECT IIF(COUNT(1) = 0, NULL, COUNT(1))
+        FROM ANDROID_SLICES_FOR_STARTUP_AND_SLICE_NAME(launches.startup_id, 'L*/*;')
+      ),
       'other_processes_spawned_count', (
         SELECT COUNT(1)
         FROM process
diff --git a/src/trace_processor/metrics/sql/android/android_surfaceflinger.sql b/src/trace_processor/metrics/sql/android/android_surfaceflinger.sql
index a24bc9b..1801d33 100644
--- a/src/trace_processor/metrics/sql/android/android_surfaceflinger.sql
+++ b/src/trace_processor/metrics/sql/android/android_surfaceflinger.sql
@@ -29,17 +29,6 @@
   'output', 'gpu_frame_missed'
 );
 
-DROP VIEW IF EXISTS android_surfaceflinger_event;
-CREATE PERFETTO VIEW android_surfaceflinger_event AS
-SELECT
-  'slice' AS track_type,
-  'Android Missed Frames' AS track_name,
-  ts,
-  dur,
-  'Frame missed' AS slice_name
-FROM frame_missed
-WHERE value = 1 AND ts IS NOT NULL;
-
 DROP VIEW IF EXISTS surfaceflinger_track;
 CREATE PERFETTO VIEW surfaceflinger_track AS
 SELECT tr.id AS track_id, t.utid, t.tid
diff --git a/src/trace_processor/metrics/sql/android/android_trusty_workqueues.sql b/src/trace_processor/metrics/sql/android/android_trusty_workqueues.sql
deleted file mode 100644
index d34b225..0000000
--- a/src/trace_processor/metrics/sql/android/android_trusty_workqueues.sql
+++ /dev/null
@@ -1,21 +0,0 @@
--- Gather the `nop_work_func` slices and the CPU they each ran on and use that
--- information to generate a metric that displays just the Trusty workqueue
--- events grouped by CPU.
-DROP VIEW IF EXISTS android_trusty_workqueues_event;
-CREATE PERFETTO VIEW android_trusty_workqueues_event AS
-SELECT
-  'slice' AS track_type,
-  name AS slice_name,
-  ts,
-  dur,
-  'Cpu ' || EXTRACT_ARG(arg_set_id, 'cpu') AS track_name,
-  'Trusty Workqueues' AS group_name
-FROM slice
-WHERE slice.name GLOB 'nop_work_func*';
-
--- Generate the final metric output. This is empty because we're only using the
--- metric to generate custom tracks, and so don't have any aggregate data to
--- generate.
-DROP VIEW IF EXISTS android_trusty_workqueues_output;
-CREATE PERFETTO VIEW android_trusty_workqueues_output AS
-SELECT AndroidTrustyWorkqueues();
diff --git a/src/trace_processor/metrics/sql/android/codec_metrics.sql b/src/trace_processor/metrics/sql/android/codec_metrics.sql
index 68b4646..21ab859 100644
--- a/src/trace_processor/metrics/sql/android/codec_metrics.sql
+++ b/src/trace_processor/metrics/sql/android/codec_metrics.sql
@@ -14,13 +14,20 @@
 -- limitations under the License.
 --
 
+INCLUDE PERFETTO MODULE linux.cpu.utilization.thread;
+INCLUDE PERFETTO MODULE linux.cpu.utilization.slice;
+INCLUDE PERFETTO MODULE slices.with_context;
+INCLUDE PERFETTO MODULE slices.cpu_time;
+
 SELECT RUN_METRIC('android/android_cpu.sql');
+SELECT RUN_METRIC('android/android_powrails.sql');
 
 -- Attaching thread proto with media thread name
 DROP VIEW IF EXISTS core_type_proto_per_thread_name;
 CREATE PERFETTO VIEW core_type_proto_per_thread_name AS
 SELECT
-thread.name as thread_name,
+utid,
+thread.name AS thread_name,
 core_type_proto_per_thread.proto AS proto
 FROM core_type_proto_per_thread
 JOIN thread using(utid)
@@ -28,54 +35,32 @@
       thread.name = 'CodecLooper'
 GROUP BY thread.name;
 
--- aggregate all cpu the codec threads
-DROP VIEW IF EXISTS codec_per_thread_cpu_use;
-CREATE PERFETTO VIEW codec_per_thread_cpu_use AS
-SELECT
-  upid,
-  process.name AS process_name,
-  thread.name AS thread_name,
-  CAST(SUM(sched.dur) as INT64) AS cpu_time_ns,
-  COUNT(DISTINCT utid) AS num_threads
-FROM sched
-JOIN thread USING(utid)
-JOIN process USING(upid)
-WHERE thread.name = 'MediaCodec_loop' OR
-      thread.name = 'CodecLooper'
-GROUP BY process.name, thread.name;
-
 -- All process that has codec thread
-DROP VIEW IF EXISTS android_codec_process;
-CREATE PERFETTO VIEW android_codec_process AS
+DROP TABLE IF EXISTS android_codec_process;
+CREATE PERFETTO TABLE android_codec_process AS
 SELECT
+  utid,
   upid,
-  process.name as process_name
-FROM sched
-JOIN thread using(utid)
+  process.name AS process_name
+FROM thread
 JOIN process using(upid)
 WHERE thread.name = 'MediaCodec_loop' OR
       thread.name = 'CodecLooper'
-GROUP BY process_name;
+GROUP BY process_name, thread.name;
 
--- Total cpu for a process
-DROP VIEW IF EXISTS codec_total_per_process_cpu_use;
-CREATE PERFETTO VIEW codec_total_per_process_cpu_use AS
+-- Getting cpu cycles for the threads
+DROP VIEW IF EXISTS cpu_cycles_runtime;
+CREATE PERFETTO VIEW cpu_cycles_runtime AS
 SELECT
-  upid,
+  utid,
+  megacycles,
+  runtime,
+  proto,
   process_name,
-  CAST(SUM(sched.dur) as INT64) AS media_process_cpu_time_ns
-FROM sched
-JOIN thread using(utid)
-JOIN android_codec_process using(upid)
-GROUP BY process_name;
-
--- Joining total process with media thread table
-DROP VIEW IF EXISTS codec_per_process_thread_cpu_use;
-CREATE PERFETTO VIEW codec_per_process_thread_cpu_use AS
-SELECT
-  *
-FROM codec_total_per_process_cpu_use
-JOIN codec_per_thread_cpu_use using(process_name);
+  thread_name
+FROM android_codec_process
+JOIN cpu_cycles_per_thread using(utid)
+JOIN core_type_proto_per_thread_name using(utid);
 
 -- Traces are collected using specific traits in codec framework. These traits
 -- are mapped to actual names of slices and then combined with other tables to
@@ -92,68 +77,75 @@
   ELSE $slice_name
 END;
 
--- traits strings from codec framework
+-- Traits strings from codec framework
 DROP TABLE IF EXISTS trace_trait_table;
-CREATE TABLE trace_trait_table(
-  trace_trait  varchar(100));
-insert into trace_trait_table (trace_trait) values
-  ('MediaCodec'),
-  ('CCodec'),
-  ('C2PooledBlockPool'),
-  ('C2BufferQueueBlockPool'),
-  ('Codec2'),
-  ('ACodec'),
-  ('FrameDecoder');
+CREATE TABLE trace_trait_table(trace_trait TEXT UNIQUE);
+INSERT INTO trace_trait_table VALUES
+  ('MediaCodec::'),
+  ('CCodec::'),
+  ('CCodecBufferChannel::'),
+  ('C2PooledBlockPool::'),
+  ('C2hal::'),
+  ('ACodec::'),
+  ('FrameDecoder::');
 
 -- Maps traits to slice strings. Any string with '@' is considered to indicate
 -- the same trace with different information.Hence those strings are delimited
--- using '@' and considered as part of single trace.
-DROP VIEW IF EXISTS codec_slices;
-CREATE PERFETTO VIEW codec_slices AS
+-- using '@' and considered as part of single slice.
+
+-- View to hold slice ids(sid) and the assigned slice ids for codec slices.
+DROP TABLE IF EXISTS codec_slices;
+CREATE PERFETTO TABLE codec_slices AS
+WITH
+  __codec_slices AS (
+    SELECT DISTINCT
+      extract_codec_string(name, '@') AS codec_string,
+      slice.id AS sid,
+      slice.name AS sname
+    FROM slice
+    JOIN trace_trait_table ON slice.name glob trace_trait || '*'
+  ),
+  _codec_slices AS (
+    SELECT DISTINCT codec_string,
+      ROW_NUMBER() OVER() AS codec_slice_idx
+    FROM __codec_slices
+    GROUP BY codec_string
+  )
 SELECT
-  DISTINCT extract_codec_string(slice.name, '@') as codec_slice_string
-FROM slice
-JOIN trace_trait_table ON slice.name glob  '*' || trace_trait || '*';
+  codec_slice_idx,
+  a.codec_string,
+  sid
+FROM __codec_slices a
+JOIN _codec_slices b USING(codec_string);
 
--- combine slice and thread info
-DROP VIEW IF EXISTS slice_with_utid;
-CREATE PERFETTO VIEW slice_with_utid AS
-SELECT
-  extract_codec_string(slice.name, '@') as codec_string,
-  ts,
-  dur,
-  upid,
-  slice.name as slice_name,
-  slice.id as slice_id, utid,
-  thread.name as thread_name
-FROM slice
-JOIN thread_track ON thread_track.id = slice.track_id
-JOIN thread USING (utid);
-
--- Combine with thread_state info
-DROP TABLE IF EXISTS slice_thread_state_breakdown;
-CREATE VIRTUAL TABLE slice_thread_state_breakdown
-USING SPAN_LEFT_JOIN(
-  slice_with_utid PARTITIONED utid,
-  thread_state PARTITIONED utid
-);
-
--- Get cpu_running_time for all the slices of interest
-DROP VIEW IF EXISTS slice_cpu_running;
-CREATE PERFETTO VIEW slice_cpu_running AS
+-- Combine slice and and cpu dur and cycles info
+DROP TABLE IF EXISTS codec_slice_cpu_running;
+CREATE PERFETTO TABLE codec_slice_cpu_running AS
 SELECT
   codec_string,
-  sum(dur) as cpu_time,
-  sum(case when state = 'Running' then dur else 0 end) as cpu_run_ns,
-  thread_name,
-  process.name as process_name,
-  slice_id,
-  slice_name
-FROM slice_thread_state_breakdown
-LEFT JOIN process using(upid)
-where codec_string in (select codec_slice_string from codec_slices)
-GROUP BY codec_string, thread_name, process_name;
+  MIN(ts) AS ts,
+  MAX(ts + t.dur) AS max_ts,
+  SUM(t.dur) AS dur,
+  SUM(ct.cpu_time) AS cpu_run_ns,
+  SUM(megacycles) AS cpu_cycles,
+  cc.thread_name,
+  cc.process_name
+FROM codec_slices
+JOIN thread_slice t ON(sid = t.id)
+JOIN thread_slice_cpu_cycles cc ON(sid = cc.id)
+JOIN thread_slice_cpu_time ct ON(sid = ct.id)
+GROUP BY codec_slice_idx, cc.thread_name, cc.process_name;
 
+-- POWER consumed during codec use.
+DROP VIEW IF EXISTS codec_power_mw;
+CREATE PERFETTO VIEW codec_power_mw AS
+SELECT
+  AndroidCodecMetrics_Rail_Info (
+    'energy', tot_used_power,
+    'power_mw', tot_used_power / (powrail_end_ts - powrail_start_ts)
+  ) AS proto,
+  name
+FROM avg_used_powers;
 
 -- Generate proto for the trace
 DROP VIEW IF EXISTS metrics_per_slice_type;
@@ -163,26 +155,25 @@
   codec_string,
   AndroidCodecMetrics_Detail(
     'thread_name', thread_name,
-    'total_cpu_ns', CAST(cpu_time as INT64),
-    'running_cpu_ns', CAST(cpu_run_ns as INT64)
+    'total_cpu_ns', CAST(dur AS INT64),
+    'running_cpu_ns', CAST(cpu_run_ns AS INT64),
+    'cpu_cycles', CAST(cpu_cycles AS INT64)
   ) AS proto
-FROM slice_cpu_running;
+FROM codec_slice_cpu_running;
 
 -- Generating codec framework cpu metric
 DROP VIEW IF EXISTS codec_metrics_output;
 CREATE PERFETTO VIEW codec_metrics_output AS
-SELECT AndroidCodecMetrics(
+SELECT AndroidCodecMetrics (
   'cpu_usage', (
     SELECT RepeatedField(
       AndroidCodecMetrics_CpuUsage(
         'process_name', process_name,
         'thread_name', thread_name,
-        'thread_cpu_ns', CAST((cpu_time_ns) as INT64),
-        'num_threads', num_threads,
-        'core_data', core_type_proto_per_thread_name.proto
+        'thread_cpu_ns', CAST((runtime) AS INT64),
+        'core_data', proto
       )
-    ) FROM codec_per_process_thread_cpu_use
-      JOIN core_type_proto_per_thread_name using(thread_name)
+    ) FROM cpu_cycles_runtime
   ),
   'codec_function', (
     SELECT RepeatedField (
@@ -192,5 +183,20 @@
         'detail', metrics_per_slice_type.proto
       )
     ) FROM metrics_per_slice_type
+  ),
+  'energy', (
+    AndroidCodecMetrics_Energy(
+      'total_energy', (SELECT SUM(tot_used_power) FROM avg_used_powers),
+      'duration', (SELECT MAX(powrail_end_ts) - MIN(powrail_start_ts)  FROM avg_used_powers),
+      'power_mw', (SELECT SUM(tot_used_power) /  (MAX(powrail_end_ts) - MIN(powrail_start_ts)) FROM avg_used_powers),
+      'rail', (
+        SELECT RepeatedField (
+          AndroidCodecMetrics_Rail (
+            'name', name,
+            'info', codec_power_mw.proto
+          )
+        ) FROM codec_power_mw
+      )
+    )
   )
 );
diff --git a/src/trace_processor/metrics/sql/android/jank/frames.sql b/src/trace_processor/metrics/sql/android/jank/frames.sql
index 8f72e96..481bb4b 100644
--- a/src/trace_processor/metrics/sql/android/jank/frames.sql
+++ b/src/trace_processor/metrics/sql/android/jank/frames.sql
@@ -122,30 +122,55 @@
 -- the commit/composite slices on the main thread.
 DROP TABLE IF EXISTS android_jank_cuj_sf_frame;
 CREATE PERFETTO TABLE android_jank_cuj_sf_frame AS
+WITH android_jank_cuj_timeline_sf_frame AS (
+    SELECT DISTINCT
+      cuj_id,
+      CAST(timeline.name AS INTEGER) AS vsync,
+      timeline.display_frame_token
+    FROM android_jank_cuj_vsync_boundary boundary
+    JOIN actual_frame_timeline_slice timeline
+      ON
+        boundary.upid = timeline.upid
+        AND CAST(timeline.name AS INTEGER) >= vsync_min
+        AND CAST(timeline.name AS INTEGER) <= vsync_max
+    WHERE
+        boundary.layer_id IS NULL
+      OR (
+        timeline.layer_name GLOB '*#*'
+        AND boundary.layer_id = CAST(STR_SPLIT(timeline.layer_name, '#', 1) AS INTEGER))
+),
+android_jank_cuj_sf_frame_base AS (
+    SELECT DISTINCT
+      boundary.cuj_id,
+      boundary.vsync,
+      boundary.ts,
+      boundary.ts_main_thread_start,
+      boundary.ts_end,
+      boundary.dur,
+      actual_timeline.jank_tag = 'Self Jank' AS sf_missed,
+      NULL AS app_missed, -- for simplicity align schema with android_jank_cuj_frame
+      jank_tag,
+      jank_type,
+      prediction_type,
+      present_type,
+      gpu_composition,
+      -- In case expected timeline is missing, as a fallback we use the typical frame deadline
+      -- for 60Hz.
+      -- See similar expression in android_jank_cuj_frame_timeline.
+      COALESCE(expected_timeline.dur, 16600000) AS dur_expected
+    FROM android_jank_cuj_sf_main_thread_frame_boundary boundary
+    JOIN android_jank_cuj_sf_process sf_process
+    JOIN actual_frame_timeline_slice actual_timeline
+      ON actual_timeline.upid = sf_process.upid
+        AND boundary.vsync = CAST(actual_timeline.name AS INTEGER)
+    JOIN android_jank_cuj_timeline_sf_frame ft
+      ON CAST(actual_timeline.name AS INTEGER) = ft.display_frame_token
+        AND boundary.cuj_id = ft.cuj_id
+    LEFT JOIN expected_frame_timeline_slice expected_timeline
+      ON expected_timeline.upid = actual_timeline.upid
+        AND expected_timeline.name = actual_timeline.name
+)
 SELECT
-  cuj_id,
-  ROW_NUMBER() OVER (PARTITION BY cuj_id ORDER BY vsync ASC) AS frame_number,
-  vsync,
-  boundary.ts,
-  boundary.ts_main_thread_start,
-  boundary.ts_end,
-  boundary.dur,
-  actual_timeline.jank_tag = 'Self Jank' AS sf_missed,
-  NULL AS app_missed, -- for simplicity align schema with android_jank_cuj_frame
-  jank_tag,
-  jank_type,
-  prediction_type,
-  present_type,
-  gpu_composition,
-  -- In case expected timeline is missing, as a fallback we use the typical frame deadline
-  -- for 60Hz.
-  -- See similar expression in android_jank_cuj_frame_timeline.
-  COALESCE(expected_timeline.dur, 16600000) AS dur_expected
-FROM android_jank_cuj_sf_main_thread_frame_boundary boundary
-JOIN android_jank_cuj_sf_process sf_process
-JOIN actual_frame_timeline_slice actual_timeline
-  ON actual_timeline.upid = sf_process.upid
-    AND boundary.vsync = CAST(actual_timeline.name AS INTEGER)
-LEFT JOIN expected_frame_timeline_slice expected_timeline
-  ON expected_timeline.upid = actual_timeline.upid
-    AND expected_timeline.name = actual_timeline.name;
+ *,
+ ROW_NUMBER() OVER (PARTITION BY cuj_id ORDER BY vsync ASC) AS frame_number
+FROM android_jank_cuj_sf_frame_base;
diff --git a/src/trace_processor/metrics/sql/android/jank/internal/derived_events.sql b/src/trace_processor/metrics/sql/android/jank/internal/derived_events.sql
deleted file mode 100644
index 2211539..0000000
--- a/src/trace_processor/metrics/sql/android/jank/internal/derived_events.sql
+++ /dev/null
@@ -1,83 +0,0 @@
---
--- Copyright 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
---
---     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.
-
-
-DROP VIEW IF EXISTS android_jank_cuj_event;
-CREATE PERFETTO VIEW android_jank_cuj_event AS
--- Computed CUJ boundaries.
-SELECT
-  'slice' AS track_type,
-  cuj.cuj_name AS track_name,
-  boundary.ts,
-  boundary.dur,
-  cuj.cuj_name || ' (adjusted, id=' || cuj_id || ') ' AS slice_name,
-  'CUJ Boundaries' AS group_name
-FROM android_jank_cuj cuj
-JOIN android_jank_cuj_boundary boundary USING (cuj_id)
-UNION ALL
--- Computed frame boundaries on the Main Thread.
-SELECT
-  'slice' AS track_type,
-  cuj.cuj_name || ' MT ' || vsync AS track_name,
-  boundary.ts,
-  boundary.dur,
-  vsync || '' AS slice_name,
-  cuj.cuj_name || ' - MT frame boundaries' AS group_name
-FROM android_jank_cuj cuj
-JOIN android_jank_cuj_main_thread_frame_boundary boundary USING (cuj_id)
-UNION ALL
--- Computed frame boundaries on the Render Thread.
-SELECT
-  'slice' AS track_type,
-  cuj.cuj_name || ' RT ' || vsync AS track_name,
-  boundary.ts,
-  boundary.dur,
-  vsync || '' AS slice_name,
-  cuj.cuj_name || ' - RT frame boundaries' AS group_name
-FROM android_jank_cuj cuj
-JOIN android_jank_cuj_render_thread_frame_boundary boundary USING (cuj_id)
-UNION ALL
--- Computed overall frame boundaries not specific to any thread.
-SELECT
-  'slice' AS track_type,
-  cuj.cuj_name || ' ' || vsync AS track_name,
-  f.ts,
-  f.dur,
-  vsync || ' [app_missed=' || f.app_missed || ']' AS slice_name,
-  cuj.cuj_name || ' - frames' AS group_name
-FROM android_jank_cuj cuj
-JOIN android_jank_cuj_frame f USING (cuj_id)
-UNION ALL
--- Computed frame boundaries on the SF Main Thread
-SELECT
-  'slice' AS track_type,
-  cuj.cuj_name || ' SF MT ' || vsync AS track_name,
-  boundary.ts,
-  boundary.dur,
-  vsync || '' AS slice_name,
-  cuj.cuj_name || ' - SF MT frame boundaries' AS group_name
-FROM android_jank_cuj cuj
-JOIN android_jank_cuj_sf_main_thread_frame_boundary boundary USING (cuj_id)
-UNION ALL
--- Computed frame boundaries on the SF RenderEngine Thread.
-SELECT
-  'slice' AS track_type,
-  cuj.cuj_name || ' SF RE ' || vsync AS track_name,
-  boundary.ts,
-  boundary.dur,
-  vsync || '' AS slice_name,
-  cuj.cuj_name || ' - SF RE frame boundaries' AS group_name
-FROM android_jank_cuj cuj
-JOIN android_jank_cuj_sf_render_engine_frame_boundary boundary USING (cuj_id);
diff --git a/src/trace_processor/metrics/sql/android/jank/relevant_slices.sql b/src/trace_processor/metrics/sql/android/jank/relevant_slices.sql
index 9c43318..536dea7 100644
--- a/src/trace_processor/metrics/sql/android/jank/relevant_slices.sql
+++ b/src/trace_processor/metrics/sql/android/jank/relevant_slices.sql
@@ -67,6 +67,7 @@
 -- Ignore child slice e.g. "Choreographer#doFrame - resynced to 1234 in 20.0ms"
   AND slice.name not GLOB '*resynced*'
   AND slice.dur > 0
+  AND vsync > 0
   AND (vsync >= begin_vsync OR begin_vsync is NULL)
   AND (vsync <= end_vsync OR end_vsync is NULL)
   -- In some malformed traces we see nested doFrame slices.
diff --git a/src/trace_processor/metrics/sql/android/startup/slow_start_reasons.sql b/src/trace_processor/metrics/sql/android/startup/slow_start_reasons.sql
index 21e17b1..8a4a7dd 100644
--- a/src/trace_processor/metrics/sql/android/startup/slow_start_reasons.sql
+++ b/src/trace_processor/metrics/sql/android/startup/slow_start_reasons.sql
@@ -19,6 +19,8 @@
 SELECT RUN_METRIC('android/startup/thread_state_breakdown.sql');
 SELECT RUN_METRIC('android/startup/system_state.sql');
 SELECT RUN_METRIC('android/startup/mcycles_per_launch.sql');
+-- Define helper functions related to slow start thresholds
+SELECT RUN_METRIC('android/startup/slow_start_thresholds.sql');
 
 CREATE OR REPLACE PERFETTO FUNCTION _is_spans_overlapping(
   ts1 LONG,
@@ -44,72 +46,97 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_main_thread_time_for_launch_in_runnable_state(
   startup_id LONG, num_threads INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceThreadSection(
-    'start_timestamp', ts, 'end_timestamp', ts + dur,
-    'thread_utid', utid, 'thread_name', thread_name))
+  SELECT AndroidStartupMetric_TraceThreadSectionInfo(
+    'start_timestamp', MIN(ts),
+    'end_timestamp', MAX(ts + dur),
+    'thread_section', RepeatedField(AndroidStartupMetric_TraceThreadSection(
+      'start_timestamp', ts, 'end_timestamp', ts + dur,
+      'thread_tid', tid, 'process_pid', pid,
+      'thread_name', thread_name)))
   FROM (
-    SELECT ts, dur, utid, thread_name
-    FROM launch_threads_by_thread_state l
+    SELECT p.pid, ts, dur, thread.tid, thread_name
+    FROM launch_threads_by_thread_state l, android_startup_processes p
     JOIN thread USING (utid)
     WHERE l.startup_id = $startup_id AND (state GLOB "R" OR state GLOB "R+") AND l.is_main_thread
+      AND p.startup_id = $startup_id
     ORDER BY dur DESC
     LIMIT $num_threads);
 
 CREATE OR REPLACE PERFETTO FUNCTION get_main_thread_time_for_launch_and_state(
   startup_id LONG, state STRING, num_threads INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceThreadSection(
-    'start_timestamp', ts, 'end_timestamp', ts + dur,
-    'thread_utid', utid, 'thread_name', thread_name))
+  SELECT AndroidStartupMetric_TraceThreadSectionInfo(
+    'start_timestamp', MIN(ts),
+    'end_timestamp', MAX(ts + dur),
+    'thread_section', RepeatedField(AndroidStartupMetric_TraceThreadSection(
+      'start_timestamp', ts, 'end_timestamp', ts + dur,
+      'thread_tid', tid, 'process_pid', pid,
+      'thread_name', thread_name)))
   FROM (
-    SELECT ts, dur, utid, thread_name
-    FROM launch_threads_by_thread_state l
+    SELECT p.pid, ts, dur, thread.tid, thread_name
+    FROM launch_threads_by_thread_state l, android_startup_processes p
     JOIN thread USING (utid)
     WHERE l.startup_id = $startup_id AND state GLOB $state AND l.is_main_thread
+      AND p.startup_id = $startup_id
     ORDER BY dur DESC
     LIMIT $num_threads);
 
 CREATE OR REPLACE PERFETTO FUNCTION get_main_thread_time_for_launch_state_and_io_wait(
   startup_id INT, state STRING, io_wait BOOL, num_threads INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceThreadSection(
-    'start_timestamp', ts, 'end_timestamp', ts + dur,
-    'thread_utid', utid, 'thread_name', thread_name))
+  SELECT AndroidStartupMetric_TraceThreadSectionInfo(
+    'start_timestamp', MIN(ts),
+    'end_timestamp', MAX(ts + dur),
+    'thread_section', RepeatedField(AndroidStartupMetric_TraceThreadSection(
+      'start_timestamp', ts, 'end_timestamp', ts + dur,
+      'thread_tid', tid, 'process_pid', pid,
+      'thread_name', thread_name)))
   FROM (
-    SELECT ts, dur, utid, thread_name
-    FROM launch_threads_by_thread_state l
+    SELECT p.pid, ts, dur, thread.tid, thread_name
+    FROM launch_threads_by_thread_state l, android_startup_processes p
     JOIN thread USING (utid)
     WHERE l.startup_id = $startup_id AND state GLOB $state
       AND l.is_main_thread AND l.io_wait = $io_wait
+      AND p.startup_id = $startup_id
     ORDER BY dur DESC
     LIMIT $num_threads);
 
 CREATE OR REPLACE PERFETTO FUNCTION get_thread_time_for_launch_state_and_thread(
   startup_id INT, state STRING, thread_name STRING, num_threads INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceThreadSection(
-    'start_timestamp', ts, 'end_timestamp', ts + dur,
-    'thread_utid', utid, 'thread_name', thread_name))
+  SELECT AndroidStartupMetric_TraceThreadSectionInfo(
+    'start_timestamp', MIN(ts),
+    'end_timestamp', MAX(ts + dur),
+    'thread_section', RepeatedField(AndroidStartupMetric_TraceThreadSection(
+      'start_timestamp', ts, 'end_timestamp', ts + dur,
+      'thread_tid', tid, 'process_pid', pid,
+      'thread_name', thread_name)))
   FROM (
-    SELECT ts, dur, utid, thread_name
-    FROM launch_threads_by_thread_state l
+    SELECT p.pid, ts, dur, thread.tid, thread_name
+    FROM launch_threads_by_thread_state l, android_startup_processes p
     JOIN thread USING (utid)
     WHERE l.startup_id = $startup_id AND state GLOB $state AND thread_name = $thread_name
+      AND p.startup_id = $startup_id
     ORDER BY dur DESC
     LIMIT $num_threads);
 
 CREATE OR REPLACE PERFETTO FUNCTION get_missing_baseline_profile_for_launch(
   startup_id LONG, pkg_name STRING)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'start_timestamp', slice_ts,
-    'end_timestamp', slice_ts + slice_dur,
-    'slice_id', slice_id,
-    'slice_name', slice_name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', pid,
+      'start_timestamp', slice_ts,
+      'end_timestamp', slice_ts + slice_dur,
+      'slice_id', slice_id,
+      'slice_name', slice_name)),
+    'start_timestamp', MIN(slice_ts),
+    'end_timestamp', MAX(slice_ts + slice_dur))
   FROM (
-    SELECT slice_ts, slice_dur, slice_id, slice_name
+    SELECT p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
     FROM ANDROID_SLICES_FOR_STARTUP_AND_SLICE_NAME($startup_id,
-      "location=* status=* filter=* reason=*")
+      "location=* status=* filter=* reason=*"), android_startup_processes p
     WHERE
       -- when location is the package odex file and the reason is "install" or "install-dm",
       -- if the compilation filter is not "speed-profile", baseline/cloud profile is missing.
@@ -117,36 +144,49 @@
         GLOB ("*" || $pkg_name || "*odex")
       AND (STR_SPLIT(slice_name, " reason=", 1) = "install"
       OR STR_SPLIT(slice_name, " reason=", 1) = "install-dm")
+      AND p.startup_id = $startup_id
     ORDER BY slice_dur DESC
     LIMIT 1);
 
 CREATE OR REPLACE PERFETTO FUNCTION get_run_from_apk(startup_id LONG)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'start_timestamp', slice_ts,
-    'end_timestamp', slice_ts + slice_dur,
-    'slice_id', slice_id,
-    'slice_name', slice_name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', pid,
+      'start_timestamp', slice_ts,
+      'end_timestamp', slice_ts + slice_dur,
+      'slice_id', slice_id,
+      'slice_name', slice_name)),
+    'start_timestamp', MIN(slice_ts),
+    'end_timestamp', MAX(slice_ts + slice_dur))
   FROM (
-    SELECT slice_ts, slice_dur, slice_id, slice_name
-    FROM android_thread_slices_for_all_startups
+    SELECT p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
+    FROM android_thread_slices_for_all_startups l, android_startup_processes p
     WHERE
-      startup_id = $startup_id AND is_main_thread AND
+      l.startup_id = $startup_id AND is_main_thread AND
       slice_name GLOB "location=* status=* filter=* reason=*" AND
       STR_SPLIT(STR_SPLIT(slice_name, " filter=", 1), " reason=", 0)
         GLOB ("*" || "run-from-apk" || "*")
+      AND p.startup_id = $startup_id
     ORDER BY slice_dur DESC
     LIMIT 1);
 
-CREATE OR REPLACE PERFETTO FUNCTION get_unlock_running_during_launch_slice(startup_id LONG)
+CREATE OR REPLACE PERFETTO FUNCTION get_unlock_running_during_launch_slice(startup_id LONG,
+  pid INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'start_timestamp', slice_ts,
-    'end_timestamp', slice_ts + slice_dur,
-    'slice_id', slice_id,
-    'slice_name', slice_name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', $pid,
+      'start_timestamp', slice_ts,
+      'end_timestamp', slice_ts + slice_dur,
+      'slice_id', slice_id,
+      'slice_name', slice_name)),
+    'start_timestamp', MIN(slice_ts),
+    'end_timestamp', MAX(slice_ts + slice_dur))
   FROM (
-    SELECT slice.ts as slice_ts, slice.dur as slice_dur,
+    SELECT tid, slice.ts as slice_ts, slice.dur as slice_dur,
       slice.id as slice_id, slice.name as slice_name
     FROM slice, android_startups launches
     JOIN thread_track ON slice.track_id = thread_track.id
@@ -161,15 +201,21 @@
 
 CREATE OR REPLACE PERFETTO FUNCTION get_gc_activity(startup_id LONG, num_slices INT)
 RETURNS PROTO  AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'start_timestamp', slice_ts,
-    'end_timestamp', slice_ts + slice_dur,
-    'slice_id', slice_id,
-    'slice_name', slice_name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', pid,
+      'start_timestamp', slice_ts,
+      'end_timestamp', slice_ts + slice_dur,
+      'slice_id', slice_id,
+      'slice_name', slice_name)),
+    'start_timestamp', MIN(slice_ts),
+    'end_timestamp', MAX(slice_ts + slice_dur))
   FROM (
-    SELECT slice_ts, slice_dur, slice_id, slice_name
-    FROM android_thread_slices_for_all_startups slice
+    SELECT p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
+    FROM android_thread_slices_for_all_startups slice, android_startup_processes p
     WHERE
+      p.startup_id = $startup_id AND
       slice.startup_id = $startup_id AND
       (
         slice_name GLOB "*semispace GC" OR
@@ -182,15 +228,21 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_dur_on_main_thread_for_startup_and_slice(
   startup_id LONG, slice_name STRING, num_slices INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'start_timestamp', slice_ts,
-    'end_timestamp', slice_ts + slice_dur,
-    'slice_id', slice_id,
-    'slice_name', slice_name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', pid,
+      'start_timestamp', slice_ts,
+      'end_timestamp', slice_ts + slice_dur,
+      'slice_id', slice_id,
+      'slice_name', slice_name)),
+    'start_timestamp', MIN(slice_ts),
+    'end_timestamp', MAX(slice_ts + slice_dur))
   FROM (
-    SELECT slice_ts, slice_dur, slice_id, slice_name
-    FROM android_thread_slices_for_all_startups l
-    WHERE startup_id = $startup_id
+    SELECT p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
+    FROM android_thread_slices_for_all_startups l,
+      android_startup_processes p
+    WHERE l.startup_id = $startup_id AND p.startup_id == $startup_id
       AND slice_name GLOB $slice_name
     ORDER BY slice_dur DESC
     LIMIT $num_slices);
@@ -198,22 +250,28 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_main_thread_binder_transactions_blocked(
   startup_id LONG, threshold DOUBLE, num_slices INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'start_timestamp', slice_ts, 'end_timestamp', slice_ts + slice_dur,
-    'slice_id', slice_id, 'slice_name', slice_name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', pid,
+      'start_timestamp', slice_ts, 'end_timestamp', slice_ts + slice_dur,
+      'slice_id', slice_id, 'slice_name', slice_name)),
+    'start_timestamp', MIN(slice_ts),
+    'end_timestamp', MAX(slice_ts + slice_dur))
   FROM (
-    SELECT request.slice_ts as slice_ts, request.slice_dur as slice_dur,
+    SELECT pid, request.tid as tid, request.slice_ts as slice_ts, request.slice_dur as slice_dur,
       request.id as slice_id, request.slice_name as slice_name
     FROM (
-      SELECT slice_id as id, slice_dur, thread_name, process.name as process,
+      SELECT p.pid, tid, slice_id as id, slice_dur, thread_name, process.name as process,
         s.arg_set_id, is_main_thread,
         slice_ts, s.utid, slice_name
-      FROM android_thread_slices_for_all_startups s
+      FROM android_thread_slices_for_all_startups s,
+        android_startup_processes p
       JOIN process ON (
         EXTRACT_ARG(s.arg_set_id, "destination process") = process.pid
       )
-      WHERE startup_id = $startup_id AND slice_name GLOB "binder transaction"
-        AND slice_dur > $threshold
+      WHERE s.startup_id = $startup_id AND slice_name GLOB "binder transaction"
+        AND slice_dur > $threshold AND p.startup_id = $startup_id
     ) request
     JOIN following_flow(request.id) arrow
     JOIN slice reply ON reply.id = arrow.slice_in
@@ -223,13 +281,20 @@
     LIMIT $num_slices);
 
 CREATE OR REPLACE PERFETTO FUNCTION get_slices_concurrent_to_launch(
-  startup_id INT, slice_glob STRING, num_slices INT)
+  startup_id INT, slice_glob STRING, num_slices INT, pid INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'start_timestamp', ts, 'end_timestamp', ts + dur,
-    'slice_id', id, 'slice_name', name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', $pid,
+      'start_timestamp', ts, 'end_timestamp', ts + dur,
+      'slice_id', id, 'slice_name', name)),
+    'start_timestamp', MIN(ts),
+    'end_timestamp', MAX(ts + dur))
   FROM (
-    SELECT s.ts as ts, dur, id, name FROM slice s
+    SELECT thread.tid, s.ts as ts, dur, s.id, s.name FROM slice s
+    JOIN thread_track t ON s.track_id = t.id
+    JOIN thread USING(utid)
     JOIN (
       SELECT ts, ts_end
       FROM android_startups
@@ -241,13 +306,18 @@
     ORDER BY dur DESC LIMIT $num_slices);
 
 CREATE OR REPLACE PERFETTO FUNCTION get_slices_for_startup_and_slice_name(
-  startup_id INT, slice_name STRING, num_slices INT)
+  startup_id INT, slice_name STRING, num_slices INT, pid int)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'start_timestamp', slice_ts, 'end_timestamp', slice_ts + slice_dur,
-    'slice_id', slice_id, 'slice_name', slice_name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', $pid,
+      'start_timestamp', slice_ts, 'end_timestamp', slice_ts + slice_dur,
+      'slice_id', slice_id, 'slice_name', slice_name)),
+    'start_timestamp', MIN(slice_ts),
+    'end_timestamp', MAX(slice_ts + slice_dur))
   FROM (
-    SELECT slice_ts, slice_dur, slice_id, slice_name
+    SELECT tid, slice_ts, slice_dur, slice_id, slice_name
     FROM android_thread_slices_for_all_startups
     WHERE startup_id = $startup_id AND slice_name GLOB $slice_name
     ORDER BY slice_dur DESC
@@ -311,7 +381,8 @@
             'unit', 'TRUE_OR_FALSE') as expected_val,
           AndroidStartupMetric_ActualValue(
             'value', TRUE) as actual_val,
-          get_run_from_apk(launch.startup_id) as trace_slices,
+          get_run_from_apk(launch.startup_id)
+            as trace_slices,
           NULL as trace_threads,
           NULL as extra
         FROM android_startups launch
@@ -328,7 +399,9 @@
             'unit', 'TRUE_OR_FALSE') as expected_val,
           AndroidStartupMetric_ActualValue(
             'value', TRUE) as actual_val,
-          get_unlock_running_during_launch_slice(launch.startup_id) as trace_slices,
+          get_unlock_running_during_launch_slice(launch.startup_id,
+            (SELECT pid FROM android_startup_processes WHERE launch.startup_id = startup_id))
+            as trace_slices,
           NULL as trace_threads,
           NULL as extra
         FROM android_startups launch
@@ -362,7 +435,8 @@
             'unit', 'TRUE_OR_FALSE') as expected_val,
           AndroidStartupMetric_ActualValue(
             'value', TRUE) as actual_val,
-          get_gc_activity(launch.startup_id, 1) as trace_slices,
+          get_gc_activity(launch.startup_id, 1)
+            as trace_slices,
           NULL as trace_threads,
           NULL as extra
         FROM android_startups launch
@@ -411,7 +485,7 @@
           'MAIN_THREAD_TIME_SPENT_IN_RUNNABLE' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 15,
+            'value', threshold_runnable_percentage(),
             'unit', 'PERCENTAGE',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
@@ -419,11 +493,13 @@
               main_thread_time_for_launch_in_runnable_state(launch.startup_id) * 100 / launch.dur,
             'dur', main_thread_time_for_launch_in_runnable_state(launch.startup_id)) as actual_val,
           NULL as trace_slices,
-          get_main_thread_time_for_launch_in_runnable_state(launch.startup_id, 3) as trace_threads,
+          get_main_thread_time_for_launch_in_runnable_state(launch.startup_id, 3)
+            as trace_threads,
           NULL as extra
         FROM android_startups launch
         WHERE launch.startup_id = $startup_id
-          AND main_thread_time_for_launch_in_runnable_state(launch.startup_id) > launch.dur * 0.15
+          AND main_thread_time_for_launch_in_runnable_state(launch.startup_id) >
+            launch.dur / 100 * threshold_runnable_percentage()
 
         UNION ALL
         SELECT 'Main Thread - Time spent in interruptible sleep state' as slow_cause,
@@ -431,17 +507,19 @@
           'MAIN_THREAD_TIME_SPENT_IN_INTERRUPTIBLE_SLEEP' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 2900000000,
+            'value', threshold_interruptible_sleep_ns(),
             'unit', 'NS',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
             'value', main_thread_time_for_launch_and_state(launch.startup_id, 'S')) as actual_val,
           NULL as trace_slices,
-          get_main_thread_time_for_launch_and_state(launch.startup_id, 'S', 3) as trace_threads,
+          get_main_thread_time_for_launch_and_state(launch.startup_id, 'S', 3)
+            as trace_threads,
           NULL as extra
         FROM android_startups launch
         WHERE launch.startup_id = $startup_id
-          AND main_thread_time_for_launch_and_state(launch.startup_id, 'S') > 2900e6
+          AND main_thread_time_for_launch_and_state(launch.startup_id, 'S') >
+            threshold_interruptible_sleep_ns()
 
         UNION ALL
         SELECT 'Main Thread - Time spent in Blocking I/O' as slow_cause,
@@ -449,7 +527,7 @@
           'MAIN_THREAD_TIME_SPENT_IN_BLOCKING_IO' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 450000000,
+            'value', threshold_blocking_io_ns(),
             'unit', 'NS',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
@@ -457,11 +535,13 @@
               launch.startup_id, 'D*', TRUE)) as actual_val,
           NULL as trace_slices,
           get_main_thread_time_for_launch_state_and_io_wait(
-            launch.startup_id, 'D*', TRUE, 3) as trace_threads,
+            launch.startup_id, 'D*', TRUE, 3)
+            as trace_threads,
           NULL as extra
         FROM android_startups launch
         WHERE launch.startup_id = $startup_id
-          AND main_thread_time_for_launch_state_and_io_wait(launch.startup_id, 'D*', TRUE) > 450e6
+          AND main_thread_time_for_launch_state_and_io_wait(launch.startup_id, 'D*', TRUE) >
+            threshold_blocking_io_ns()
 
         UNION ALL
         SELECT 'Main Thread - Time spent in OpenDexFilesFromOat*' as slow_cause,
@@ -469,7 +549,7 @@
           'MAIN_THREAD_TIME_SPENT_IN_OPEN_DEX_FILES_FROM_OAT' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 20,
+            'value', threshold_open_dex_files_from_oat_percentage(),
             'unit', 'PERCENTAGE',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
@@ -484,7 +564,8 @@
         FROM android_startups launch
         WHERE launch.startup_id = $startup_id AND
           android_sum_dur_on_main_thread_for_startup_and_slice(
-          launch.startup_id, 'OpenDexFilesFromOat*') > launch.dur * 0.2
+          launch.startup_id, 'OpenDexFilesFromOat*') >
+            launch.dur / 100 * threshold_open_dex_files_from_oat_percentage()
 
         UNION ALL
         SELECT 'Time spent in bindApplication' as slow_cause,
@@ -492,7 +573,7 @@
           'TIME_SPENT_IN_BIND_APPLICATION' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 1250000000,
+            'value', threshold_bind_application_ns(),
             'unit', 'NS',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
@@ -504,7 +585,8 @@
           NULL as extra
         FROM android_startups launch
         WHERE launch.startup_id = $startup_id
-          AND android_sum_dur_for_startup_and_slice(launch.startup_id, 'bindApplication') > 1250e6
+          AND android_sum_dur_for_startup_and_slice(launch.startup_id, 'bindApplication') >
+            threshold_bind_application_ns()
 
         UNION ALL
         SELECT 'Time spent in view inflation' as slow_cause,
@@ -512,7 +594,7 @@
           'TIME_SPENT_IN_VIEW_INFLATION' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 450000000,
+            'value', threshold_view_inflation_ns(),
             'unit', 'NS',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
@@ -524,7 +606,8 @@
           NULL as extra
         FROM android_startups launch
         WHERE launch.startup_id = $startup_id
-          AND android_sum_dur_for_startup_and_slice(launch.startup_id, 'inflate') > 450e6
+          AND android_sum_dur_for_startup_and_slice(launch.startup_id, 'inflate') >
+            threshold_view_inflation_ns()
 
         UNION ALL
         SELECT 'Time spent in ResourcesManager#getResources' as slow_cause,
@@ -532,20 +615,22 @@
           'TIME_SPENT_IN_RESOURCES_MANAGER_GET_RESOURCES' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 130000000,
+            'value', threshold_resources_manager_get_resources_ns(),
             'unit', 'NS',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
             'value', android_sum_dur_for_startup_and_slice(
               launch.startup_id, 'ResourcesManager#getResources')) as actual_val,
           get_dur_on_main_thread_for_startup_and_slice(
-            launch.startup_id, 'ResourcesManager#getResources', 3) as trace_slices,
+            launch.startup_id, 'ResourcesManager#getResources', 3)
+            as trace_slices,
           NULL as trace_threads,
           NULL as extra
         FROM android_startups launch
         WHERE launch.startup_id = $startup_id
           AND android_sum_dur_for_startup_and_slice(
-          launch.startup_id, 'ResourcesManager#getResources') > 130e6
+          launch.startup_id, 'ResourcesManager#getResources') >
+            threshold_resources_manager_get_resources_ns()
 
         UNION ALL
         SELECT 'Time spent verifying classes' as slow_cause,
@@ -553,7 +638,7 @@
           'TIME_SPENT_VERIFYING_CLASSES' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 15,
+            'value', threshold_verify_classes_percentage(),
             'unit', 'PERCENTAGE',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
@@ -568,7 +653,7 @@
         FROM android_startups launch
         WHERE launch.startup_id = $startup_id AND
           android_sum_dur_for_startup_and_slice(launch.startup_id, 'VerifyClass*')
-            > launch.dur * 0.15
+            > launch.dur / 100 * threshold_verify_classes_percentage()
 
         UNION ALL
         SELECT 'Potential CPU contention with another process' AS slow_cause,
@@ -576,18 +661,20 @@
           'POTENTIAL_CPU_CONTENTION_WITH_ANOTHER_PROCESS' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 100000000,
+            'value', threshold_potential_cpu_contention_ns(),
             'unit', 'NS',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
             'value',
               main_thread_time_for_launch_in_runnable_state(launch.startup_id)) as actual_val,
           NULL as trace_slices,
-          get_main_thread_time_for_launch_in_runnable_state(launch.startup_id, 3) as trace_threads,
+          get_main_thread_time_for_launch_in_runnable_state(launch.startup_id, 3)
+            as trace_threads,
           NULL as extra
         FROM android_startups launch
         WHERE launch.startup_id = $startup_id AND
-          main_thread_time_for_launch_in_runnable_state(launch.startup_id) > 100e6 AND
+          main_thread_time_for_launch_in_runnable_state(launch.startup_id) >
+            threshold_potential_cpu_contention_ns() AND
           most_active_process_for_launch(launch.startup_id) IS NOT NULL
 
         UNION ALL
@@ -596,7 +683,7 @@
           'JIT_ACTIVITY' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 100000000,
+            'value', threshold_jit_activity_ns(),
             'unit', 'NS',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
@@ -604,7 +691,8 @@
               launch.startup_id, 'Running', 'Jit thread pool')) as actual_val,
           NULL as trace_slices,
           get_thread_time_for_launch_state_and_thread(
-            launch.startup_id, 'Running', 'Jit thread pool', 3) as trace_threads,
+            launch.startup_id, 'Running', 'Jit thread pool', 3)
+            as trace_threads,
           NULL as extra
         FROM android_startups launch
         WHERE launch.startup_id = $startup_id
@@ -612,7 +700,7 @@
           launch.startup_id,
           'Running',
           'Jit thread pool'
-        ) > 100e6
+        ) > threshold_jit_activity_ns()
 
         UNION ALL
         SELECT 'Main Thread - Lock contention' as slow_cause,
@@ -620,7 +708,7 @@
           'MAIN_THREAD_LOCK_CONTENTION' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 20,
+            'value', threshold_lock_contention_percentage(),
             'unit', 'PERCENTAGE',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
@@ -628,7 +716,7 @@
               launch.startup_id, 'Lock contention on*') * 100 / launch.dur,
             'dur', android_sum_dur_on_main_thread_for_startup_and_slice(
               launch.startup_id, 'Lock contention on*')) as actual_val,
-          get_dur_on_main_thread_for_startup_and_slice(launch.startup_id, 'lock contention on*', 3)
+          get_dur_on_main_thread_for_startup_and_slice(launch.startup_id, 'Lock contention on*', 3)
             as trace_slices,
           NULL as trace_threads,
           NULL as extra
@@ -637,7 +725,7 @@
           AND android_sum_dur_on_main_thread_for_startup_and_slice(
           launch.startup_id,
           'Lock contention on*'
-        ) > launch.dur * 0.2
+        ) > launch.dur / 100 * threshold_lock_contention_percentage()
 
         UNION ALL
         SELECT 'Main Thread - Monitor contention' as slow_cause,
@@ -645,7 +733,7 @@
           'MAIN_THREAD_MONITOR_CONTENTION' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 15,
+            'value', threshold_monitor_contention_percentage(),
             'unit', 'PERCENTAGE',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
@@ -654,7 +742,8 @@
             'dur', android_sum_dur_on_main_thread_for_startup_and_slice(
               launch.startup_id, 'Lock contention on a monitor*')) as actual_val,
           get_dur_on_main_thread_for_startup_and_slice(
-            launch.startup_id, 'lock contention on a monitor*', 3) as trace_slices,
+            launch.startup_id, 'Lock contention on a monitor*', 3)
+            as trace_slices,
           NULL as trace_threads,
           NULL as extra
         FROM android_startups launch
@@ -662,7 +751,7 @@
           AND android_sum_dur_on_main_thread_for_startup_and_slice(
             launch.startup_id,
             'Lock contention on a monitor*'
-          ) > launch.dur * 0.15
+          ) > launch.dur / 100 * threshold_monitor_contention_percentage()
 
         UNION ALL
         SELECT 'JIT compiled methods' as slow_cause,
@@ -670,14 +759,15 @@
           'JIT_COMPILED_METHODS' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 65,
+            'value', threshold_jit_compiled_methods_count(),
             'unit', 'COUNT',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
             'value', (SELECT COUNT(1)
               FROM ANDROID_SLICES_FOR_STARTUP_AND_SLICE_NAME(launch.startup_id, 'JIT compiling*')
               WHERE thread_name = 'Jit thread pool')) as actual_val,
-          get_slices_for_startup_and_slice_name(launch.startup_id, 'JIT compiling*', 3)
+          get_slices_for_startup_and_slice_name(launch.startup_id, 'JIT compiling*', 3,
+            (SELECT pid FROM android_startup_processes WHERE launch.startup_id = startup_id))
             as trace_slices,
           NULL as traced_threads,
           NULL as extra
@@ -686,7 +776,7 @@
           AND (
           SELECT COUNT(1)
           FROM ANDROID_SLICES_FOR_STARTUP_AND_SLICE_NAME(launch.startup_id, 'JIT compiling*')
-          WHERE thread_name = 'Jit thread pool') > 65
+          WHERE thread_name = 'Jit thread pool') > threshold_jit_compiled_methods_count()
 
         UNION ALL
         SELECT 'Broadcast dispatched count' as slow_cause,
@@ -694,13 +784,14 @@
           'BROADCAST_DISPATCHED_COUNT' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 15,
+            'value', threshold_broadcast_dispatched_count(),
             'unit', 'COUNT',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
             'value', count_slices_concurrent_to_launch(launch.startup_id,
               'Broadcast dispatched*')) as actual_val,
-          get_slices_concurrent_to_launch(launch.startup_id, 'Broadcast dispatched*', 3)
+          get_slices_concurrent_to_launch(launch.startup_id, 'Broadcast dispatched*', 3,
+            (SELECT pid FROM android_startup_processes WHERE launch.startup_id = startup_id))
             as trace_slices,
           NULL as trace_threads,
           NULL as extra
@@ -708,7 +799,7 @@
         WHERE launch.startup_id = $startup_id
           AND count_slices_concurrent_to_launch(
           launch.startup_id,
-          'Broadcast dispatched*') > 15
+          'Broadcast dispatched*') > threshold_broadcast_dispatched_count()
 
         UNION ALL
         SELECT 'Broadcast received count' as slow_cause,
@@ -716,13 +807,14 @@
           'BROADCAST_RECEIVED_COUNT' as reason_id,
           'WARNING' as severity,
           AndroidStartupMetric_ThresholdValue(
-            'value', 50,
+            'value', threshold_broadcast_received_count(),
             'unit', 'COUNT',
             'higher_expected', FALSE) as expected_val,
           AndroidStartupMetric_ActualValue(
             'value', count_slices_concurrent_to_launch(launch.startup_id,
               'broadcastReceiveReg*')) as actual_val,
-          get_slices_concurrent_to_launch(launch.startup_id, 'broadcastReceiveReg*', 3)
+          get_slices_concurrent_to_launch(launch.startup_id, 'broadcastReceiveReg*', 3,
+            (SELECT pid FROM android_startup_processes WHERE launch.startup_id = startup_id))
             as trace_slices,
           NULL as trace_threads,
           NULL as extra
@@ -730,7 +822,7 @@
         WHERE launch.startup_id = $startup_id
           AND count_slices_concurrent_to_launch(
             launch.startup_id,
-            'broadcastReceiveReg*') > 50
+            'broadcastReceiveReg*') > threshold_broadcast_received_count()
 
         UNION ALL
         SELECT 'Startup running concurrent to launch' as slow_cause,
@@ -768,7 +860,8 @@
             'unit', 'TRUE_OR_FALSE') as expected_val,
           AndroidStartupMetric_ActualValue(
             'value', TRUE) as actual_val,
-          get_main_thread_binder_transactions_blocked(launch.startup_id, 2e7, 3) as trace_slices,
+          get_main_thread_binder_transactions_blocked(launch.startup_id, 2e7, 3)
+            as trace_slices,
           NULL as trace_threads,
           NULL as extra
         FROM android_startups launch
diff --git a/src/trace_processor/metrics/sql/android/startup/slow_start_thresholds.sql b/src/trace_processor/metrics/sql/android/startup/slow_start_thresholds.sql
new file mode 100644
index 0000000..f8ef31b
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/startup/slow_start_thresholds.sql
@@ -0,0 +1,111 @@
+--
+-- Copyright 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
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+INCLUDE PERFETTO MODULE android.startup.startups;
+
+DROP VIEW IF EXISTS slow_start_thresholds;
+CREATE PERFETTO VIEW slow_start_thresholds AS
+SELECT
+  15 AS runnable_percentage,
+  2900000000 AS interruptible_sleep_ns,
+  450000000 AS blocking_io_ns,
+  20 AS open_dex_files_from_oat_percentage,
+  1250000000 AS bind_application_ns,
+  450000000 AS view_inflation_ns,
+  130000000 AS resources_manager_get_resources_ns,
+  15 AS verify_classes_percentage,
+  100000000 AS potential_cpu_contention_ns,
+  100000000 AS jit_activity_ns,
+  20 AS lock_contention_percentage,
+  15 AS monitor_contention_percentage,
+  65 AS jit_compiled_methods_count,
+  15 AS broadcast_dispatched_count,
+  50 AS broadcast_received_count;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_runnable_percentage()
+RETURNS INT AS
+  SELECT runnable_percentage 
+  FROM slow_start_thresholds;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_interruptible_sleep_ns()
+RETURNS INT AS
+  SELECT interruptible_sleep_ns
+  FROM slow_start_thresholds;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_blocking_io_ns()
+RETURNS INT AS
+  SELECT blocking_io_ns
+  FROM slow_start_thresholds;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_open_dex_files_from_oat_percentage()
+RETURNS INT AS
+  SELECT open_dex_files_from_oat_percentage
+  FROM slow_start_thresholds;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_bind_application_ns()
+RETURNS INT AS
+  SELECT bind_application_ns
+  FROM slow_start_thresholds;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_view_inflation_ns()
+RETURNS INT AS
+  SELECT view_inflation_ns
+  FROM slow_start_thresholds;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_resources_manager_get_resources_ns()
+RETURNS INT AS
+  SELECT resources_manager_get_resources_ns
+  FROM slow_start_thresholds;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_verify_classes_percentage()
+RETURNS INT AS
+  SELECT verify_classes_percentage
+  FROM slow_start_thresholds;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_potential_cpu_contention_ns()
+RETURNS INT AS
+  SELECT potential_cpu_contention_ns
+  FROM slow_start_thresholds;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_jit_activity_ns()
+RETURNS INT AS
+  SELECT jit_activity_ns
+  FROM slow_start_thresholds;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_lock_contention_percentage()
+RETURNS INT AS
+  SELECT lock_contention_percentage
+  FROM slow_start_thresholds;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_monitor_contention_percentage()
+RETURNS INT AS
+  SELECT monitor_contention_percentage
+  FROM slow_start_thresholds;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_jit_compiled_methods_count()
+RETURNS INT AS
+  SELECT jit_compiled_methods_count
+  FROM slow_start_thresholds;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_broadcast_dispatched_count()
+RETURNS INT AS
+  SELECT broadcast_dispatched_count
+  FROM slow_start_thresholds;
+
+CREATE OR REPLACE PERFETTO FUNCTION threshold_broadcast_received_count()
+RETURNS INT AS
+  SELECT broadcast_received_count
+  FROM slow_start_thresholds;
diff --git a/src/trace_processor/metrics/sql/android/wattson_app_startup.sql b/src/trace_processor/metrics/sql/android/wattson_app_startup.sql
deleted file mode 100644
index b242550..0000000
--- a/src/trace_processor/metrics/sql/android/wattson_app_startup.sql
+++ /dev/null
@@ -1,45 +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 android.startup.startups;
-
-DROP VIEW IF EXISTS _app_startup_window;
-CREATE PERFETTO VIEW _app_startup_window AS
-SELECT
-  ts,
-  dur,
-  startup_id as period_id
-FROM android_startups;
-
-SELECT RUN_METRIC(
-  'android/wattson_rail_relations.sql',
-  'window_table', '_app_startup_window'
-);
-
-DROP VIEW IF EXISTS wattson_app_startup_output;
-CREATE PERFETTO VIEW wattson_app_startup_output AS
-SELECT AndroidWattsonTimePeriodMetric(
-  'metric_version', 2,
-  'period_info', (
-    SELECT RepeatedField(
-      AndroidWattsonEstimateInfo(
-        'period_id', period_id,
-        'period_dur', period_dur,
-        'cpu_subsystem', proto
-      )
-    )
-    FROM _estimate_cpu_subsystem_sum
-  )
-);
diff --git a/src/trace_processor/metrics/sql/android/wattson_app_startup_rails.sql b/src/trace_processor/metrics/sql/android/wattson_app_startup_rails.sql
new file mode 100644
index 0000000..4490d5f
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/wattson_app_startup_rails.sql
@@ -0,0 +1,46 @@
+
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+INCLUDE PERFETTO MODULE android.startup.startups;
+
+DROP VIEW IF EXISTS _app_startup_window;
+CREATE PERFETTO VIEW _app_startup_window AS
+SELECT
+  ts,
+  dur,
+  startup_id as period_id
+FROM android_startups;
+
+SELECT RUN_METRIC(
+  'android/wattson_rail_relations.sql',
+  'window_table', '_app_startup_window'
+);
+
+DROP VIEW IF EXISTS wattson_app_startup_rails_output;
+CREATE PERFETTO VIEW wattson_app_startup_rails_output AS
+SELECT AndroidWattsonTimePeriodMetric(
+  'metric_version', 4,
+  'power_model_version', 1,
+  'period_info', (
+    SELECT RepeatedField(
+      AndroidWattsonEstimateInfo(
+        'period_id', period_id,
+        'period_dur', period_dur,
+        'cpu_subsystem', proto
+      )
+    )
+    FROM _estimate_cpu_subsystem_sum
+  )
+);
diff --git a/src/trace_processor/metrics/sql/android/wattson_markers_rails.sql b/src/trace_processor/metrics/sql/android/wattson_markers_rails.sql
new file mode 100644
index 0000000..f18a6fa
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/wattson_markers_rails.sql
@@ -0,0 +1,48 @@
+
+-- 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 wattson.curves.estimates;
+
+DROP VIEW IF EXISTS _wattson_period_windows;
+CREATE PERFETTO VIEW _wattson_period_windows AS
+SELECT
+  -- Requirement is there is exactly one pair of start/stop
+  (SELECT ts FROM slice WHERE name == 'wattson_start') as ts,
+  (SELECT ts FROM slice WHERE name == 'wattson_stop')
+  - (SELECT ts FROM slice WHERE name == 'wattson_start') as dur,
+  1 as period_id;
+
+SELECT RUN_METRIC(
+  'android/wattson_rail_relations.sql',
+  'window_table',
+  '_wattson_period_windows'
+);
+
+DROP VIEW IF EXISTS wattson_markers_rails_output;
+CREATE PERFETTO VIEW wattson_markers_rails_output AS
+SELECT AndroidWattsonTimePeriodMetric(
+  'metric_version', 4,
+  'power_model_version', 1,
+  'period_info', (
+    SELECT RepeatedField(
+      AndroidWattsonEstimateInfo(
+        'period_id', period_id,
+        'period_dur', period_dur,
+        'cpu_subsystem', proto
+      )
+    )
+    FROM _estimate_cpu_subsystem_sum
+  )
+);
diff --git a/src/trace_processor/metrics/sql/android/wattson_markers_threads.sql b/src/trace_processor/metrics/sql/android/wattson_markers_threads.sql
new file mode 100644
index 0000000..4074a20
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/wattson_markers_threads.sql
@@ -0,0 +1,48 @@
+
+-- 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 wattson.curves.estimates;
+INCLUDE PERFETTO MODULE viz.summary.threads_w_processes;
+
+DROP VIEW IF EXISTS _wattson_period_window;
+CREATE PERFETTO VIEW _wattson_period_window AS
+SELECT
+  -- Requirement is there is exactly one pair of start/stop
+  (SELECT ts FROM slice WHERE name == 'wattson_start') as ts,
+  (SELECT ts FROM slice WHERE name == 'wattson_stop')
+  - (SELECT ts FROM slice WHERE name == 'wattson_start') as dur,
+  1 as period_id;
+
+SELECT RUN_METRIC(
+  'android/wattson_tasks_attribution.sql',
+  'window_table',
+  '_wattson_period_window'
+);
+
+DROP VIEW IF EXISTS wattson_markers_threads_output;
+CREATE PERFETTO VIEW wattson_markers_threads_output AS
+SELECT AndroidWattsonTasksAttributionMetric(
+  'metric_version', 4,
+  'power_model_version', 1,
+  'period_info', (
+    SELECT RepeatedField(
+      AndroidWattsonTaskPeriodInfo(
+        'period_id', period_id,
+        'task_info', proto
+      )
+    )
+    FROM _wattson_per_task
+  )
+);
diff --git a/src/trace_processor/metrics/sql/android/wattson_rail_relations.sql b/src/trace_processor/metrics/sql/android/wattson_rail_relations.sql
index 57b805b..4c80ebb 100644
--- a/src/trace_processor/metrics/sql/android/wattson_rail_relations.sql
+++ b/src/trace_processor/metrics/sql/android/wattson_rail_relations.sql
@@ -16,13 +16,7 @@
 -- This file established the tables that define the relationships between rails
 -- and subrails as well as the hierarchical power estimates of each rail
 
-INCLUDE PERFETTO MODULE wattson.curves.ungrouped;
-
--- Take only the Wattson estimations that are in the window of interest
-DROP TABLE IF EXISTS _windowed_wattson;
-CREATE VIRTUAL TABLE _windowed_wattson
-USING
-  SPAN_JOIN({{window_table}}, _system_state_mw);
+INCLUDE PERFETTO MODULE wattson.curves.estimates;
 
 -- The most basic rail components that form the "building blocks" from which all
 -- other rails and components are derived. Average power over the entire trace
@@ -39,19 +33,27 @@
   (SELECT m.policy FROM _dev_cpu_policy_map AS m WHERE m.cpu = 6) as cpu6_poli,
   (SELECT m.policy FROM _dev_cpu_policy_map AS m WHERE m.cpu = 7) as cpu7_poli,
   -- Converts all mW of all slices into average mW of total trace
-  SUM(dur * cpu0_mw) / SUM(dur) as cpu0_mw,
-  SUM(dur * cpu1_mw) / SUM(dur) as cpu1_mw,
-  SUM(dur * cpu2_mw) / SUM(dur) as cpu2_mw,
-  SUM(dur * cpu3_mw) / SUM(dur) as cpu3_mw,
-  SUM(dur * cpu4_mw) / SUM(dur) as cpu4_mw,
-  SUM(dur * cpu5_mw) / SUM(dur) as cpu5_mw,
-  SUM(dur * cpu6_mw) / SUM(dur) as cpu6_mw,
-  SUM(dur * cpu7_mw) / SUM(dur) as cpu7_mw,
-  SUM(dur * dsu_scu_mw) / SUM(dur) as dsu_scu_mw,
-  SUM(dur) as period_dur,
-  period_id
-FROM _windowed_wattson
-GROUP BY period_id;
+  SUM(ii.dur * ss.cpu0_mw) / SUM(ii.dur) as cpu0_mw,
+  SUM(ii.dur * ss.cpu1_mw) / SUM(ii.dur) as cpu1_mw,
+  SUM(ii.dur * ss.cpu2_mw) / SUM(ii.dur) as cpu2_mw,
+  SUM(ii.dur * ss.cpu3_mw) / SUM(ii.dur) as cpu3_mw,
+  SUM(ii.dur * ss.cpu4_mw) / SUM(ii.dur) as cpu4_mw,
+  SUM(ii.dur * ss.cpu5_mw) / SUM(ii.dur) as cpu5_mw,
+  SUM(ii.dur * ss.cpu6_mw) / SUM(ii.dur) as cpu6_mw,
+  SUM(ii.dur * ss.cpu7_mw) / SUM(ii.dur) as cpu7_mw,
+  SUM(ii.dur * ss.dsu_scu_mw) / SUM(ii.dur) as dsu_scu_mw,
+  SUM(ii.dur) as period_dur,
+  w.period_id
+FROM _interval_intersect!(
+  (
+    (SELECT period_id AS id, * FROM {{window_table}}),
+    _ii_subquery!(_system_state_mw)
+  ),
+  ()
+) ii
+JOIN {{window_table}} AS w ON w.period_id = id_0
+JOIN _system_state_mw AS ss ON ss._auto_id = id_1
+GROUP BY w.period_id;
 
 -- Macro that filters out CPUs that are unrelated to the policy of the table
 -- passed in, and does some bookkeeping to put data in expected format
@@ -76,47 +78,77 @@
     is_defined,
     period_id,
     period_dur,
-    cast_double!(IIF(is_defined, sum_mw, NULL)) as estimate_mw,
+    cast_double!(IIF(is_defined, sum_mw, NULL)) as estimated_mw,
+    cast_double!(
+      IIF(is_defined, sum_mw * period_dur / 1e9, NULL)
+    ) as estimated_mws,
     AndroidWattsonPolicyEstimate(
-      'estimate_mw', cast_double!(IIF(is_defined, sum_mw, NULL)),
+      'estimated_mw', cast_double!(IIF(is_defined, sum_mw, NULL)),
+      'estimated_mws', cast_double!(
+        IIF(is_defined, sum_mw * period_dur / 1e9, NULL)
+      ),
       'cpu0', IIF(
         cpu0_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu0_mw),
+        AndroidWattsonCpuEstimate(
+          'estimated_mw', cpu0_mw,
+          'estimated_mws', cpu0_mw * period_dur / 1e9
+        ),
         NULL
       ),
       'cpu1', IIF(
         cpu1_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu1_mw),
+        AndroidWattsonCpuEstimate(
+          'estimated_mw', cpu1_mw,
+          'estimated_mws', cpu1_mw * period_dur / 1e9
+        ),
         NULL
       ),
       'cpu2', IIF(
         cpu2_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu2_mw),
+        AndroidWattsonCpuEstimate(
+          'estimated_mw', cpu2_mw,
+          'estimated_mws', cpu2_mw * period_dur / 1e9
+        ),
         NULL
       ),
       'cpu3', IIF(
         cpu3_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu3_mw),
+        AndroidWattsonCpuEstimate(
+          'estimated_mw', cpu3_mw,
+          'estimated_mws', cpu3_mw * period_dur / 1e9
+        ),
         NULL
       ),
       'cpu4', IIF(
         cpu4_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu4_mw),
+        AndroidWattsonCpuEstimate(
+          'estimated_mw', cpu4_mw,
+          'estimated_mws', cpu4_mw * period_dur / 1e9
+        ),
         NULL
       ),
       'cpu5', IIF(
         cpu5_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu5_mw),
+        AndroidWattsonCpuEstimate(
+          'estimated_mw', cpu5_mw,
+          'estimated_mws', cpu5_mw * period_dur / 1e9
+        ),
         NULL
       ),
       'cpu6', IIF(
         cpu6_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu6_mw),
+        AndroidWattsonCpuEstimate(
+          'estimated_mw', cpu6_mw,
+          'estimated_mws', cpu6_mw * period_dur / 1e9
+        ),
         NULL
       ),
       'cpu7', IIF(
         cpu7_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu7_mw),
+        AndroidWattsonCpuEstimate(
+          'estimated_mw', cpu7_mw,
+          'estimated_mws', cpu7_mw * period_dur / 1e9
+        ),
         NULL
       )
     ) AS proto
@@ -304,21 +336,21 @@
     period_id,
     period_dur,
     dsu_scu.dsu_scu_mw,
-    IIF(p0.is_defined, p0.estimate_mw, NULL) as p0_mw,
+    IIF(p0.is_defined, p0.estimated_mw, NULL) as p0_mw,
     IIF(p0.is_defined, p0.proto, NULL) as p0_proto,
-    IIF(p1.is_defined, p1.estimate_mw, NULL) as p1_mw,
+    IIF(p1.is_defined, p1.estimated_mw, NULL) as p1_mw,
     IIF(p1.is_defined, p1.proto, NULL) as p1_proto,
-    IIF(p2.is_defined, p2.estimate_mw, NULL) as p2_mw,
+    IIF(p2.is_defined, p2.estimated_mw, NULL) as p2_mw,
     IIF(p2.is_defined, p2.proto, NULL) as p2_proto,
-    IIF(p3.is_defined, p3.estimate_mw, NULL) as p3_mw,
+    IIF(p3.is_defined, p3.estimated_mw, NULL) as p3_mw,
     IIF(p3.is_defined, p3.proto, NULL) as p3_proto,
-    IIF(p4.is_defined, p4.estimate_mw, NULL) as p4_mw,
+    IIF(p4.is_defined, p4.estimated_mw, NULL) as p4_mw,
     IIF(p4.is_defined, p4.proto, NULL) as p4_proto,
-    IIF(p5.is_defined, p5.estimate_mw, NULL) as p5_mw,
+    IIF(p5.is_defined, p5.estimated_mw, NULL) as p5_mw,
     IIF(p5.is_defined, p5.proto, NULL) as p5_proto,
-    IIF(p6.is_defined, p6.estimate_mw, NULL) as p6_mw,
+    IIF(p6.is_defined, p6.estimated_mw, NULL) as p6_mw,
     IIF(p6.is_defined, p6.proto, NULL) as p6_proto,
-    IIF(p7.is_defined, p7.estimate_mw, NULL) as p7_mw,
+    IIF(p7.is_defined, p7.estimated_mw, NULL) as p7_mw,
     IIF(p7.is_defined, p7.proto, NULL) as p7_proto
   FROM _estimate_policy0_proto AS p0
   JOIN _estimate_policy1_proto AS p1 USING (period_id, period_dur)
@@ -344,7 +376,8 @@
   period_id,
   period_dur,
   AndroidWattsonCpuSubsystemEstimate(
-    'estimate_mw', sum_mw,
+    'estimated_mw', sum_mw,
+    'estimated_mws', sum_mw * period_dur / 1e9,
     'policy0', p0_proto,
     'policy1', p1_proto,
     'policy2', p2_proto,
@@ -353,7 +386,10 @@
     'policy5', p5_proto,
     'policy6', p6_proto,
     'policy7', p7_proto,
-    'dsu_scu', AndroidWattsonDsuScuEstimate('estimate_mw', dsu_scu_mw)
+    'dsu_scu', AndroidWattsonDsuScuEstimate(
+      'estimated_mw', dsu_scu_mw,
+      'estimated_mws', dsu_scu_mw * period_dur / 1e9
+    )
   ) as proto
 FROM components_w_sum;
 
diff --git a/src/trace_processor/metrics/sql/android/wattson_tasks_attribution.sql b/src/trace_processor/metrics/sql/android/wattson_tasks_attribution.sql
index 8f98714..d023354 100644
--- a/src/trace_processor/metrics/sql/android/wattson_tasks_attribution.sql
+++ b/src/trace_processor/metrics/sql/android/wattson_tasks_attribution.sql
@@ -13,58 +13,152 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 
-INCLUDE PERFETTO MODULE wattson.curves.grouped;
+INCLUDE PERFETTO MODULE wattson.curves.estimates;
+INCLUDE PERFETTO MODULE wattson.curves.idle_attribution;
 INCLUDE PERFETTO MODULE viz.summary.threads_w_processes;
 
 -- Take only the Wattson estimations that are in the window of interest
-DROP TABLE IF EXISTS _windowed_wattson;
-CREATE VIRTUAL TABLE _windowed_wattson
-USING
-  SPAN_JOIN({{window_table}}, _system_state_mw);
+DROP VIEW IF EXISTS _windowed_wattson;
+CREATE PERFETTO VIEW _windowed_wattson AS
+SELECT
+  ii.ts,
+  ii.dur,
+  ii.id_1 as period_id,
+  ss.cpu0_mw,
+  ss.cpu1_mw,
+  ss.cpu2_mw,
+  ss.cpu3_mw,
+  ss.cpu4_mw,
+  ss.cpu5_mw,
+  ss.cpu6_mw,
+  ss.cpu7_mw,
+  ss.dsu_scu_mw
+FROM _interval_intersect!(
+  (
+    _ii_subquery!(_system_state_mw),
+    (SELECT ts, dur, period_id as id FROM {{window_table}})
+  ),
+  ()
+) ii
+JOIN _system_state_mw AS ss ON ss._auto_id = id_0;
 
 -- "Unpivot" the table so that table can by PARTITIONED BY cpu
 DROP TABLE IF EXISTS _unioned_windowed_wattson;
 CREATE PERFETTO TABLE _unioned_windowed_wattson AS
-  SELECT ts, dur, 0 as cpu, cpu0_mw as estimate_mw
+  SELECT ts, dur, 0 as cpu, cpu0_mw as estimated_mw, period_id
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 0 = cpu)
   UNION ALL
-  SELECT ts, dur, 1 as cpu, cpu1_mw as estimate_mw
+  SELECT ts, dur, 1 as cpu, cpu1_mw as estimated_mw, period_id
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 1 = cpu)
   UNION ALL
-  SELECT ts, dur, 2 as cpu, cpu2_mw as estimate_mw
+  SELECT ts, dur, 2 as cpu, cpu2_mw as estimated_mw, period_id
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 2 = cpu)
   UNION ALL
-  SELECT ts, dur, 3 as cpu, cpu3_mw as estimate_mw
+  SELECT ts, dur, 3 as cpu, cpu3_mw as estimated_mw, period_id
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 3 = cpu)
   UNION ALL
-  SELECT ts, dur, 4 as cpu, cpu4_mw as estimate_mw
+  SELECT ts, dur, 4 as cpu, cpu4_mw as estimated_mw, period_id
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 4 = cpu)
   UNION ALL
-  SELECT ts, dur, 5 as cpu, cpu5_mw as estimate_mw
+  SELECT ts, dur, 5 as cpu, cpu5_mw as estimated_mw, period_id
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 5 = cpu)
   UNION ALL
-  SELECT ts, dur, 6 as cpu, cpu6_mw as estimate_mw
+  SELECT ts, dur, 6 as cpu, cpu6_mw as estimated_mw, period_id
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 6 = cpu)
   UNION ALL
-  SELECT ts, dur, 7 as cpu, cpu7_mw as estimate_mw
+  SELECT ts, dur, 7 as cpu, cpu7_mw as estimated_mw, period_id
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 7 = cpu)
   UNION ALL
-  SELECT ts, dur, -1 as cpu, dsu_scu_mw as estimate_mw
+  SELECT ts, dur, -1 as cpu, dsu_scu_mw as estimated_mw, period_id
   FROM _windowed_wattson;
 
 DROP TABLE IF EXISTS _windowed_threads_system_state;
-CREATE VIRTUAL TABLE _windowed_threads_system_state
-USING
-  SPAN_JOIN(
-    _unioned_windowed_wattson partitioned cpu,
-    _sched_w_thread_process_package_summary partitioned cpu
-  );
+CREATE PERFETTO TABLE _windowed_threads_system_state AS
+SELECT
+  ii.ts,
+  ii.dur,
+  ii.cpu,
+  uw.estimated_mw,
+  s.thread_name,
+  s.process_name,
+  s.tid,
+  s.pid,
+  s.utid,
+  uw.period_id
+FROM _interval_intersect!(
+  (
+    _ii_subquery!(_unioned_windowed_wattson),
+    _ii_subquery!(_sched_w_thread_process_package_summary)
+  ),
+  (cpu)
+) ii
+JOIN _unioned_windowed_wattson AS uw ON uw._auto_id = id_0
+JOIN _sched_w_thread_process_package_summary AS s ON s._auto_id = id_1;
+
+-- Get idle overhead attribution per thread
+DROP VIEW IF EXISTS _per_thread_idle_attribution;
+CREATE PERFETTO VIEW _per_thread_idle_attribution AS
+SELECT
+  SUM(cost.estimated_mw * cost.dur) / 1e9 as idle_cost_mws,
+  cost.utid,
+  ii.id_1 as period_id
+FROM _interval_intersect!(
+  (
+    _ii_subquery!(_idle_transition_cost),
+    (SELECT ts, dur, period_id as id FROM {{window_table}})
+  ),
+  ()
+) ii
+JOIN _idle_transition_cost as cost ON cost._auto_id = id_0
+GROUP BY utid, period_id;
+
+-- Group by unique thread ID and disregard CPUs, summing of power over all CPUs
+-- and all instances of the thread
+DROP VIEW IF EXISTS _wattson_thread_attribution;
+CREATE PERFETTO VIEW _wattson_thread_attribution AS
+SELECT
+  -- active time of thread divided by total time where Wattson is defined
+  SUM(estimated_mw * dur) / 1000000000 as estimated_mws,
+  (
+    SUM(estimated_mw * dur) / (SELECT SUM(dur) from _windowed_wattson)
+  ) as estimated_mw,
+  idle_cost_mws,
+  thread_name,
+  process_name,
+  tid,
+  pid,
+  period_id
+FROM _windowed_threads_system_state
+LEFT JOIN _per_thread_idle_attribution USING (utid, period_id)
+GROUP BY utid, period_id
+ORDER BY estimated_mw DESC;
+
+-- Create proto format task attribution for each period
+DROP VIEW IF EXISTS _wattson_per_task;
+CREATE PERFETTO VIEW _wattson_per_task AS
+SELECT
+  period_id,
+  (
+    SELECT RepeatedField(
+      AndroidWattsonTaskInfo(
+        'estimated_mws', ROUND(estimated_mws, 6),
+        'estimated_mw', ROUND(estimated_mw, 6),
+        'idle_transitions_mws', ROUND(idle_cost_mws, 6),
+        'thread_name', thread_name,
+        'process_name', process_name,
+        'thread_id', tid,
+        'process_id', pid
+      )
+    )
+  ) as proto
+FROM _wattson_thread_attribution
+GROUP BY period_id;
 
diff --git a/src/trace_processor/metrics/sql/android/wattson_trace_rails.sql b/src/trace_processor/metrics/sql/android/wattson_trace_rails.sql
index ab035c0..3c4af17 100644
--- a/src/trace_processor/metrics/sql/android/wattson_trace_rails.sql
+++ b/src/trace_processor/metrics/sql/android/wattson_trace_rails.sql
@@ -13,17 +13,15 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 
-INCLUDE PERFETTO MODULE wattson.curves.ungrouped;
+INCLUDE PERFETTO MODULE wattson.curves.estimates;
 
--- The power calculations need to use the same time period in which energy
--- calculations were made for consistency
+-- This metric is defined to be for entire trace duration
 DROP VIEW IF EXISTS _wattson_period_windows;
 CREATE PERFETTO VIEW _wattson_period_windows AS
 SELECT
-  MIN(ts) as ts,
-  MAX(ts) - MIN(ts) as dur,
-  1 as period_id
-FROM _system_state_mw;
+  trace_start() as ts,
+  trace_dur() as dur,
+  1 as period_id;
 
 SELECT RUN_METRIC(
   'android/wattson_rail_relations.sql',
@@ -33,7 +31,8 @@
 DROP VIEW IF EXISTS wattson_trace_rails_output;
 CREATE PERFETTO VIEW wattson_trace_rails_output AS
 SELECT AndroidWattsonTimePeriodMetric(
-  'metric_version', 2,
+  'metric_version', 4,
+  'power_model_version', 1,
   'period_info', (
     SELECT RepeatedField(
       AndroidWattsonEstimateInfo(
diff --git a/src/trace_processor/metrics/sql/android/wattson_trace_threads.sql b/src/trace_processor/metrics/sql/android/wattson_trace_threads.sql
index 455b876..fb8071f 100644
--- a/src/trace_processor/metrics/sql/android/wattson_trace_threads.sql
+++ b/src/trace_processor/metrics/sql/android/wattson_trace_threads.sql
@@ -13,56 +13,35 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 
-INCLUDE PERFETTO MODULE wattson.curves.grouped;
+INCLUDE PERFETTO MODULE wattson.curves.estimates;
 INCLUDE PERFETTO MODULE viz.summary.threads_w_processes;
 
-DROP VIEW IF EXISTS _wattson_period_windows;
-CREATE PERFETTO VIEW _wattson_period_windows AS
+-- This metric is defined to be for entire trace duration
+DROP VIEW IF EXISTS _wattson_period_window;
+CREATE PERFETTO VIEW _wattson_period_window AS
 SELECT
-  MIN(ts) as ts,
-  MAX(ts) - MIN(ts) as dur,
-  1 as period_id
-FROM _system_state_mw;
+  trace_start() as ts,
+  trace_dur() as dur,
+  1 as period_id;
 
 SELECT RUN_METRIC(
   'android/wattson_tasks_attribution.sql',
   'window_table',
-  '_wattson_period_windows'
+  '_wattson_period_window'
 );
 
--- Group by unique thread ID and disregard CPUs, summing of power over all CPUs
--- and all instances of the thread
-DROP VIEW IF EXISTS _wattson_thread_attribution;
-CREATE PERFETTO VIEW _wattson_thread_attribution AS
-SELECT
-  -- active time of thread divided by total time of trace
-  SUM(estimate_mw * dur) / 1000000000 as estimate_mws,
-  (
-    SUM(estimate_mw * dur) / (SELECT SUM(dur) from _windowed_wattson)
-  ) as estimate_mw,
-  thread_name,
-  process_name,
-  tid,
-  pid
-FROM _windowed_threads_system_state
-GROUP BY utid
-ORDER BY estimate_mw DESC;
-
 DROP VIEW IF EXISTS wattson_trace_threads_output;
 CREATE PERFETTO VIEW wattson_trace_threads_output AS
 SELECT AndroidWattsonTasksAttributionMetric(
-  'metric_version', 1,
-  'task_info', (
+  'metric_version', 4,
+  'power_model_version', 1,
+  'period_info', (
     SELECT RepeatedField(
-      AndroidWattsonTaskInfo(
-        'estimate_mws', ROUND(estimate_mws, 6),
-        'estimate_mw', ROUND(estimate_mw, 6),
-        'thread_name', thread_name,
-        'process_name', process_name,
-        'thread_id', tid,
-        'process_id', pid
+      AndroidWattsonTaskPeriodInfo(
+        'period_id', period_id,
+        'task_info', proto
       )
     )
-    FROM _wattson_thread_attribution
+    FROM _wattson_per_task
   )
 );
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_base.sql b/src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_base.sql
index 788ff3b..3545234 100644
--- a/src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_base.sql
+++ b/src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_base.sql
@@ -55,22 +55,6 @@
           ELSE "unknown" END)
   ELSE "regular" END AS delay_type;
 
--- Checks if slice has a descendant with provided name.
-CREATE OR REPLACE PERFETTO FUNCTION _has_descendant_slice_with_name(
-  -- Id of the slice to check descendants of.
-  id INT,
-  -- Name of potential descendant slice.
-  descendant_name STRING
-)
--- Whether `descendant_name` is a name of an descendant slice.
-RETURNS BOOL AS
-SELECT EXISTS(
-  SELECT 1
-  FROM descendant_slice($id)
-  WHERE name = $descendant_name
-  LIMIT 1
-);
-
 -- Get all EventLatency events for scroll updates to use their
 -- flows later on to decide how much time we waited from queueing the event
 -- until we started processing it.
@@ -87,10 +71,16 @@
   {{slice_table_name}} AS s JOIN args USING(arg_set_id)
 WHERE
   NAME = "EventLatency"
-  AND (args.string_value GLOB "*GESTURE_SCROLL_UPDATE"
-  OR args.string_value = "GESTURE_SCROLL_END")
-  AND _has_descendant_slice_with_name(
-    s.id, "SubmitCompositorFrameToPresentationCompositorFrame")
+  AND EXISTS(
+    SELECT 1
+    FROM descendant_slice(s.id)
+    WHERE name = "SubmitCompositorFrameToPresentationCompositorFrame"
+    LIMIT 1
+    )
+    AND (
+      args.string_value GLOB "*GESTURE_SCROLL_UPDATE"
+      OR args.string_value = "GESTURE_SCROLL_END"
+    )
 ORDER BY trace_id;
 
 -- Get all chrome_latency_info_for_gesture_slices where trace_ids are not -1,
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_scroll_inputs_per_frame.sql b/src/trace_processor/metrics/sql/chrome/chrome_scroll_inputs_per_frame.sql
index 94d2794..62d84af 100644
--- a/src/trace_processor/metrics/sql/chrome/chrome_scroll_inputs_per_frame.sql
+++ b/src/trace_processor/metrics/sql/chrome/chrome_scroll_inputs_per_frame.sql
@@ -21,7 +21,22 @@
 -- The numbers mentioned above are estimates in the ideal case scenario.
 
 INCLUDE PERFETTO MODULE chrome.scroll_jank.utils;
-INCLUDE PERFETTO MODULE common.slices;
+
+-- Checks if slice has a descendant with provided name.
+CREATE OR REPLACE PERFETTO FUNCTION _has_descendant_slice_with_name(
+  -- Id of the slice to check descendants of.
+  id INT,
+  -- Name of potential descendant slice.
+  descendant_name STRING
+)
+-- Whether `descendant_name` is a name of an descendant slice.
+RETURNS BOOL AS
+SELECT EXISTS(
+  SELECT 1
+  FROM descendant_slice($id)
+  WHERE name = $descendant_name
+  LIMIT 1
+);
 
 -- Grab all GestureScrollUpdate slices.
 DROP VIEW IF EXISTS chrome_all_scroll_updates;
@@ -29,7 +44,7 @@
 SELECT
   S.id,
   chrome_get_most_recent_scroll_begin_id(ts) AS scroll_id,
-  has_descendant_slice_with_name(S.id, "SubmitCompositorFrameToPresentationCompositorFrame")
+  _has_descendant_slice_with_name(S.id, "SubmitCompositorFrameToPresentationCompositorFrame")
   AS is_presented,
   ts,
   dur,
diff --git a/src/trace_processor/metrics/sql/common/parent_slice.sql b/src/trace_processor/metrics/sql/common/parent_slice.sql
index d5c6f24..5478d93 100644
--- a/src/trace_processor/metrics/sql/common/parent_slice.sql
+++ b/src/trace_processor/metrics/sql/common/parent_slice.sql
@@ -13,5 +13,3 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 --
-
-INCLUDE PERFETTO MODULE deprecated.v42.common.slices;
diff --git a/src/trace_processor/metrics/sql/experimental/chrome_dropped_frames.sql b/src/trace_processor/metrics/sql/experimental/chrome_dropped_frames.sql
index 2c33bc0..1fef9b1 100644
--- a/src/trace_processor/metrics/sql/experimental/chrome_dropped_frames.sql
+++ b/src/trace_processor/metrics/sql/experimental/chrome_dropped_frames.sql
@@ -53,36 +53,6 @@
 JOIN process
   ON dropped_frames_with_upid.upid = process.upid;
 
--- Create the derived event track for dropped frames.
--- All tracks generated from chrome_dropped_frames_event are
--- placed under a track group named 'Dropped Frames', whose summary
--- track is the first track ('All Processes') in chrome_dropped_frames_event.
--- Note that the 'All Processes' track is generated only when dropped frames
--- come from more than one origin process.
-DROP VIEW IF EXISTS chrome_dropped_frames_event;
-CREATE PERFETTO VIEW chrome_dropped_frames_event AS
-SELECT
-  'slice' AS track_type,
-  'All Processes' AS track_name,
-  ts,
-  0 AS dur,
-  'Dropped Frame' AS slice_name,
-  'Dropped Frames' AS group_name
-FROM dropped_frames_with_process_info
-WHERE (SELECT COUNT(DISTINCT process_id)
-                    FROM dropped_frames_with_process_info) > 1
-GROUP BY ts
-UNION ALL
-SELECT
-  'slice' AS track_type,
-  COALESCE(process_name, 'Process') || ' ' || process_id AS track_name,
-  ts,
-  0 AS dur,
-  'Dropped Frame' AS slice_name,
-  'Dropped Frames' AS group_name
-FROM dropped_frames_with_process_info
-GROUP BY process_id, ts;
-
 -- Create the dropped frames metric output.
 DROP VIEW IF EXISTS chrome_dropped_frames_output;
 CREATE PERFETTO VIEW chrome_dropped_frames_output AS
diff --git a/src/trace_processor/metrics/sql/experimental/chrome_long_latency.sql b/src/trace_processor/metrics/sql/experimental/chrome_long_latency.sql
index d8b49c6..d8e1eb2 100644
--- a/src/trace_processor/metrics/sql/experimental/chrome_long_latency.sql
+++ b/src/trace_processor/metrics/sql/experimental/chrome_long_latency.sql
@@ -63,36 +63,6 @@
   ON long_latency_with_upid.upid = process.upid
 GROUP BY ts, process.pid;
 
--- Create the derived event track for long latency.
--- All tracks generated from chrome_long_latency_event are
--- placed under a track group named 'Long Latency', whose summary
--- track is the first track ('All Processes') in chrome_long_latency_event.
--- Note that the 'All Processes' track is generated only when there are more
--- than one source of long latency events.
-DROP VIEW IF EXISTS chrome_long_latency_event;
-CREATE PERFETTO VIEW chrome_long_latency_event AS
-SELECT
-  'slice' AS track_type,
-  'All Processes' AS track_name,
-  ts,
-  0 AS dur,
-  event_type AS slice_name,
-  'Long Latency' AS group_name
-FROM long_latency_with_process_info
-WHERE (SELECT COUNT(DISTINCT process_id)
-                    FROM long_latency_with_process_info) > 1
-GROUP BY ts
-UNION ALL
-SELECT
-  'slice' AS track_type,
-  process_name || ' ' || process_id AS track_name,
-  ts,
-  0 AS dur,
-  event_type AS slice_name,
-  'Long Latency' AS group_name
-FROM long_latency_with_process_info
-GROUP BY ts;
-
 -- Create the long latency metric output.
 DROP VIEW IF EXISTS chrome_long_latency_output;
 CREATE PERFETTO VIEW chrome_long_latency_output AS
diff --git a/src/trace_processor/metrics/sql/trace_metadata.sql b/src/trace_processor/metrics/sql/trace_metadata.sql
index cbad75d..1e45573 100644
--- a/src/trace_processor/metrics/sql/trace_metadata.sql
+++ b/src/trace_processor/metrics/sql/trace_metadata.sql
@@ -13,6 +13,7 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 --
+INCLUDE PERFETTO MODULE android.suspend;
 
 DROP VIEW IF EXISTS trace_metadata_output;
 CREATE PERFETTO VIEW trace_metadata_output AS
@@ -22,6 +23,9 @@
   'android_build_fingerprint', (
     SELECT str_value FROM metadata WHERE name = 'android_build_fingerprint'
   ),
+  'android_device_manufacturer', (
+    SELECT str_value FROM metadata WHERE name = 'android_device_manufacturer'
+  ),
   'statsd_triggering_subscription_id', (
     SELECT int_value FROM metadata
     WHERE name = 'statsd_triggering_subscription_id'
@@ -49,5 +53,22 @@
   'tracing_started_ns', (
     SELECT int_value FROM metadata
     WHERE name='tracing_started_ns'
+  ),
+  'android_sdk_version', (
+    SELECT int_value FROM metadata
+    WHERE name = 'android_sdk_version'
+  ),
+  'suspend_count', (
+    SELECT COUNT() FROM android_suspend_state WHERE power_state = 'suspended'
+  ),
+  'data_loss_count', (
+      SELECT COUNT()
+      FROM stats
+      WHERE severity = 'data_loss' AND value > 0
+  ),
+  'error_count', (
+      SELECT COUNT()
+      FROM stats
+      WHERE severity = 'error' AND value > 0
   )
 );
diff --git a/src/trace_processor/perfetto_sql/engine/BUILD.gn b/src/trace_processor/perfetto_sql/engine/BUILD.gn
index a721a51..adb6b6d 100644
--- a/src/trace_processor/perfetto_sql/engine/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/engine/BUILD.gn
@@ -20,14 +20,8 @@
   sources = [
     "created_function.cc",
     "created_function.h",
-    "function_util.cc",
-    "function_util.h",
     "perfetto_sql_engine.cc",
     "perfetto_sql_engine.h",
-    "perfetto_sql_parser.cc",
-    "perfetto_sql_parser.h",
-    "perfetto_sql_preprocessor.cc",
-    "perfetto_sql_preprocessor.h",
     "runtime_table_function.cc",
     "runtime_table_function.h",
     "table_pointer_module.cc",
@@ -50,17 +44,15 @@
     "../../util",
     "../../util:sql_argument",
     "../../util:stdlib",
+    "../parser",
+    "../preprocessor",
+    "../tokenizer",
   ]
 }
 
 perfetto_unittest_source_set("unittests") {
   testonly = true
-  sources = [
-    "perfetto_sql_engine_unittest.cc",
-    "perfetto_sql_parser_unittest.cc",
-    "perfetto_sql_preprocessor_unittest.cc",
-    "perfetto_sql_test_utils.h",
-  ]
+  sources = [ "perfetto_sql_engine_unittest.cc" ]
   deps = [
     ":engine",
     "../../../../gn:default_deps",
diff --git a/src/trace_processor/perfetto_sql/engine/created_function.cc b/src/trace_processor/perfetto_sql/engine/created_function.cc
index be4f956..1c01c17 100644
--- a/src/trace_processor/perfetto_sql/engine/created_function.cc
+++ b/src/trace_processor/perfetto_sql/engine/created_function.cc
@@ -21,8 +21,8 @@
 #include <stack>
 
 #include "perfetto/base/status.h"
-#include "src/trace_processor/perfetto_sql/engine/function_util.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
+#include "src/trace_processor/perfetto_sql/parser/function_util.h"
 #include "src/trace_processor/sqlite/scoped_db.h"
 #include "src/trace_processor/sqlite/sql_source.h"
 #include "src/trace_processor/sqlite/sqlite_engine.h"
diff --git a/src/trace_processor/perfetto_sql/engine/created_function.h b/src/trace_processor/perfetto_sql/engine/created_function.h
index 4d744e5..f93a4e3 100644
--- a/src/trace_processor/perfetto_sql/engine/created_function.h
+++ b/src/trace_processor/perfetto_sql/engine/created_function.h
@@ -23,8 +23,8 @@
 
 #include "perfetto/base/status.h"
 #include "perfetto/trace_processor/basic_types.h"
-#include "src/trace_processor/perfetto_sql/engine/function_util.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/sql_function.h"
+#include "src/trace_processor/perfetto_sql/parser/function_util.h"
 #include "src/trace_processor/sqlite/sql_source.h"
 #include "src/trace_processor/types/destructible.h"
 #include "src/trace_processor/util/sql_argument.h"
diff --git a/src/trace_processor/perfetto_sql/engine/function_util.cc b/src/trace_processor/perfetto_sql/engine/function_util.cc
deleted file mode 100644
index ae3edde..0000000
--- a/src/trace_processor/perfetto_sql/engine/function_util.cc
+++ /dev/null
@@ -1,121 +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.
- */
-
-#include "src/trace_processor/perfetto_sql/engine/function_util.h"
-
-#include "perfetto/base/status.h"
-#include "perfetto/ext/base/string_view.h"
-#include "src/trace_processor/sqlite/sqlite_utils.h"
-#include "src/trace_processor/util/status_macros.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-std::string FunctionPrototype::ToString() const {
-  return function_name + "(" + SerializeArguments(arguments) + ")";
-}
-
-base::Status ParseFunctionName(base::StringView raw, base::StringView& out) {
-  size_t function_name_end = raw.find('(');
-  if (function_name_end == base::StringView::npos)
-    return base::ErrStatus("unable to find bracket starting argument list");
-
-  base::StringView function_name = raw.substr(0, function_name_end);
-  if (!sql_argument::IsValidName(function_name)) {
-    return base::ErrStatus("function name %s is not alphanumeric",
-                           function_name.ToStdString().c_str());
-  }
-  out = function_name;
-  return base::OkStatus();
-}
-
-base::Status ParsePrototype(base::StringView raw, FunctionPrototype& out) {
-  // Examples of function prototypes:
-  // ANDROID_SDK_LEVEL()
-  // STARTUP_SLICE(dur_ns INT)
-  // FIND_NEXT_SLICE_WITH_NAME(ts INT, name STRING)
-
-  base::StringView function_name;
-  RETURN_IF_ERROR(ParseFunctionName(raw, function_name));
-
-  size_t function_name_end = function_name.size();
-  size_t args_start = function_name_end + 1;
-  size_t args_end = raw.find(')', args_start);
-  if (args_end == base::StringView::npos)
-    return base::ErrStatus("unable to find bracket ending argument list");
-
-  base::StringView args_str = raw.substr(args_start, args_end - args_start);
-  RETURN_IF_ERROR(sql_argument::ParseArgumentDefinitions(args_str.ToStdString(),
-                                                         out.arguments));
-
-  out.function_name = function_name.ToStdString();
-  return base::OkStatus();
-}
-
-base::Status SqliteRetToStatus(sqlite3* db,
-                               const std::string& function_name,
-                               int ret) {
-  if (ret != SQLITE_ROW && ret != SQLITE_DONE) {
-    return base::ErrStatus("%s: SQLite error while executing function body: %s",
-                           function_name.c_str(), sqlite3_errmsg(db));
-  }
-  return base::OkStatus();
-}
-
-base::Status MaybeBindArgument(sqlite3_stmt* stmt,
-                               const std::string& function_name,
-                               const sql_argument::ArgumentDefinition& arg,
-                               sqlite3_value* value) {
-  int index = sqlite3_bind_parameter_index(stmt, arg.dollar_name().c_str());
-
-  // If the argument is not in the query, this just means its an unused
-  // argument which we can just ignore.
-  if (index == 0)
-    return base::Status();
-
-  int ret = sqlite3_bind_value(stmt, index, value);
-  if (ret != SQLITE_OK) {
-    return base::ErrStatus(
-        "%s: SQLite error while binding value to argument %s: %s",
-        function_name.c_str(), arg.name().c_str(),
-        sqlite3_errmsg(sqlite3_db_handle(stmt)));
-  }
-  return base::OkStatus();
-}
-
-base::Status MaybeBindIntArgument(sqlite3_stmt* stmt,
-                                  const std::string& function_name,
-                                  const sql_argument::ArgumentDefinition& arg,
-                                  int64_t value) {
-  int index = sqlite3_bind_parameter_index(stmt, arg.dollar_name().c_str());
-
-  // If the argument is not in the query, this just means its an unused
-  // argument which we can just ignore.
-  if (index == 0)
-    return base::Status();
-
-  int ret = sqlite3_bind_int64(stmt, index, value);
-  if (ret != SQLITE_OK) {
-    return base::ErrStatus(
-        "%s: SQLite error while binding value to argument %s: %s",
-        function_name.c_str(), arg.name().c_str(),
-        sqlite3_errmsg(sqlite3_db_handle(stmt)));
-  }
-  return base::OkStatus();
-}
-
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/perfetto_sql/engine/function_util.h b/src/trace_processor/perfetto_sql/engine/function_util.h
deleted file mode 100644
index 1467e7e..0000000
--- a/src/trace_processor/perfetto_sql/engine/function_util.h
+++ /dev/null
@@ -1,67 +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.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_FUNCTION_UTIL_H_
-#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_FUNCTION_UTIL_H_
-
-#include <sqlite3.h>
-#include <optional>
-#include <string>
-
-#include "perfetto/base/status.h"
-#include "perfetto/ext/base/string_view.h"
-#include "src/trace_processor/util/sql_argument.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-struct FunctionPrototype {
-  std::string function_name;
-  std::vector<sql_argument::ArgumentDefinition> arguments;
-
-  std::string ToString() const;
-
-  bool operator==(const FunctionPrototype& other) const {
-    return function_name == other.function_name && arguments == other.arguments;
-  }
-  bool operator!=(const FunctionPrototype& other) const {
-    return !(*this == other);
-  }
-};
-
-base::Status ParseFunctionName(base::StringView raw,
-                               base::StringView& function_name);
-
-base::Status ParsePrototype(base::StringView raw, FunctionPrototype& out);
-
-base::Status SqliteRetToStatus(sqlite3* db,
-                               const std::string& function_name,
-                               int ret);
-
-base::Status MaybeBindArgument(sqlite3_stmt*,
-                               const std::string& function_name,
-                               const sql_argument::ArgumentDefinition&,
-                               sqlite3_value*);
-
-base::Status MaybeBindIntArgument(sqlite3_stmt*,
-                                  const std::string& function_name,
-                                  const sql_argument::ArgumentDefinition&,
-                                  int64_t);
-
-}  // namespace trace_processor
-}  // namespace perfetto
-
-#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_FUNCTION_UTIL_H_
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 86bf583..36bd391 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
@@ -44,11 +44,11 @@
 #include "src/trace_processor/db/runtime_table.h"
 #include "src/trace_processor/db/table.h"
 #include "src/trace_processor/perfetto_sql/engine/created_function.h"
-#include "src/trace_processor/perfetto_sql/engine/function_util.h"
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h"
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h"
 #include "src/trace_processor/perfetto_sql/engine/runtime_table_function.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
+#include "src/trace_processor/perfetto_sql/parser/function_util.h"
+#include "src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.h"
+#include "src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.h"
 #include "src/trace_processor/sqlite/db_sqlite_table.h"
 #include "src/trace_processor/sqlite/scoped_db.h"
 #include "src/trace_processor/sqlite/sql_source.h"
@@ -563,9 +563,9 @@
 base::Status PerfettoSqlEngine::ExecuteCreateTable(
     const PerfettoSqlParser::CreateTable& create_table) {
   PERFETTO_TP_TRACE(metatrace::Category::QUERY_TIMELINE,
-                    "CREATE_PERFETTO_TABLE",
+                    "CREATE PERFETTO TABLE",
                     [&create_table](metatrace::Record* record) {
-                      record->AddArg("Table", create_table.name);
+                      record->AddArg("table_name", create_table.name);
                     });
 
   auto stmt_or = engine_->PrepareStatement(create_table.sql);
@@ -631,6 +631,11 @@
 
 base::Status PerfettoSqlEngine::ExecuteCreateView(
     const PerfettoSqlParser::CreateView& create_view) {
+  PERFETTO_TP_TRACE(metatrace::Category::QUERY_TIMELINE, "CREATE PERFETTO VIEW",
+                    [&create_view](metatrace::Record* record) {
+                      record->AddArg("view_name", create_view.name);
+                    });
+
   // Verify that the underlying SQL statement is valid.
   auto stmt = sqlite_engine()->PrepareStatement(create_view.select_sql);
   RETURN_IF_ERROR(stmt.status());
@@ -677,7 +682,8 @@
       sqlite_engine()->GetFunctionContext(name, kSupportedArgCount));
   if (!ctx) {
     return base::ErrStatus(
-        "EXPERIMENTAL_MEMOIZE: Function %s(INT) does not exist", name.c_str());
+        "EXPERIMENTAL_MEMOIZE: Function '%s'(INT) does not exist",
+        name.c_str());
   }
   return CreatedFunction::EnableMemoization(ctx);
 }
@@ -685,28 +691,46 @@
 base::Status PerfettoSqlEngine::ExecuteInclude(
     const PerfettoSqlParser::Include& include,
     const PerfettoSqlParser& parser) {
-  std::string key = include.key;
-  PERFETTO_TP_TRACE(metatrace::Category::QUERY_TIMELINE, "Include",
-                    [key](metatrace::Record* r) { r->AddArg("Module", key); });
+  PERFETTO_TP_TRACE(
+      metatrace::Category::QUERY_TIMELINE, "INCLUDE PERFETTO MODULE",
+      [&include](metatrace::Record* r) { r->AddArg("include", include.key); });
 
+  std::string key = include.key;
   if (key == "*") {
-    for (auto moduleIt = modules_.GetIterator(); moduleIt; ++moduleIt) {
-      RETURN_IF_ERROR(IncludeModuleImpl(moduleIt.value(), key, parser));
+    for (auto package = packages_.GetIterator(); package; ++package) {
+      RETURN_IF_ERROR(IncludePackageImpl(package.value(), key, parser));
     }
     return base::OkStatus();
   }
 
-  std::string module_name = sql_modules::GetModuleName(key);
-  auto* module = FindModule(module_name);
-  if (!module) {
-    return base::ErrStatus("INCLUDE: Unknown module name provided - %s",
-                           key.c_str());
+  std::string package_name = sql_modules::GetPackageName(key);
+
+  auto* package = FindPackage(package_name);
+  if (!package) {
+    if (package_name == "common") {
+      return base::ErrStatus(
+          "INCLUDE: Package `common` has been removed and most of the "
+          "functionality has been moved to other packages. Check "
+          "`slices.with_context` for replacement for `common.slices` and "
+          "`time.conversion` for replacement for `common.timestamps`. The "
+          "documentation for Perfetto standard library can be found at "
+          "https://perfetto.dev/docs/analysis/stdlib-docs.");
+    }
+    return base::ErrStatus("INCLUDE: Package '%s' not found", key.c_str());
   }
-  return IncludeModuleImpl(*module, key, parser);
+  return IncludePackageImpl(*package, key, parser);
 }
 
 base::Status PerfettoSqlEngine::ExecuteCreateIndex(
     const PerfettoSqlParser::CreateIndex& index) {
+  PERFETTO_TP_TRACE(metatrace::Category::QUERY_TIMELINE,
+                    "CREATE PERFETTO INDEX",
+                    [&index](metatrace::Record* record) {
+                      record->AddArg("index_name", index.name);
+                      record->AddArg("table_name", index.table_name);
+                      record->AddArg("cols", base::Join(index.col_names, ", "));
+                    });
+
   Table* t = GetMutableTableOrNull(index.table_name);
   if (!t) {
     return base::ErrStatus("CREATE PERFETTO INDEX: Table '%s' not found",
@@ -729,6 +753,12 @@
 
 base::Status PerfettoSqlEngine::ExecuteDropIndex(
     const PerfettoSqlParser::DropIndex& index) {
+  PERFETTO_TP_TRACE(metatrace::Category::QUERY_TIMELINE, "DROP PERFETTO INDEX",
+                    [&index](metatrace::Record* record) {
+                      record->AddArg("index_name", index.name);
+                      record->AddArg("table_name", index.table_name);
+                    });
+
   Table* t = GetMutableTableOrNull(index.table_name);
   if (!t) {
     return base::ErrStatus("DROP PERFETTO INDEX: Table '%s' not found",
@@ -738,35 +768,34 @@
   return base::OkStatus();
 }
 
-base::Status PerfettoSqlEngine::IncludeModuleImpl(
-    sql_modules::RegisteredModule& module,
-    const std::string& key,
+base::Status PerfettoSqlEngine::IncludePackageImpl(
+    sql_modules::RegisteredPackage& package,
+    const std::string& include_key,
     const PerfettoSqlParser& parser) {
-  if (!key.empty() && key.back() == '*') {
+  if (!include_key.empty() && include_key.back() == '*') {
     // If the key ends with a wildcard, iterate through all the keys in the
     // module and include matching ones.
-    std::string prefix = key.substr(0, key.size() - 1);
-    for (auto fileIt = module.include_key_to_file.GetIterator(); fileIt;
-         ++fileIt) {
-      if (!base::StartsWith(fileIt.key(), prefix))
+    std::string prefix = include_key.substr(0, include_key.size() - 1);
+    for (auto module = package.modules.GetIterator(); module; ++module) {
+      if (!base::StartsWith(module.key(), prefix))
         continue;
       PERFETTO_TP_TRACE(
           metatrace::Category::QUERY_TIMELINE,
           "Include (expanded from wildcard)",
-          [&](metatrace::Record* r) { r->AddArg("Module", fileIt.key()); });
-      RETURN_IF_ERROR(IncludeFileImpl(fileIt.value(), fileIt.key(), parser));
+          [&](metatrace::Record* r) { r->AddArg("Module", module.key()); });
+      RETURN_IF_ERROR(IncludeModuleImpl(module.value(), module.key(), parser));
     }
     return base::OkStatus();
   }
-  auto* module_file = module.include_key_to_file.Find(key);
+  auto* module_file = package.modules.Find(include_key);
   if (!module_file) {
-    return base::ErrStatus("INCLUDE: unknown module '%s'", key.c_str());
+    return base::ErrStatus("INCLUDE: unknown module '%s'", include_key.c_str());
   }
-  return IncludeFileImpl(*module_file, key, parser);
+  return IncludeModuleImpl(*module_file, include_key, parser);
 }
 
-base::Status PerfettoSqlEngine::IncludeFileImpl(
-    sql_modules::RegisteredModule::ModuleFile& file,
+base::Status PerfettoSqlEngine::IncludeModuleImpl(
+    sql_modules::RegisteredPackage::ModuleFile& file,
     const std::string& key,
     const PerfettoSqlParser& parser) {
   // INCLUDE is noop for already included files.
@@ -788,6 +817,14 @@
 
 base::Status PerfettoSqlEngine::ExecuteCreateFunction(
     const PerfettoSqlParser::CreateFunction& cf) {
+  PERFETTO_TP_TRACE(metatrace::Category::QUERY_TIMELINE,
+                    "CREATE PERFETTO FUNCTION",
+                    [&cf](metatrace::Record* record) {
+                      record->AddArg("name", cf.prototype.function_name);
+                      record->AddArg("prototype", cf.prototype.ToString());
+                      record->AddArg("returns", cf.returns);
+                    });
+
   if (!cf.is_table) {
     return RegisterRuntimeFunction(cf.replace, cf.prototype, cf.returns,
                                    cf.sql);
@@ -915,6 +952,12 @@
 
 base::Status PerfettoSqlEngine::ExecuteCreateMacro(
     const PerfettoSqlParser::CreateMacro& create_macro) {
+  PERFETTO_TP_TRACE(metatrace::Category::QUERY_TIMELINE,
+                    "CREATE PERFETTO MACRO",
+                    [&create_macro](metatrace::Record* record) {
+                      record->AddArg("name", create_macro.name.sql());
+                    });
+
   // Check that the argument types is one of the allowed types.
   for (const auto& [name, type] : create_macro.args) {
     if (!IsTokenAllowedInMacro(type.sql())) {
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 50eb1ff..f09d8c8 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
@@ -33,12 +33,12 @@
 #include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/db/runtime_table.h"
 #include "src/trace_processor/db/table.h"
-#include "src/trace_processor/perfetto_sql/engine/function_util.h"
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h"
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h"
 #include "src/trace_processor/perfetto_sql/engine/runtime_table_function.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/sql_function.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
+#include "src/trace_processor/perfetto_sql/parser/function_util.h"
+#include "src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.h"
+#include "src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_result.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_window_function.h"
 #include "src/trace_processor/sqlite/db_sqlite_table.h"
@@ -194,16 +194,16 @@
 
   SqliteEngine* sqlite_engine() { return engine_.get(); }
 
-  // Makes new SQL module available to import.
-  void RegisterModule(const std::string& name,
-                      sql_modules::RegisteredModule module) {
-    modules_.Erase(name);
-    modules_.Insert(name, std::move(module));
+  // Makes new SQL package available to include.
+  void RegisterPackage(const std::string& name,
+                       sql_modules::RegisteredPackage package) {
+    packages_.Erase(name);
+    packages_.Insert(name, std::move(package));
   }
 
-  // Fetches registered SQL module.
-  sql_modules::RegisteredModule* FindModule(const std::string& name) {
-    return modules_.Find(name);
+  // Fetches registered SQL package.
+  sql_modules::RegisteredPackage* FindPackage(const std::string& name) {
+    return packages_.Find(name);
   }
 
   // Returns the number of objects (tables, views, functions etc) registered
@@ -317,18 +317,17 @@
       const std::vector<sql_argument::ArgumentDefinition>& schema,
       const char* tag) const;
 
-  // Given a module and a key, include the correct file(s) from the module.
+  // Given a package and a key, include the correct file(s) from the package.
   // The key can contain a wildcard to include all files in the module with the
   // matching prefix.
-  base::Status IncludeModuleImpl(sql_modules::RegisteredModule& module,
-                                 const std::string& key,
-                                 const PerfettoSqlParser& parser);
+  base::Status IncludePackageImpl(sql_modules::RegisteredPackage&,
+                                  const std::string& key,
+                                  const PerfettoSqlParser&);
 
-  // Import a given file.
-  base::Status IncludeFileImpl(
-      sql_modules::RegisteredModule::ModuleFile& module,
-      const std::string& key,
-      const PerfettoSqlParser& parser);
+  // Include a given module.
+  base::Status IncludeModuleImpl(sql_modules::RegisteredPackage::ModuleFile&,
+                                 const std::string& key,
+                                 const PerfettoSqlParser&);
 
   StringPool* pool_ = nullptr;
   // If true, engine will perform additional consistency checks when e.g.
@@ -344,7 +343,7 @@
   DbSqliteModule::Context* runtime_table_context_ = nullptr;
   DbSqliteModule::Context* static_table_context_ = nullptr;
   DbSqliteModule::Context* static_table_fn_context_ = nullptr;
-  base::FlatHashMap<std::string, sql_modules::RegisteredModule> modules_;
+  base::FlatHashMap<std::string, sql_modules::RegisteredPackage> packages_;
   base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro> macros_;
   std::unique_ptr<SqliteEngine> engine_;
 };
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 9116e4c..a1e8f54 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
@@ -30,12 +30,12 @@
   PerfettoSqlEngine engine_{&pool_, true};
 };
 
-sql_modules::RegisteredModule CreateTestModule(
+sql_modules::RegisteredPackage CreateTestPackage(
     std::vector<std::pair<std::string, std::string>> files) {
-  sql_modules::RegisteredModule result;
+  sql_modules::RegisteredPackage result;
   for (auto& file : files) {
-    result.include_key_to_file[file.first] =
-        sql_modules::RegisteredModule::ModuleFile{file.second, false};
+    result.modules[file.first] =
+        sql_modules::RegisteredPackage::ModuleFile{file.second, false};
   }
   return result;
 }
@@ -54,7 +54,7 @@
 
   res = engine_.Execute(
       SqlSource::FromExecuteQuery("creatE PeRfEttO FUNCTION foo(x INT, y LONG) "
-                                  "RETURNS INT AS select :x + :y"));
+                                  "RETURNS INT AS select $x + $y"));
   ASSERT_TRUE(res.ok()) << res.status().c_message();
 }
 
@@ -283,43 +283,38 @@
 }
 
 TEST_F(PerfettoSqlEngineTest, Include_All) {
-  engine_.RegisterModule(
-      "foo", CreateTestModule(
+  engine_.RegisterPackage(
+      "foo", CreateTestPackage(
                  {{"foo.foo", "CREATE PERFETTO TABLE foo AS SELECT 42 AS x"}}));
-  engine_.RegisterModule(
+  engine_.RegisterPackage(
       "bar",
-      CreateTestModule(
+      CreateTestPackage(
           {{"bar.bar", "CREATE PERFETTO TABLE bar AS SELECT 42 AS x "}}));
 
   auto res_create =
       engine_.Execute(SqlSource::FromExecuteQuery("INCLUDE PERFETTO MODULE *"));
   ASSERT_TRUE(res_create.ok()) << res_create.status().c_message();
-  ASSERT_TRUE(
-      engine_.FindModule("foo")->include_key_to_file["foo.foo"].included);
-  ASSERT_TRUE(
-      engine_.FindModule("bar")->include_key_to_file["bar.bar"].included);
+  ASSERT_TRUE(engine_.FindPackage("foo")->modules["foo.foo"].included);
+  ASSERT_TRUE(engine_.FindPackage("bar")->modules["bar.bar"].included);
 }
 
 TEST_F(PerfettoSqlEngineTest, Include_Module) {
-  engine_.RegisterModule(
-      "foo", CreateTestModule({
+  engine_.RegisterPackage(
+      "foo", CreateTestPackage({
                  {"foo.foo1", "CREATE PERFETTO TABLE foo1 AS SELECT 42 AS x"},
                  {"foo.foo2", "CREATE PERFETTO TABLE foo2 AS SELECT 42 AS x"},
              }));
-  engine_.RegisterModule(
+  engine_.RegisterPackage(
       "bar",
-      CreateTestModule(
+      CreateTestPackage(
           {{"bar.bar", "CREATE PERFETTO TABLE bar AS SELECT 42 AS x "}}));
 
   auto res_create = engine_.Execute(
       SqlSource::FromExecuteQuery("INCLUDE PERFETTO MODULE foo.*"));
   ASSERT_TRUE(res_create.ok()) << res_create.status().c_message();
-  ASSERT_TRUE(
-      engine_.FindModule("foo")->include_key_to_file["foo.foo1"].included);
-  ASSERT_TRUE(
-      engine_.FindModule("foo")->include_key_to_file["foo.foo2"].included);
-  ASSERT_FALSE(
-      engine_.FindModule("bar")->include_key_to_file["bar.bar"].included);
+  ASSERT_TRUE(engine_.FindPackage("foo")->modules["foo.foo1"].included);
+  ASSERT_TRUE(engine_.FindPackage("foo")->modules["foo.foo2"].included);
+  ASSERT_FALSE(engine_.FindPackage("bar")->modules["bar.bar"].included);
 }
 
 TEST_F(PerfettoSqlEngineTest, MismatchedRange) {
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc
deleted file mode 100644
index 2c71e64..0000000
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc
+++ /dev/null
@@ -1,656 +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.
- */
-
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h"
-
-#include <algorithm>
-#include <cctype>
-#include <functional>
-#include <optional>
-#include <string>
-#include <utility>
-#include <vector>
-
-#include "perfetto/base/compiler.h"
-#include "perfetto/base/logging.h"
-#include "perfetto/base/status.h"
-#include "perfetto/ext/base/flat_hash_map.h"
-#include "perfetto/ext/base/string_utils.h"
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h"
-#include "src/trace_processor/sqlite/sql_source.h"
-#include "src/trace_processor/sqlite/sqlite_tokenizer.h"
-
-namespace perfetto {
-namespace trace_processor {
-namespace {
-
-using Token = SqliteTokenizer::Token;
-using Statement = PerfettoSqlParser::Statement;
-
-enum class State {
-  kDrop,
-  kDropPerfetto,
-  kCreate,
-  kCreateOr,
-  kCreateOrReplace,
-  kCreateOrReplacePerfetto,
-  kCreatePerfetto,
-  kInclude,
-  kIncludePerfetto,
-  kPassthrough,
-  kStmtStart,
-};
-
-bool KeywordEqual(std::string_view expected, std::string_view actual) {
-  PERFETTO_DCHECK(std::all_of(expected.begin(), expected.end(), islower));
-  return std::equal(expected.begin(), expected.end(), actual.begin(),
-                    actual.end(),
-                    [](char a, char b) { return a == tolower(b); });
-}
-
-bool TokenIsSqliteKeyword(std::string_view keyword, SqliteTokenizer::Token t) {
-  return t.token_type == SqliteTokenType::TK_GENERIC_KEYWORD &&
-         KeywordEqual(keyword, t.str);
-}
-
-bool TokenIsCustomKeyword(std::string_view keyword, SqliteTokenizer::Token t) {
-  return t.token_type == SqliteTokenType::TK_ID && KeywordEqual(keyword, t.str);
-}
-
-bool IsValidModuleWord(const std::string& word) {
-  for (const char& c : word) {
-    if (!std::isalnum(c) && (c != '_') && !std::islower(c)) {
-      return false;
-    }
-  }
-  return true;
-}
-
-bool ValidateModuleName(const std::string& name) {
-  if (name.empty()) {
-    return false;
-  }
-
-  std::vector<std::string> packages = base::SplitString(name, ".");
-
-  // The last part of the path can be a wildcard.
-  if (!packages.empty() && packages.back() == "*") {
-    packages.pop_back();
-  }
-
-  // The rest of the path must be valid words.
-  return std::find_if(packages.begin(), packages.end(),
-                      std::not_fn(IsValidModuleWord)) == packages.end();
-}
-
-}  // namespace
-
-PerfettoSqlParser::PerfettoSqlParser(
-    SqlSource source,
-    const base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro>&
-        macros)
-    : preprocessor_(std::move(source), macros),
-      tokenizer_(SqlSource::FromTraceProcessorImplementation("")) {}
-
-bool PerfettoSqlParser::Next() {
-  PERFETTO_CHECK(status_.ok());
-
-  if (!preprocessor_.NextStatement()) {
-    status_ = preprocessor_.status();
-    return false;
-  }
-  tokenizer_.Reset(preprocessor_.statement());
-
-  State state = State::kStmtStart;
-  std::optional<Token> first_non_space_token;
-  for (Token token = tokenizer_.Next();; token = tokenizer_.Next()) {
-    // Space should always be completely ignored by any logic below as it will
-    // never change the current state in the state machine.
-    if (token.token_type == SqliteTokenType::TK_SPACE) {
-      continue;
-    }
-
-    if (token.IsTerminal()) {
-      // If we have a non-space character we've seen, just return all the stuff
-      // after that point.
-      if (first_non_space_token) {
-        statement_ = SqliteSql{};
-        statement_sql_ = tokenizer_.Substr(*first_non_space_token, token);
-        return true;
-      }
-      // This means we've seen a semi-colon without any non-space content. Just
-      // try and find the next statement as this "statement" is a noop.
-      if (token.token_type == SqliteTokenType::TK_SEMI) {
-        continue;
-      }
-      // This means we've reached the end of the SQL.
-      PERFETTO_DCHECK(token.str.empty());
-      return false;
-    }
-
-    // If we've not seen a space character, keep track of the current position.
-    if (!first_non_space_token) {
-      first_non_space_token = token;
-    }
-
-    switch (state) {
-      case State::kPassthrough:
-        statement_ = SqliteSql{};
-        statement_sql_ = preprocessor_.statement();
-        return true;
-      case State::kStmtStart:
-        if (TokenIsSqliteKeyword("create", token)) {
-          state = State::kCreate;
-        } else if (TokenIsCustomKeyword("include", token)) {
-          state = State::kInclude;
-        } else if (TokenIsSqliteKeyword("drop", token)) {
-          state = State::kDrop;
-        } else {
-          state = State::kPassthrough;
-        }
-        break;
-      case State::kInclude:
-        if (TokenIsCustomKeyword("perfetto", token)) {
-          state = State::kIncludePerfetto;
-        } else {
-          return ErrorAtToken(token,
-                              "Use 'INCLUDE PERFETTO MODULE {include_key}'.");
-        }
-        break;
-      case State::kIncludePerfetto:
-        if (TokenIsCustomKeyword("module", token)) {
-          return ParseIncludePerfettoModule(*first_non_space_token);
-        } else {
-          return ErrorAtToken(token,
-                              "Use 'INCLUDE PERFETTO MODULE {include_key}'.");
-        }
-      case State::kDrop:
-        if (TokenIsCustomKeyword("perfetto", token)) {
-          state = State::kDropPerfetto;
-        } else {
-          state = State::kPassthrough;
-        }
-        break;
-      case State::kDropPerfetto:
-        if (TokenIsSqliteKeyword("index", token)) {
-          return ParseDropPerfettoIndex(*first_non_space_token);
-        } else {
-          return ErrorAtToken(token, "Only Perfetto index can be dropped");
-        }
-      case State::kCreate:
-        if (TokenIsSqliteKeyword("trigger", token)) {
-          // TODO(lalitm): add this to the "errors" documentation page
-          // explaining why this is the case.
-          return ErrorAtToken(
-              token, "Creating triggers is not supported in PerfettoSQL.");
-        }
-        if (TokenIsCustomKeyword("perfetto", token)) {
-          state = State::kCreatePerfetto;
-        } else if (TokenIsSqliteKeyword("or", token)) {
-          state = State::kCreateOr;
-        } else {
-          state = State::kPassthrough;
-        }
-        break;
-      case State::kCreateOr:
-        state = TokenIsSqliteKeyword("replace", token) ? State::kCreateOrReplace
-                                                       : State::kPassthrough;
-        break;
-      case State::kCreateOrReplace:
-        state = TokenIsCustomKeyword("perfetto", token)
-                    ? State::kCreateOrReplacePerfetto
-                    : State::kPassthrough;
-        break;
-      case State::kCreateOrReplacePerfetto:
-      case State::kCreatePerfetto:
-        bool replace = state == State::kCreateOrReplacePerfetto;
-        if (TokenIsCustomKeyword("function", token)) {
-          return ParseCreatePerfettoFunction(replace, *first_non_space_token);
-        }
-        if (TokenIsSqliteKeyword("table", token)) {
-          return ParseCreatePerfettoTableOrView(replace, *first_non_space_token,
-                                                TableOrView::kTable);
-        }
-        if (TokenIsSqliteKeyword("view", token)) {
-          return ParseCreatePerfettoTableOrView(replace, *first_non_space_token,
-                                                TableOrView::kView);
-        }
-        if (TokenIsCustomKeyword("macro", token)) {
-          return ParseCreatePerfettoMacro(replace);
-        }
-        if (TokenIsSqliteKeyword("index", token)) {
-          return ParseCreatePerfettoIndex(replace, *first_non_space_token);
-        }
-        base::StackString<1024> err(
-            "Expected 'FUNCTION', 'TABLE', 'MACRO' OR 'INDEX' after 'CREATE "
-            "PERFETTO', received '%*s'.",
-            static_cast<int>(token.str.size()), token.str.data());
-        return ErrorAtToken(token, err.c_str());
-    }
-  }
-}
-
-bool PerfettoSqlParser::ParseIncludePerfettoModule(
-    Token first_non_space_token) {
-  auto tok = tokenizer_.NextNonWhitespace();
-  auto terminal = tokenizer_.NextTerminal();
-  std::string key = tokenizer_.Substr(tok, terminal).sql();
-
-  if (!ValidateModuleName(key)) {
-    base::StackString<1024> err(
-        "Include key should be a dot-separated list of module names, with the "
-        "last name optionally being a wildcard: '%s'",
-        key.c_str());
-    return ErrorAtToken(tok, err.c_str());
-  }
-
-  statement_ = Include{key};
-  statement_sql_ = tokenizer_.Substr(first_non_space_token, terminal);
-  return true;
-}
-
-bool PerfettoSqlParser::ParseCreatePerfettoTableOrView(
-    bool replace,
-    Token first_non_space_token,
-    TableOrView table_or_view) {
-  Token table_name = tokenizer_.NextNonWhitespace();
-  if (table_name.token_type != SqliteTokenType::TK_ID) {
-    base::StackString<1024> err("Invalid table name %.*s",
-                                static_cast<int>(table_name.str.size()),
-                                table_name.str.data());
-    return ErrorAtToken(table_name, err.c_str());
-  }
-  std::string name(table_name.str);
-  std::vector<sql_argument::ArgumentDefinition> schema;
-
-  auto token = tokenizer_.NextNonWhitespace();
-
-  // If the next token is a left parenthesis, then the table or view have a
-  // schema.
-  if (token.token_type == SqliteTokenType::TK_LP) {
-    if (!ParseArguments(schema)) {
-      return false;
-    }
-    token = tokenizer_.NextNonWhitespace();
-  }
-
-  if (!TokenIsSqliteKeyword("as", token)) {
-    base::StackString<1024> err(
-        "Expected 'AS' after table_name, received "
-        "%*s.",
-        static_cast<int>(token.str.size()), token.str.data());
-    return ErrorAtToken(token, err.c_str());
-  }
-
-  Token first = tokenizer_.NextNonWhitespace();
-  Token terminal = tokenizer_.NextTerminal();
-  switch (table_or_view) {
-    case TableOrView::kTable:
-      statement_ = CreateTable{replace, std::move(name),
-                               tokenizer_.Substr(first, terminal), schema};
-      break;
-    case TableOrView::kView:
-      SqlSource original_statement =
-          tokenizer_.Substr(first_non_space_token, terminal);
-      SqlSource header = SqlSource::FromTraceProcessorImplementation(
-          "CREATE VIEW " + name + " AS ");
-      SqlSource::Rewriter rewriter(original_statement);
-      tokenizer_.Rewrite(rewriter, first_non_space_token, first, header,
-                         SqliteTokenizer::EndToken::kExclusive);
-      statement_ = CreateView{replace, std::move(name),
-                              tokenizer_.Substr(first, terminal),
-                              std::move(rewriter).Build(), schema};
-      break;
-  }
-  statement_sql_ = tokenizer_.Substr(first_non_space_token, terminal);
-  return true;
-}
-
-bool PerfettoSqlParser::ParseCreatePerfettoIndex(bool replace,
-                                                 Token first_non_space_token) {
-  Token index_name_tok = tokenizer_.NextNonWhitespace();
-  if (index_name_tok.token_type != SqliteTokenType::TK_ID) {
-    base::StackString<1024> err("Invalid index name %.*s",
-                                static_cast<int>(index_name_tok.str.size()),
-                                index_name_tok.str.data());
-    return ErrorAtToken(index_name_tok, err.c_str());
-  }
-  std::string index_name(index_name_tok.str);
-
-  auto token = tokenizer_.NextNonWhitespace();
-  if (!TokenIsSqliteKeyword("on", token)) {
-    base::StackString<1024> err("Expected 'ON' after index name, received %*s.",
-                                static_cast<int>(token.str.size()),
-                                token.str.data());
-    return ErrorAtToken(token, err.c_str());
-  }
-
-  Token table_name_tok = tokenizer_.NextNonWhitespace();
-  if (table_name_tok.token_type != SqliteTokenType::TK_ID) {
-    base::StackString<1024> err("Invalid table name %.*s",
-                                static_cast<int>(table_name_tok.str.size()),
-                                table_name_tok.str.data());
-    return ErrorAtToken(table_name_tok, err.c_str());
-  }
-  std::string table_name(table_name_tok.str);
-
-  token = tokenizer_.NextNonWhitespace();
-  if (token.token_type != SqliteTokenType::TK_LP) {
-    base::StackString<1024> err(
-        "Expected parenthesis after table name, received '%*s'.",
-        static_cast<int>(token.str.size()), token.str.data());
-    return ErrorAtToken(token, err.c_str());
-  }
-
-  std::vector<std::string> cols;
-
-  do {
-    Token col_name_tok = tokenizer_.NextNonWhitespace();
-    cols.push_back(std::string(col_name_tok.str));
-    token = tokenizer_.NextNonWhitespace();
-  } while (token.token_type == SqliteTokenType::TK_COMMA);
-
-  if (token.token_type != SqliteTokenType::TK_RP) {
-    base::StackString<1024> err("Expected closed parenthesis, received '%*s'.",
-                                static_cast<int>(token.str.size()),
-                                token.str.data());
-    return ErrorAtToken(token, err.c_str());
-  }
-
-  Token terminal = tokenizer_.NextTerminal();
-  statement_sql_ = tokenizer_.Substr(first_non_space_token, terminal);
-  statement_ = CreateIndex{replace, index_name, table_name, cols};
-  return true;
-}
-
-bool PerfettoSqlParser::ParseDropPerfettoIndex(
-    SqliteTokenizer::Token first_non_space_token) {
-  Token index_name_tok = tokenizer_.NextNonWhitespace();
-  if (index_name_tok.token_type != SqliteTokenType::TK_ID) {
-    base::StackString<1024> err("Invalid index name %.*s",
-                                static_cast<int>(index_name_tok.str.size()),
-                                index_name_tok.str.data());
-    return ErrorAtToken(index_name_tok, err.c_str());
-  }
-  std::string index_name(index_name_tok.str);
-
-  auto token = tokenizer_.NextNonWhitespace();
-  if (!TokenIsSqliteKeyword("on", token)) {
-    base::StackString<1024> err("Expected 'ON' after index name, received %*s.",
-                                static_cast<int>(token.str.size()),
-                                token.str.data());
-    return ErrorAtToken(token, err.c_str());
-  }
-
-  Token table_name_tok = tokenizer_.NextNonWhitespace();
-  if (table_name_tok.token_type != SqliteTokenType::TK_ID) {
-    base::StackString<1024> err("Invalid table name %.*s",
-                                static_cast<int>(table_name_tok.str.size()),
-                                table_name_tok.str.data());
-    return ErrorAtToken(table_name_tok, err.c_str());
-  }
-  std::string table_name(table_name_tok.str);
-
-  token = tokenizer_.NextNonWhitespace();
-  if (!token.IsTerminal()) {
-    return ErrorAtToken(
-        token, "Nothing is allowed after table name in DROP PERFETTO INDEX");
-  }
-  statement_sql_ = tokenizer_.Substr(first_non_space_token, token);
-  statement_ = DropIndex{index_name, table_name};
-  return true;
-}
-
-bool PerfettoSqlParser::ParseCreatePerfettoFunction(
-    bool replace,
-    Token first_non_space_token) {
-  Token function_name = tokenizer_.NextNonWhitespace();
-  if (function_name.token_type != SqliteTokenType::TK_ID) {
-    // TODO(lalitm): add a link to create function documentation.
-    base::StackString<1024> err("Invalid function name %.*s",
-                                static_cast<int>(function_name.str.size()),
-                                function_name.str.data());
-    return ErrorAtToken(function_name, err.c_str());
-  }
-
-  // TK_LP == '(' (i.e. left parenthesis).
-  if (Token lp = tokenizer_.NextNonWhitespace();
-      lp.token_type != SqliteTokenType::TK_LP) {
-    // TODO(lalitm): add a link to create function documentation.
-    return ErrorAtToken(lp, "Malformed function prototype: '(' expected");
-  }
-
-  std::vector<sql_argument::ArgumentDefinition> args;
-  if (!ParseArguments(args)) {
-    return false;
-  }
-
-  if (Token returns = tokenizer_.NextNonWhitespace();
-      !TokenIsCustomKeyword("returns", returns)) {
-    // TODO(lalitm): add a link to create function documentation.
-    return ErrorAtToken(returns, "Expected keyword 'returns'");
-  }
-
-  Token ret_token = tokenizer_.NextNonWhitespace();
-  std::string ret;
-  bool table_return = TokenIsSqliteKeyword("table", ret_token);
-  if (table_return) {
-    if (Token lp = tokenizer_.NextNonWhitespace();
-        lp.token_type != SqliteTokenType::TK_LP) {
-      // TODO(lalitm): add a link to create function documentation.
-      return ErrorAtToken(lp, "Malformed table return: '(' expected");
-    }
-    // Table function return.
-    std::vector<sql_argument::ArgumentDefinition> ret_args;
-    if (!ParseArguments(ret_args)) {
-      return false;
-    }
-    ret = sql_argument::SerializeArguments(ret_args);
-  } else if (ret_token.token_type != SqliteTokenType::TK_ID) {
-    // TODO(lalitm): add a link to create function documentation.
-    return ErrorAtToken(ret_token, "Invalid return type");
-  } else {
-    // Scalar function return.
-    ret = ret_token.str;
-  }
-
-  if (Token as_token = tokenizer_.NextNonWhitespace();
-      !TokenIsSqliteKeyword("as", as_token)) {
-    // TODO(lalitm): add a link to create function documentation.
-    return ErrorAtToken(as_token, "Expected keyword 'as'");
-  }
-
-  Token first = tokenizer_.NextNonWhitespace();
-  Token terminal = tokenizer_.NextTerminal();
-  statement_ = CreateFunction{
-      replace,
-      FunctionPrototype{std::string(function_name.str), std::move(args)},
-      std::move(ret), tokenizer_.Substr(first, terminal), table_return};
-  statement_sql_ = tokenizer_.Substr(first_non_space_token, terminal);
-  return true;
-}
-
-bool PerfettoSqlParser::ParseCreatePerfettoMacro(bool replace) {
-  Token name = tokenizer_.NextNonWhitespace();
-  if (name.token_type != SqliteTokenType::TK_ID) {
-    // TODO(lalitm): add a link to create macro documentation.
-    base::StackString<1024> err("Invalid macro name %.*s",
-                                static_cast<int>(name.str.size()),
-                                name.str.data());
-    return ErrorAtToken(name, err.c_str());
-  }
-
-  // TK_LP == '(' (i.e. left parenthesis).
-  if (Token lp = tokenizer_.NextNonWhitespace();
-      lp.token_type != SqliteTokenType::TK_LP) {
-    // TODO(lalitm): add a link to create macro documentation.
-    return ErrorAtToken(lp, "Malformed macro prototype: '(' expected");
-  }
-
-  std::vector<RawArgument> raw_args;
-  std::vector<std::pair<SqlSource, SqlSource>> args;
-  if (!ParseRawArguments(raw_args)) {
-    return false;
-  }
-  for (const auto& arg : raw_args) {
-    args.emplace_back(tokenizer_.SubstrToken(arg.name),
-                      tokenizer_.SubstrToken(arg.type));
-  }
-
-  if (Token returns = tokenizer_.NextNonWhitespace();
-      !TokenIsCustomKeyword("returns", returns)) {
-    // TODO(lalitm): add a link to create macro documentation.
-    return ErrorAtToken(returns, "Expected keyword 'returns'");
-  }
-
-  Token returns_value = tokenizer_.NextNonWhitespace();
-  if (returns_value.token_type != SqliteTokenType::TK_ID) {
-    // TODO(lalitm): add a link to create function documentation.
-    return ErrorAtToken(returns_value, "Expected return type");
-  }
-
-  if (Token as_token = tokenizer_.NextNonWhitespace();
-      !TokenIsSqliteKeyword("as", as_token)) {
-    // TODO(lalitm): add a link to create macro documentation.
-    return ErrorAtToken(as_token, "Expected keyword 'as'");
-  }
-
-  Token first = tokenizer_.NextNonWhitespace();
-  Token tok = tokenizer_.NextTerminal();
-  statement_ = CreateMacro{
-      replace, tokenizer_.SubstrToken(name), std::move(args),
-      tokenizer_.SubstrToken(returns_value), tokenizer_.Substr(first, tok)};
-  return true;
-}
-
-bool PerfettoSqlParser::ParseRawArguments(std::vector<RawArgument>& args) {
-  enum TokenType {
-    kIdOrRp,
-    kId,
-    kType,
-    kCommaOrRp,
-  };
-
-  std::optional<Token> id = std::nullopt;
-  TokenType expected = kIdOrRp;
-  for (Token tok = tokenizer_.NextNonWhitespace();;
-       tok = tokenizer_.NextNonWhitespace()) {
-    // Keywords can be used as names accidentally so have an explicit error
-    // message for those.
-    if (tok.token_type == SqliteTokenType::TK_GENERIC_KEYWORD) {
-      // Ignore "key" which, while being a keyword, is also harmless in
-      // practice.
-      if (base::ToLower(std::string(tok.str)) == "key") {
-        tok.token_type = SqliteTokenType::TK_ID;
-      } else {
-        base::StackString<1024> err(
-            "Malformed function/macro prototype: %.*s is a SQL keyword so "
-            "cannot appear in a prototype",
-            static_cast<int>(tok.str.size()), tok.str.data());
-        return ErrorAtToken(tok, err.c_str());
-      }
-    }
-    if (expected == kCommaOrRp) {
-      PERFETTO_CHECK(expected == kCommaOrRp);
-      if (tok.token_type == SqliteTokenType::TK_RP) {
-        return true;
-      }
-      if (tok.token_type == SqliteTokenType::TK_COMMA) {
-        expected = kId;
-        continue;
-      }
-      return ErrorAtToken(tok, "')' or ',' expected");
-    }
-    if (expected == kType) {
-      if (tok.token_type != SqliteTokenType::TK_ID) {
-        // TODO(lalitm): add a link to documentation.
-        base::StackString<1024> err("%.*s is not a valid argument type",
-                                    static_cast<int>(tok.str.size()),
-                                    tok.str.data());
-        return ErrorAtToken(tok, err.c_str());
-      }
-      PERFETTO_CHECK(id);
-      args.push_back({*id, tok});
-      id = std::nullopt;
-      expected = kCommaOrRp;
-      continue;
-    }
-
-    // kIdOrRp only happens on the very first token.
-    if (tok.token_type == SqliteTokenType::TK_RP && expected == kIdOrRp) {
-      return true;
-    }
-
-    if (tok.token_type != SqliteTokenType::TK_ID) {
-      // TODO(lalitm): add a link to documentation.
-      base::StackString<1024> err("%.*s is not a valid argument name",
-                                  static_cast<int>(tok.str.size()),
-                                  tok.str.data());
-      return ErrorAtToken(tok, err.c_str());
-    }
-    id = tok;
-    expected = kType;
-    continue;
-  }
-}
-
-bool PerfettoSqlParser::ParseArguments(
-    std::vector<sql_argument::ArgumentDefinition>& args) {
-  std::vector<RawArgument> raw_args;
-  if (!ParseRawArguments(raw_args)) {
-    return false;
-  }
-  for (const auto& raw_arg : raw_args) {
-    std::optional<sql_argument::ArgumentDefinition> arg =
-        ResolveRawArgument(raw_arg);
-    if (!arg) {
-      return false;
-    }
-    args.emplace_back(std::move(*arg));
-  }
-  return true;
-}
-
-std::optional<sql_argument::ArgumentDefinition>
-PerfettoSqlParser::ResolveRawArgument(RawArgument arg) {
-  std::string arg_name = tokenizer_.SubstrToken(arg.name).sql();
-  std::string arg_type = tokenizer_.SubstrToken(arg.type).sql();
-  if (!sql_argument::IsValidName(base::StringView(arg_name))) {
-    base::StackString<1024> err("Name %s is not alphanumeric",
-                                arg_name.c_str());
-    ErrorAtToken(arg.name, err.c_str());
-    return std::nullopt;
-  }
-  std::optional<sql_argument::Type> parsed_arg_type =
-      sql_argument::ParseType(base::StringView(arg_type));
-  if (!parsed_arg_type) {
-    base::StackString<1024> err("Invalid type %s", arg_type.c_str());
-    ErrorAtToken(arg.type, err.c_str());
-    return std::nullopt;
-  }
-  return sql_argument::ArgumentDefinition("$" + arg_name, *parsed_arg_type);
-}
-
-bool PerfettoSqlParser::ErrorAtToken(const SqliteTokenizer::Token& token,
-                                     const char* error,
-                                     ...) {
-  std::string traceback = tokenizer_.AsTraceback(token);
-  status_ = base::ErrStatus("%s%s", traceback.c_str(), error);
-  return false;
-}
-
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h b/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h
deleted file mode 100644
index bafa3e7..0000000
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h
+++ /dev/null
@@ -1,209 +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.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_PARSER_H_
-#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_PARSER_H_
-
-#include <optional>
-#include <string>
-#include <utility>
-#include <variant>
-#include <vector>
-
-#include "function_util.h"
-#include "perfetto/ext/base/flat_hash_map.h"
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h"
-#include "src/trace_processor/sqlite/sql_source.h"
-#include "src/trace_processor/sqlite/sqlite_tokenizer.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-// Parser for PerfettoSQL statements. This class provides an iterator-style
-// interface for reading all PerfettoSQL statements from a block of SQL.
-//
-// Usage:
-// PerfettoSqlParser parser(my_sql_string.c_str());
-// while (parser.Next()) {
-//   auto& stmt = parser.statement();
-//   // Handle |stmt| here
-// }
-// RETURN_IF_ERROR(r.status());
-class PerfettoSqlParser {
- public:
-  // Indicates that the specified SQLite SQL was extracted directly from a
-  // PerfettoSQL statement and should be directly executed with SQLite.
-  struct SqliteSql {};
-  // Indicates that the specified SQL was a CREATE PERFETTO FUNCTION statement
-  // with the following parameters.
-  struct CreateFunction {
-    bool replace;
-    FunctionPrototype prototype;
-    std::string returns;
-    SqlSource sql;
-    bool is_table;
-  };
-  // Indicates that the specified SQL was a CREATE PERFETTO TABLE statement
-  // with the following parameters.
-  struct CreateTable {
-    bool replace;
-    std::string name;
-    // SQL source for the select statement.
-    SqlSource sql;
-    std::vector<sql_argument::ArgumentDefinition> schema;
-  };
-  // Indicates that the specified SQL was a CREATE PERFETTO VIEW statement
-  // with the following parameters.
-  struct CreateView {
-    bool replace;
-    std::string name;
-    // SQL source for the select statement.
-    SqlSource select_sql;
-    // SQL source corresponding to the rewritten statement creating the
-    // underlying view.
-    SqlSource create_view_sql;
-    std::vector<sql_argument::ArgumentDefinition> schema;
-  };
-  // Indicates that the specified SQL was a CREATE PERFETTO INDEX statement
-  // with the following parameters.
-  struct CreateIndex {
-    bool replace = false;
-    std::string name;
-    std::string table_name;
-    std::vector<std::string> col_names;
-  };
-  // Indicates that the specified SQL was a DROP PERFETTO INDEX statement
-  // with the following parameters.
-  struct DropIndex {
-    std::string name;
-    std::string table_name;
-  };
-  // Indicates that the specified SQL was a INCLUDE PERFETTO MODULE statement
-  // with the following parameter.
-  struct Include {
-    std::string key;
-  };
-  // Indicates that the specified SQL was a CREATE PERFETTO MACRO statement
-  // with the following parameter.
-  struct CreateMacro {
-    bool replace;
-    SqlSource name;
-    std::vector<std::pair<SqlSource, SqlSource>> args;
-    SqlSource returns;
-    SqlSource sql;
-  };
-  using Statement = std::variant<CreateFunction,
-                                 CreateIndex,
-                                 CreateMacro,
-                                 CreateTable,
-                                 CreateView,
-                                 DropIndex,
-                                 Include,
-                                 SqliteSql>;
-
-  // Creates a new SQL parser with the a block of PerfettoSQL statements.
-  // Concretely, the passed string can contain >1 statement.
-  explicit PerfettoSqlParser(
-      SqlSource,
-      const base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro>&);
-
-  // Attempts to parse to the next statement in the SQL. Returns true if
-  // a statement was successfully parsed and false if EOF was reached or the
-  // statement was not parsed correctly.
-  //
-  // Note: if this function returns false, callers *must* call |status()|: it
-  // is undefined behaviour to not do so.
-  bool Next();
-
-  // Returns the current statement which was parsed. This function *must not* be
-  // called unless |Next()| returned true.
-  Statement& statement() {
-    PERFETTO_DCHECK(statement_.has_value());
-    return statement_.value();
-  }
-
-  // Returns the full statement which was parsed. This should return
-  // |statement()| and Perfetto SQL code that's in front. This function *must
-  // not* be called unless |Next()| returned true.
-  const SqlSource& statement_sql() const {
-    PERFETTO_CHECK(statement_sql_);
-    return *statement_sql_;
-  }
-
-  // Returns the error status for the parser. This will be |base::OkStatus()|
-  // until an unrecoverable error is encountered.
-  const base::Status& status() const { return status_; }
-
- private:
-  // This cannot be moved because we keep pointers into |sql_| in
-  // |preprocessor_|.
-  PerfettoSqlParser(PerfettoSqlParser&&) = delete;
-  PerfettoSqlParser& operator=(PerfettoSqlParser&&) = delete;
-
-  // Most of the code needs sql_argument::ArgumentDefinition, but we explcitly
-  // track raw arguments separately, as macro implementations need access to
-  // the underlying tokens.
-  struct RawArgument {
-    SqliteTokenizer::Token name;
-    SqliteTokenizer::Token type;
-  };
-
-  bool ParseCreatePerfettoFunction(
-      bool replace,
-      SqliteTokenizer::Token first_non_space_token);
-
-  enum class TableOrView {
-    kTable,
-    kView,
-  };
-  bool ParseCreatePerfettoTableOrView(
-      bool replace,
-      SqliteTokenizer::Token first_non_space_token,
-      TableOrView table_or_view);
-
-  bool ParseIncludePerfettoModule(SqliteTokenizer::Token first_non_space_token);
-
-  bool ParseCreatePerfettoMacro(bool replace);
-
-  bool ParseCreatePerfettoIndex(bool replace,
-                                SqliteTokenizer::Token first_non_space_token);
-
-  bool ParseDropPerfettoIndex(SqliteTokenizer::Token first_non_space_token);
-
-  // Convert a "raw" argument (i.e. one that points to specific tokens) to the
-  // argument definition consumed by the rest of the SQL code.
-  // Guarantees to call ErrorAtToken if std::nullopt is returned.
-  std::optional<sql_argument::ArgumentDefinition> ResolveRawArgument(
-      RawArgument arg);
-  // Parse the arguments in their raw token form.
-  bool ParseRawArguments(std::vector<RawArgument>&);
-  // Same as above, but also convert the raw tokens into argument definitions.
-  bool ParseArguments(std::vector<sql_argument::ArgumentDefinition>&);
-
-  bool ErrorAtToken(const SqliteTokenizer::Token&, const char* error, ...);
-
-  PerfettoSqlPreprocessor preprocessor_;
-  SqliteTokenizer tokenizer_;
-
-  base::Status status_;
-  std::optional<SqlSource> statement_sql_;
-  std::optional<Statement> statement_;
-};
-
-}  // namespace trace_processor
-}  // namespace perfetto
-
-#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_PARSER_H_
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser_unittest.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser_unittest.cc
deleted file mode 100644
index bc26039..0000000
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser_unittest.cc
+++ /dev/null
@@ -1,368 +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.
- */
-
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h"
-
-#include <cstdint>
-#include <variant>
-#include <vector>
-
-#include "perfetto/base/logging.h"
-#include "perfetto/ext/base/status_or.h"
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_test_utils.h"
-#include "src/trace_processor/sqlite/sql_source.h"
-#include "test/gtest_and_gmock.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-using Result = PerfettoSqlParser::Statement;
-using Statement = PerfettoSqlParser::Statement;
-using SqliteSql = PerfettoSqlParser::SqliteSql;
-using CreateFn = PerfettoSqlParser::CreateFunction;
-using CreateTable = PerfettoSqlParser::CreateTable;
-using CreateView = PerfettoSqlParser::CreateView;
-using Include = PerfettoSqlParser::Include;
-using CreateMacro = PerfettoSqlParser::CreateMacro;
-using CreateIndex = PerfettoSqlParser::CreateIndex;
-
-namespace {
-
-class PerfettoSqlParserTest : public ::testing::Test {
- protected:
-  base::StatusOr<std::vector<PerfettoSqlParser::Statement>> Parse(
-      SqlSource sql) {
-    PerfettoSqlParser parser(sql, macros_);
-    std::vector<PerfettoSqlParser::Statement> results;
-    while (parser.Next()) {
-      results.push_back(std::move(parser.statement()));
-    }
-    if (!parser.status().ok()) {
-      return parser.status();
-    }
-    return results;
-  }
-
-  base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro> macros_;
-};
-
-TEST_F(PerfettoSqlParserTest, Empty) {
-  ASSERT_THAT(*Parse(SqlSource::FromExecuteQuery("")), testing::IsEmpty());
-}
-
-TEST_F(PerfettoSqlParserTest, SemiColonTerminatedStatement) {
-  SqlSource res = SqlSource::FromExecuteQuery("SELECT * FROM slice;");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(), Statement{SqliteSql{}});
-  ASSERT_EQ(parser.statement_sql(), FindSubstr(res, "SELECT * FROM slice"));
-}
-
-TEST_F(PerfettoSqlParserTest, MultipleStmts) {
-  auto res =
-      SqlSource::FromExecuteQuery("SELECT * FROM slice; SELECT * FROM s");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(), Statement{SqliteSql{}});
-  ASSERT_EQ(parser.statement_sql().sql(),
-            FindSubstr(res, "SELECT * FROM slice").sql());
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(), Statement{SqliteSql{}});
-  ASSERT_EQ(parser.statement_sql().sql(),
-            FindSubstr(res, "SELECT * FROM s").sql());
-}
-
-TEST_F(PerfettoSqlParserTest, IgnoreOnlySpace) {
-  auto res = SqlSource::FromExecuteQuery(" ; SELECT * FROM s; ; ;");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(), Statement{SqliteSql{}});
-  ASSERT_EQ(parser.statement_sql().sql(),
-            FindSubstr(res, "SELECT * FROM s").sql());
-}
-
-TEST_F(PerfettoSqlParserTest, CreatePerfettoFunctionScalar) {
-  auto res = SqlSource::FromExecuteQuery(
-      "create perfetto function foo() returns INT as select 1");
-  ASSERT_THAT(*Parse(res), testing::ElementsAre(CreateFn{
-                               false, FunctionPrototype{"foo", {}}, "INT",
-                               FindSubstr(res, "select 1"), false}));
-
-  res = SqlSource::FromExecuteQuery(
-      "create perfetto function bar(x INT, y LONG) returns STRING as "
-      "select 'foo'");
-  ASSERT_THAT(*Parse(res),
-              testing::ElementsAre(
-                  CreateFn{false,
-                           FunctionPrototype{
-                               "bar",
-                               {
-                                   {"$x", sql_argument::Type::kInt},
-                                   {"$y", sql_argument::Type::kLong},
-                               },
-                           },
-                           "STRING", FindSubstr(res, "select 'foo'"), false}));
-
-  res = SqlSource::FromExecuteQuery(
-      "CREATE perfetto FuNcTiOn bar(x INT, y LONG) returnS STRING As "
-      "select 'foo'");
-  ASSERT_THAT(*Parse(res),
-              testing::ElementsAre(
-                  CreateFn{false,
-                           FunctionPrototype{
-                               "bar",
-                               {
-                                   {"$x", sql_argument::Type::kInt},
-                                   {"$y", sql_argument::Type::kLong},
-                               },
-                           },
-                           "STRING", FindSubstr(res, "select 'foo'"), false}));
-}
-
-TEST_F(PerfettoSqlParserTest, CreateOrReplacePerfettoFunctionScalar) {
-  auto res = SqlSource::FromExecuteQuery(
-      "create or replace perfetto function foo() returns INT as select 1");
-  ASSERT_THAT(*Parse(res), testing::ElementsAre(CreateFn{
-                               true, FunctionPrototype{"foo", {}}, "INT",
-                               FindSubstr(res, "select 1"), false}));
-}
-
-TEST_F(PerfettoSqlParserTest, CreatePerfettoFunctionScalarError) {
-  auto res = SqlSource::FromExecuteQuery(
-      "create perfetto function foo( returns INT as select 1");
-  ASSERT_FALSE(Parse(res).status().ok());
-
-  res = SqlSource::FromExecuteQuery(
-      "create perfetto function foo(x INT) as select 1");
-  ASSERT_FALSE(Parse(res).status().ok());
-
-  res = SqlSource::FromExecuteQuery(
-      "create perfetto function foo(x INT) returns INT");
-  ASSERT_FALSE(Parse(res).status().ok());
-}
-
-TEST_F(PerfettoSqlParserTest, CreatePerfettoFunctionAndOther) {
-  auto res = SqlSource::FromExecuteQuery(
-      "create perfetto function foo() returns INT as select 1; select foo()");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  CreateFn fn{false, FunctionPrototype{"foo", {}}, "INT",
-              FindSubstr(res, "select 1"), false};
-  ASSERT_EQ(parser.statement(), Statement{fn});
-  ASSERT_EQ(
-      parser.statement_sql().sql(),
-      FindSubstr(res, "create perfetto function foo() returns INT as select 1")
-          .sql());
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(), Statement{SqliteSql{}});
-  ASSERT_EQ(parser.statement_sql().sql(),
-            FindSubstr(res, "select foo()").sql());
-}
-
-TEST_F(PerfettoSqlParserTest, IncludePerfettoTrivial) {
-  auto res =
-      SqlSource::FromExecuteQuery("include perfetto module cheese.bre_ad;");
-  ASSERT_THAT(*Parse(res), testing::ElementsAre(Include{"cheese.bre_ad"}));
-}
-
-TEST_F(PerfettoSqlParserTest, IncludePerfettoErrorAdditionalChars) {
-  auto res = SqlSource::FromExecuteQuery(
-      "include perfetto module cheese.bre_ad blabla;");
-  ASSERT_FALSE(Parse(res).status().ok());
-}
-
-TEST_F(PerfettoSqlParserTest, IncludePerfettoErrorWrongModuleName) {
-  auto res =
-      SqlSource::FromExecuteQuery("include perfetto module chees*e.bre_ad;");
-  ASSERT_FALSE(Parse(res).status().ok());
-}
-
-TEST_F(PerfettoSqlParserTest, CreatePerfettoMacro) {
-  auto res = SqlSource::FromExecuteQuery(
-      "create perfetto macro foo(a1 Expr, b1 TableOrSubquery,c3_d "
-      "TableOrSubquery2 ) returns TableOrSubquery3 as random sql snippet");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(
-      parser.statement(),
-      Statement(CreateMacro{
-          false,
-          FindSubstr(res, "foo"),
-          {
-              {FindSubstr(res, "a1"), FindSubstr(res, "Expr")},
-              {FindSubstr(res, "b1"), FindSubstr(res, "TableOrSubquery")},
-              {FindSubstr(res, "c3_d"), FindSubstr(res, "TableOrSubquery2")},
-          },
-          FindSubstr(res, "TableOrSubquery3"),
-          FindSubstr(res, "random sql snippet")}));
-  ASSERT_FALSE(parser.Next());
-}
-
-TEST_F(PerfettoSqlParserTest, CreateOrReplacePerfettoMacro) {
-  auto res = SqlSource::FromExecuteQuery(
-      "create or replace perfetto macro foo() returns Expr as 1");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(), Statement(CreateMacro{true,
-                                                      FindSubstr(res, "foo"),
-                                                      {},
-                                                      FindSubstr(res, "Expr"),
-                                                      FindSubstr(res, "1")}));
-  ASSERT_FALSE(parser.Next());
-}
-
-TEST_F(PerfettoSqlParserTest, CreatePerfettoMacroAndOther) {
-  auto res = SqlSource::FromExecuteQuery(
-      "create perfetto macro foo() returns sql1 as random sql snippet; "
-      "select 1");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(), Statement(CreateMacro{
-                                    false,
-                                    FindSubstr(res, "foo"),
-                                    {},
-                                    FindSubstr(res, "sql1"),
-                                    FindSubstr(res, "random sql snippet"),
-                                }));
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(), Statement(SqliteSql{}));
-  ASSERT_EQ(parser.statement_sql(), FindSubstr(res, "select 1"));
-  ASSERT_FALSE(parser.Next());
-}
-
-TEST_F(PerfettoSqlParserTest, CreatePerfettoTable) {
-  auto res = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO TABLE foo AS SELECT 42 AS bar");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(),
-            Statement(CreateTable{
-                false, "foo", FindSubstr(res, "SELECT 42 AS bar"), {}}));
-  ASSERT_FALSE(parser.Next());
-}
-
-TEST_F(PerfettoSqlParserTest, CreateOrReplacePerfettoTable) {
-  auto res = SqlSource::FromExecuteQuery(
-      "CREATE OR REPLACE PERFETTO TABLE foo AS SELECT 42 AS bar");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(),
-            Statement(CreateTable{
-                true, "foo", FindSubstr(res, "SELECT 42 AS bar"), {}}));
-  ASSERT_FALSE(parser.Next());
-}
-
-TEST_F(PerfettoSqlParserTest, CreatePerfettoTableWithSchema) {
-  auto res = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO TABLE foo(bar INT) AS SELECT 42 AS bar");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(), Statement(CreateTable{
-                                    false,
-                                    "foo",
-                                    FindSubstr(res, "SELECT 42 AS bar"),
-                                    {{"$bar", sql_argument::Type::kInt}},
-                                }));
-  ASSERT_FALSE(parser.Next());
-}
-
-TEST_F(PerfettoSqlParserTest, CreatePerfettoTableAndOther) {
-  auto res = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO TABLE foo AS SELECT 42 AS bar; select 1");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(),
-            Statement(CreateTable{
-                false, "foo", FindSubstr(res, "SELECT 42 AS bar"), {}}));
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(), Statement(SqliteSql{}));
-  ASSERT_EQ(parser.statement_sql(), FindSubstr(res, "select 1"));
-  ASSERT_FALSE(parser.Next());
-}
-
-TEST_F(PerfettoSqlParserTest, CreatePerfettoView) {
-  auto res = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO VIEW foo AS SELECT 42 AS bar");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(
-      parser.statement(),
-      Statement(CreateView{
-          false,
-          "foo",
-          SqlSource::FromExecuteQuery("SELECT 42 AS bar"),
-          SqlSource::FromExecuteQuery("CREATE VIEW foo AS SELECT 42 AS bar"),
-          {}}));
-  ASSERT_FALSE(parser.Next());
-}
-
-TEST_F(PerfettoSqlParserTest, CreateOrReplacePerfettoView) {
-  auto res = SqlSource::FromExecuteQuery(
-      "CREATE OR REPLACE PERFETTO VIEW foo AS SELECT 42 AS bar");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(
-      parser.statement(),
-      Statement(CreateView{
-          true,
-          "foo",
-          SqlSource::FromExecuteQuery("SELECT 42 AS bar"),
-          SqlSource::FromExecuteQuery("CREATE VIEW foo AS SELECT 42 AS bar"),
-          {}}));
-  ASSERT_FALSE(parser.Next());
-}
-
-TEST_F(PerfettoSqlParserTest, CreatePerfettoViewAndOther) {
-  auto res = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO VIEW foo AS SELECT 42 AS bar; select 1");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(
-      parser.statement(),
-      Statement(CreateView{
-          false,
-          "foo",
-          SqlSource::FromExecuteQuery("SELECT 42 AS bar"),
-          SqlSource::FromExecuteQuery("CREATE VIEW foo AS SELECT 42 AS bar"),
-          {}}));
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(), Statement(SqliteSql{}));
-  ASSERT_EQ(parser.statement_sql(), FindSubstr(res, "select 1"));
-  ASSERT_FALSE(parser.Next());
-}
-
-TEST_F(PerfettoSqlParserTest, CreatePerfettoViewWithSchema) {
-  auto res = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO VIEW foo(foo STRING, bar INT) AS SELECT 'a' as foo, 42 "
-      "AS bar");
-  PerfettoSqlParser parser(res, macros_);
-  ASSERT_TRUE(parser.Next());
-  ASSERT_EQ(parser.statement(),
-            Statement(CreateView{
-                false,
-                "foo",
-                SqlSource::FromExecuteQuery("SELECT 'a' as foo, 42 AS bar"),
-                SqlSource::FromExecuteQuery(
-                    "CREATE VIEW foo AS SELECT 'a' as foo, 42 AS bar"),
-                {{"$foo", sql_argument::Type::kString},
-                 {"$bar", sql_argument::Type::kInt}},
-            }));
-  ASSERT_FALSE(parser.Next());
-}
-
-}  // namespace
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.cc
deleted file mode 100644
index a381468..0000000
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.cc
+++ /dev/null
@@ -1,607 +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.
- */
-
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h"
-
-#include <algorithm>
-#include <cstddef>
-#include <cstdint>
-#include <optional>
-#include <string>
-#include <unordered_map>
-#include <unordered_set>
-#include <utility>
-#include <vector>
-
-#include "perfetto/base/logging.h"
-#include "perfetto/base/status.h"
-#include "perfetto/ext/base/flat_hash_map.h"
-#include "perfetto/ext/base/status_or.h"
-#include "perfetto/ext/base/string_utils.h"
-#include "src/trace_processor/sqlite/sql_source.h"
-#include "src/trace_processor/sqlite/sqlite_tokenizer.h"
-#include "src/trace_processor/util/status_macros.h"
-
-namespace perfetto::trace_processor {
-namespace {
-
-enum IntrinsicMacro {
-  kStringify,
-  kTokenZipJoin,
-  kPrefixedTokenZipJoin,
-  kTokenApply,
-  kTokenMapJoin,
-  kTokenMapJoinWithCapture,
-  kComma,
-  kOther
-};
-
-IntrinsicMacro MacroNameToEnum(const std::string& macro_name) {
-  if (macro_name == "__intrinsic_stringify")
-    return kStringify;
-  if (macro_name == "__intrinsic_token_zip_join")
-    return kTokenZipJoin;
-  if (macro_name == "__intrinsic_prefixed_token_zip_join")
-    return kPrefixedTokenZipJoin;
-  if (macro_name == "__intrinsic_token_apply")
-    return kTokenApply;
-  if (macro_name == "__intrinsic_token_map_join")
-    return kTokenMapJoin;
-  if (macro_name == "__intrinsic_token_map_join_with_capture")
-    return kTokenMapJoinWithCapture;
-  if (macro_name == "__intrinsic_token_comma")
-    return kComma;
-
-  return kOther;
-}
-
-base::Status ErrorAtToken(const SqliteTokenizer& tokenizer,
-                          const SqliteTokenizer::Token& token,
-                          const char* error) {
-  std::string traceback = tokenizer.AsTraceback(token);
-  return base::ErrStatus("%s%s", traceback.c_str(), error);
-}
-
-struct InvocationArg {
-  std::optional<SqlSource> arg;
-  bool has_more;
-};
-
-base::StatusOr<InvocationArg> ParseMacroInvocationArg(
-    SqliteTokenizer& tokenizer,
-    SqliteTokenizer::Token& tok,
-    bool has_prev_args) {
-  uint32_t nested_parens = 0;
-  bool seen_token_in_arg = false;
-  auto start = tokenizer.NextNonWhitespace();
-  for (tok = start;; tok = tokenizer.NextNonWhitespace()) {
-    if (tok.IsTerminal()) {
-      if (tok.token_type == SqliteTokenType::TK_SEMI) {
-        // TODO(b/290185551): add a link to macro documentation.
-        return ErrorAtToken(tokenizer, tok,
-                            "Semi-colon is not allowed in macro invocation");
-      }
-      // TODO(b/290185551): add a link to macro documentation.
-      return ErrorAtToken(tokenizer, tok, "Macro invocation not complete");
-    }
-
-    bool is_arg_terminator = tok.token_type == SqliteTokenType::TK_RP ||
-                             tok.token_type == SqliteTokenType::TK_COMMA;
-    if (nested_parens == 0 && is_arg_terminator) {
-      bool token_required =
-          has_prev_args || tok.token_type != SqliteTokenType::TK_RP;
-      if (!seen_token_in_arg && token_required) {
-        // TODO(b/290185551): add a link to macro documentation.
-        return ErrorAtToken(tokenizer, tok, "Macro arg is empty");
-      }
-      return InvocationArg{
-          seen_token_in_arg ? std::make_optional(tokenizer.Substr(start, tok))
-                            : std::optional<SqlSource>(std::nullopt),
-          tok.token_type == SqliteTokenType::TK_COMMA,
-      };
-    }
-    seen_token_in_arg = true;
-
-    if (tok.token_type == SqliteTokenType::TK_LP) {
-      nested_parens++;
-      continue;
-    }
-    if (tok.token_type == SqliteTokenType::TK_RP) {
-      nested_parens--;
-      continue;
-    }
-  }
-}
-
-base::StatusOr<std::optional<SqlSource>> ExecuteStringify(
-    const SqliteTokenizer& tokenizer,
-    const SqliteTokenizer::Token& name_token,
-    const std::vector<SqlSource>& args) {
-  if (args.empty()) {
-    return ErrorAtToken(tokenizer, name_token,
-                        "stringify: stringify must not be empty");
-  }
-
-  // Track the set of variables that, even if we see during stringify, we ignore
-  // and stringify them anyway.
-  std::unordered_set<std::string> ignored_variables;
-  for (uint32_t i = 1; i < args.size(); ++i) {
-    ignored_variables.emplace(args[i].sql());
-  }
-
-  // Ensure that we don't stringifiy any SQL variables present (unless they were
-  // explcitily marked as ignored).
-  SqliteTokenizer t(args[0]);
-  for (auto tok = t.NextNonWhitespace(); !tok.IsTerminal();
-       tok = t.NextNonWhitespace()) {
-    if (tok.token_type == SqliteTokenType::TK_VARIABLE &&
-        !ignored_variables.count(std::string(tok.str.substr(1)))) {
-      return {std::nullopt};
-    }
-  }
-  std::string res = "'" + args[0].sql() + "'";
-  return {SqlSource::FromTraceProcessorImplementation(std::move(res))};
-}
-
-void RewriteIntrinsicMacro(const std::string& macro_name,
-                           std::optional<SqlSource>& res,
-                           std::vector<SqlSource>& token_list,
-                           SqliteTokenizer& tokenizer,
-                           SqlSource::Rewriter& rewriter,
-                           SqliteTokenizer::Token prev,
-                           SqliteTokenizer::Token tok) {
-  if (res) {
-    tokenizer.Rewrite(rewriter, prev, tok, *std::move(res),
-                      SqliteTokenizer::EndToken::kInclusive);
-    return;
-  }
-
-  // We failed to rewrite because a variable was still present in SQL.
-  // Just readd the stringify SQL with newly expanded token list.
-  std::vector<std::string> pieces;
-  pieces.reserve(token_list.size());
-  for (const auto& list : token_list) {
-    if (base::TrimWhitespace(list.sql()) == ",") {
-      pieces.emplace_back("__intrinsic_token_comma!()");
-    } else {
-      pieces.emplace_back(list.sql());
-    }
-  }
-  tokenizer.Rewrite(rewriter, prev, tok,
-                    SqlSource::FromTraceProcessorImplementation(
-                        macro_name + "!(" + base::Join(pieces, ", ") + ")"),
-                    SqliteTokenizer::EndToken::kInclusive);
-}
-
-}  // namespace
-
-PerfettoSqlPreprocessor::PerfettoSqlPreprocessor(
-    SqlSource source,
-    const base::FlatHashMap<std::string, Macro>& macros)
-    : global_tokenizer_(std::move(source)), macros_(&macros) {}
-
-bool PerfettoSqlPreprocessor::NextStatement() {
-  PERFETTO_CHECK(status_.ok());
-
-  // Skip through any number of semi-colons (representing empty statements).
-  SqliteTokenizer::Token tok = global_tokenizer_.NextNonWhitespace();
-  while (tok.token_type == SqliteTokenType::TK_SEMI) {
-    tok = global_tokenizer_.NextNonWhitespace();
-  }
-
-  // If we still see a terminal token at this point, we must have hit EOF.
-  if (tok.IsTerminal()) {
-    PERFETTO_DCHECK(tok.token_type != SqliteTokenType::TK_SEMI);
-    return false;
-  }
-
-  SqlSource stmt =
-      global_tokenizer_.Substr(tok, global_tokenizer_.NextTerminal());
-  auto stmt_or = RewriteInternal(stmt, {});
-  if (stmt_or.ok()) {
-    statement_ = std::move(*stmt_or);
-    return true;
-  }
-  status_ = stmt_or.status();
-  return false;
-}
-
-base::StatusOr<SqlSource> PerfettoSqlPreprocessor::RewriteInternal(
-    const SqlSource& source,
-    const std::unordered_map<std::string, SqlSource>& arg_bindings) {
-  SqlSource::Rewriter rewriter(source);
-  SqliteTokenizer tokenizer(source);
-  for (SqliteTokenizer::Token tok = tokenizer.NextNonWhitespace(), prev;;
-       prev = tok, tok = tokenizer.NextNonWhitespace()) {
-    if (tok.IsTerminal()) {
-      break;
-    }
-    if (tok.token_type == SqliteTokenType::TK_VARIABLE &&
-        !seen_macros_.empty()) {
-      PERFETTO_CHECK(tok.str.size() >= 2);
-      if (tok.str[0] != '$') {
-        return ErrorAtToken(tokenizer, tok, "Variables must start with $");
-      }
-      auto binding_it = arg_bindings.find(std::string(tok.str.substr(1)));
-      if (binding_it == arg_bindings.end()) {
-        // TODO(lalitm): reenable making this an error once we actually pass
-        // macros around in graph_scan instead of bare-SQL.
-        // return ErrorAtToken(tokenizer, tok, "Variable not found");
-        continue;
-      }
-      tokenizer.RewriteToken(rewriter, tok, binding_it->second);
-      continue;
-    }
-    if (tok.token_type != SqliteTokenType::TK_ILLEGAL || tok.str != "!") {
-      continue;
-    }
-
-    const auto& name_token = prev;
-    if (name_token.token_type == SqliteTokenType::TK_VARIABLE) {
-      // TODO(b/290185551): add a link to macro documentation.
-      return ErrorAtToken(tokenizer, name_token,
-                          "Macro name cannot be a variable");
-    }
-    if (name_token.token_type != SqliteTokenType::TK_ID) {
-      // TODO(b/290185551): add a link to macro documentation.
-      return ErrorAtToken(tokenizer, name_token, "Macro invocation is invalid");
-    }
-
-    // Go to the opening parenthesis of the macro invocation.
-    tok = tokenizer.NextNonWhitespace();
-
-    std::string macro_name(name_token.str);
-    IntrinsicMacro macro_enum = MacroNameToEnum(macro_name);
-    ASSIGN_OR_RETURN(std::vector<SqlSource> token_list,
-                     ParseTokenList(tokenizer, tok, arg_bindings));
-
-    // Non intrinsic macro.
-    if (macro_enum == kOther) {
-      ASSIGN_OR_RETURN(SqlSource invocation,
-                       ExecuteMacroInvocation(tokenizer, prev, macro_name,
-                                              std::move(token_list)));
-      tokenizer.Rewrite(rewriter, prev, tok, std::move(invocation),
-                        SqliteTokenizer::EndToken::kInclusive);
-      continue;
-    }
-
-    // Token comma instrinsic macro requires special handling.
-    if (macro_enum == kComma) {
-      if (!token_list.empty()) {
-        return ErrorAtToken(tokenizer, name_token,
-                            "token_comma: no arguments allowd");
-      }
-      tokenizer.Rewrite(rewriter, prev, tok,
-                        SqlSource::FromTraceProcessorImplementation(","),
-                        SqliteTokenizer::EndToken::kInclusive);
-      continue;
-    }
-
-    // Intrinsic macros.
-    std::optional<SqlSource> res;
-    switch (macro_enum) {
-      case kStringify: {
-        ASSIGN_OR_RETURN(res,
-                         ExecuteStringify(tokenizer, name_token, token_list));
-        break;
-      }
-      case kTokenZipJoin: {
-        ASSIGN_OR_RETURN(
-            res, ExecuteTokenZipJoin(tokenizer, name_token, token_list, false));
-        break;
-      }
-      case kPrefixedTokenZipJoin: {
-        ASSIGN_OR_RETURN(
-            res, ExecuteTokenZipJoin(tokenizer, name_token, token_list, true));
-        break;
-      }
-      case kTokenMapJoin: {
-        ASSIGN_OR_RETURN(
-            res, ExecuteTokenMapJoin(tokenizer, name_token, token_list));
-        break;
-      }
-      case kTokenMapJoinWithCapture: {
-        ASSIGN_OR_RETURN(res, ExecuteTokenMapJoinWithCapture(
-                                  tokenizer, name_token, token_list));
-
-        break;
-      }
-
-      case kTokenApply: {
-        ASSIGN_OR_RETURN(res,
-                         ExecuteTokenApply(tokenizer, name_token, token_list));
-        break;
-      }
-      case kComma:
-      case kOther:
-        PERFETTO_FATAL("Shouldn't be reached");
-    }
-    RewriteIntrinsicMacro(macro_name, res, token_list, tokenizer, rewriter,
-                          prev, tok);
-  }
-  return std::move(rewriter).Build();
-}
-
-base::StatusOr<std::vector<SqlSource>> PerfettoSqlPreprocessor::ParseTokenList(
-    SqliteTokenizer& tokenizer,
-    SqliteTokenizer::Token& tok,
-    const std::unordered_map<std::string, SqlSource>& bindings) {
-  if (tok.token_type != SqliteTokenType::TK_LP) {
-    return ErrorAtToken(tokenizer, tok, "( expected to open token list");
-  }
-  std::vector<SqlSource> tokens;
-  bool has_more = true;
-  while (has_more) {
-    ASSIGN_OR_RETURN(InvocationArg invocation_arg,
-                     ParseMacroInvocationArg(tokenizer, tok, !tokens.empty()));
-    if (invocation_arg.arg) {
-      ASSIGN_OR_RETURN(SqlSource res,
-                       RewriteInternal(invocation_arg.arg.value(), bindings));
-      tokens.emplace_back(std::move(res));
-    }
-    has_more = invocation_arg.has_more;
-  }
-  return tokens;
-}
-
-base::StatusOr<SqlSource> PerfettoSqlPreprocessor::ExecuteMacroInvocation(
-    const SqliteTokenizer& tokenizer,
-    const SqliteTokenizer::Token& name_token,
-    const std::string& macro_name,
-    std::vector<SqlSource> token_list) {
-  Macro* macro = macros_->Find(macro_name);
-  if (!macro) {
-    // TODO(b/290185551): add a link to macro documentation.
-    base::StackString<1024> err("Macro %s does not exist", macro_name.c_str());
-    return ErrorAtToken(tokenizer, name_token, err.c_str());
-  }
-  if (seen_macros_.count(macro_name)) {
-    // TODO(b/290185551): add a link to macro documentation.
-    return ErrorAtToken(tokenizer, name_token,
-                        "Macros cannot be recursive or mutually recursive");
-  }
-  if (token_list.size() < macro->args.size()) {
-    // TODO(lalitm): add a link to macro documentation.
-    return ErrorAtToken(tokenizer, name_token,
-                        "Macro invoked with too few args");
-  }
-  if (token_list.size() > macro->args.size()) {
-    // TODO(lalitm): add a link to macro documentation.
-    return ErrorAtToken(tokenizer, name_token,
-                        "Macro invoked with too many args");
-  }
-  std::unordered_map<std::string, SqlSource> inner_bindings;
-  for (auto& t : token_list) {
-    inner_bindings.emplace(macro->args[inner_bindings.size()], std::move(t));
-  }
-  PERFETTO_CHECK(inner_bindings.size() == macro->args.size());
-
-  seen_macros_.emplace(macro->name);
-  ASSIGN_OR_RETURN(SqlSource res, RewriteInternal(macro->sql, inner_bindings));
-  seen_macros_.erase(macro->name);
-  return res;
-}
-
-base::StatusOr<std::optional<SqlSource>>
-PerfettoSqlPreprocessor::ExecuteTokenZipJoin(
-    const SqliteTokenizer& tokenizer,
-    const SqliteTokenizer::Token& name_token,
-    std::vector<SqlSource> token_list,
-    bool prefixed) {
-  if (token_list.size() != 4) {
-    return ErrorAtToken(tokenizer, name_token,
-                        "token_zip_join: must have exactly four args");
-  }
-
-  SqliteTokenizer first_tokenizer(std::move(token_list[0]));
-  SqliteTokenizer::Token inner_tok = first_tokenizer.NextNonWhitespace();
-  if (inner_tok.token_type == SqliteTokenType::TK_VARIABLE) {
-    return {std::nullopt};
-  }
-  ASSIGN_OR_RETURN(std::vector<SqlSource> first_sources,
-                   ParseTokenList(first_tokenizer, inner_tok, {}));
-
-  SqliteTokenizer second_tokenizer(std::move(token_list[1]));
-  inner_tok = second_tokenizer.NextNonWhitespace();
-  if (inner_tok.token_type == SqliteTokenType::TK_VARIABLE) {
-    return {std::nullopt};
-  }
-  ASSIGN_OR_RETURN(std::vector<SqlSource> second_sources,
-                   ParseTokenList(second_tokenizer, inner_tok, {}));
-
-  SqliteTokenizer name_tokenizer(token_list[2]);
-  inner_tok = name_tokenizer.NextNonWhitespace();
-  if (inner_tok.token_type == SqliteTokenType::TK_VARIABLE) {
-    return {std::nullopt};
-  }
-
-  size_t zip_count = std::min(first_sources.size(), second_sources.size());
-  std::vector<std::string> res;
-  for (uint32_t i = 0; i < zip_count; ++i) {
-    ASSIGN_OR_RETURN(
-        SqlSource invocation_res,
-        ExecuteMacroInvocation(tokenizer, name_token, token_list[2].sql(),
-                               {first_sources[i], second_sources[i]}));
-    res.push_back(invocation_res.sql());
-  }
-
-  if (res.empty()) {
-    return {SqlSource::FromTraceProcessorImplementation("")};
-  }
-
-  std::string zipped = base::Join(res, " " + token_list[3].sql() + " ");
-  if (prefixed) {
-    zipped = " " + token_list[3].sql() + " " + zipped;
-  }
-  return {SqlSource::FromTraceProcessorImplementation(zipped)};
-}
-
-base::StatusOr<std::optional<SqlSource>>
-PerfettoSqlPreprocessor::ExecuteTokenApply(
-    const SqliteTokenizer& tokenizer,
-    const SqliteTokenizer::Token& name_token,
-    std::vector<SqlSource> token_list) {
-  if (token_list.size() != 3) {
-    return ErrorAtToken(tokenizer, name_token,
-                        "token_apply: must have exactly three args");
-  }
-
-  SqliteTokenizer arg_list_tokenizer(token_list[0]);
-  SqliteTokenizer::Token inner_tok = arg_list_tokenizer.NextNonWhitespace();
-  if (inner_tok.token_type == SqliteTokenType::TK_VARIABLE) {
-    return {std::nullopt};
-  }
-  ASSIGN_OR_RETURN(std::vector<SqlSource> arg_list_sources,
-                   ParseTokenList(arg_list_tokenizer, inner_tok, {}));
-
-  SqliteTokenizer name_tokenizer(token_list[1]);
-  inner_tok = name_tokenizer.NextNonWhitespace();
-  if (inner_tok.token_type == SqliteTokenType::TK_VARIABLE) {
-    return {std::nullopt};
-  }
-
-  std::vector<std::string> res;
-  for (const auto& arg_list_source : arg_list_sources) {
-    SqliteTokenizer args_tokenizer(arg_list_source);
-    inner_tok = args_tokenizer.NextNonWhitespace();
-    if (inner_tok.token_type == SqliteTokenType::TK_VARIABLE) {
-      return {std::nullopt};
-    }
-
-    ASSIGN_OR_RETURN(std::vector<SqlSource> args_sources,
-                     ParseTokenList(args_tokenizer, inner_tok, {}));
-
-    ASSIGN_OR_RETURN(SqlSource invocation_res,
-                     ExecuteMacroInvocation(tokenizer, name_token,
-                                            token_list[1].sql(), args_sources));
-    res.push_back(invocation_res.sql());
-  }
-
-  if (res.empty()) {
-    return {SqlSource::FromTraceProcessorImplementation("")};
-  }
-
-  std::string zipped = base::Join(res, " " + token_list[2].sql() + " ");
-  return {SqlSource::FromTraceProcessorImplementation(zipped)};
-}
-
-base::StatusOr<std::optional<SqlSource>>
-PerfettoSqlPreprocessor::ExecuteTokenMapJoin(
-    const SqliteTokenizer& tokenizer,
-    const SqliteTokenizer::Token& name_token,
-    std::vector<SqlSource> token_list) {
-  if (token_list.size() != 3) {
-    return ErrorAtToken(tokenizer, name_token,
-                        "token_map_join: must have exactly three args");
-  }
-
-  SqliteTokenizer arg_list_tokenizer(token_list[0]);
-  SqliteTokenizer::Token inner_tok = arg_list_tokenizer.NextNonWhitespace();
-  if (inner_tok.token_type == SqliteTokenType::TK_VARIABLE) {
-    return {std::nullopt};
-  }
-  ASSIGN_OR_RETURN(std::vector<SqlSource> arg_list_sources,
-                   ParseTokenList(arg_list_tokenizer, inner_tok, {}));
-
-  SqliteTokenizer name_tokenizer(token_list[1]);
-  inner_tok = name_tokenizer.NextNonWhitespace();
-  if (inner_tok.token_type == SqliteTokenType::TK_VARIABLE) {
-    return {std::nullopt};
-  }
-
-  std::vector<std::string> res;
-  for (const auto& arg_list_source : arg_list_sources) {
-    SqliteTokenizer args_tokenizer(arg_list_source);
-    inner_tok = args_tokenizer.NextNonWhitespace();
-    if (inner_tok.token_type == SqliteTokenType::TK_VARIABLE) {
-      return {std::nullopt};
-    }
-
-    ASSIGN_OR_RETURN(
-        SqlSource invocation_res,
-        ExecuteMacroInvocation(tokenizer, name_token, token_list[1].sql(),
-                               {arg_list_source}));
-    res.push_back(invocation_res.sql());
-  }
-
-  if (res.empty()) {
-    return {SqlSource::FromTraceProcessorImplementation("")};
-  }
-
-  std::string zipped = base::Join(res, " " + token_list[2].sql() + " ");
-  return {SqlSource::FromTraceProcessorImplementation(zipped)};
-}
-
-base::StatusOr<std::optional<SqlSource>>
-PerfettoSqlPreprocessor::ExecuteTokenMapJoinWithCapture(
-    const SqliteTokenizer& tokenizer,
-    const SqliteTokenizer::Token& name_token,
-    std::vector<SqlSource> token_list) {
-  if (token_list.size() != 4) {
-    return ErrorAtToken(
-        tokenizer, name_token,
-        "token_map_join_with_capture: must have exactly four args");
-  }
-
-  SqliteTokenizer arg_list_tokenizer(token_list[0]);
-  SqliteTokenizer::Token inner_tok = arg_list_tokenizer.NextNonWhitespace();
-  if (inner_tok.token_type == SqliteTokenType::TK_VARIABLE) {
-    return {std::nullopt};
-  }
-  ASSIGN_OR_RETURN(std::vector<SqlSource> arg_list_sources,
-                   ParseTokenList(arg_list_tokenizer, inner_tok, {}));
-
-  SqliteTokenizer name_tokenizer(token_list[1]);
-  inner_tok = name_tokenizer.NextNonWhitespace();
-  if (inner_tok.token_type == SqliteTokenType::TK_VARIABLE) {
-    return {std::nullopt};
-  }
-
-  SqliteTokenizer capture_tokenizer(token_list[2]);
-  inner_tok = capture_tokenizer.NextNonWhitespace();
-  if (inner_tok.token_type == SqliteTokenType::TK_VARIABLE) {
-    return {std::nullopt};
-  }
-  ASSIGN_OR_RETURN(std::vector<SqlSource> captured_args,
-                   ParseTokenList(capture_tokenizer, inner_tok, {}));
-
-  std::vector<std::string> res;
-  for (const auto& arg_list_source : arg_list_sources) {
-    SqliteTokenizer args_tokenizer(arg_list_source);
-    inner_tok = args_tokenizer.NextNonWhitespace();
-    if (inner_tok.token_type == SqliteTokenType::TK_VARIABLE) {
-      return {std::nullopt};
-    }
-
-    std::vector<SqlSource> macro_args{arg_list_source};
-    macro_args.insert(macro_args.end(), captured_args.begin(),
-                      captured_args.end());
-    ASSIGN_OR_RETURN(
-        SqlSource invocation_res,
-        ExecuteMacroInvocation(tokenizer, name_token, token_list[1].sql(),
-                               std::move(macro_args)));
-    res.push_back(invocation_res.sql());
-  }
-
-  if (res.empty()) {
-    return {SqlSource::FromTraceProcessorImplementation("")};
-  }
-
-  std::string zipped = base::Join(res, " " + token_list[3].sql() + " ");
-  return {SqlSource::FromTraceProcessorImplementation(zipped)};
-}
-
-}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h b/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h
deleted file mode 100644
index 1ccd0a3..0000000
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h
+++ /dev/null
@@ -1,116 +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.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_PREPROCESSOR_H_
-#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_PREPROCESSOR_H_
-
-#include <optional>
-#include <string>
-#include <unordered_map>
-#include <unordered_set>
-#include <vector>
-
-#include "perfetto/base/status.h"
-#include "perfetto/ext/base/flat_hash_map.h"
-#include "perfetto/ext/base/status_or.h"
-#include "src/trace_processor/sqlite/sql_source.h"
-#include "src/trace_processor/sqlite/sqlite_tokenizer.h"
-
-namespace perfetto::trace_processor {
-
-// Preprocessor for PerfettoSQL statements. The main responsiblity of this
-// class is to perform similar functions to the C/C++ preprocessor (e.g.
-// expanding macros). It is also responsible for splitting the given SQL into
-// statements.
-class PerfettoSqlPreprocessor {
- public:
-  struct Macro {
-    bool replace;
-    std::string name;
-    std::vector<std::string> args;
-    SqlSource sql;
-  };
-
-  // Creates a preprocessor acting on the given SqlSource.
-  explicit PerfettoSqlPreprocessor(
-      SqlSource,
-      const base::FlatHashMap<std::string, Macro>&);
-
-  // Preprocesses the next SQL statement. Returns true if a statement was
-  // successfully preprocessed and false if EOF was reached or the statement was
-  // not preprocessed correctly.
-  //
-  // Note: if this function returns false, callers *must* call |status()|: it
-  // is undefined behaviour to not do so.
-  bool NextStatement();
-
-  // Returns the error status for the parser. This will be |base::OkStatus()|
-  // until an unrecoverable error is encountered.
-  const base::Status& status() const { return status_; }
-
-  // Returns the most-recent preprocessed SQL statement.
-  //
-  // Note: this function must not be called unless |NextStatement()| returned
-  // true.
-  SqlSource& statement() { return *statement_; }
-
- private:
-  base::StatusOr<SqlSource> RewriteInternal(
-      const SqlSource&,
-      const std::unordered_map<std::string, SqlSource>& arg_bindings);
-
-  base::StatusOr<std::vector<SqlSource>> ParseTokenList(
-      SqliteTokenizer& tokenizer,
-      SqliteTokenizer::Token& token,
-      const std::unordered_map<std::string, SqlSource>& arg_bindings);
-
-  base::StatusOr<SqlSource> ExecuteMacroInvocation(
-      const SqliteTokenizer& tokenizer,
-      const SqliteTokenizer::Token& name_token,
-      const std::string& macro_name,
-      std::vector<SqlSource> token_list);
-
-  base::StatusOr<std::optional<SqlSource>> ExecuteTokenZipJoin(
-      const SqliteTokenizer& tokenizer,
-      const SqliteTokenizer::Token& name_token,
-      std::vector<SqlSource> token_list,
-      bool prefixed);
-
-  base::StatusOr<std::optional<SqlSource>> ExecuteTokenApply(
-      const SqliteTokenizer& tokenizer,
-      const SqliteTokenizer::Token& name_token,
-      std::vector<SqlSource> token_list);
-
-  base::StatusOr<std::optional<SqlSource>> ExecuteTokenMapJoin(
-      const SqliteTokenizer& tokenizer,
-      const SqliteTokenizer::Token& name_token,
-      std::vector<SqlSource> token_list);
-
-  base::StatusOr<std::optional<SqlSource>> ExecuteTokenMapJoinWithCapture(
-      const SqliteTokenizer& tokenizer,
-      const SqliteTokenizer::Token& name_token,
-      std::vector<SqlSource> token_list);
-
-  SqliteTokenizer global_tokenizer_;
-  const base::FlatHashMap<std::string, Macro>* macros_ = nullptr;
-  std::unordered_set<std::string> seen_macros_;
-  std::optional<SqlSource> statement_;
-  base::Status status_;
-};
-
-}  // namespace perfetto::trace_processor
-
-#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_PREPROCESSOR_H_
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor_unittest.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor_unittest.cc
deleted file mode 100644
index 57b6a8c..0000000
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor_unittest.cc
+++ /dev/null
@@ -1,426 +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.
- */
-
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h"
-
-#include <string>
-
-#include "perfetto/ext/base/flat_hash_map.h"
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_test_utils.h"
-#include "src/trace_processor/sqlite/sql_source.h"
-#include "test/gtest_and_gmock.h"
-
-namespace perfetto::trace_processor {
-namespace {
-
-using ::testing::HasSubstr;
-
-using Macro = PerfettoSqlPreprocessor::Macro;
-
-class PerfettoSqlPreprocessorUnittest : public ::testing::Test {
- protected:
-  base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro> macros_;
-};
-
-TEST_F(PerfettoSqlPreprocessorUnittest, Empty) {
-  PerfettoSqlPreprocessor preprocessor(SqlSource::FromExecuteQuery(""),
-                                       macros_);
-  ASSERT_FALSE(preprocessor.NextStatement());
-  ASSERT_TRUE(preprocessor.status().ok());
-}
-
-TEST_F(PerfettoSqlPreprocessorUnittest, SemiColonTerminatedStatement) {
-  auto source = SqlSource::FromExecuteQuery("SELECT * FROM slice;");
-  PerfettoSqlPreprocessor preprocessor(source, macros_);
-  ASSERT_TRUE(preprocessor.NextStatement());
-  ASSERT_EQ(preprocessor.statement(),
-            FindSubstr(source, "SELECT * FROM slice"));
-  ASSERT_FALSE(preprocessor.NextStatement());
-  ASSERT_TRUE(preprocessor.status().ok());
-}
-
-TEST_F(PerfettoSqlPreprocessorUnittest, IgnoreOnlySpace) {
-  auto source = SqlSource::FromExecuteQuery(" ; SELECT * FROM s; ; ;");
-  PerfettoSqlPreprocessor preprocessor(source, macros_);
-  ASSERT_TRUE(preprocessor.NextStatement());
-  ASSERT_EQ(preprocessor.statement(), FindSubstr(source, "SELECT * FROM s"));
-  ASSERT_FALSE(preprocessor.NextStatement());
-  ASSERT_TRUE(preprocessor.status().ok());
-}
-
-TEST_F(PerfettoSqlPreprocessorUnittest, MultipleStmts) {
-  auto source =
-      SqlSource::FromExecuteQuery("SELECT * FROM slice; SELECT * FROM s");
-  PerfettoSqlPreprocessor preprocessor(source, macros_);
-  ASSERT_TRUE(preprocessor.NextStatement());
-  ASSERT_EQ(preprocessor.statement(),
-            FindSubstr(source, "SELECT * FROM slice"));
-  ASSERT_TRUE(preprocessor.NextStatement());
-  ASSERT_EQ(preprocessor.statement(), FindSubstr(source, "SELECT * FROM s"));
-  ASSERT_FALSE(preprocessor.NextStatement());
-  ASSERT_TRUE(preprocessor.status().ok());
-}
-
-TEST_F(PerfettoSqlPreprocessorUnittest, CreateMacro) {
-  auto source = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO MACRO foo(a, b) AS SELECT $a + $b");
-  PerfettoSqlPreprocessor preprocessor(source, macros_);
-  ASSERT_TRUE(preprocessor.NextStatement());
-  ASSERT_EQ(
-      preprocessor.statement(),
-      FindSubstr(source, "CREATE PERFETTO MACRO foo(a, b) AS SELECT $a + $b"));
-  ASSERT_FALSE(preprocessor.NextStatement());
-  ASSERT_TRUE(preprocessor.status().ok());
-}
-
-TEST_F(PerfettoSqlPreprocessorUnittest, SingleMacro) {
-  auto foo = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO MACRO foo(a Expr, b Expr) Returns Expr AS "
-      "SELECT $a + $b");
-  macros_.Insert(
-      "foo",
-      Macro{false, "foo", {"a", "b"}, FindSubstr(foo, "SELECT $a + $b")});
-
-  auto source = SqlSource::FromExecuteQuery(
-      "foo!((select s.ts + r.dur from s, r), 1234); SELECT 1");
-  PerfettoSqlPreprocessor preprocessor(source, macros_);
-  ASSERT_TRUE(preprocessor.NextStatement());
-  ASSERT_EQ(preprocessor.statement().AsTraceback(0),
-            "Fully expanded statement\n"
-            "  SELECT (select s.ts + r.dur from s, r) + 1234\n"
-            "  ^\n"
-            "Traceback (most recent call last):\n"
-            "  File \"stdin\" line 1 col 1\n"
-            "    foo!((select s.ts + r.dur from s, r), 1234)\n"
-            "    ^\n"
-            "  File \"stdin\" line 1 col 59\n"
-            "    SELECT $a + $b\n"
-            "    ^\n");
-  ASSERT_EQ(preprocessor.statement().AsTraceback(7),
-            "Fully expanded statement\n"
-            "  SELECT (select s.ts + r.dur from s, r) + 1234\n"
-            "         ^\n"
-            "Traceback (most recent call last):\n"
-            "  File \"stdin\" line 1 col 1\n"
-            "    foo!((select s.ts + r.dur from s, r), 1234)\n"
-            "    ^\n"
-            "  File \"stdin\" line 1 col 66\n"
-            "    SELECT $a + $b\n"
-            "           ^\n"
-            "  File \"stdin\" line 1 col 6\n"
-            "    (select s.ts + r.dur from s, r)\n"
-            "    ^\n");
-  ASSERT_EQ(preprocessor.statement().sql(),
-            "SELECT (select s.ts + r.dur from s, r) + 1234");
-  ASSERT_TRUE(preprocessor.NextStatement());
-  ASSERT_EQ(preprocessor.statement(), FindSubstr(source, "SELECT 1"));
-  ASSERT_FALSE(preprocessor.NextStatement());
-  ASSERT_TRUE(preprocessor.status().ok());
-}
-
-TEST_F(PerfettoSqlPreprocessorUnittest, NestedMacro) {
-  auto foo = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO MACRO foo(a Expr, b Expr) Returns Expr AS $a + $b");
-  macros_.Insert("foo", Macro{
-                            false,
-                            "foo",
-                            {"a", "b"},
-                            FindSubstr(foo, "$a + $b"),
-                        });
-
-  auto bar = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO MACRO bar(a, b) Returns Expr AS "
-      "tfoo!($a, $b) + foo!($b, $a)");
-  macros_.Insert("bar", Macro{
-                            false,
-                            "bar",
-                            {"a", "b"},
-                            FindSubstr(bar, "foo!($a, $b) + foo!($b, $a)"),
-                        });
-
-  auto source = SqlSource::FromExecuteQuery(
-      "SELECT bar!((select s.ts + r.dur from s, r), 1234); SELECT 1");
-  PerfettoSqlPreprocessor preprocessor(source, macros_);
-  ASSERT_TRUE(preprocessor.NextStatement());
-  ASSERT_EQ(preprocessor.statement().sql(),
-            "SELECT (select s.ts + r.dur from s, r) + 1234 + 1234 + "
-            "(select s.ts + r.dur from s, r)");
-  ASSERT_TRUE(preprocessor.NextStatement());
-  ASSERT_EQ(preprocessor.statement().sql(), "SELECT 1");
-}
-
-TEST_F(PerfettoSqlPreprocessorUnittest, Stringify) {
-  auto sf = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO MACRO sf(a Expr, b Expr) Returns Expr AS "
-      "__intrinsic_stringify!($a + $b)");
-  macros_.Insert("sf", Macro{
-                           false,
-                           "sf",
-                           {"a", "b"},
-                           FindSubstr(sf, "__intrinsic_stringify!($a + $b)"),
-                       });
-  auto bar = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO MACRO bar(a Expr, b Expr) Returns Expr AS "
-      "sf!((SELECT $a), (SELECT $b))");
-  macros_.Insert("bar", Macro{
-                            false,
-                            "bar",
-                            {"a", "b"},
-                            FindSubstr(bar, "sf!((SELECT $a), (SELECT $b))"),
-                        });
-  auto baz = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO MACRO baz(a Expr, b Expr) Returns Expr AS "
-      "SELECT bar!((SELECT $a), (SELECT $b))");
-  macros_.Insert("baz", Macro{
-                            false,
-                            "baz",
-                            {"a", "b"},
-                            FindSubstr(baz, "bar!((SELECT $a), (SELECT $b))"),
-                        });
-
-  {
-    auto source =
-        SqlSource::FromExecuteQuery("__intrinsic_stringify!(foo bar baz)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(), "'foo bar baz'");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-
-  {
-    auto source = SqlSource::FromExecuteQuery("sf!(1, 2)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(), "'1 + 2'");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-
-  {
-    auto source = SqlSource::FromExecuteQuery("baz!(1, 2)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(),
-              "'(SELECT (SELECT 1)) + (SELECT (SELECT 2))'");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-
-  {
-    auto source = SqlSource::FromExecuteQuery("__intrinsic_stringify!()");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_FALSE(preprocessor.NextStatement());
-    ASSERT_EQ(preprocessor.status().message(),
-              "Traceback (most recent call last):\n"
-              "  File \"stdin\" line 1 col 1\n"
-              "    __intrinsic_stringify!()\n"
-              "    ^\n"
-              "stringify: stringify must not be empty");
-  }
-}
-
-TEST_F(PerfettoSqlPreprocessorUnittest, ZipJoin) {
-  auto foo = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO MACRO G(a Expr, b Expr) Returns Expr AS $a AS $b");
-  macros_.Insert("G", Macro{
-                          false,
-                          "G",
-                          {"a", "b"},
-                          FindSubstr(foo, "$a AS $b"),
-                      });
-
-  auto zj = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO MACRO ZJ(a Expr, b Expr, c Expr, d Expr) Returns Expr "
-      "AS __intrinsic_token_zip_join!($a, $b, $c, $d)");
-  macros_.Insert(
-      "ZJ", Macro{
-                false,
-                "ZJ",
-                {"a", "b", "c", "d"},
-                FindSubstr(zj, "__intrinsic_token_zip_join!($a, $b, $c, $d)"),
-            });
-  {
-    auto source = SqlSource::FromExecuteQuery(
-        "__intrinsic_token_zip_join!((foo, bar), (baz, bat), G, AND)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(), "foo AS baz AND bar AS bat");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-  {
-    auto source = SqlSource::FromExecuteQuery(
-        "__intrinsic_token_zip_join!((foo, bar), (baz, bat, bada), G, AND)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(), "foo AS baz AND bar AS bat");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-  {
-    auto source = SqlSource::FromExecuteQuery(
-        "__intrinsic_token_zip_join!((foo, bar), (baz, bat, bada), G, "
-        "__intrinsic_token_comma!())");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(), "foo AS baz , bar AS bat");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-  {
-    auto source = SqlSource::FromExecuteQuery(
-        "ZJ!((foo, bar), (baz, bat, bada), G, __intrinsic_token_comma!())");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(), "foo AS baz , bar AS bat");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-}
-
-TEST_F(PerfettoSqlPreprocessorUnittest, TokenApply) {
-  auto foo = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO MACRO G(a Expr, b Expr) Returns Expr AS $a AS $b");
-  macros_.Insert("G", Macro{
-                          false,
-                          "G",
-                          {"a", "b"},
-                          FindSubstr(foo, "$a AS $b"),
-                      });
-
-  auto tp = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO MACRO TokApply(a Expr, b Expr, c Expr) Returns Expr "
-      "AS __intrinsic_token_apply!($a, $b, $c)");
-  macros_.Insert("TokApply",
-                 Macro{
-                     false,
-                     "TokApply",
-                     {"a", "b", "c"},
-                     FindSubstr(tp, "__intrinsic_token_apply!($a, $b, $c)"),
-                 });
-  {
-    auto source =
-        SqlSource::FromExecuteQuery("__intrinsic_token_apply!((), G, AND)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(), "");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-  {
-    auto source = SqlSource::FromExecuteQuery(
-        "__intrinsic_token_apply!(((foo, bar)), G, AND)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(), "foo AS bar");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-  {
-    auto source = SqlSource::FromExecuteQuery(
-        "__intrinsic_token_apply!(((foo, bar), (baz, bat)), G, AND)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(), "foo AS bar AND baz AS bat");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-  {
-    auto source = SqlSource::FromExecuteQuery(
-        "__intrinsic_token_apply!(((foo, bar), (baz, bat, bada)), G, AND)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_FALSE(preprocessor.NextStatement());
-    ASSERT_THAT(preprocessor.status().message(), HasSubstr("too many args"));
-  }
-  {
-    auto source = SqlSource::FromExecuteQuery(
-        "__intrinsic_token_apply!(((foo, bar), (baz)), G, AND)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_FALSE(preprocessor.NextStatement());
-    ASSERT_THAT(preprocessor.status().message(), HasSubstr("too few args"));
-  }
-  {
-    auto source = SqlSource::FromExecuteQuery(
-        "TokApply!(((foo, bar), (baz, bat)), G, AND)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(), "foo AS bar AND baz AS bat");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-}
-
-TEST_F(PerfettoSqlPreprocessorUnittest, TokenMapJoin) {
-  auto foo = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO MACRO G(a Expr) Returns Expr AS baza.$a");
-  macros_.Insert("G", Macro{
-                          false,
-                          "G",
-                          {"a"},
-                          FindSubstr(foo, "baza.$a"),
-                      });
-
-  auto tp = SqlSource::FromExecuteQuery(
-      "CREATE PERFETTO MACRO TokMapJoin(a Expr, b Expr, c Expr) Returns Expr "
-      "AS __intrinsic_token_map_join!($a, $b, $c)");
-  macros_.Insert("TokMapJoin",
-                 Macro{
-                     false,
-                     "TokMapJoin",
-                     {"a", "b", "c"},
-                     FindSubstr(tp, "__intrinsic_token_map_join!($a, $b, $c)"),
-                 });
-  {
-    auto source =
-        SqlSource::FromExecuteQuery("__intrinsic_token_map_join!((), G, AND)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(), "");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-  {
-    auto source = SqlSource::FromExecuteQuery(
-        "__intrinsic_token_map_join!((foo), G, AND)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(), "baza.foo");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-  {
-    auto source = SqlSource::FromExecuteQuery(
-        "__intrinsic_token_map_join!((foo, baz), G, AND)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(), "baza.foo AND baza.baz");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-  {
-    auto source =
-        SqlSource::FromExecuteQuery("TokMapJoin!((foo, bar), G, AND)");
-    PerfettoSqlPreprocessor preprocessor(source, macros_);
-    ASSERT_TRUE(preprocessor.NextStatement())
-        << preprocessor.status().message();
-    ASSERT_EQ(preprocessor.statement().sql(), "baza.foo AND baza.bar");
-    ASSERT_FALSE(preprocessor.NextStatement());
-  }
-}
-
-}  // namespace
-}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_test_utils.h b/src/trace_processor/perfetto_sql/engine/perfetto_sql_test_utils.h
deleted file mode 100644
index 78cc4dc..0000000
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_test_utils.h
+++ /dev/null
@@ -1,141 +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.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_TEST_UTILS_H_
-#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_TEST_UTILS_H_
-
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h"
-#include "src/trace_processor/sqlite/sql_source.h"
-#include "test/gtest_and_gmock.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-inline bool operator==(const SqlSource& a, const SqlSource& b) {
-  return a.sql() == b.sql();
-}
-
-inline bool operator==(const PerfettoSqlParser::SqliteSql&,
-                       const PerfettoSqlParser::SqliteSql&) {
-  return true;
-}
-
-inline bool operator==(const PerfettoSqlParser::CreateFunction& a,
-                       const PerfettoSqlParser::CreateFunction& b) {
-  return std::tie(a.returns, a.is_table, a.prototype, a.replace, a.sql) ==
-         std::tie(b.returns, b.is_table, b.prototype, b.replace, b.sql);
-}
-
-inline bool operator==(const PerfettoSqlParser::CreateTable& a,
-                       const PerfettoSqlParser::CreateTable& b) {
-  return std::tie(a.name, a.sql) == std::tie(b.name, b.sql);
-}
-
-inline bool operator==(const PerfettoSqlParser::CreateView& a,
-                       const PerfettoSqlParser::CreateView& b) {
-  return std::tie(a.name, a.create_view_sql) ==
-         std::tie(b.name, b.create_view_sql);
-}
-
-inline bool operator==(const PerfettoSqlParser::Include& a,
-                       const PerfettoSqlParser::Include& b) {
-  return std::tie(a.key) == std::tie(b.key);
-}
-
-constexpr bool operator==(const PerfettoSqlParser::CreateMacro& a,
-                          const PerfettoSqlParser::CreateMacro& b) {
-  return std::tie(a.replace, a.name, a.sql, a.args) ==
-         std::tie(b.replace, b.name, b.sql, b.args);
-}
-
-constexpr bool operator==(const PerfettoSqlParser::CreateIndex& a,
-                          const PerfettoSqlParser::CreateIndex& b) {
-  return std::tie(a.replace, a.name, a.table_name, a.col_names) ==
-         std::tie(b.replace, b.name, b.table_name, b.col_names);
-}
-
-constexpr bool operator==(const PerfettoSqlParser::DropIndex& a,
-                          const PerfettoSqlParser::DropIndex& b) {
-  return std::tie(a.name, a.table_name) == std::tie(b.name, b.table_name);
-}
-
-inline std::ostream& operator<<(std::ostream& stream, const SqlSource& sql) {
-  return stream << "SqlSource(sql=" << testing::PrintToString(sql.sql()) << ")";
-}
-
-inline std::ostream& operator<<(std::ostream& stream,
-                                const PerfettoSqlParser::Statement& line) {
-  if (std::get_if<PerfettoSqlParser::SqliteSql>(&line)) {
-    return stream << "SqliteSql()";
-  }
-  if (auto* fn = std::get_if<PerfettoSqlParser::CreateFunction>(&line)) {
-    return stream << "CreateFn(sql=" << testing::PrintToString(fn->sql)
-                  << ", prototype=" << testing::PrintToString(fn->prototype)
-                  << ", returns=" << testing::PrintToString(fn->returns)
-                  << ", is_table=" << testing::PrintToString(fn->is_table)
-                  << ", replace=" << testing::PrintToString(fn->replace) << ")";
-  }
-  if (auto* tab = std::get_if<PerfettoSqlParser::CreateTable>(&line)) {
-    return stream << "CreateTable(name=" << testing::PrintToString(tab->name)
-                  << ", sql=" << testing::PrintToString(tab->sql) << ")";
-  }
-  if (auto* tab = std::get_if<PerfettoSqlParser::CreateView>(&line)) {
-    return stream << "CreateView(name=" << testing::PrintToString(tab->name)
-                  << ", sql=" << testing::PrintToString(tab->create_view_sql)
-                  << ")";
-  }
-  if (auto* macro = std::get_if<PerfettoSqlParser::CreateMacro>(&line)) {
-    return stream << "CreateTable(name=" << testing::PrintToString(macro->name)
-                  << ", args=" << testing::PrintToString(macro->args)
-                  << ", replace=" << testing::PrintToString(macro->replace)
-                  << ", sql=" << testing::PrintToString(macro->sql) << ")";
-  }
-  PERFETTO_FATAL("Unknown type");
-}
-
-template <typename T>
-inline bool operator==(const base::StatusOr<T>& a, const base::StatusOr<T>& b) {
-  return a.status().ok() == b.ok() &&
-         a.status().message() == b.status().message() &&
-         (!a.ok() || a.value() == b.value());
-}
-
-inline std::ostream& operator<<(std::ostream& stream, const base::Status& a) {
-  return stream << "base::Status(ok=" << a.ok()
-                << ", message=" << testing::PrintToString(a.message()) << ")";
-}
-
-template <typename T>
-inline std::ostream& operator<<(std::ostream& stream,
-                                const base::StatusOr<T>& a) {
-  std::string val = a.ok() ? testing::PrintToString(a.value()) : "";
-  return stream << "base::StatusOr(status="
-                << testing::PrintToString(a.status()) << ", value=" << val
-                << ")";
-}
-
-inline SqlSource FindSubstr(const SqlSource& source,
-                            const std::string& needle) {
-  size_t off = source.sql().find(needle);
-  PERFETTO_CHECK(off != std::string::npos);
-  return source.Substr(static_cast<uint32_t>(off),
-                       static_cast<uint32_t>(needle.size()));
-}
-
-}  // namespace trace_processor
-}  // namespace perfetto
-
-#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_TEST_UTILS_H_
diff --git a/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc b/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
index a2de04a..ad59b12 100644
--- a/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
+++ b/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
@@ -29,8 +29,8 @@
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/public/compiler.h"
-#include "src/trace_processor/perfetto_sql/engine/function_util.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
+#include "src/trace_processor/perfetto_sql/parser/function_util.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_result.h"
 #include "src/trace_processor/sqlite/module_lifecycle_manager.h"
 #include "src/trace_processor/sqlite/sqlite_utils.h"
diff --git a/src/trace_processor/perfetto_sql/engine/runtime_table_function.h b/src/trace_processor/perfetto_sql/engine/runtime_table_function.h
index b3a7b5b..51f0a07 100644
--- a/src/trace_processor/perfetto_sql/engine/runtime_table_function.h
+++ b/src/trace_processor/perfetto_sql/engine/runtime_table_function.h
@@ -24,7 +24,7 @@
 #include <vector>
 
 #include "perfetto/base/logging.h"
-#include "src/trace_processor/perfetto_sql/engine/function_util.h"
+#include "src/trace_processor/perfetto_sql/parser/function_util.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_module.h"
 #include "src/trace_processor/sqlite/module_lifecycle_manager.h"
 #include "src/trace_processor/sqlite/sql_source.h"
diff --git a/src/trace_processor/perfetto_sql/grammar/BUILD.gn b/src/trace_processor/perfetto_sql/grammar/BUILD.gn
new file mode 100644
index 0000000..8dec0d1
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/grammar/BUILD.gn
@@ -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("../../../../gn/test.gni")
+
+assert(enable_perfetto_trace_processor_sqlite)
+
+source_set("grammar") {
+  sources = [
+    "perfettosql_grammar.c",
+    "perfettosql_grammar.h",
+    "perfettosql_keywordhash.h",
+    "perfettosql_keywordhash_helper.h",
+  ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../../../base",
+  ]
+  visibility = [
+    "../parser",
+    "../tokenizer",
+    "../tokenizer:tokenize_internal",
+  ]
+  if (perfetto_build_standalone) {
+    configs -= [ "//gn/standalone:extra_warnings" ]  # nogncheck
+  } else {
+    cflags_c = [
+      "-Wno-unused-parameter",
+      "-Wno-unreachable-code",
+    ]
+  }
+}
diff --git a/src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.c b/src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.c
new file mode 100644
index 0000000..fe27c73
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.c
@@ -0,0 +1,3991 @@
+/* This file is automatically generated by Lemon from input grammar
+** source file "src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.y".
+*/
+/*
+** 2000-05-29
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** Driver template for the LEMON parser generator.
+**
+** The "lemon" program processes an LALR(1) input grammar file, then uses
+** this template to construct a parser.  The "lemon" program inserts text
+** at each "%%" line.  Also, any "P-a-r-s-e" identifer prefix (without the
+** interstitial "-" characters) contained in this template is changed into
+** the value of the %name directive from the grammar.  Otherwise, the content
+** of this template is copied straight through into the generate parser
+** source file.
+**
+** The following is the concatenation of all %include directives from the
+** input grammar file:
+*/
+/************ Begin %include sections from the grammar ************************/
+#include <stddef.h>
+
+#define YYNOERRORRECOVERY 1
+/**************** End of %include directives **********************************/
+/* These constants specify the various numeric values for terminal symbols.
+***************** Begin token definitions *************************************/
+#ifndef TK_CREATE
+#define TK_CREATE                          1
+#define TK_REPLACE                         2
+#define TK_PERFETTO                        3
+#define TK_MACRO                           4
+#define TK_INCLUDE                         5
+#define TK_MODULE                          6
+#define TK_RETURNS                         7
+#define TK_FUNCTION                        8
+#define TK_OR                              9
+#define TK_AND                            10
+#define TK_NOT                            11
+#define TK_IS                             12
+#define TK_MATCH                          13
+#define TK_LIKE_KW                        14
+#define TK_BETWEEN                        15
+#define TK_IN                             16
+#define TK_ISNULL                         17
+#define TK_NOTNULL                        18
+#define TK_NE                             19
+#define TK_EQ                             20
+#define TK_GT                             21
+#define TK_LE                             22
+#define TK_LT                             23
+#define TK_GE                             24
+#define TK_ESCAPE                         25
+#define TK_BITAND                         26
+#define TK_BITOR                          27
+#define TK_LSHIFT                         28
+#define TK_RSHIFT                         29
+#define TK_PLUS                           30
+#define TK_MINUS                          31
+#define TK_STAR                           32
+#define TK_SLASH                          33
+#define TK_REM                            34
+#define TK_CONCAT                         35
+#define TK_PTR                            36
+#define TK_COLLATE                        37
+#define TK_BITNOT                         38
+#define TK_ON                             39
+#define TK_ID                             40
+#define TK_ABORT                          41
+#define TK_ACTION                         42
+#define TK_AFTER                          43
+#define TK_ANALYZE                        44
+#define TK_ASC                            45
+#define TK_ATTACH                         46
+#define TK_BEFORE                         47
+#define TK_BEGIN                          48
+#define TK_BY                             49
+#define TK_CASCADE                        50
+#define TK_CAST                           51
+#define TK_COLUMNKW                       52
+#define TK_CONFLICT                       53
+#define TK_DATABASE                       54
+#define TK_DEFERRED                       55
+#define TK_DESC                           56
+#define TK_DETACH                         57
+#define TK_DO                             58
+#define TK_EACH                           59
+#define TK_END                            60
+#define TK_EXCLUSIVE                      61
+#define TK_EXPLAIN                        62
+#define TK_FAIL                           63
+#define TK_FOR                            64
+#define TK_IGNORE                         65
+#define TK_IMMEDIATE                      66
+#define TK_INITIALLY                      67
+#define TK_INSTEAD                        68
+#define TK_NO                             69
+#define TK_PLAN                           70
+#define TK_QUERY                          71
+#define TK_KEY                            72
+#define TK_OF                             73
+#define TK_OFFSET                         74
+#define TK_PRAGMA                         75
+#define TK_RAISE                          76
+#define TK_RECURSIVE                      77
+#define TK_RELEASE                        78
+#define TK_RESTRICT                       79
+#define TK_ROW                            80
+#define TK_ROWS                           81
+#define TK_ROLLBACK                       82
+#define TK_SAVEPOINT                      83
+#define TK_TEMP                           84
+#define TK_TRIGGER                        85
+#define TK_VACUUM                         86
+#define TK_VIEW                           87
+#define TK_VIRTUAL                        88
+#define TK_WITH                           89
+#define TK_WITHOUT                        90
+#define TK_NULLS                          91
+#define TK_FIRST                          92
+#define TK_LAST                           93
+#define TK_EXCEPT                         94
+#define TK_INTERSECT                      95
+#define TK_UNION                          96
+#define TK_CURRENT                        97
+#define TK_FOLLOWING                      98
+#define TK_PARTITION                      99
+#define TK_PRECEDING                      100
+#define TK_RANGE                          101
+#define TK_UNBOUNDED                      102
+#define TK_EXCLUDE                        103
+#define TK_GROUPS                         104
+#define TK_OTHERS                         105
+#define TK_TIES                           106
+#define TK_WITHIN                         107
+#define TK_GENERATED                      108
+#define TK_ALWAYS                         109
+#define TK_MATERIALIZED                   110
+#define TK_REINDEX                        111
+#define TK_RENAME                         112
+#define TK_CTIME_KW                       113
+#define TK_IF                             114
+#define TK_ANY                            115
+#define TK_COMMIT                         116
+#define TK_TO                             117
+#define TK_TABLE                          118
+#define TK_EXISTS                         119
+#define TK_LP                             120
+#define TK_RP                             121
+#define TK_AS                             122
+#define TK_COMMA                          123
+#define TK_STRING                         124
+#define TK_CONSTRAINT                     125
+#define TK_DEFAULT                        126
+#define TK_INDEXED                        127
+#define TK_NULL                           128
+#define TK_PRIMARY                        129
+#define TK_UNIQUE                         130
+#define TK_CHECK                          131
+#define TK_REFERENCES                     132
+#define TK_AUTOINCR                       133
+#define TK_INSERT                         134
+#define TK_DELETE                         135
+#define TK_UPDATE                         136
+#define TK_SET                            137
+#define TK_DEFERRABLE                     138
+#define TK_FOREIGN                        139
+#define TK_DROP                           140
+#define TK_ALL                            141
+#define TK_SELECT                         142
+#define TK_VALUES                         143
+#define TK_DISTINCT                       144
+#define TK_DOT                            145
+#define TK_FROM                           146
+#define TK_JOIN                           147
+#define TK_JOIN_KW                        148
+#define TK_USING                          149
+#define TK_ORDER                          150
+#define TK_GROUP                          151
+#define TK_HAVING                         152
+#define TK_LIMIT                          153
+#define TK_WHERE                          154
+#define TK_RETURNING                      155
+#define TK_INTO                           156
+#define TK_NOTHING                        157
+#define TK_FLOAT                          158
+#define TK_BLOB                           159
+#define TK_INTEGER                        160
+#define TK_VARIABLE                       161
+#define TK_CASE                           162
+#define TK_WHEN                           163
+#define TK_THEN                           164
+#define TK_ELSE                           165
+#define TK_INDEX                          166
+#define TK_SEMI                           167
+#define TK_ALTER                          168
+#define TK_ADD                            169
+#define TK_WINDOW                         170
+#define TK_OVER                           171
+#define TK_FILTER                         172
+#define TK_TRANSACTION                    173
+#define TK_SPACE                          174
+#define TK_ILLEGAL                        175
+#endif
+/**************** End token definitions ***************************************/
+
+/* The next sections is a series of control #defines.
+** various aspects of the generated parser.
+**    YYCODETYPE         is the data type used to store the integer codes
+**                       that represent terminal and non-terminal symbols.
+**                       "unsigned char" is used if there are fewer than
+**                       256 symbols.  Larger types otherwise.
+**    YYNOCODE           is a number of type YYCODETYPE that is not used for
+**                       any terminal or nonterminal symbol.
+**    YYFALLBACK         If defined, this indicates that one or more tokens
+**                       (also known as: "terminal symbols") have fall-back
+**                       values which should be used if the original symbol
+**                       would not parse.  This permits keywords to sometimes
+**                       be used as identifiers, for example.
+**    YYACTIONTYPE       is the data type used for "action codes" - numbers
+**                       that indicate what to do in response to the next
+**                       token.
+**    PerfettoSqlParserTOKENTYPE     is the data type used for minor type for terminal
+**                       symbols.  Background: A "minor type" is a semantic
+**                       value associated with a terminal or non-terminal
+**                       symbols.  For example, for an "ID" terminal symbol,
+**                       the minor type might be the name of the identifier.
+**                       Each non-terminal can have a different minor type.
+**                       Terminal symbols all have the same minor type, though.
+**                       This macros defines the minor type for terminal 
+**                       symbols.
+**    YYMINORTYPE        is the data type used for all minor types.
+**                       This is typically a union of many types, one of
+**                       which is PerfettoSqlParserTOKENTYPE.  The entry in the union
+**                       for terminal symbols is called "yy0".
+**    YYSTACKDEPTH       is the maximum depth of the parser's stack.  If
+**                       zero the stack is dynamically sized using realloc()
+**    PerfettoSqlParserARG_SDECL     A static variable declaration for the %extra_argument
+**    PerfettoSqlParserARG_PDECL     A parameter declaration for the %extra_argument
+**    PerfettoSqlParserARG_PARAM     Code to pass %extra_argument as a subroutine parameter
+**    PerfettoSqlParserARG_STORE     Code to store %extra_argument into yypParser
+**    PerfettoSqlParserARG_FETCH     Code to extract %extra_argument from yypParser
+**    PerfettoSqlParserCTX_*         As PerfettoSqlParserARG_ except for %extra_context
+**    YYERRORSYMBOL      is the code number of the error symbol.  If not
+**                       defined, then do no error processing.
+**    YYNSTATE           the combined number of states.
+**    YYNRULE            the number of rules in the grammar
+**    YYNTOKEN           Number of terminal symbols
+**    YY_MAX_SHIFT       Maximum value for shift actions
+**    YY_MIN_SHIFTREDUCE Minimum value for shift-reduce actions
+**    YY_MAX_SHIFTREDUCE Maximum value for shift-reduce actions
+**    YY_ERROR_ACTION    The yy_action[] code for syntax error
+**    YY_ACCEPT_ACTION   The yy_action[] code for accept
+**    YY_NO_ACTION       The yy_action[] code for no-op
+**    YY_MIN_REDUCE      Minimum value for reduce actions
+**    YY_MAX_REDUCE      Maximum value for reduce actions
+*/
+#ifndef INTERFACE
+# define INTERFACE 1
+#endif
+/************* Begin control #defines *****************************************/
+#define YYCODETYPE unsigned short int
+#define YYNOCODE 309
+#define YYACTIONTYPE unsigned short int
+#define YYWILDCARD 115
+#define PerfettoSqlParserTOKENTYPE void*
+typedef union {
+  int yyinit;
+  PerfettoSqlParserTOKENTYPE yy0;
+} YYMINORTYPE;
+#ifndef YYSTACKDEPTH
+#define YYSTACKDEPTH 100
+#endif
+#define PerfettoSqlParserARG_SDECL
+#define PerfettoSqlParserARG_PDECL
+#define PerfettoSqlParserARG_PARAM
+#define PerfettoSqlParserARG_FETCH
+#define PerfettoSqlParserARG_STORE
+#define PerfettoSqlParserCTX_SDECL
+#define PerfettoSqlParserCTX_PDECL
+#define PerfettoSqlParserCTX_PARAM
+#define PerfettoSqlParserCTX_FETCH
+#define PerfettoSqlParserCTX_STORE
+#define YYFALLBACK 1
+#define YYNSTATE             579
+#define YYNRULE              405
+#define YYNRULE_WITH_ACTION  0
+#define YYNTOKEN             176
+#define YY_MAX_SHIFT         578
+#define YY_MIN_SHIFTREDUCE   838
+#define YY_MAX_SHIFTREDUCE   1242
+#define YY_ERROR_ACTION      1243
+#define YY_ACCEPT_ACTION     1244
+#define YY_NO_ACTION         1245
+#define YY_MIN_REDUCE        1246
+#define YY_MAX_REDUCE        1650
+/************* End control #defines *******************************************/
+#define YY_NLOOKAHEAD ((int)(sizeof(yy_lookahead)/sizeof(yy_lookahead[0])))
+
+/* Define the yytestcase() macro to be a no-op if is not already defined
+** otherwise.
+**
+** Applications can choose to define yytestcase() in the %include section
+** to a macro that can assist in verifying code coverage.  For production
+** code the yytestcase() macro should be turned off.  But it is useful
+** for testing.
+*/
+#ifndef yytestcase
+# define yytestcase(X)
+#endif
+
+
+/* Next are the tables used to determine what action to take based on the
+** current state and lookahead token.  These tables are used to implement
+** functions that take a state number and lookahead value and return an
+** action integer.  
+**
+** Suppose the action integer is N.  Then the action is determined as
+** follows
+**
+**   0 <= N <= YY_MAX_SHIFT             Shift N.  That is, push the lookahead
+**                                      token onto the stack and goto state N.
+**
+**   N between YY_MIN_SHIFTREDUCE       Shift to an arbitrary state then
+**     and YY_MAX_SHIFTREDUCE           reduce by rule N-YY_MIN_SHIFTREDUCE.
+**
+**   N == YY_ERROR_ACTION               A syntax error has occurred.
+**
+**   N == YY_ACCEPT_ACTION              The parser accepts its input.
+**
+**   N == YY_NO_ACTION                  No such action.  Denotes unused
+**                                      slots in the yy_action[] table.
+**
+**   N between YY_MIN_REDUCE            Reduce by rule N-YY_MIN_REDUCE
+**     and YY_MAX_REDUCE
+**
+** The action table is constructed as a single large table named yy_action[].
+** Given state S and lookahead X, the action is computed as either:
+**
+**    (A)   N = yy_action[ yy_shift_ofst[S] + X ]
+**    (B)   N = yy_default[S]
+**
+** The (A) formula is preferred.  The B formula is used instead if
+** yy_lookahead[yy_shift_ofst[S]+X] is not equal to X.
+**
+** The formulas above are for computing the action when the lookahead is
+** a terminal symbol.  If the lookahead is a non-terminal (as occurs after
+** a reduce action) then the yy_reduce_ofst[] array is used in place of
+** the yy_shift_ofst[] array.
+**
+** The following are the tables generated in this section:
+**
+**  yy_action[]        A single table containing all actions.
+**  yy_lookahead[]     A table containing the lookahead for each entry in
+**                     yy_action.  Used to detect hash collisions.
+**  yy_shift_ofst[]    For each state, the offset into yy_action for
+**                     shifting terminals.
+**  yy_reduce_ofst[]   For each state, the offset into yy_action for
+**                     shifting non-terminals after a reduce.
+**  yy_default[]       Default action for each state.
+**
+*********** Begin parsing tables **********************************************/
+#define YY_ACTTAB_COUNT (2098)
+static const YYACTIONTYPE yy_action[] = {
+ /*     0 */  1607,  119,  116,  231,  282,  119,  116,  231, 1607,  126,
+ /*    10 */   128,  411,   81, 1217, 1217, 1054, 1057, 1044, 1044,  124,
+ /*    20 */   124,  125,  125,  125,  125, 1529,  123,  123,  123,  123,
+ /*    30 */   122,  122,  121,  121,  121,  120,  117,  449,  120,  117,
+ /*    40 */   449, 1041, 1041, 1055, 1058,  974,  122,  122,  121,  121,
+ /*    50 */   121,  120,  117,  449,  449,  342,  975,  121,  121,  121,
+ /*    60 */   120,  117,  449,  119,  116,  231,  126,  128,  411,   81,
+ /*    70 */  1217, 1217, 1054, 1057, 1044, 1044,  124,  124,  125,  125,
+ /*    80 */   125,  125,  564,  123,  123,  123,  123,  122,  122,  121,
+ /*    90 */   121,  121,  120,  117,  449,  126,  128,  411,   81, 1217,
+ /*   100 */  1217, 1054, 1057, 1044, 1044,  124,  124,  125,  125,  125,
+ /*   110 */   125, 1279,  123,  123,  123,  123,  122,  122,  121,  121,
+ /*   120 */   121,  120,  117,  449,  126,  128,  411,   81, 1217, 1217,
+ /*   130 */  1054, 1057, 1044, 1044,  124,  124,  125,  125,  125,  125,
+ /*   140 */  1493,  123,  123,  123,  123,  122,  122,  121,  121,  121,
+ /*   150 */   120,  117,  449,  564,  564, 1164, 1045, 1164,  376, 1282,
+ /*   160 */   129,  125,  125,  125,  125,  118,  123,  123,  123,  123,
+ /*   170 */   122,  122,  121,  121,  121,  120,  117,  449, 1010,  450,
+ /*   180 */   529,  238,  368,  125,  125,  125,  125,   87,  123,  123,
+ /*   190 */   123,  123,  122,  122,  121,  121,  121,  120,  117,  449,
+ /*   200 */   233,  529,  487,  359,  458,  455,  454,  415,  127,  100,
+ /*   210 */  1138,  119,  116,  231,  453, 1140, 1642,  418, 1642,  126,
+ /*   220 */   128,  411,   81, 1217, 1217, 1054, 1057, 1044, 1044,  124,
+ /*   230 */   124,  125,  125,  125,  125,  548,  123,  123,  123,  123,
+ /*   240 */   122,  122,  121,  121,  121,  120,  117,  449,  126,  128,
+ /*   250 */   411,   81, 1217, 1217, 1054, 1057, 1044, 1044,  124,  124,
+ /*   260 */   125,  125,  125,  125, 1281,  123,  123,  123,  123,  122,
+ /*   270 */   122,  121,  121,  121,  120,  117,  449,  126,  128,  411,
+ /*   280 */    81, 1217, 1217, 1054, 1057, 1044, 1044,  124,  124,  125,
+ /*   290 */   125,  125,  125, 1557,  123,  123,  123,  123,  122,  122,
+ /*   300 */   121,  121,  121,  120,  117,  449,  328, 1101, 1101,  510,
+ /*   310 */   357,  482,  345,  530,  530,  126,  128,  411,   81, 1217,
+ /*   320 */  1217, 1054, 1057, 1044, 1044,  124,  124,  125,  125,  125,
+ /*   330 */   125,  321,  123,  123,  123,  123,  122,  122,  121,  121,
+ /*   340 */   121,  120,  117,  449,  126,  128,  411,   81, 1217, 1217,
+ /*   350 */  1054, 1057, 1044, 1044,  124,  124,  125,  125,  125,  125,
+ /*   360 */   464,  123,  123,  123,  123,  122,  122,  121,  121,  121,
+ /*   370 */   120,  117,  449,  126,  128,  411,   81, 1217, 1217, 1054,
+ /*   380 */  1057, 1044, 1044,  124,  124,  125,  125,  125,  125,  879,
+ /*   390 */   123,  123,  123,  123,  122,  122,  121,  121,  121,  120,
+ /*   400 */   117,  449, 1138,  929,  929,  572,  521, 1140, 1643,  225,
+ /*   410 */  1643,  126,  128,  411,   81, 1217, 1217, 1054, 1057, 1044,
+ /*   420 */  1044,  124,  124,  125,  125,  125,  125,  872,  123,  123,
+ /*   430 */   123,  123,  122,  122,  121,  121,  121,  120,  117,  449,
+ /*   440 */    98,  126,  128,  411,   81, 1217, 1217, 1054, 1057, 1044,
+ /*   450 */  1044,  124,  124,  125,  125,  125,  125,   88,  123,  123,
+ /*   460 */   123,  123,  122,  122,  121,  121,  121,  120,  117,  449,
+ /*   470 */   126,  128,  411,   81, 1217, 1217, 1054, 1057, 1044, 1044,
+ /*   480 */   124,  124,  125,  125,  125,  125,  513,  123,  123,  123,
+ /*   490 */   123,  122,  122,  121,  121,  121,  120,  117,  449,  123,
+ /*   500 */   123,  123,  123,  122,  122,  121,  121,  121,  120,  117,
+ /*   510 */   449, 1613,  144,  570,  974,  519,  144,  570,  445,  216,
+ /*   520 */   570,  407,  522, 1032,  554,  975,  425, 1317,  531, 1296,
+ /*   530 */   518,  377, 1031,   74,   74, 1098, 1317,   51,   51, 1098,
+ /*   540 */    74,   74,  498, 1020,  230,  536,  288, 1019,  288,  476,
+ /*   550 */    16,   16,  288, 1015,  288,  288,  368,  288,  567,  288,
+ /*   560 */   508,  288,  261,  227,  567,  319,  571,  567,  553,  319,
+ /*   570 */   571,  567,  532, 1563,    6,  553,  555, 1019, 1019, 1021,
+ /*   580 */   569,  494, 1177,  520,  119,  116,  231,  471,  371,  445,
+ /*   590 */   445,  438,  126,  128,  411,   81, 1217, 1217, 1054, 1057,
+ /*   600 */  1044, 1044,  124,  124,  125,  125,  125,  125, 1010,  123,
+ /*   610 */   123,  123,  123,  122,  122,  121,  121,  121,  120,  117,
+ /*   620 */   449,  126,  128,  411,   81, 1217, 1217, 1054, 1057, 1044,
+ /*   630 */  1044,  124,  124,  125,  125,  125,  125,  524,  123,  123,
+ /*   640 */   123,  123,  122,  122,  121,  121,  121,  120,  117,  449,
+ /*   650 */   126,  128,  411,   81, 1217, 1217, 1054, 1057, 1044, 1044,
+ /*   660 */   124,  124,  125,  125,  125,  125, 1193,  123,  123,  123,
+ /*   670 */   123,  122,  122,  121,  121,  121,  120,  117,  449,  126,
+ /*   680 */   128,  411,   81, 1217, 1217, 1054, 1057, 1044, 1044,  124,
+ /*   690 */   124,  125,  125,  125,  125,  144,  123,  123,  123,  123,
+ /*   700 */   122,  122,  121,  121,  121,  120,  117,  449,  126,  115,
+ /*   710 */   411,   81, 1217, 1217, 1054, 1057, 1044, 1044,  124,  124,
+ /*   720 */   125,  125,  125,  125,  228,  123,  123,  123,  123,  122,
+ /*   730 */   122,  121,  121,  121,  120,  117,  449, 1239, 1584,  297,
+ /*   740 */   357,  570,  399, 1238, 1159, 1644,  400,  106,  319,  571,
+ /*   750 */  1194,  233,  366, 1193,  388,  458,  455,  454,  444,  443,
+ /*   760 */   570,   13,   13,  211, 1159,  453, 1268, 1159,  380,  346,
+ /*   770 */   288,  348,  288,  320, 1193,  419,  109, 1193, 1268,  475,
+ /*   780 */    42,   42,  567,  427, 1498,  390,  103,  128,  411,   81,
+ /*   790 */  1217, 1217, 1054, 1057, 1044, 1044,  124,  124,  125,  125,
+ /*   800 */   125,  125,  349,  123,  123,  123,  123,  122,  122,  121,
+ /*   810 */   121,  121,  120,  117,  449,  411,   81, 1217, 1217, 1054,
+ /*   820 */  1057, 1044, 1044,  124,  124,  125,  125,  125,  125,  113,
+ /*   830 */   123,  123,  123,  123,  122,  122,  121,  121,  121,  120,
+ /*   840 */   117,  449,  570,  178,    2,  462,  462,  344,  111,  111,
+ /*   850 */   347,  293,  153,  570,  404,  403,  112,  193,  447,  916,
+ /*   860 */   462, 1194,   74,   74, 1193,  475,  474,  475,  875,  565,
+ /*   870 */  1498, 1500, 1498,   74,   74, 1313,    2,  462,  462,  374,
+ /*   880 */   374, 1438,  370,  293,  153, 1193, 1225,  284, 1225,  960,
+ /*   890 */   427,  288,  462,  288,  559,  424, 1269,  553, 1118,  546,
+ /*   900 */   444,  443,  478,  567,   12,  552,  478,  144,  553, 1222,
+ /*   910 */  1193, 1193,  570,  960,  229, 1193,  442,  251, 1355,  314,
+ /*   920 */  1119,  421,  915,  288,  382,  288,  373,  545,  378, 1438,
+ /*   930 */   570, 1031,   74,   74,  533,  567, 1170,  562,    4, 1117,
+ /*   940 */   502, 1031,  568,  113,   85,  447, 1019,    8, 1159,  251,
+ /*   950 */    13,   13, 1020,  463,  209,  875, 1019,  527,  527,    6,
+ /*   960 */   319,  571,  111,  111,  292,  234,  447,  525, 1159,  570,
+ /*   970 */   112, 1159,  447, 1244,    1,    1, 1019, 1019, 1021, 1022,
+ /*   980 */    28, 1170,  503,  565, 1213,  463, 1019, 1019, 1021,   42,
+ /*   990 */    42,  178,  466, 1556, 1194, 1194, 1224, 1193, 1193, 1194,
+ /*  1000 */   570, 1193, 1193,  867,  208, 1223,  325, 1587,  559,  240,
+ /*  1010 */   278, 1300,  327,  469,  330,  468,  239,  405, 1193, 1193,
+ /*  1020 */    42,   42,  328, 1193, 1193,  459,  461,  461, 1225,  542,
+ /*  1030 */  1225,  208,  293,  153,  541,  459,  301,  459,  148,  324,
+ /*  1040 */  1612,  461,  904,  935,  396, 1031,  934, 1159,  237,  236,
+ /*  1050 */   235,  562,    4, 1586,  852,  370,  568,  199,  570,  447,
+ /*  1060 */  1019,  149, 1193, 1193,    9,  540,  264, 1159, 1213, 1299,
+ /*  1070 */  1159, 1193,  288, 1193,  288, 1193,  319,  571,   72,   72,
+ /*  1080 */   447, 1297,  570,  152,  567, 1194,  375,  867, 1193,  375,
+ /*  1090 */  1019, 1019, 1021, 1022,   28,  417,  155,  245,  251,  340,
+ /*  1100 */  1320,  290,   74,   74, 1159,  537,  537,    6, 1194, 1193,
+ /*  1110 */   339, 1193,  843,  394,  895,  578,  539,  544,  845,  496,
+ /*  1120 */   544,  496,  549,  844, 1159,  113,  855, 1159,  247,  496,
+ /*  1130 */   485,  391, 1193,  467,  463,  393,  275,  315,  381,  172,
+ /*  1140 */   427,  852,  143,  896,  111,  111, 1194, 1194,  940, 1193,
+ /*  1150 */  1193,  194,  112,    5,  447, 1194, 1193, 1194, 1193, 1194,
+ /*  1160 */  1193, 1320, 1193, 1322,  246,  565,  155,  513, 1196,  394,
+ /*  1170 */  1193, 1193, 1076,  538,  507,  281, 1298,  281,   90, 1193,
+ /*  1180 */   504, 1193,  962, 1193,  245, 1096,  340,  567,  290,  299,
+ /*  1190 */   559,  302,  570,  412, 1193,  319,  571,  339, 1437,  305,
+ /*  1200 */   394,  422,  578,  901,  210, 1193, 1266, 1528, 1485,  225,
+ /*  1210 */   570,  542,  134,  134,  902,  247,  543,  370,  391, 1193,
+ /*  1220 */  1181,  473,  393,  275, 1322, 1334,  172, 1031,  113,  143,
+ /*  1230 */    13,   13,  219,  562,    4,  567,  502,  288,  568,  288,
+ /*  1240 */  1194,  447, 1019, 1193,  430,  378, 1437,  111,  111,  567,
+ /*  1250 */   113,  246, 1196,  496,  513,  112,  394,  447,  158,  432,
+ /*  1260 */   487,  359,  447,  329, 1193,   12,  570,  521,  565,  111,
+ /*  1270 */   111,  939, 1019, 1019, 1021, 1022,   28,  112, 1194,  447,
+ /*  1280 */   412, 1193,  319,  571,  570,   35,   13,   13,  479, 1194,
+ /*  1290 */   565,  570, 1193,  559, 1492,   97,  570,  960,  410,    3,
+ /*  1300 */   435,  961, 1193, 1194,   13,   13, 1193, 1181,  473, 1118,
+ /*  1310 */   502,   13,   13, 1193,  542,  559,  136,  136,  547,  541,
+ /*  1320 */   113,  960,  229,  307,  432,  440,  548, 1193,  158,  882,
+ /*  1330 */  1031, 1119,  356,  558,  570,  477,  562,    4,  205,  111,
+ /*  1340 */   111,  568,   80,  486,  447, 1019,  150,  112, 1436,  447,
+ /*  1350 */  1117,  208, 1031,  570,   13,   13, 1561,    6,  562,    4,
+ /*  1360 */   565,  111,  111,  568,  397,  447,  447, 1019,  203,  112,
+ /*  1370 */   570,  447,  570,   74,   74, 1019, 1019, 1021, 1022,   28,
+ /*  1380 */   513,  513,  565,  522,  291,  559,  513,  447, 1271,  416,
+ /*  1390 */    13,   13,   74,   74,  570,  378, 1436, 1019, 1019, 1021,
+ /*  1400 */  1022,   28, 1562,    6,  204,  523,  308,  559,  316, 1560,
+ /*  1410 */     6, 1239,  113,  882,   44,   44,  399, 1139, 1559,    6,
+ /*  1420 */  1329, 1325, 1031,  306,  480,  570,  364,  446,  562,    4,
+ /*  1430 */   515,  111,  111,  568,  410,  433,  447, 1019,  300,  112,
+ /*  1440 */  1278,  447,  300,  561, 1031,   74,   74,  146, 1213,   37,
+ /*  1450 */   562,    4,  565,  551,  288,  568,  288,  447,  447, 1019,
+ /*  1460 */   852,  288,  570,  288,  570, 1328,  567, 1019, 1019, 1021,
+ /*  1470 */  1022,   28,   97,  567,  288,  550,  288,  559,  291,  447,
+ /*  1480 */   448,  570,   45,   45,   46,   46,  567,  563,  416, 1019,
+ /*  1490 */  1019, 1021, 1022,   28,  420,  180,  288,  570,  288,  370,
+ /*  1500 */   433,   47,   47,  245, 1212,  340,  434,  290,  567,  370,
+ /*  1510 */   289,  110,  289,  108, 1031,  570,  339,   56,   56,  394,
+ /*  1520 */   562,    4,  567,  570,  492,  568,  570,  219,  447, 1019,
+ /*  1530 */   264,  570, 1213,  570,  247,   57,   57,  391,  319,  571,
+ /*  1540 */   154,  393,  275,   15,   15,  172,   48,   48,  143,  447,
+ /*  1550 */   156,   58,   58,   49,   49,  287,  570,  509,  570, 1019,
+ /*  1560 */  1019, 1021, 1022,   28,  570,  238,  570,   91,  215,  570,
+ /*  1570 */   246,  434,  570, 1539,  570,  394,   59,   59,   60,   60,
+ /*  1580 */  1601,  439,  523,  570,   61,   61,   62,   62,  489,   63,
+ /*  1590 */    63,  415,   64,   64,   65,   65,  570,  493,  410,  412,
+ /*  1600 */  1541,  319,  571,   66,   66,  570,  472,  410,  500,  570,
+ /*  1610 */   154,  570, 1233,  570,  318,  570,   67,   67,  570, 1537,
+ /*  1620 */   156,  570,  509,  570,  410,   50,   50,  473,  570,   52,
+ /*  1630 */    52,   14,   14,  132,  132,  133,  133,  570,   69,   69,
+ /*  1640 */   218,   53,   53,   70,   70,  570,  439,  570,   71,   71,
+ /*  1650 */   570,   32,  570,  935,  103,  570,  934,   54,   54,  570,
+ /*  1660 */   451,  570,  260,  570,  886,  165,  165,  166,  166,  296,
+ /*  1670 */    78,   78,   55,   55,  570,  135,  135,  244,  570,   73,
+ /*  1680 */    73,  163,  163,  137,  137,  570,  322,  570,  103,  570,
+ /*  1690 */   286,  227,  428,  517,  131,  131,  570,  336,  164,  164,
+ /*  1700 */   570, 1135,  570,  401,  248,  157,  157,  141,  141,  140,
+ /*  1710 */   140,  570,  894,  570,  893,  570,  138,  138,  570,  334,
+ /*  1720 */   139,  139,   76,   76,  501,  488,  490,  250, 1083,  333,
+ /*  1730 */  1023,   68,   68,   77,   77,   75,   75,  162,   43,   43,
+ /*  1740 */   355, 1353,  103, 1012,  337,  263,  354,  303,  495,  338,
+ /*  1750 */   263,   31,  341,  497,   19,  263,  360, 1079,  103,  260,
+ /*  1760 */   965,  161,  263,  103,  977,  978,  534, 1095, 1094, 1095,
+ /*  1770 */  1094,  865,  933,  151,  127,  932, 1367,  127,  201, 1366,
+ /*  1780 */   481, 1544,  350, 1517, 1516,  505,  361, 1363,  365,  369,
+ /*  1790 */  1575,  221, 1144, 1376, 1421, 1349,  556,  557, 1426,  387,
+ /*  1800 */   514, 1361,  389,  213, 1257, 1256, 1258, 1594,  279,   11,
+ /*  1810 */  1346,  311, 1083,  312, 1023,  313,  395, 1597,  243, 1403,
+ /*  1820 */  1408,  298,  352,  353,  224,  304, 1396,  499, 1413, 1358,
+ /*  1830 */  1359,  198, 1412,  456,  470,  408,   33,  332,   83,  423,
+ /*  1840 */  1296,  886,  460, 1233,  182, 1489, 1488,  560,  272,   90,
+ /*  1850 */   241, 1230,  217, 1536, 1534,  398, 1316,  191, 1315, 1314,
+ /*  1860 */  1609,  426,  177,  483, 1286,  331,  358, 1611, 1610,  402,
+ /*  1870 */  1285, 1284, 1307,  484,  220,  385, 1306,  206,  207,  575,
+ /*  1880 */   253,   98,   96,  506,  257, 1357, 1356,  512,  436,  280,
+ /*  1890 */   259,  437,  130,  548,  184,  266,  265,  186,  187,  188,
+ /*  1900 */   189,  441,  230,   10,  107,   99,  526, 1187, 1417,  379,
+ /*  1910 */   406,  195,  333,  367,  491,  277,  576,  386,  271,  273,
+ /*  1920 */   409, 1259,  274,  179, 1483, 1254,  276, 1249, 1409,   36,
+ /*  1930 */   232,  452,   82,   17,   18,  573,  323, 1494, 1415, 1414,
+ /*  1940 */    38,  457,  170, 1339,  372, 1183,  142, 1182,  214,   86,
+ /*  1950 */    89,  884,  326,  145, 1521,  384,  212, 1338,  383,  222,
+ /*  1960 */   223,  294, 1522,  897, 1381, 1380, 1520, 1519,   79,  465,
+ /*  1970 */   309,  310,   84,  295, 1505,  363,  171,  335,  242, 1093,
+ /*  1980 */   147, 1470,  343,  183, 1091,  317,  173, 1212,  249,  185,
+ /*  1990 */   918,  516,  351,  252,  190,  429, 1107,  431,  192,   92,
+ /*  2000 */    93,   94,  174,   95,  175,  176, 1110,  254, 1106,  159,
+ /*  2010 */    20,  256,  362, 1566, 1565,  255, 1099,  263,  196,  167,
+ /*  2020 */  1227,  511,  168,  258, 1580,  197, 1146,   39, 1145,  226,
+ /*  2030 */   283,  285,  200,  969,  262,  169,  963,  127,  413,  181,
+ /*  2040 */   414,   21,  528, 1150,   34,   22,  102, 1060,  202,  160,
+ /*  2050 */   101,  535,   23, 1175,   24,   25, 1161, 1165, 1163,    7,
+ /*  2060 */  1168,  103, 1169,   26,   27,  104,  574, 1074, 1061, 1059,
+ /*  2070 */  1116, 1064, 1115,  267,  268,  105,  566,  839, 1063,   40,
+ /*  2080 */  1602,  269,  928,  577, 1024,  866,  114,   29,   30,  854,
+ /*  2090 */   392,   41, 1245, 1245, 1245, 1245, 1245,  270,
+};
+static const YYCODETYPE yy_lookahead[] = {
+ /*     0 */   200,  257,  258,  259,  198,  257,  258,  259,  208,    9,
+ /*    10 */    10,   11,   12,   13,   14,   15,   16,   17,   18,   19,
+ /*    20 */    20,   21,   22,   23,   24,  281,   26,   27,   28,   29,
+ /*    30 */    30,   31,   32,   33,   34,   35,   36,   37,   35,   36,
+ /*    40 */    37,   13,   14,   15,   16,   45,   30,   31,   32,   33,
+ /*    50 */    34,   35,   36,   37,   37,  181,   56,   32,   33,   34,
+ /*    60 */    35,   36,   37,  257,  258,  259,    9,   10,   11,   12,
+ /*    70 */    13,   14,   15,   16,   17,   18,   19,   20,   21,   22,
+ /*    80 */    23,   24,  197,   26,   27,   28,   29,   30,   31,   32,
+ /*    90 */    33,   34,   35,   36,   37,    9,   10,   11,   12,   13,
+ /*   100 */    14,   15,   16,   17,   18,   19,   20,   21,   22,   23,
+ /*   110 */    24,  201,   26,   27,   28,   29,   30,   31,   32,   33,
+ /*   120 */    34,   35,   36,   37,    9,   10,   11,   12,   13,   14,
+ /*   130 */    15,   16,   17,   18,   19,   20,   21,   22,   23,   24,
+ /*   140 */   266,   26,   27,   28,   29,   30,   31,   32,   33,   34,
+ /*   150 */    35,   36,   37,  268,  269,   98,  128,  100,  204,  201,
+ /*   160 */    74,   21,   22,   23,   24,   25,   26,   27,   28,   29,
+ /*   170 */    30,   31,   32,   33,   34,   35,   36,   37,    2,   11,
+ /*   180 */   181,   13,  181,   21,   22,   23,   24,   72,   26,   27,
+ /*   190 */    28,   29,   30,   31,   32,   33,   34,   35,   36,   37,
+ /*   200 */   125,  181,  135,  136,  129,  130,  131,   39,  123,  123,
+ /*   210 */   115,  257,  258,  259,  139,  120,  121,  216,  123,    9,
+ /*   220 */    10,   11,   12,   13,   14,   15,   16,   17,   18,   19,
+ /*   230 */    20,   21,   22,   23,   24,  150,   26,   27,   28,   29,
+ /*   240 */    30,   31,   32,   33,   34,   35,   36,   37,    9,   10,
+ /*   250 */    11,   12,   13,   14,   15,   16,   17,   18,   19,   20,
+ /*   260 */    21,   22,   23,   24,  201,   26,   27,   28,   29,   30,
+ /*   270 */    31,   32,   33,   34,   35,   36,   37,    9,   10,   11,
+ /*   280 */    12,   13,   14,   15,   16,   17,   18,   19,   20,   21,
+ /*   290 */    22,   23,   24,  294,   26,   27,   28,   29,   30,   31,
+ /*   300 */    32,   33,   34,   35,   36,   37,  138,  134,  135,  136,
+ /*   310 */   134,  135,  136,  293,  294,    9,   10,   11,   12,   13,
+ /*   320 */    14,   15,   16,   17,   18,   19,   20,   21,   22,   23,
+ /*   330 */    24,  121,   26,   27,   28,   29,   30,   31,   32,   33,
+ /*   340 */    34,   35,   36,   37,    9,   10,   11,   12,   13,   14,
+ /*   350 */    15,   16,   17,   18,   19,   20,   21,   22,   23,   24,
+ /*   360 */   121,   26,   27,   28,   29,   30,   31,   32,   33,   34,
+ /*   370 */    35,   36,   37,    9,   10,   11,   12,   13,   14,   15,
+ /*   380 */    16,   17,   18,   19,   20,   21,   22,   23,   24,  121,
+ /*   390 */    26,   27,   28,   29,   30,   31,   32,   33,   34,   35,
+ /*   400 */    36,   37,  115,   94,   95,   96,   11,  120,  121,  123,
+ /*   410 */   123,    9,   10,   11,   12,   13,   14,   15,   16,   17,
+ /*   420 */    18,   19,   20,   21,   22,   23,   24,  121,   26,   27,
+ /*   430 */    28,   29,   30,   31,   32,   33,   34,   35,   36,   37,
+ /*   440 */   154,    9,   10,   11,   12,   13,   14,   15,   16,   17,
+ /*   450 */    18,   19,   20,   21,   22,   23,   24,  122,   26,   27,
+ /*   460 */    28,   29,   30,   31,   32,   33,   34,   35,   36,   37,
+ /*   470 */     9,   10,   11,   12,   13,   14,   15,   16,   17,   18,
+ /*   480 */    19,   20,   21,   22,   23,   24,  181,   26,   27,   28,
+ /*   490 */    29,   30,   31,   32,   33,   34,   35,   36,   37,   26,
+ /*   500 */    27,   28,   29,   30,   31,   32,   33,   34,   35,   36,
+ /*   510 */    37,  214,   89,  181,   45,  192,   89,  181,  197,  155,
+ /*   520 */   181,  192,  127,  121,  192,   56,  221,  208,  192,  210,
+ /*   530 */   181,  204,  113,  201,  202,   43,  217,  201,  202,   47,
+ /*   540 */   201,  202,  181,  124,  171,  172,  223,  128,  225,  126,
+ /*   550 */   201,  202,  223,  121,  225,  223,  181,  225,  235,  223,
+ /*   560 */    68,  225,  239,  240,  235,  142,  143,  235,  236,  142,
+ /*   570 */   143,  235,  236,  296,  297,  236,  244,  158,  159,  160,
+ /*   580 */   181,  252,  121,  244,  257,  258,  259,  282,  181,  268,
+ /*   590 */   269,  216,    9,   10,   11,   12,   13,   14,   15,   16,
+ /*   600 */    17,   18,   19,   20,   21,   22,   23,   24,    2,   26,
+ /*   610 */    27,   28,   29,   30,   31,   32,   33,   34,   35,   36,
+ /*   620 */    37,    9,   10,   11,   12,   13,   14,   15,   16,   17,
+ /*   630 */    18,   19,   20,   21,   22,   23,   24,  181,   26,   27,
+ /*   640 */    28,   29,   30,   31,   32,   33,   34,   35,   36,   37,
+ /*   650 */     9,   10,   11,   12,   13,   14,   15,   16,   17,   18,
+ /*   660 */    19,   20,   21,   22,   23,   24,   40,   26,   27,   28,
+ /*   670 */    29,   30,   31,   32,   33,   34,   35,   36,   37,    9,
+ /*   680 */    10,   11,   12,   13,   14,   15,   16,   17,   18,   19,
+ /*   690 */    20,   21,   22,   23,   24,   89,   26,   27,   28,   29,
+ /*   700 */    30,   31,   32,   33,   34,   35,   36,   37,    9,   10,
+ /*   710 */    11,   12,   13,   14,   15,   16,   17,   18,   19,   20,
+ /*   720 */    21,   22,   23,   24,  181,   26,   27,   28,   29,   30,
+ /*   730 */    31,   32,   33,   34,   35,   36,   37,  115,  181,  192,
+ /*   740 */   134,  181,  120,  121,   81,  288,  289,  164,  142,  143,
+ /*   750 */   124,  125,   85,  127,   87,  129,  130,  131,   30,   31,
+ /*   760 */   181,  201,  202,  122,  101,  139,  181,  104,  181,   85,
+ /*   770 */   223,   87,  225,  181,  148,  215,  164,   40,  193,  181,
+ /*   780 */   201,  202,  235,  181,  181,  118,  123,   10,   11,   12,
+ /*   790 */    13,   14,   15,   16,   17,   18,   19,   20,   21,   22,
+ /*   800 */    23,   24,  118,   26,   27,   28,   29,   30,   31,   32,
+ /*   810 */    33,   34,   35,   36,   37,   11,   12,   13,   14,   15,
+ /*   820 */    16,   17,   18,   19,   20,   21,   22,   23,   24,   11,
+ /*   830 */    26,   27,   28,   29,   30,   31,   32,   33,   34,   35,
+ /*   840 */    36,   37,  181,  181,  176,  177,  178,  245,   30,   31,
+ /*   850 */   166,  183,  184,  181,   30,   31,   38,  120,   40,    2,
+ /*   860 */   192,  124,  201,  202,  127,  267,  268,  269,   40,   51,
+ /*   870 */   267,  268,  269,  201,  202,  181,  176,  177,  178,  300,
+ /*   880 */   301,  256,  181,  183,  184,  148,  158,  121,  160,  123,
+ /*   890 */   181,  223,  192,  225,   76,   11,  181,  236,   41,   69,
+ /*   900 */    30,   31,  243,  235,  198,  244,  247,   89,  236,   39,
+ /*   910 */    40,   40,  181,  147,  148,   40,  244,  249,  242,  243,
+ /*   920 */    63,   37,   65,  223,  232,  225,  234,   97,  303,  304,
+ /*   930 */   181,  113,  201,  202,   32,  235,  106,  119,  120,   82,
+ /*   940 */   181,  113,  124,   11,  120,  127,  128,   15,   81,  249,
+ /*   950 */   201,  202,  124,  285,  245,  127,  128,  295,  296,  297,
+ /*   960 */   142,  143,   30,   31,  215,   90,  148,  236,  101,  181,
+ /*   970 */    38,  104,   40,  305,  306,  307,  158,  159,  160,  161,
+ /*   980 */   162,  151,  276,   51,   40,  285,  158,  159,  160,  201,
+ /*   990 */   202,  181,  108,  292,  124,  124,  126,  127,  127,  124,
+ /*  1000 */   181,   40,  127,   40,  181,  135,  122,  307,   76,  125,
+ /*  1010 */   126,  211,  128,  129,  130,  131,  132,  194,  148,  148,
+ /*  1020 */   201,  202,  138,  148,   40,  181,  177,  178,  158,   97,
+ /*  1030 */   160,  181,  183,  184,  102,  191,  277,  193,   77,  189,
+ /*  1040 */   121,  192,  123,  141,  194,  113,  144,   81,  134,  135,
+ /*  1050 */   136,  119,  120,    0,    1,  181,  124,  123,  181,  127,
+ /*  1060 */   128,   77,   40,   40,  120,   99,  122,  101,  124,  211,
+ /*  1070 */   104,   40,  223,   40,  225,   40,  142,  143,  201,  202,
+ /*  1080 */   148,  181,  181,  120,  235,  124,  298,  124,  127,  301,
+ /*  1090 */   158,  159,  160,  161,  162,  185,  222,   44,  249,   46,
+ /*  1100 */   218,   48,  201,  202,   81,  295,  296,  297,  124,  148,
+ /*  1110 */    57,  127,   55,   60,   50,   62,  150,  298,   61,  181,
+ /*  1120 */   301,  181,   99,   66,  101,   11,   84,  104,   75,  181,
+ /*  1130 */    88,   78,  148,   69,  285,   82,   83,  236,  261,   86,
+ /*  1140 */   181,    1,   89,   79,   30,   31,  124,  124,   32,  127,
+ /*  1150 */   127,  120,   38,  120,   40,  124,   40,  124,  127,  124,
+ /*  1160 */   127,  279,  127,  218,  111,   51,  292,  181,   40,  116,
+ /*  1170 */   148,  148,  130,  150,  264,  223,  211,  225,  156,  148,
+ /*  1180 */   270,  148,  147,  148,   44,   60,   46,  235,   48,  251,
+ /*  1190 */    76,  251,  181,  140,   40,  142,  143,   57,  256,  251,
+ /*  1200 */    60,  137,   62,   55,  245,   40,  192,  221,  166,  123,
+ /*  1210 */   181,   97,  201,  202,   66,   75,  102,  181,   78,   40,
+ /*  1220 */   167,  168,   82,   83,  279,  225,   86,  113,   11,   89,
+ /*  1230 */   201,  202,  146,  119,  120,  235,  181,  223,  124,  225,
+ /*  1240 */   124,  127,  128,  127,  215,  303,  304,   30,   31,  235,
+ /*  1250 */    11,  111,  124,  181,  181,   38,  116,   40,  222,  181,
+ /*  1260 */   135,  136,  148,  181,  148,  198,  181,   11,   51,   30,
+ /*  1270 */    31,   32,  158,  159,  160,  161,  162,   38,  124,   40,
+ /*  1280 */   140,  127,  142,  143,  181,  120,  201,  202,  227,  124,
+ /*  1290 */    51,  181,  127,   76,  221,   39,  181,  123,  237,  120,
+ /*  1300 */   215,  147,  148,  124,  201,  202,  127,  167,  168,   41,
+ /*  1310 */   181,  201,  202,  148,   97,   76,  201,  202,  215,  102,
+ /*  1320 */    11,  147,  148,  251,  246,  215,  150,  148,  292,   40,
+ /*  1330 */   113,   63,  277,   65,  181,  252,  119,  120,  271,   30,
+ /*  1340 */    31,  124,   11,  276,  127,  128,  170,   38,  256,   40,
+ /*  1350 */    82,  181,  113,  181,  201,  202,  296,  297,  119,  120,
+ /*  1360 */    51,   30,   31,  124,  194,  148,  127,  128,  215,   38,
+ /*  1370 */   181,   40,  181,  201,  202,  158,  159,  160,  161,  162,
+ /*  1380 */   181,  181,   51,  127,  214,   76,  181,  148,  195,  196,
+ /*  1390 */   201,  202,  201,  202,  181,  303,  304,  158,  159,  160,
+ /*  1400 */   161,  162,  296,  297,  215,  149,  277,   76,  236,  296,
+ /*  1410 */   297,  115,   11,  124,  201,  202,  120,  121,  296,  297,
+ /*  1420 */   221,  221,  113,  192,  227,  181,  221,  236,  119,  120,
+ /*  1430 */   192,   30,   31,  124,  237,  181,  127,  128,  243,   38,
+ /*  1440 */   181,   40,  247,  192,  113,  201,  202,  120,   40,  122,
+ /*  1450 */   119,  120,   51,  144,  223,  124,  225,  148,  127,  128,
+ /*  1460 */     1,  223,  181,  225,  181,  192,  235,  158,  159,  160,
+ /*  1470 */   161,  162,   39,  235,  223,  144,  225,   76,  308,  148,
+ /*  1480 */   236,  181,  201,  202,  201,  202,  235,  195,  196,  158,
+ /*  1490 */   159,  160,  161,  162,  286,  287,  223,  181,  225,  181,
+ /*  1500 */   246,  201,  202,   44,  123,   46,  181,   48,  235,  181,
+ /*  1510 */   223,  163,  225,  165,  113,  181,   57,  201,  202,   60,
+ /*  1520 */   119,  120,  235,  181,   39,  124,  181,  146,  127,  128,
+ /*  1530 */   122,  181,  124,  181,   75,  201,  202,   78,  142,  143,
+ /*  1540 */   222,   82,   83,  201,  202,   86,  201,  202,   89,  148,
+ /*  1550 */   222,  201,  202,  201,  202,  120,  181,  181,  181,  158,
+ /*  1560 */   159,  160,  161,  162,  181,   13,  181,  154,  155,  181,
+ /*  1570 */   111,  246,  181,  181,  181,  116,  201,  202,  201,  202,
+ /*  1580 */   145,  181,  149,  181,  201,  202,  201,  202,  227,  201,
+ /*  1590 */   202,   39,  201,  202,  201,  202,  181,  227,  237,  140,
+ /*  1600 */   181,  142,  143,  201,  202,  181,  181,  237,   11,  181,
+ /*  1610 */   292,  181,   52,  181,  227,  181,  201,  202,  181,  181,
+ /*  1620 */   292,  181,  246,  181,  237,  201,  202,  168,  181,  201,
+ /*  1630 */   202,  201,  202,  201,  202,  201,  202,  181,  201,  202,
+ /*  1640 */   155,  201,  202,  201,  202,  181,  246,  181,  201,  202,
+ /*  1650 */   181,   20,  181,  141,  123,  181,  144,  201,  202,  181,
+ /*  1660 */   121,  181,  123,  181,  133,  201,  202,  201,  202,  112,
+ /*  1670 */   201,  202,  201,  202,  181,  201,  202,  117,  181,  201,
+ /*  1680 */   202,  201,  202,  201,  202,  181,  121,  181,  123,  181,
+ /*  1690 */   239,  240,   58,   11,  201,  202,  181,  140,  201,  202,
+ /*  1700 */   181,  121,  181,  123,  122,  201,  202,  201,  202,  201,
+ /*  1710 */   202,  181,  126,  181,  128,  181,  201,  202,  181,  128,
+ /*  1720 */   201,  202,  201,  202,  127,  136,  136,  145,   40,  138,
+ /*  1730 */    40,  201,  202,  201,  202,  201,  202,  121,  201,  202,
+ /*  1740 */   121,  241,  123,  121,  181,  123,  157,  157,  121,  181,
+ /*  1750 */   123,  120,  181,  121,  120,  123,  121,  121,  123,  123,
+ /*  1760 */   121,  121,  123,  123,   92,   93,  150,  158,  158,  160,
+ /*  1770 */   160,  121,  121,  123,  123,  121,  181,  123,  238,  181,
+ /*  1780 */   181,  181,  181,  181,  181,  181,  181,  181,  181,  181,
+ /*  1790 */   302,  199,  110,  181,  181,  181,  181,  279,  181,  181,
+ /*  1800 */   273,  181,  181,  224,  181,  181,  181,  181,  272,  226,
+ /*  1810 */   238,  238,  124,  238,  124,  238,  179,  188,  283,  250,
+ /*  1820 */   254,  228,  278,  229,  213,  229,  250,  278,  254,  242,
+ /*  1830 */   242,  120,  254,  205,   39,  254,  123,  204,  120,   67,
+ /*  1840 */   210,  133,  190,   52,  283,  204,  204,  263,  145,  156,
+ /*  1850 */   283,   54,  155,  187,  187,  206,  203,  120,  203,  203,
+ /*  1860 */   203,  187,    9,  114,  203,  203,  228,  209,  209,  206,
+ /*  1870 */   205,  203,  212,  187,  226,  228,  212,  232,  232,  114,
+ /*  1880 */   186,  154,  163,  187,  186,  242,  242,   64,  122,  187,
+ /*  1890 */   186,   37,  153,  150,  219,  103,  187,  220,  220,  220,
+ /*  1900 */   220,   91,  171,  120,  163,  152,  151,   83,  219,  187,
+ /*  1910 */   229,  219,  138,  206,  229,  173,  180,  187,  186,  186,
+ /*  1920 */   229,  187,  182,  120,  229,  180,  182,  180,  255,  253,
+ /*  1930 */   132,   72,  120,  120,  120,  206,  121,  266,  255,  255,
+ /*  1940 */   253,   72,  123,  233,  232,  167,  121,  167,  120,  280,
+ /*  1950 */   280,   40,  122,  207,  198,  229,  231,  233,  230,  199,
+ /*  1960 */   199,  207,  198,   42,  248,  248,  198,  198,  198,  109,
+ /*  1970 */   265,  265,  120,   72,  275,  274,   53,  169,  117,  121,
+ /*  1980 */   118,  260,  143,  156,  121,  262,  137,  123,  122,  146,
+ /*  1990 */   119,  291,  118,  149,  146,   58,  167,   53,  156,   20,
+ /*  2000 */    20,   20,  137,   20,  137,  137,  127,   49,  167,   48,
+ /*  2010 */   120,   39,  166,  299,  299,  145,   73,  123,   73,  284,
+ /*  2020 */    80,   59,  284,  145,  304,   39,  121,  120,  110,  145,
+ /*  2030 */   121,  121,  120,  127,   49,  284,  147,  123,  290,  287,
+ /*  2040 */   290,   49,  122,  121,  120,   49,  154,  121,  123,  121,
+ /*  2050 */   123,  120,   49,  121,   49,   49,  100,   80,   98,   10,
+ /*  2060 */   105,  123,   80,   49,   49,  146,   11,  121,  121,  121,
+ /*  2070 */   121,   60,  121,  123,  120,  146,  123,   70,  121,  120,
+ /*  2080 */   145,  145,  141,   71,  121,  121,  120,  120,  120,  119,
+ /*  2090 */   117,  122,  309,  309,  309,  309,  309,  145,  309,  309,
+ /*  2100 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2110 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2120 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2130 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2140 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2150 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2160 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2170 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2180 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2190 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2200 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2210 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2220 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2230 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2240 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2250 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2260 */   309,  309,  309,  309,  309,  309,  309,  309,  309,  309,
+ /*  2270 */   309,  309,  309,  309,
+};
+#define YY_SHIFT_COUNT    (578)
+#define YY_SHIFT_MIN      (0)
+#define YY_SHIFT_MAX      (2055)
+static const unsigned short int yy_shift_ofst[] = {
+ /*     0 */  1140, 1053, 1459,  818,  818,  427,  932, 1114, 1217, 1401,
+ /*    10 */  1401, 1401,  606,    0,    0,  115,  670, 1401, 1401, 1401,
+ /*    20 */  1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401,
+ /*    30 */  1401,  870,  870,  626, 1023, 1023,  423,  427,  427,  427,
+ /*    40 */   427,  427,   57,   86,  210,  239,  268,  306,  335,  364,
+ /*    50 */   402,  432,  461,  583,  612,  641,  670,  670,  670,  670,
+ /*    60 */   670,  670,  670,  670,  670,  670,  670,  670,  670,  670,
+ /*    70 */   670,  670,  670,  699,  670,  670,  777,  804,  804, 1239,
+ /*    80 */  1309, 1331, 1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401,
+ /*    90 */  1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401,
+ /*   100 */  1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401,
+ /*   110 */  1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401,
+ /*   120 */  1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401, 1401,
+ /*   130 */  1401,  140,  162,  162,  162,  162,  162,  162,  162,  473,
+ /*   140 */    16,   25,  875,  961,  984,  168,  871,  871,  871,  871,
+ /*   150 */   871,  728,  728, 1042,  934,  934,  934,    3,  934,   67,
+ /*   160 */   373,  373,  373,   17,   17, 2098, 2098,  884,  884,  884,
+ /*   170 */   875,  857, 1022,  737,  737,  737,  737,  857,  966,  871,
+ /*   180 */    95,  287,  871,  871,  871,  871,  871,  871,  871,  871,
+ /*   190 */   871,  871,  871,  871,  871,  871,  871,  871,  871,  871,
+ /*   200 */   871, 1256,  871,  663,  663, 1125,  867,  867, 1128,  395,
+ /*   210 */   395, 1128, 1176, 1396, 2098, 2098, 2098, 2098, 2098, 2098,
+ /*   220 */  2098,  828,  419,  419,   75, 1031, 1116, 1033, 1035, 1154,
+ /*   230 */  1165, 1179,  871,  871,  871, 1064, 1064, 1064,  871,  871,
+ /*   240 */   871,  871,  871,  871,  871,  871,  871,  871,  871,  871,
+ /*   250 */   871,  176,  871,  871,  871,  871,  871,  871,  871,  871,
+ /*   260 */   871,  766,  871,  871,  871,  944,  830,  871, 1268,  871,
+ /*   270 */   871,  871,  871,  871,  871,  871,  871,  871,  824,  173,
+ /*   280 */   492,  309, 1408, 1408, 1408, 1408, 1174,  902,  309,  309,
+ /*   290 */  1057,  919, 1531, 1327, 1552,  469, 1560, 1485, 1413, 1086,
+ /*   300 */  1413, 1597,  286, 1485, 1485,  286, 1485, 1086, 1597,  469,
+ /*   310 */   469, 1433, 1433, 1433, 1433,   85,   85, 1348, 1381, 1512,
+ /*   320 */  1711, 1795, 1795, 1795, 1713, 1718, 1718, 1795, 1772, 1711,
+ /*   330 */  1795, 1708, 1795, 1772, 1795, 1791, 1791, 1703, 1703, 1797,
+ /*   340 */  1797, 1703, 1693, 1697, 1737, 1853, 1749, 1749, 1749, 1749,
+ /*   350 */  1703, 1765, 1727, 1697, 1697, 1727, 1737, 1853, 1727, 1853,
+ /*   360 */  1727, 1703, 1765, 1719, 1823, 1703, 1765, 1766, 1854, 1854,
+ /*   370 */  1711, 1703, 1739, 1743, 1792, 1792, 1810, 1810, 1731, 1783,
+ /*   380 */  1703, 1741, 1739, 1753, 1755, 1727, 1711, 1703, 1765, 1703,
+ /*   390 */  1765, 1824, 1824, 1742, 1742, 1742, 2098, 2098, 2098, 2098,
+ /*   400 */  2098, 2098, 2098, 2098, 2098, 2098, 2098, 2098, 2098, 2098,
+ /*   410 */  2098,   28,  684,  622, 1296,  914,  963,  667, 1539, 1565,
+ /*   420 */  1580, 1289, 1586, 1148, 1591, 1557, 1631, 1582, 1589, 1590,
+ /*   430 */  1619, 1634, 1622, 1627, 1632, 1635, 1682, 1688, 1636, 1639,
+ /*   440 */  1640, 1672, 1616, 1609, 1610, 1650, 1651, 1435, 1654, 1690,
+ /*   450 */  1774, 1798, 1803, 1859, 1812, 1813, 1815, 1814, 1869, 1819,
+ /*   460 */  1825, 1778, 1780, 1828, 1911, 1830, 1860, 1921, 1852, 1901,
+ /*   470 */  1923, 1808, 1861, 1862, 1858, 1863, 1839, 1827, 1849, 1864,
+ /*   480 */  1864, 1866, 1843, 1871, 1844, 1874, 1829, 1848, 1865, 1864,
+ /*   490 */  1867, 1937, 1944, 1864, 1842, 1979, 1980, 1981, 1983, 1868,
+ /*   500 */  1879, 1958, 1870, 1841, 1961, 1890, 1972, 1846, 1943, 1894,
+ /*   510 */  1945, 1940, 1962, 1878, 1986, 1905, 1907, 1918, 1884, 1909,
+ /*   520 */  1910, 1906, 1985, 1912, 1889, 1914, 1992, 1922, 1924, 1920,
+ /*   530 */  1925, 1926, 1927, 1928, 1996, 1892, 1931, 1932, 2003, 2005,
+ /*   540 */  2006, 1956, 1977, 1960, 2049, 1982, 1955, 1938, 2014, 2015,
+ /*   550 */  1919, 1929, 1946, 1914, 1947, 1948, 1949, 1950, 1951, 1954,
+ /*   560 */  2011, 1957, 1959, 1963, 1964, 1966, 1967, 1953, 1935, 1936,
+ /*   570 */  1952, 1968, 1941, 1969, 1970, 2055, 1973, 2007, 2012,
+};
+#define YY_REDUCE_COUNT (410)
+#define YY_REDUCE_MIN   (-256)
+#define YY_REDUCE_MAX   (1770)
+static const short yy_reduce_ofst[] = {
+ /*     0 */   668,  700,  849,  332,  336,  323,  788,  579,  819,  339,
+ /*    10 */   661,  672,  329,  -46,  327, -256, -194,  560,  749, 1029,
+ /*    20 */  1085,  731, 1110, 1153, 1189,  901, 1103, 1172,  877, 1191,
+ /*    30 */  1244,  598,  603, 1170,  662,  810,  547, 1014, 1231, 1238,
+ /*    40 */  1251, 1273, -252, -252, -252, -252, -252, -252, -252, -252,
+ /*    50 */  -252, -252, -252, -252, -252, -252, -252, -252, -252, -252,
+ /*    60 */  -252, -252, -252, -252, -252, -252, -252, -252, -252, -252,
+ /*    70 */  -252, -252, -252, -252, -252, -252, -252, -252, -252,  349,
+ /*    80 */  1011, 1115, 1213, 1281, 1283, 1300, 1316, 1334, 1342, 1345,
+ /*    90 */  1350, 1352, 1375, 1377, 1383, 1385, 1388, 1391, 1393, 1402,
+ /*   100 */  1415, 1424, 1428, 1430, 1432, 1434, 1437, 1440, 1442, 1447,
+ /*   110 */  1456, 1464, 1466, 1469, 1471, 1474, 1478, 1480, 1482, 1493,
+ /*   120 */  1497, 1504, 1506, 1508, 1515, 1519, 1521, 1530, 1532, 1534,
+ /*   130 */  1537, -252, -252, -252, -252, -252, -252, -252, -252, -252,
+ /*   140 */  -252, -252,  844,  874, 1036,  319,  850,  305, 1318, 1328,
+ /*   150 */    20, -115,  321,  910,  952, 1287,  952, -252, 1287, 1067,
+ /*   160 */   625,  942, 1092, -252, -252, -252, -252, -200, -200, -200,
+ /*   170 */   585,  882, -126,  938,  940,  948, 1072,  945,  277,    1,
+ /*   180 */   457,  457,  823,  602,  709,  959,  986, 1073, 1199, 1200,
+ /*   190 */   759, 1078, 1055, 1254, 1325, 1129, 1376, 1205,  375,  701,
+ /*   200 */  1400,  676,   -1, 1060, 1106,  706, 1113, 1122, 1193,  659,
+ /*   210 */  1195, 1292,  692, 1000, 1208, 1061, 1197, 1361, 1370, 1451,
+ /*   220 */  1387,  -90,  -42,   63,  297,  361,  399,  407,  456,  543,
+ /*   230 */   557,  587,  592,  694,  715,  800,  858,  965,  900, 1082,
+ /*   240 */  1259, 1392, 1419, 1425, 1438, 1563, 1568, 1571, 1595, 1598,
+ /*   250 */  1599, 1083, 1600, 1601, 1602, 1603, 1604, 1605, 1606, 1607,
+ /*   260 */  1608, 1500, 1612, 1613, 1614, 1540, 1488, 1615, 1518, 1617,
+ /*   270 */   399, 1618, 1620, 1621, 1623, 1624, 1625, 1626, 1592, 1527,
+ /*   280 */  1536, 1579, 1572, 1573, 1575, 1577, 1500, 1583, 1579, 1579,
+ /*   290 */  1637, 1611, 1628, 1629, 1630, 1633, 1535, 1566, 1569, 1593,
+ /*   300 */  1576, 1544, 1594, 1574, 1578, 1596, 1581, 1638, 1549, 1641,
+ /*   310 */  1642, 1587, 1588, 1643, 1644, 1645, 1646, 1584, 1647, 1648,
+ /*   320 */  1649, 1653, 1655, 1656, 1652, 1658, 1659, 1657, 1660, 1663,
+ /*   330 */  1661, 1665, 1662, 1664, 1668, 1561, 1567, 1666, 1667, 1669,
+ /*   340 */  1670, 1674, 1671, 1673, 1676, 1675, 1677, 1678, 1679, 1680,
+ /*   350 */  1686, 1694, 1681, 1683, 1684, 1685, 1687, 1689, 1691, 1692,
+ /*   360 */  1695, 1696, 1698, 1699, 1701, 1702, 1704, 1700, 1705, 1706,
+ /*   370 */  1707, 1709, 1710, 1712, 1714, 1715, 1716, 1717, 1720, 1721,
+ /*   380 */  1722, 1723, 1724, 1725, 1728, 1726, 1729, 1730, 1732, 1734,
+ /*   390 */  1733, 1740, 1744, 1736, 1745, 1747, 1735, 1738, 1746, 1748,
+ /*   400 */  1750, 1752, 1754, 1760, 1761, 1751, 1756, 1764, 1768, 1769,
+ /*   410 */  1770,
+};
+static const YYACTIONTYPE yy_default[] = {
+ /*     0 */  1648, 1648, 1648, 1478, 1243, 1354, 1243, 1243, 1243, 1478,
+ /*    10 */  1478, 1478, 1243, 1384, 1384, 1531, 1276, 1243, 1243, 1243,
+ /*    20 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1477, 1243,
+ /*    30 */  1243, 1243, 1243, 1243, 1564, 1564, 1243, 1243, 1243, 1243,
+ /*    40 */  1243, 1243, 1243, 1393, 1243, 1243, 1243, 1243, 1243, 1400,
+ /*    50 */  1479, 1480, 1243, 1243, 1243, 1243, 1530, 1532, 1495, 1407,
+ /*    60 */  1406, 1405, 1404, 1513, 1372, 1398, 1391, 1395, 1479, 1474,
+ /*    70 */  1475, 1473, 1626, 1243, 1480, 1394, 1442, 1441, 1458, 1243,
+ /*    80 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243,
+ /*    90 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243,
+ /*   100 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243,
+ /*   110 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243,
+ /*   120 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243,
+ /*   130 */  1243, 1450, 1457, 1456, 1455, 1464, 1454, 1451, 1444, 1443,
+ /*   140 */  1445, 1446, 1267, 1243, 1243, 1318, 1243, 1243, 1243, 1243,
+ /*   150 */  1243, 1243, 1243, 1264, 1550, 1549, 1243, 1447, 1243, 1276,
+ /*   160 */  1435, 1434, 1433, 1461, 1448, 1460, 1459, 1600, 1599, 1538,
+ /*   170 */  1243, 1243, 1496, 1243, 1243, 1243, 1243, 1243, 1564, 1243,
+ /*   180 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243,
+ /*   190 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243,
+ /*   200 */  1243, 1374, 1243, 1564, 1564, 1276, 1564, 1564, 1272, 1375,
+ /*   210 */  1375, 1272, 1378, 1243, 1545, 1345, 1345, 1345, 1345, 1354,
+ /*   220 */  1345, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243,
+ /*   230 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243,
+ /*   240 */  1243, 1243, 1243, 1243, 1243, 1535, 1533, 1243, 1243, 1243,
+ /*   250 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243,
+ /*   260 */  1243, 1243, 1243, 1243, 1243, 1350, 1243, 1243, 1243, 1243,
+ /*   270 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1593, 1277, 1243,
+ /*   280 */  1508, 1332, 1350, 1350, 1350, 1350, 1352, 1344, 1333, 1331,
+ /*   290 */  1250, 1615, 1293, 1243, 1288, 1384, 1640, 1410, 1399, 1351,
+ /*   300 */  1399, 1637, 1397, 1410, 1410, 1397, 1410, 1351, 1637, 1384,
+ /*   310 */  1384, 1374, 1374, 1374, 1374, 1378, 1378, 1476, 1351, 1344,
+ /*   320 */  1486, 1319, 1319, 1319, 1311, 1243, 1243, 1319, 1308, 1486,
+ /*   330 */  1319, 1293, 1319, 1308, 1319, 1640, 1640, 1360, 1360, 1639,
+ /*   340 */  1639, 1360, 1496, 1623, 1419, 1321, 1327, 1327, 1327, 1327,
+ /*   350 */  1360, 1261, 1397, 1623, 1623, 1397, 1419, 1321, 1397, 1321,
+ /*   360 */  1397, 1360, 1261, 1512, 1634, 1360, 1261, 1243, 1490, 1490,
+ /*   370 */  1486, 1360, 1392, 1378, 1574, 1574, 1387, 1387, 1582, 1481,
+ /*   380 */  1360, 1243, 1392, 1390, 1388, 1397, 1486, 1360, 1261, 1360,
+ /*   390 */  1261, 1596, 1596, 1592, 1592, 1592, 1608, 1608, 1295, 1645,
+ /*   400 */  1645, 1545, 1295, 1277, 1277, 1608, 1276, 1276, 1276, 1276,
+ /*   410 */  1276, 1243, 1243, 1243, 1243, 1243, 1603, 1243, 1243, 1243,
+ /*   420 */  1243, 1243, 1243, 1243, 1243, 1540, 1497, 1364, 1243, 1243,
+ /*   430 */  1243, 1243, 1243, 1243, 1243, 1243, 1551, 1243, 1243, 1243,
+ /*   440 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1424, 1243, 1243,
+ /*   450 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1265,
+ /*   460 */  1243, 1243, 1243, 1542, 1291, 1243, 1243, 1243, 1243, 1243,
+ /*   470 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1401,
+ /*   480 */  1402, 1365, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1416,
+ /*   490 */  1243, 1243, 1243, 1411, 1243, 1243, 1243, 1243, 1243, 1243,
+ /*   500 */  1243, 1243, 1636, 1243, 1243, 1243, 1243, 1243, 1243, 1511,
+ /*   510 */  1510, 1243, 1243, 1362, 1243, 1243, 1243, 1243, 1243, 1243,
+ /*   520 */  1243, 1243, 1243, 1243, 1243, 1389, 1243, 1243, 1243, 1243,
+ /*   530 */  1579, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243, 1243,
+ /*   540 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1379, 1243, 1243,
+ /*   550 */  1243, 1243, 1243, 1627, 1243, 1243, 1243, 1243, 1243, 1243,
+ /*   560 */  1243, 1243, 1243, 1243, 1243, 1243, 1243, 1619, 1428, 1425,
+ /*   570 */  1243, 1243, 1335, 1243, 1243, 1243, 1255, 1243, 1246,
+};
+/********** End of lemon-generated parsing tables *****************************/
+
+/* The next table maps tokens (terminal symbols) into fallback tokens.  
+** If a construct like the following:
+** 
+**      %fallback ID X Y Z.
+**
+** appears in the grammar, then ID becomes a fallback token for X, Y,
+** and Z.  Whenever one of the tokens X, Y, or Z is input to the parser
+** but it does not parse, the type of the token is changed to ID and
+** the parse is retried before an error is thrown.
+**
+** This feature can be used, for example, to cause some keywords in a language
+** to revert to identifiers if they keyword does not apply in the context where
+** it appears.
+*/
+#ifdef YYFALLBACK
+static const YYCODETYPE yyFallback[] = {
+    0,  /*          $ => nothing */
+    0,  /*     CREATE => nothing */
+   40,  /*    REPLACE => ID */
+    0,  /*   PERFETTO => nothing */
+    0,  /*      MACRO => nothing */
+    0,  /*    INCLUDE => nothing */
+    0,  /*     MODULE => nothing */
+    0,  /*    RETURNS => nothing */
+    0,  /*   FUNCTION => nothing */
+    0,  /*         OR => nothing */
+    0,  /*        AND => nothing */
+    0,  /*        NOT => nothing */
+    0,  /*         IS => nothing */
+   40,  /*      MATCH => ID */
+   40,  /*    LIKE_KW => ID */
+    0,  /*    BETWEEN => nothing */
+    0,  /*         IN => nothing */
+    0,  /*     ISNULL => nothing */
+    0,  /*    NOTNULL => nothing */
+    0,  /*         NE => nothing */
+    0,  /*         EQ => nothing */
+    0,  /*         GT => nothing */
+    0,  /*         LE => nothing */
+    0,  /*         LT => nothing */
+    0,  /*         GE => nothing */
+    0,  /*     ESCAPE => nothing */
+    0,  /*     BITAND => nothing */
+    0,  /*      BITOR => nothing */
+    0,  /*     LSHIFT => nothing */
+    0,  /*     RSHIFT => nothing */
+    0,  /*       PLUS => nothing */
+    0,  /*      MINUS => nothing */
+    0,  /*       STAR => nothing */
+    0,  /*      SLASH => nothing */
+    0,  /*        REM => nothing */
+    0,  /*     CONCAT => nothing */
+    0,  /*        PTR => nothing */
+    0,  /*    COLLATE => nothing */
+    0,  /*     BITNOT => nothing */
+    0,  /*         ON => nothing */
+    0,  /*         ID => nothing */
+   40,  /*      ABORT => ID */
+   40,  /*     ACTION => ID */
+   40,  /*      AFTER => ID */
+   40,  /*    ANALYZE => ID */
+   40,  /*        ASC => ID */
+   40,  /*     ATTACH => ID */
+   40,  /*     BEFORE => ID */
+   40,  /*      BEGIN => ID */
+   40,  /*         BY => ID */
+   40,  /*    CASCADE => ID */
+   40,  /*       CAST => ID */
+   40,  /*   COLUMNKW => ID */
+   40,  /*   CONFLICT => ID */
+   40,  /*   DATABASE => ID */
+   40,  /*   DEFERRED => ID */
+   40,  /*       DESC => ID */
+   40,  /*     DETACH => ID */
+   40,  /*         DO => ID */
+   40,  /*       EACH => ID */
+   40,  /*        END => ID */
+   40,  /*  EXCLUSIVE => ID */
+   40,  /*    EXPLAIN => ID */
+   40,  /*       FAIL => ID */
+   40,  /*        FOR => ID */
+   40,  /*     IGNORE => ID */
+   40,  /*  IMMEDIATE => ID */
+   40,  /*  INITIALLY => ID */
+   40,  /*    INSTEAD => ID */
+   40,  /*         NO => ID */
+   40,  /*       PLAN => ID */
+   40,  /*      QUERY => ID */
+   40,  /*        KEY => ID */
+   40,  /*         OF => ID */
+   40,  /*     OFFSET => ID */
+   40,  /*     PRAGMA => ID */
+   40,  /*      RAISE => ID */
+   40,  /*  RECURSIVE => ID */
+   40,  /*    RELEASE => ID */
+   40,  /*   RESTRICT => ID */
+   40,  /*        ROW => ID */
+   40,  /*       ROWS => ID */
+   40,  /*   ROLLBACK => ID */
+   40,  /*  SAVEPOINT => ID */
+   40,  /*       TEMP => ID */
+   40,  /*    TRIGGER => ID */
+   40,  /*     VACUUM => ID */
+   40,  /*       VIEW => ID */
+   40,  /*    VIRTUAL => ID */
+   40,  /*       WITH => ID */
+   40,  /*    WITHOUT => ID */
+   40,  /*      NULLS => ID */
+   40,  /*      FIRST => ID */
+   40,  /*       LAST => ID */
+   40,  /*     EXCEPT => ID */
+   40,  /*  INTERSECT => ID */
+   40,  /*      UNION => ID */
+   40,  /*    CURRENT => ID */
+   40,  /*  FOLLOWING => ID */
+   40,  /*  PARTITION => ID */
+   40,  /*  PRECEDING => ID */
+   40,  /*      RANGE => ID */
+   40,  /*  UNBOUNDED => ID */
+   40,  /*    EXCLUDE => ID */
+   40,  /*     GROUPS => ID */
+   40,  /*     OTHERS => ID */
+   40,  /*       TIES => ID */
+   40,  /*     WITHIN => ID */
+   40,  /*  GENERATED => ID */
+   40,  /*     ALWAYS => ID */
+   40,  /* MATERIALIZED => ID */
+   40,  /*    REINDEX => ID */
+   40,  /*     RENAME => ID */
+   40,  /*   CTIME_KW => ID */
+   40,  /*         IF => ID */
+    0,  /*        ANY => nothing */
+    0,  /*     COMMIT => nothing */
+    0,  /*         TO => nothing */
+    0,  /*      TABLE => nothing */
+    0,  /*     EXISTS => nothing */
+    0,  /*         LP => nothing */
+    0,  /*         RP => nothing */
+    0,  /*         AS => nothing */
+    0,  /*      COMMA => nothing */
+    0,  /*     STRING => nothing */
+    0,  /* CONSTRAINT => nothing */
+    0,  /*    DEFAULT => nothing */
+    0,  /*    INDEXED => nothing */
+    0,  /*       NULL => nothing */
+    0,  /*    PRIMARY => nothing */
+    0,  /*     UNIQUE => nothing */
+    0,  /*      CHECK => nothing */
+    0,  /* REFERENCES => nothing */
+    0,  /*   AUTOINCR => nothing */
+    0,  /*     INSERT => nothing */
+    0,  /*     DELETE => nothing */
+    0,  /*     UPDATE => nothing */
+    0,  /*        SET => nothing */
+    0,  /* DEFERRABLE => nothing */
+    0,  /*    FOREIGN => nothing */
+    0,  /*       DROP => nothing */
+    0,  /*        ALL => nothing */
+    0,  /*     SELECT => nothing */
+    0,  /*     VALUES => nothing */
+    0,  /*   DISTINCT => nothing */
+    0,  /*        DOT => nothing */
+    0,  /*       FROM => nothing */
+    0,  /*       JOIN => nothing */
+    0,  /*    JOIN_KW => nothing */
+    0,  /*      USING => nothing */
+    0,  /*      ORDER => nothing */
+    0,  /*      GROUP => nothing */
+    0,  /*     HAVING => nothing */
+    0,  /*      LIMIT => nothing */
+    0,  /*      WHERE => nothing */
+    0,  /*  RETURNING => nothing */
+    0,  /*       INTO => nothing */
+    0,  /*    NOTHING => nothing */
+    0,  /*      FLOAT => nothing */
+    0,  /*       BLOB => nothing */
+    0,  /*    INTEGER => nothing */
+    0,  /*   VARIABLE => nothing */
+    0,  /*       CASE => nothing */
+    0,  /*       WHEN => nothing */
+    0,  /*       THEN => nothing */
+    0,  /*       ELSE => nothing */
+    0,  /*      INDEX => nothing */
+    0,  /*       SEMI => nothing */
+    0,  /*      ALTER => nothing */
+    0,  /*        ADD => nothing */
+    0,  /*     WINDOW => nothing */
+    0,  /*       OVER => nothing */
+    0,  /*     FILTER => nothing */
+    0,  /* TRANSACTION => nothing */
+    0,  /*      SPACE => nothing */
+    0,  /*    ILLEGAL => nothing */
+};
+#endif /* YYFALLBACK */
+
+/* The following structure represents a single element of the
+** parser's stack.  Information stored includes:
+**
+**   +  The state number for the parser at this level of the stack.
+**
+**   +  The value of the token stored at this level of the stack.
+**      (In other words, the "major" token.)
+**
+**   +  The semantic value stored at this level of the stack.  This is
+**      the information used by the action routines in the grammar.
+**      It is sometimes called the "minor" token.
+**
+** After the "shift" half of a SHIFTREDUCE action, the stateno field
+** actually contains the reduce action for the second half of the
+** SHIFTREDUCE.
+*/
+struct yyStackEntry {
+  YYACTIONTYPE stateno;  /* The state-number, or reduce action in SHIFTREDUCE */
+  YYCODETYPE major;      /* The major token value.  This is the code
+                         ** number for the token at this stack level */
+  YYMINORTYPE minor;     /* The user-supplied minor token value.  This
+                         ** is the value of the token  */
+};
+typedef struct yyStackEntry yyStackEntry;
+
+/* The state of the parser is completely contained in an instance of
+** the following structure */
+struct yyParser {
+  yyStackEntry *yytos;          /* Pointer to top element of the stack */
+#ifdef YYTRACKMAXSTACKDEPTH
+  int yyhwm;                    /* High-water mark of the stack */
+#endif
+#ifndef YYNOERRORRECOVERY
+  int yyerrcnt;                 /* Shifts left before out of the error */
+#endif
+  PerfettoSqlParserARG_SDECL                /* A place to hold %extra_argument */
+  PerfettoSqlParserCTX_SDECL                /* A place to hold %extra_context */
+#if YYSTACKDEPTH<=0
+  int yystksz;                  /* Current side of the stack */
+  yyStackEntry *yystack;        /* The parser's stack */
+  yyStackEntry yystk0;          /* First stack entry */
+#else
+  yyStackEntry yystack[YYSTACKDEPTH];  /* The parser's stack */
+  yyStackEntry *yystackEnd;            /* Last entry in the stack */
+#endif
+};
+typedef struct yyParser yyParser;
+
+#include <assert.h>
+#ifndef NDEBUG
+#include <stdio.h>
+static FILE *yyTraceFILE = 0;
+static char *yyTracePrompt = 0;
+#endif /* NDEBUG */
+
+#ifndef NDEBUG
+/* 
+** Turn parser tracing on by giving a stream to which to write the trace
+** and a prompt to preface each trace message.  Tracing is turned off
+** by making either argument NULL 
+**
+** Inputs:
+** <ul>
+** <li> A FILE* to which trace output should be written.
+**      If NULL, then tracing is turned off.
+** <li> A prefix string written at the beginning of every
+**      line of trace output.  If NULL, then tracing is
+**      turned off.
+** </ul>
+**
+** Outputs:
+** None.
+*/
+void PerfettoSqlParserTrace(FILE *TraceFILE, char *zTracePrompt){
+  yyTraceFILE = TraceFILE;
+  yyTracePrompt = zTracePrompt;
+  if( yyTraceFILE==0 ) yyTracePrompt = 0;
+  else if( yyTracePrompt==0 ) yyTraceFILE = 0;
+}
+#endif /* NDEBUG */
+
+#if defined(YYCOVERAGE) || !defined(NDEBUG)
+/* For tracing shifts, the names of all terminals and nonterminals
+** are required.  The following table supplies these names */
+static const char *const yyTokenName[] = { 
+  /*    0 */ "$",
+  /*    1 */ "CREATE",
+  /*    2 */ "REPLACE",
+  /*    3 */ "PERFETTO",
+  /*    4 */ "MACRO",
+  /*    5 */ "INCLUDE",
+  /*    6 */ "MODULE",
+  /*    7 */ "RETURNS",
+  /*    8 */ "FUNCTION",
+  /*    9 */ "OR",
+  /*   10 */ "AND",
+  /*   11 */ "NOT",
+  /*   12 */ "IS",
+  /*   13 */ "MATCH",
+  /*   14 */ "LIKE_KW",
+  /*   15 */ "BETWEEN",
+  /*   16 */ "IN",
+  /*   17 */ "ISNULL",
+  /*   18 */ "NOTNULL",
+  /*   19 */ "NE",
+  /*   20 */ "EQ",
+  /*   21 */ "GT",
+  /*   22 */ "LE",
+  /*   23 */ "LT",
+  /*   24 */ "GE",
+  /*   25 */ "ESCAPE",
+  /*   26 */ "BITAND",
+  /*   27 */ "BITOR",
+  /*   28 */ "LSHIFT",
+  /*   29 */ "RSHIFT",
+  /*   30 */ "PLUS",
+  /*   31 */ "MINUS",
+  /*   32 */ "STAR",
+  /*   33 */ "SLASH",
+  /*   34 */ "REM",
+  /*   35 */ "CONCAT",
+  /*   36 */ "PTR",
+  /*   37 */ "COLLATE",
+  /*   38 */ "BITNOT",
+  /*   39 */ "ON",
+  /*   40 */ "ID",
+  /*   41 */ "ABORT",
+  /*   42 */ "ACTION",
+  /*   43 */ "AFTER",
+  /*   44 */ "ANALYZE",
+  /*   45 */ "ASC",
+  /*   46 */ "ATTACH",
+  /*   47 */ "BEFORE",
+  /*   48 */ "BEGIN",
+  /*   49 */ "BY",
+  /*   50 */ "CASCADE",
+  /*   51 */ "CAST",
+  /*   52 */ "COLUMNKW",
+  /*   53 */ "CONFLICT",
+  /*   54 */ "DATABASE",
+  /*   55 */ "DEFERRED",
+  /*   56 */ "DESC",
+  /*   57 */ "DETACH",
+  /*   58 */ "DO",
+  /*   59 */ "EACH",
+  /*   60 */ "END",
+  /*   61 */ "EXCLUSIVE",
+  /*   62 */ "EXPLAIN",
+  /*   63 */ "FAIL",
+  /*   64 */ "FOR",
+  /*   65 */ "IGNORE",
+  /*   66 */ "IMMEDIATE",
+  /*   67 */ "INITIALLY",
+  /*   68 */ "INSTEAD",
+  /*   69 */ "NO",
+  /*   70 */ "PLAN",
+  /*   71 */ "QUERY",
+  /*   72 */ "KEY",
+  /*   73 */ "OF",
+  /*   74 */ "OFFSET",
+  /*   75 */ "PRAGMA",
+  /*   76 */ "RAISE",
+  /*   77 */ "RECURSIVE",
+  /*   78 */ "RELEASE",
+  /*   79 */ "RESTRICT",
+  /*   80 */ "ROW",
+  /*   81 */ "ROWS",
+  /*   82 */ "ROLLBACK",
+  /*   83 */ "SAVEPOINT",
+  /*   84 */ "TEMP",
+  /*   85 */ "TRIGGER",
+  /*   86 */ "VACUUM",
+  /*   87 */ "VIEW",
+  /*   88 */ "VIRTUAL",
+  /*   89 */ "WITH",
+  /*   90 */ "WITHOUT",
+  /*   91 */ "NULLS",
+  /*   92 */ "FIRST",
+  /*   93 */ "LAST",
+  /*   94 */ "EXCEPT",
+  /*   95 */ "INTERSECT",
+  /*   96 */ "UNION",
+  /*   97 */ "CURRENT",
+  /*   98 */ "FOLLOWING",
+  /*   99 */ "PARTITION",
+  /*  100 */ "PRECEDING",
+  /*  101 */ "RANGE",
+  /*  102 */ "UNBOUNDED",
+  /*  103 */ "EXCLUDE",
+  /*  104 */ "GROUPS",
+  /*  105 */ "OTHERS",
+  /*  106 */ "TIES",
+  /*  107 */ "WITHIN",
+  /*  108 */ "GENERATED",
+  /*  109 */ "ALWAYS",
+  /*  110 */ "MATERIALIZED",
+  /*  111 */ "REINDEX",
+  /*  112 */ "RENAME",
+  /*  113 */ "CTIME_KW",
+  /*  114 */ "IF",
+  /*  115 */ "ANY",
+  /*  116 */ "COMMIT",
+  /*  117 */ "TO",
+  /*  118 */ "TABLE",
+  /*  119 */ "EXISTS",
+  /*  120 */ "LP",
+  /*  121 */ "RP",
+  /*  122 */ "AS",
+  /*  123 */ "COMMA",
+  /*  124 */ "STRING",
+  /*  125 */ "CONSTRAINT",
+  /*  126 */ "DEFAULT",
+  /*  127 */ "INDEXED",
+  /*  128 */ "NULL",
+  /*  129 */ "PRIMARY",
+  /*  130 */ "UNIQUE",
+  /*  131 */ "CHECK",
+  /*  132 */ "REFERENCES",
+  /*  133 */ "AUTOINCR",
+  /*  134 */ "INSERT",
+  /*  135 */ "DELETE",
+  /*  136 */ "UPDATE",
+  /*  137 */ "SET",
+  /*  138 */ "DEFERRABLE",
+  /*  139 */ "FOREIGN",
+  /*  140 */ "DROP",
+  /*  141 */ "ALL",
+  /*  142 */ "SELECT",
+  /*  143 */ "VALUES",
+  /*  144 */ "DISTINCT",
+  /*  145 */ "DOT",
+  /*  146 */ "FROM",
+  /*  147 */ "JOIN",
+  /*  148 */ "JOIN_KW",
+  /*  149 */ "USING",
+  /*  150 */ "ORDER",
+  /*  151 */ "GROUP",
+  /*  152 */ "HAVING",
+  /*  153 */ "LIMIT",
+  /*  154 */ "WHERE",
+  /*  155 */ "RETURNING",
+  /*  156 */ "INTO",
+  /*  157 */ "NOTHING",
+  /*  158 */ "FLOAT",
+  /*  159 */ "BLOB",
+  /*  160 */ "INTEGER",
+  /*  161 */ "VARIABLE",
+  /*  162 */ "CASE",
+  /*  163 */ "WHEN",
+  /*  164 */ "THEN",
+  /*  165 */ "ELSE",
+  /*  166 */ "INDEX",
+  /*  167 */ "SEMI",
+  /*  168 */ "ALTER",
+  /*  169 */ "ADD",
+  /*  170 */ "WINDOW",
+  /*  171 */ "OVER",
+  /*  172 */ "FILTER",
+  /*  173 */ "TRANSACTION",
+  /*  174 */ "SPACE",
+  /*  175 */ "ILLEGAL",
+  /*  176 */ "explain",
+  /*  177 */ "cmdx",
+  /*  178 */ "cmd",
+  /*  179 */ "transtype",
+  /*  180 */ "trans_opt",
+  /*  181 */ "nm",
+  /*  182 */ "savepoint_opt",
+  /*  183 */ "create_table",
+  /*  184 */ "createkw",
+  /*  185 */ "temp",
+  /*  186 */ "ifnotexists",
+  /*  187 */ "dbnm",
+  /*  188 */ "create_table_args",
+  /*  189 */ "columnlist",
+  /*  190 */ "conslist_opt",
+  /*  191 */ "table_option_set",
+  /*  192 */ "select",
+  /*  193 */ "table_option",
+  /*  194 */ "columnname",
+  /*  195 */ "typetoken",
+  /*  196 */ "typename",
+  /*  197 */ "signed",
+  /*  198 */ "scanpt",
+  /*  199 */ "scantok",
+  /*  200 */ "ccons",
+  /*  201 */ "term",
+  /*  202 */ "expr",
+  /*  203 */ "onconf",
+  /*  204 */ "sortorder",
+  /*  205 */ "autoinc",
+  /*  206 */ "eidlist_opt",
+  /*  207 */ "refargs",
+  /*  208 */ "defer_subclause",
+  /*  209 */ "generated",
+  /*  210 */ "refarg",
+  /*  211 */ "refact",
+  /*  212 */ "init_deferred_pred_opt",
+  /*  213 */ "tconscomma",
+  /*  214 */ "tcons",
+  /*  215 */ "sortlist",
+  /*  216 */ "eidlist",
+  /*  217 */ "defer_subclause_opt",
+  /*  218 */ "resolvetype",
+  /*  219 */ "orconf",
+  /*  220 */ "ifexists",
+  /*  221 */ "fullname",
+  /*  222 */ "wqlist",
+  /*  223 */ "selectnowith",
+  /*  224 */ "multiselect_op",
+  /*  225 */ "oneselect",
+  /*  226 */ "distinct",
+  /*  227 */ "selcollist",
+  /*  228 */ "from",
+  /*  229 */ "where_opt",
+  /*  230 */ "groupby_opt",
+  /*  231 */ "having_opt",
+  /*  232 */ "orderby_opt",
+  /*  233 */ "limit_opt",
+  /*  234 */ "window_clause",
+  /*  235 */ "values",
+  /*  236 */ "nexprlist",
+  /*  237 */ "sclp",
+  /*  238 */ "as",
+  /*  239 */ "seltablist",
+  /*  240 */ "stl_prefix",
+  /*  241 */ "joinop",
+  /*  242 */ "on_using",
+  /*  243 */ "indexed_by",
+  /*  244 */ "exprlist",
+  /*  245 */ "xfullname",
+  /*  246 */ "idlist",
+  /*  247 */ "indexed_opt",
+  /*  248 */ "nulls",
+  /*  249 */ "with",
+  /*  250 */ "where_opt_ret",
+  /*  251 */ "setlist",
+  /*  252 */ "insert_cmd",
+  /*  253 */ "idlist_opt",
+  /*  254 */ "upsert",
+  /*  255 */ "returning",
+  /*  256 */ "filter_over",
+  /*  257 */ "likeop",
+  /*  258 */ "between_op",
+  /*  259 */ "in_op",
+  /*  260 */ "paren_exprlist",
+  /*  261 */ "case_operand",
+  /*  262 */ "case_exprlist",
+  /*  263 */ "case_else",
+  /*  264 */ "uniqueflag",
+  /*  265 */ "collate",
+  /*  266 */ "vinto",
+  /*  267 */ "nmnum",
+  /*  268 */ "minus_num",
+  /*  269 */ "plus_num",
+  /*  270 */ "trigger_decl",
+  /*  271 */ "trigger_cmd_list",
+  /*  272 */ "trigger_time",
+  /*  273 */ "trigger_event",
+  /*  274 */ "foreach_clause",
+  /*  275 */ "when_clause",
+  /*  276 */ "trigger_cmd",
+  /*  277 */ "trnm",
+  /*  278 */ "tridxby",
+  /*  279 */ "raisetype",
+  /*  280 */ "database_kw_opt",
+  /*  281 */ "key_opt",
+  /*  282 */ "add_column_fullname",
+  /*  283 */ "kwcolumn_opt",
+  /*  284 */ "carglist",
+  /*  285 */ "create_vtab",
+  /*  286 */ "vtabarglist",
+  /*  287 */ "vtabarg",
+  /*  288 */ "vtabargtoken",
+  /*  289 */ "lp",
+  /*  290 */ "anylist",
+  /*  291 */ "wqas",
+  /*  292 */ "wqitem",
+  /*  293 */ "windowdefn_list",
+  /*  294 */ "windowdefn",
+  /*  295 */ "window",
+  /*  296 */ "frame_opt",
+  /*  297 */ "range_or_rows",
+  /*  298 */ "frame_bound_s",
+  /*  299 */ "frame_exclude_opt",
+  /*  300 */ "frame_bound_e",
+  /*  301 */ "frame_bound",
+  /*  302 */ "frame_exclude",
+  /*  303 */ "filter_clause",
+  /*  304 */ "over_clause",
+  /*  305 */ "input",
+  /*  306 */ "cmdlist",
+  /*  307 */ "ecmd",
+  /*  308 */ "conslist",
+};
+#endif /* defined(YYCOVERAGE) || !defined(NDEBUG) */
+
+#ifndef NDEBUG
+/* For tracing reduce actions, the names of all rules are required.
+*/
+static const char *const yyRuleName[] = {
+ /*   0 */ "explain ::= EXPLAIN",
+ /*   1 */ "explain ::= EXPLAIN QUERY PLAN",
+ /*   2 */ "cmdx ::= cmd",
+ /*   3 */ "cmd ::= BEGIN transtype trans_opt",
+ /*   4 */ "transtype ::=",
+ /*   5 */ "transtype ::= DEFERRED",
+ /*   6 */ "transtype ::= IMMEDIATE",
+ /*   7 */ "transtype ::= EXCLUSIVE",
+ /*   8 */ "cmd ::= COMMIT|END trans_opt",
+ /*   9 */ "cmd ::= ROLLBACK trans_opt",
+ /*  10 */ "cmd ::= SAVEPOINT nm",
+ /*  11 */ "cmd ::= RELEASE savepoint_opt nm",
+ /*  12 */ "cmd ::= ROLLBACK trans_opt TO savepoint_opt nm",
+ /*  13 */ "create_table ::= createkw temp TABLE ifnotexists nm dbnm",
+ /*  14 */ "createkw ::= CREATE",
+ /*  15 */ "ifnotexists ::=",
+ /*  16 */ "ifnotexists ::= IF NOT EXISTS",
+ /*  17 */ "temp ::= TEMP",
+ /*  18 */ "temp ::=",
+ /*  19 */ "create_table_args ::= LP columnlist conslist_opt RP table_option_set",
+ /*  20 */ "create_table_args ::= AS select",
+ /*  21 */ "table_option_set ::=",
+ /*  22 */ "table_option_set ::= table_option_set COMMA table_option",
+ /*  23 */ "table_option ::= WITHOUT nm",
+ /*  24 */ "table_option ::= nm",
+ /*  25 */ "columnname ::= nm typetoken",
+ /*  26 */ "typetoken ::=",
+ /*  27 */ "typetoken ::= typename LP signed RP",
+ /*  28 */ "typetoken ::= typename LP signed COMMA signed RP",
+ /*  29 */ "typename ::= typename ID|STRING",
+ /*  30 */ "scanpt ::=",
+ /*  31 */ "scantok ::=",
+ /*  32 */ "ccons ::= CONSTRAINT nm",
+ /*  33 */ "ccons ::= DEFAULT scantok term",
+ /*  34 */ "ccons ::= DEFAULT LP expr RP",
+ /*  35 */ "ccons ::= DEFAULT PLUS scantok term",
+ /*  36 */ "ccons ::= DEFAULT MINUS scantok term",
+ /*  37 */ "ccons ::= DEFAULT scantok ID|INDEXED",
+ /*  38 */ "ccons ::= NOT NULL onconf",
+ /*  39 */ "ccons ::= PRIMARY KEY sortorder onconf autoinc",
+ /*  40 */ "ccons ::= UNIQUE onconf",
+ /*  41 */ "ccons ::= CHECK LP expr RP",
+ /*  42 */ "ccons ::= REFERENCES nm eidlist_opt refargs",
+ /*  43 */ "ccons ::= defer_subclause",
+ /*  44 */ "ccons ::= COLLATE ID|STRING",
+ /*  45 */ "generated ::= LP expr RP",
+ /*  46 */ "generated ::= LP expr RP ID",
+ /*  47 */ "autoinc ::=",
+ /*  48 */ "autoinc ::= AUTOINCR",
+ /*  49 */ "refargs ::=",
+ /*  50 */ "refargs ::= refargs refarg",
+ /*  51 */ "refarg ::= MATCH nm",
+ /*  52 */ "refarg ::= ON INSERT refact",
+ /*  53 */ "refarg ::= ON DELETE refact",
+ /*  54 */ "refarg ::= ON UPDATE refact",
+ /*  55 */ "refact ::= SET NULL",
+ /*  56 */ "refact ::= SET DEFAULT",
+ /*  57 */ "refact ::= CASCADE",
+ /*  58 */ "refact ::= RESTRICT",
+ /*  59 */ "refact ::= NO ACTION",
+ /*  60 */ "defer_subclause ::= NOT DEFERRABLE init_deferred_pred_opt",
+ /*  61 */ "defer_subclause ::= DEFERRABLE init_deferred_pred_opt",
+ /*  62 */ "init_deferred_pred_opt ::=",
+ /*  63 */ "init_deferred_pred_opt ::= INITIALLY DEFERRED",
+ /*  64 */ "init_deferred_pred_opt ::= INITIALLY IMMEDIATE",
+ /*  65 */ "conslist_opt ::=",
+ /*  66 */ "tconscomma ::= COMMA",
+ /*  67 */ "tcons ::= CONSTRAINT nm",
+ /*  68 */ "tcons ::= PRIMARY KEY LP sortlist autoinc RP onconf",
+ /*  69 */ "tcons ::= UNIQUE LP sortlist RP onconf",
+ /*  70 */ "tcons ::= CHECK LP expr RP onconf",
+ /*  71 */ "tcons ::= FOREIGN KEY LP eidlist RP REFERENCES nm eidlist_opt refargs defer_subclause_opt",
+ /*  72 */ "defer_subclause_opt ::=",
+ /*  73 */ "onconf ::=",
+ /*  74 */ "onconf ::= ON CONFLICT resolvetype",
+ /*  75 */ "orconf ::=",
+ /*  76 */ "orconf ::= OR resolvetype",
+ /*  77 */ "resolvetype ::= IGNORE",
+ /*  78 */ "resolvetype ::= REPLACE",
+ /*  79 */ "cmd ::= DROP TABLE ifexists fullname",
+ /*  80 */ "ifexists ::= IF EXISTS",
+ /*  81 */ "ifexists ::=",
+ /*  82 */ "cmd ::= createkw temp VIEW ifnotexists nm dbnm eidlist_opt AS select",
+ /*  83 */ "cmd ::= DROP VIEW ifexists fullname",
+ /*  84 */ "cmd ::= select",
+ /*  85 */ "select ::= WITH wqlist selectnowith",
+ /*  86 */ "select ::= WITH RECURSIVE wqlist selectnowith",
+ /*  87 */ "select ::= selectnowith",
+ /*  88 */ "selectnowith ::= selectnowith multiselect_op oneselect",
+ /*  89 */ "multiselect_op ::= UNION",
+ /*  90 */ "multiselect_op ::= UNION ALL",
+ /*  91 */ "multiselect_op ::= EXCEPT|INTERSECT",
+ /*  92 */ "oneselect ::= SELECT distinct selcollist from where_opt groupby_opt having_opt orderby_opt limit_opt",
+ /*  93 */ "oneselect ::= SELECT distinct selcollist from where_opt groupby_opt having_opt window_clause orderby_opt limit_opt",
+ /*  94 */ "values ::= VALUES LP nexprlist RP",
+ /*  95 */ "values ::= values COMMA LP nexprlist RP",
+ /*  96 */ "distinct ::= DISTINCT",
+ /*  97 */ "distinct ::= ALL",
+ /*  98 */ "distinct ::=",
+ /*  99 */ "sclp ::=",
+ /* 100 */ "selcollist ::= sclp scanpt expr scanpt as",
+ /* 101 */ "selcollist ::= sclp scanpt STAR",
+ /* 102 */ "selcollist ::= sclp scanpt nm DOT STAR",
+ /* 103 */ "as ::= AS nm",
+ /* 104 */ "as ::=",
+ /* 105 */ "from ::=",
+ /* 106 */ "from ::= FROM seltablist",
+ /* 107 */ "stl_prefix ::= seltablist joinop",
+ /* 108 */ "stl_prefix ::=",
+ /* 109 */ "seltablist ::= stl_prefix nm dbnm as on_using",
+ /* 110 */ "seltablist ::= stl_prefix nm dbnm as indexed_by on_using",
+ /* 111 */ "seltablist ::= stl_prefix nm dbnm LP exprlist RP as on_using",
+ /* 112 */ "seltablist ::= stl_prefix LP select RP as on_using",
+ /* 113 */ "seltablist ::= stl_prefix LP seltablist RP as on_using",
+ /* 114 */ "dbnm ::=",
+ /* 115 */ "dbnm ::= DOT nm",
+ /* 116 */ "fullname ::= nm",
+ /* 117 */ "fullname ::= nm DOT nm",
+ /* 118 */ "xfullname ::= nm",
+ /* 119 */ "xfullname ::= nm DOT nm",
+ /* 120 */ "xfullname ::= nm DOT nm AS nm",
+ /* 121 */ "xfullname ::= nm AS nm",
+ /* 122 */ "joinop ::= COMMA|JOIN",
+ /* 123 */ "joinop ::= JOIN_KW JOIN",
+ /* 124 */ "joinop ::= JOIN_KW nm JOIN",
+ /* 125 */ "joinop ::= JOIN_KW nm nm JOIN",
+ /* 126 */ "on_using ::= ON expr",
+ /* 127 */ "on_using ::= USING LP idlist RP",
+ /* 128 */ "on_using ::=",
+ /* 129 */ "indexed_opt ::=",
+ /* 130 */ "indexed_by ::= INDEXED BY nm",
+ /* 131 */ "indexed_by ::= NOT INDEXED",
+ /* 132 */ "orderby_opt ::=",
+ /* 133 */ "orderby_opt ::= ORDER BY sortlist",
+ /* 134 */ "sortlist ::= sortlist COMMA expr sortorder nulls",
+ /* 135 */ "sortlist ::= expr sortorder nulls",
+ /* 136 */ "sortorder ::= ASC",
+ /* 137 */ "sortorder ::= DESC",
+ /* 138 */ "sortorder ::=",
+ /* 139 */ "nulls ::= NULLS FIRST",
+ /* 140 */ "nulls ::= NULLS LAST",
+ /* 141 */ "nulls ::=",
+ /* 142 */ "groupby_opt ::=",
+ /* 143 */ "groupby_opt ::= GROUP BY nexprlist",
+ /* 144 */ "having_opt ::=",
+ /* 145 */ "having_opt ::= HAVING expr",
+ /* 146 */ "limit_opt ::=",
+ /* 147 */ "limit_opt ::= LIMIT expr",
+ /* 148 */ "limit_opt ::= LIMIT expr OFFSET expr",
+ /* 149 */ "limit_opt ::= LIMIT expr COMMA expr",
+ /* 150 */ "cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret",
+ /* 151 */ "where_opt ::=",
+ /* 152 */ "where_opt ::= WHERE expr",
+ /* 153 */ "where_opt_ret ::=",
+ /* 154 */ "where_opt_ret ::= WHERE expr",
+ /* 155 */ "where_opt_ret ::= RETURNING selcollist",
+ /* 156 */ "where_opt_ret ::= WHERE expr RETURNING selcollist",
+ /* 157 */ "cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret",
+ /* 158 */ "setlist ::= setlist COMMA nm EQ expr",
+ /* 159 */ "setlist ::= setlist COMMA LP idlist RP EQ expr",
+ /* 160 */ "setlist ::= nm EQ expr",
+ /* 161 */ "setlist ::= LP idlist RP EQ expr",
+ /* 162 */ "cmd ::= with insert_cmd INTO xfullname idlist_opt select upsert",
+ /* 163 */ "cmd ::= with insert_cmd INTO xfullname idlist_opt DEFAULT VALUES returning",
+ /* 164 */ "upsert ::=",
+ /* 165 */ "upsert ::= RETURNING selcollist",
+ /* 166 */ "upsert ::= ON CONFLICT LP sortlist RP where_opt DO UPDATE SET setlist where_opt upsert",
+ /* 167 */ "upsert ::= ON CONFLICT LP sortlist RP where_opt DO NOTHING upsert",
+ /* 168 */ "upsert ::= ON CONFLICT DO NOTHING returning",
+ /* 169 */ "upsert ::= ON CONFLICT DO UPDATE SET setlist where_opt returning",
+ /* 170 */ "returning ::= RETURNING selcollist",
+ /* 171 */ "insert_cmd ::= INSERT orconf",
+ /* 172 */ "insert_cmd ::= REPLACE",
+ /* 173 */ "idlist_opt ::=",
+ /* 174 */ "idlist_opt ::= LP idlist RP",
+ /* 175 */ "idlist ::= idlist COMMA nm",
+ /* 176 */ "idlist ::= nm",
+ /* 177 */ "expr ::= LP expr RP",
+ /* 178 */ "expr ::= ID|INDEXED|JOIN_KW",
+ /* 179 */ "expr ::= nm DOT nm",
+ /* 180 */ "expr ::= nm DOT nm DOT nm",
+ /* 181 */ "term ::= NULL|FLOAT|BLOB",
+ /* 182 */ "term ::= STRING",
+ /* 183 */ "term ::= INTEGER",
+ /* 184 */ "expr ::= VARIABLE",
+ /* 185 */ "expr ::= expr COLLATE ID|STRING",
+ /* 186 */ "expr ::= CAST LP expr AS typetoken RP",
+ /* 187 */ "expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist RP",
+ /* 188 */ "expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist ORDER BY sortlist RP",
+ /* 189 */ "expr ::= ID|INDEXED|JOIN_KW LP STAR RP",
+ /* 190 */ "expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist RP filter_over",
+ /* 191 */ "expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist ORDER BY sortlist RP filter_over",
+ /* 192 */ "expr ::= ID|INDEXED|JOIN_KW LP STAR RP filter_over",
+ /* 193 */ "term ::= CTIME_KW",
+ /* 194 */ "expr ::= LP nexprlist COMMA expr RP",
+ /* 195 */ "expr ::= expr AND expr",
+ /* 196 */ "expr ::= expr OR expr",
+ /* 197 */ "expr ::= expr LT|GT|GE|LE expr",
+ /* 198 */ "expr ::= expr EQ|NE expr",
+ /* 199 */ "expr ::= expr BITAND|BITOR|LSHIFT|RSHIFT expr",
+ /* 200 */ "expr ::= expr PLUS|MINUS expr",
+ /* 201 */ "expr ::= expr STAR|SLASH|REM expr",
+ /* 202 */ "expr ::= expr CONCAT expr",
+ /* 203 */ "likeop ::= NOT LIKE_KW|MATCH",
+ /* 204 */ "expr ::= expr likeop expr",
+ /* 205 */ "expr ::= expr likeop expr ESCAPE expr",
+ /* 206 */ "expr ::= expr ISNULL|NOTNULL",
+ /* 207 */ "expr ::= expr NOT NULL",
+ /* 208 */ "expr ::= expr IS expr",
+ /* 209 */ "expr ::= expr IS NOT expr",
+ /* 210 */ "expr ::= expr IS NOT DISTINCT FROM expr",
+ /* 211 */ "expr ::= expr IS DISTINCT FROM expr",
+ /* 212 */ "expr ::= NOT expr",
+ /* 213 */ "expr ::= BITNOT expr",
+ /* 214 */ "expr ::= PLUS|MINUS expr",
+ /* 215 */ "expr ::= expr PTR expr",
+ /* 216 */ "between_op ::= BETWEEN",
+ /* 217 */ "between_op ::= NOT BETWEEN",
+ /* 218 */ "expr ::= expr between_op expr AND expr",
+ /* 219 */ "in_op ::= IN",
+ /* 220 */ "in_op ::= NOT IN",
+ /* 221 */ "expr ::= expr in_op LP exprlist RP",
+ /* 222 */ "expr ::= LP select RP",
+ /* 223 */ "expr ::= expr in_op LP select RP",
+ /* 224 */ "expr ::= expr in_op nm dbnm paren_exprlist",
+ /* 225 */ "expr ::= EXISTS LP select RP",
+ /* 226 */ "expr ::= CASE case_operand case_exprlist case_else END",
+ /* 227 */ "case_exprlist ::= case_exprlist WHEN expr THEN expr",
+ /* 228 */ "case_exprlist ::= WHEN expr THEN expr",
+ /* 229 */ "case_else ::= ELSE expr",
+ /* 230 */ "case_else ::=",
+ /* 231 */ "case_operand ::=",
+ /* 232 */ "exprlist ::=",
+ /* 233 */ "nexprlist ::= nexprlist COMMA expr",
+ /* 234 */ "nexprlist ::= expr",
+ /* 235 */ "paren_exprlist ::=",
+ /* 236 */ "paren_exprlist ::= LP exprlist RP",
+ /* 237 */ "cmd ::= createkw uniqueflag INDEX ifnotexists nm dbnm ON nm LP sortlist RP where_opt",
+ /* 238 */ "uniqueflag ::= UNIQUE",
+ /* 239 */ "uniqueflag ::=",
+ /* 240 */ "eidlist_opt ::=",
+ /* 241 */ "eidlist_opt ::= LP eidlist RP",
+ /* 242 */ "eidlist ::= eidlist COMMA nm collate sortorder",
+ /* 243 */ "eidlist ::= nm collate sortorder",
+ /* 244 */ "collate ::=",
+ /* 245 */ "collate ::= COLLATE ID|STRING",
+ /* 246 */ "cmd ::= DROP INDEX ifexists fullname",
+ /* 247 */ "cmd ::= VACUUM vinto",
+ /* 248 */ "cmd ::= VACUUM nm vinto",
+ /* 249 */ "vinto ::= INTO expr",
+ /* 250 */ "vinto ::=",
+ /* 251 */ "cmd ::= PRAGMA nm dbnm",
+ /* 252 */ "cmd ::= PRAGMA nm dbnm EQ nmnum",
+ /* 253 */ "cmd ::= PRAGMA nm dbnm LP nmnum RP",
+ /* 254 */ "cmd ::= PRAGMA nm dbnm EQ minus_num",
+ /* 255 */ "cmd ::= PRAGMA nm dbnm LP minus_num RP",
+ /* 256 */ "plus_num ::= PLUS INTEGER|FLOAT",
+ /* 257 */ "minus_num ::= MINUS INTEGER|FLOAT",
+ /* 258 */ "cmd ::= createkw trigger_decl BEGIN trigger_cmd_list END",
+ /* 259 */ "trigger_decl ::= temp TRIGGER ifnotexists nm dbnm trigger_time trigger_event ON fullname foreach_clause when_clause",
+ /* 260 */ "trigger_time ::= BEFORE|AFTER",
+ /* 261 */ "trigger_time ::= INSTEAD OF",
+ /* 262 */ "trigger_time ::=",
+ /* 263 */ "trigger_event ::= DELETE|INSERT",
+ /* 264 */ "trigger_event ::= UPDATE",
+ /* 265 */ "trigger_event ::= UPDATE OF idlist",
+ /* 266 */ "when_clause ::=",
+ /* 267 */ "when_clause ::= WHEN expr",
+ /* 268 */ "trigger_cmd_list ::= trigger_cmd_list trigger_cmd SEMI",
+ /* 269 */ "trigger_cmd_list ::= trigger_cmd SEMI",
+ /* 270 */ "trnm ::= nm DOT nm",
+ /* 271 */ "tridxby ::= INDEXED BY nm",
+ /* 272 */ "tridxby ::= NOT INDEXED",
+ /* 273 */ "trigger_cmd ::= UPDATE orconf trnm tridxby SET setlist from where_opt scanpt",
+ /* 274 */ "trigger_cmd ::= scanpt insert_cmd INTO trnm idlist_opt select upsert scanpt",
+ /* 275 */ "trigger_cmd ::= DELETE FROM trnm tridxby where_opt scanpt",
+ /* 276 */ "trigger_cmd ::= scanpt select scanpt",
+ /* 277 */ "expr ::= RAISE LP IGNORE RP",
+ /* 278 */ "expr ::= RAISE LP raisetype COMMA nm RP",
+ /* 279 */ "raisetype ::= ROLLBACK",
+ /* 280 */ "raisetype ::= ABORT",
+ /* 281 */ "raisetype ::= FAIL",
+ /* 282 */ "cmd ::= DROP TRIGGER ifexists fullname",
+ /* 283 */ "cmd ::= ATTACH database_kw_opt expr AS expr key_opt",
+ /* 284 */ "cmd ::= DETACH database_kw_opt expr",
+ /* 285 */ "key_opt ::=",
+ /* 286 */ "key_opt ::= KEY expr",
+ /* 287 */ "cmd ::= REINDEX",
+ /* 288 */ "cmd ::= REINDEX nm dbnm",
+ /* 289 */ "cmd ::= ANALYZE",
+ /* 290 */ "cmd ::= ANALYZE nm dbnm",
+ /* 291 */ "cmd ::= ALTER TABLE fullname RENAME TO nm",
+ /* 292 */ "cmd ::= ALTER TABLE add_column_fullname ADD kwcolumn_opt columnname carglist",
+ /* 293 */ "cmd ::= ALTER TABLE fullname DROP kwcolumn_opt nm",
+ /* 294 */ "add_column_fullname ::= fullname",
+ /* 295 */ "cmd ::= ALTER TABLE fullname RENAME kwcolumn_opt nm TO nm",
+ /* 296 */ "cmd ::= create_vtab",
+ /* 297 */ "cmd ::= create_vtab LP vtabarglist RP",
+ /* 298 */ "create_vtab ::= createkw VIRTUAL TABLE ifnotexists nm dbnm USING nm",
+ /* 299 */ "vtabarg ::=",
+ /* 300 */ "vtabargtoken ::= ANY",
+ /* 301 */ "vtabargtoken ::= lp anylist RP",
+ /* 302 */ "lp ::= LP",
+ /* 303 */ "with ::= WITH wqlist",
+ /* 304 */ "with ::= WITH RECURSIVE wqlist",
+ /* 305 */ "wqas ::= AS",
+ /* 306 */ "wqas ::= AS MATERIALIZED",
+ /* 307 */ "wqas ::= AS NOT MATERIALIZED",
+ /* 308 */ "wqitem ::= nm eidlist_opt wqas LP select RP",
+ /* 309 */ "wqlist ::= wqitem",
+ /* 310 */ "wqlist ::= wqlist COMMA wqitem",
+ /* 311 */ "windowdefn_list ::= windowdefn_list COMMA windowdefn",
+ /* 312 */ "windowdefn ::= nm AS LP window RP",
+ /* 313 */ "window ::= PARTITION BY nexprlist orderby_opt frame_opt",
+ /* 314 */ "window ::= nm PARTITION BY nexprlist orderby_opt frame_opt",
+ /* 315 */ "window ::= ORDER BY sortlist frame_opt",
+ /* 316 */ "window ::= nm ORDER BY sortlist frame_opt",
+ /* 317 */ "window ::= nm frame_opt",
+ /* 318 */ "frame_opt ::=",
+ /* 319 */ "frame_opt ::= range_or_rows frame_bound_s frame_exclude_opt",
+ /* 320 */ "frame_opt ::= range_or_rows BETWEEN frame_bound_s AND frame_bound_e frame_exclude_opt",
+ /* 321 */ "range_or_rows ::= RANGE|ROWS|GROUPS",
+ /* 322 */ "frame_bound_s ::= frame_bound",
+ /* 323 */ "frame_bound_s ::= UNBOUNDED PRECEDING",
+ /* 324 */ "frame_bound_e ::= frame_bound",
+ /* 325 */ "frame_bound_e ::= UNBOUNDED FOLLOWING",
+ /* 326 */ "frame_bound ::= expr PRECEDING|FOLLOWING",
+ /* 327 */ "frame_bound ::= CURRENT ROW",
+ /* 328 */ "frame_exclude_opt ::=",
+ /* 329 */ "frame_exclude_opt ::= EXCLUDE frame_exclude",
+ /* 330 */ "frame_exclude ::= NO OTHERS",
+ /* 331 */ "frame_exclude ::= CURRENT ROW",
+ /* 332 */ "frame_exclude ::= GROUP|TIES",
+ /* 333 */ "window_clause ::= WINDOW windowdefn_list",
+ /* 334 */ "filter_over ::= filter_clause over_clause",
+ /* 335 */ "filter_over ::= over_clause",
+ /* 336 */ "filter_over ::= filter_clause",
+ /* 337 */ "over_clause ::= OVER LP window RP",
+ /* 338 */ "over_clause ::= OVER nm",
+ /* 339 */ "filter_clause ::= FILTER LP WHERE expr RP",
+ /* 340 */ "input ::= cmdlist",
+ /* 341 */ "cmdlist ::= cmdlist ecmd",
+ /* 342 */ "cmdlist ::= ecmd",
+ /* 343 */ "ecmd ::= SEMI",
+ /* 344 */ "ecmd ::= cmdx SEMI",
+ /* 345 */ "ecmd ::= explain cmdx SEMI",
+ /* 346 */ "trans_opt ::=",
+ /* 347 */ "trans_opt ::= TRANSACTION",
+ /* 348 */ "trans_opt ::= TRANSACTION nm",
+ /* 349 */ "savepoint_opt ::= SAVEPOINT",
+ /* 350 */ "savepoint_opt ::=",
+ /* 351 */ "cmd ::= create_table create_table_args",
+ /* 352 */ "table_option_set ::= table_option",
+ /* 353 */ "columnlist ::= columnlist COMMA columnname carglist",
+ /* 354 */ "columnlist ::= columnname carglist",
+ /* 355 */ "nm ::= ID|INDEXED|JOIN_KW",
+ /* 356 */ "nm ::= STRING",
+ /* 357 */ "typetoken ::= typename",
+ /* 358 */ "typename ::= ID|STRING",
+ /* 359 */ "signed ::= plus_num",
+ /* 360 */ "signed ::= minus_num",
+ /* 361 */ "carglist ::= carglist ccons",
+ /* 362 */ "carglist ::=",
+ /* 363 */ "ccons ::= NULL onconf",
+ /* 364 */ "ccons ::= GENERATED ALWAYS AS generated",
+ /* 365 */ "ccons ::= AS generated",
+ /* 366 */ "conslist_opt ::= COMMA conslist",
+ /* 367 */ "conslist ::= conslist tconscomma tcons",
+ /* 368 */ "conslist ::= tcons",
+ /* 369 */ "tconscomma ::=",
+ /* 370 */ "defer_subclause_opt ::= defer_subclause",
+ /* 371 */ "resolvetype ::= raisetype",
+ /* 372 */ "selectnowith ::= oneselect",
+ /* 373 */ "oneselect ::= values",
+ /* 374 */ "sclp ::= selcollist COMMA",
+ /* 375 */ "as ::= ID|STRING",
+ /* 376 */ "indexed_opt ::= indexed_by",
+ /* 377 */ "returning ::=",
+ /* 378 */ "expr ::= term",
+ /* 379 */ "likeop ::= LIKE_KW|MATCH",
+ /* 380 */ "case_operand ::= expr",
+ /* 381 */ "exprlist ::= nexprlist",
+ /* 382 */ "nmnum ::= plus_num",
+ /* 383 */ "nmnum ::= nm",
+ /* 384 */ "nmnum ::= ON",
+ /* 385 */ "nmnum ::= DELETE",
+ /* 386 */ "nmnum ::= DEFAULT",
+ /* 387 */ "plus_num ::= INTEGER|FLOAT",
+ /* 388 */ "foreach_clause ::=",
+ /* 389 */ "foreach_clause ::= FOR EACH ROW",
+ /* 390 */ "trnm ::= nm",
+ /* 391 */ "tridxby ::=",
+ /* 392 */ "database_kw_opt ::= DATABASE",
+ /* 393 */ "database_kw_opt ::=",
+ /* 394 */ "kwcolumn_opt ::=",
+ /* 395 */ "kwcolumn_opt ::= COLUMNKW",
+ /* 396 */ "vtabarglist ::= vtabarg",
+ /* 397 */ "vtabarglist ::= vtabarglist COMMA vtabarg",
+ /* 398 */ "vtabarg ::= vtabarg vtabargtoken",
+ /* 399 */ "anylist ::=",
+ /* 400 */ "anylist ::= anylist LP anylist RP",
+ /* 401 */ "anylist ::= anylist ANY",
+ /* 402 */ "with ::=",
+ /* 403 */ "windowdefn_list ::= windowdefn",
+ /* 404 */ "window ::= frame_opt",
+};
+#endif /* NDEBUG */
+
+
+#if YYSTACKDEPTH<=0
+/*
+** Try to increase the size of the parser stack.  Return the number
+** of errors.  Return 0 on success.
+*/
+static int yyGrowStack(yyParser *p){
+  int newSize;
+  int idx;
+  yyStackEntry *pNew;
+
+  newSize = p->yystksz*2 + 100;
+  idx = p->yytos ? (int)(p->yytos - p->yystack) : 0;
+  if( p->yystack==&p->yystk0 ){
+    pNew = malloc(newSize*sizeof(pNew[0]));
+    if( pNew ) pNew[0] = p->yystk0;
+  }else{
+    pNew = realloc(p->yystack, newSize*sizeof(pNew[0]));
+  }
+  if( pNew ){
+    p->yystack = pNew;
+    p->yytos = &p->yystack[idx];
+#ifndef NDEBUG
+    if( yyTraceFILE ){
+      fprintf(yyTraceFILE,"%sStack grows from %d to %d entries.\n",
+              yyTracePrompt, p->yystksz, newSize);
+    }
+#endif
+    p->yystksz = newSize;
+  }
+  return pNew==0; 
+}
+#endif
+
+/* Datatype of the argument to the memory allocated passed as the
+** second argument to PerfettoSqlParserAlloc() below.  This can be changed by
+** putting an appropriate #define in the %include section of the input
+** grammar.
+*/
+#ifndef YYMALLOCARGTYPE
+# define YYMALLOCARGTYPE size_t
+#endif
+
+/* Initialize a new parser that has already been allocated.
+*/
+void PerfettoSqlParserInit(void *yypRawParser PerfettoSqlParserCTX_PDECL){
+  yyParser *yypParser = (yyParser*)yypRawParser;
+  PerfettoSqlParserCTX_STORE
+#ifdef YYTRACKMAXSTACKDEPTH
+  yypParser->yyhwm = 0;
+#endif
+#if YYSTACKDEPTH<=0
+  yypParser->yytos = NULL;
+  yypParser->yystack = NULL;
+  yypParser->yystksz = 0;
+  if( yyGrowStack(yypParser) ){
+    yypParser->yystack = &yypParser->yystk0;
+    yypParser->yystksz = 1;
+  }
+#endif
+#ifndef YYNOERRORRECOVERY
+  yypParser->yyerrcnt = -1;
+#endif
+  yypParser->yytos = yypParser->yystack;
+  yypParser->yystack[0].stateno = 0;
+  yypParser->yystack[0].major = 0;
+#if YYSTACKDEPTH>0
+  yypParser->yystackEnd = &yypParser->yystack[YYSTACKDEPTH-1];
+#endif
+}
+
+#ifndef PerfettoSqlParser_ENGINEALWAYSONSTACK
+/* 
+** This function allocates a new parser.
+** The only argument is a pointer to a function which works like
+** malloc.
+**
+** Inputs:
+** A pointer to the function used to allocate memory.
+**
+** Outputs:
+** A pointer to a parser.  This pointer is used in subsequent calls
+** to PerfettoSqlParser and PerfettoSqlParserFree.
+*/
+void *PerfettoSqlParserAlloc(void *(*mallocProc)(YYMALLOCARGTYPE) PerfettoSqlParserCTX_PDECL){
+  yyParser *yypParser;
+  yypParser = (yyParser*)(*mallocProc)( (YYMALLOCARGTYPE)sizeof(yyParser) );
+  if( yypParser ){
+    PerfettoSqlParserCTX_STORE
+    PerfettoSqlParserInit(yypParser PerfettoSqlParserCTX_PARAM);
+  }
+  return (void*)yypParser;
+}
+#endif /* PerfettoSqlParser_ENGINEALWAYSONSTACK */
+
+
+/* The following function deletes the "minor type" or semantic value
+** associated with a symbol.  The symbol can be either a terminal
+** or nonterminal. "yymajor" is the symbol code, and "yypminor" is
+** a pointer to the value to be deleted.  The code used to do the 
+** deletions is derived from the %destructor and/or %token_destructor
+** directives of the input grammar.
+*/
+static void yy_destructor(
+  yyParser *yypParser,    /* The parser */
+  YYCODETYPE yymajor,     /* Type code for object to destroy */
+  YYMINORTYPE *yypminor   /* The object to be destroyed */
+){
+  PerfettoSqlParserARG_FETCH
+  PerfettoSqlParserCTX_FETCH
+  switch( yymajor ){
+    /* Here is inserted the actions which take place when a
+    ** terminal or non-terminal is destroyed.  This can happen
+    ** when the symbol is popped from the stack during a
+    ** reduce or during error processing or when a parser is 
+    ** being destroyed before it is finished parsing.
+    **
+    ** Note: during a reduce, the only symbols destroyed are those
+    ** which appear on the RHS of the rule, but which are *not* used
+    ** inside the C code.
+    */
+/********* Begin destructor definitions ***************************************/
+/********* End destructor definitions *****************************************/
+    default:  break;   /* If no destructor action specified: do nothing */
+  }
+}
+
+/*
+** Pop the parser's stack once.
+**
+** If there is a destructor routine associated with the token which
+** is popped from the stack, then call it.
+*/
+static void yy_pop_parser_stack(yyParser *pParser){
+  yyStackEntry *yytos;
+  assert( pParser->yytos!=0 );
+  assert( pParser->yytos > pParser->yystack );
+  yytos = pParser->yytos--;
+#ifndef NDEBUG
+  if( yyTraceFILE ){
+    fprintf(yyTraceFILE,"%sPopping %s\n",
+      yyTracePrompt,
+      yyTokenName[yytos->major]);
+  }
+#endif
+  yy_destructor(pParser, yytos->major, &yytos->minor);
+}
+
+/*
+** Clear all secondary memory allocations from the parser
+*/
+void PerfettoSqlParserFinalize(void *p){
+  yyParser *pParser = (yyParser*)p;
+  while( pParser->yytos>pParser->yystack ) yy_pop_parser_stack(pParser);
+#if YYSTACKDEPTH<=0
+  if( pParser->yystack!=&pParser->yystk0 ) free(pParser->yystack);
+#endif
+}
+
+#ifndef PerfettoSqlParser_ENGINEALWAYSONSTACK
+/* 
+** Deallocate and destroy a parser.  Destructors are called for
+** all stack elements before shutting the parser down.
+**
+** If the YYPARSEFREENEVERNULL macro exists (for example because it
+** is defined in a %include section of the input grammar) then it is
+** assumed that the input pointer is never NULL.
+*/
+void PerfettoSqlParserFree(
+  void *p,                    /* The parser to be deleted */
+  void (*freeProc)(void*)     /* Function used to reclaim memory */
+){
+#ifndef YYPARSEFREENEVERNULL
+  if( p==0 ) return;
+#endif
+  PerfettoSqlParserFinalize(p);
+  (*freeProc)(p);
+}
+#endif /* PerfettoSqlParser_ENGINEALWAYSONSTACK */
+
+/*
+** Return the peak depth of the stack for a parser.
+*/
+#ifdef YYTRACKMAXSTACKDEPTH
+int PerfettoSqlParserStackPeak(void *p){
+  yyParser *pParser = (yyParser*)p;
+  return pParser->yyhwm;
+}
+#endif
+
+/* This array of booleans keeps track of the parser statement
+** coverage.  The element yycoverage[X][Y] is set when the parser
+** is in state X and has a lookahead token Y.  In a well-tested
+** systems, every element of this matrix should end up being set.
+*/
+#if defined(YYCOVERAGE)
+static unsigned char yycoverage[YYNSTATE][YYNTOKEN];
+#endif
+
+/*
+** Write into out a description of every state/lookahead combination that
+**
+**   (1)  has not been used by the parser, and
+**   (2)  is not a syntax error.
+**
+** Return the number of missed state/lookahead combinations.
+*/
+#if defined(YYCOVERAGE)
+int PerfettoSqlParserCoverage(FILE *out){
+  int stateno, iLookAhead, i;
+  int nMissed = 0;
+  for(stateno=0; stateno<YYNSTATE; stateno++){
+    i = yy_shift_ofst[stateno];
+    for(iLookAhead=0; iLookAhead<YYNTOKEN; iLookAhead++){
+      if( yy_lookahead[i+iLookAhead]!=iLookAhead ) continue;
+      if( yycoverage[stateno][iLookAhead]==0 ) nMissed++;
+      if( out ){
+        fprintf(out,"State %d lookahead %s %s\n", stateno,
+                yyTokenName[iLookAhead],
+                yycoverage[stateno][iLookAhead] ? "ok" : "missed");
+      }
+    }
+  }
+  return nMissed;
+}
+#endif
+
+/*
+** Find the appropriate action for a parser given the terminal
+** look-ahead token iLookAhead.
+*/
+static YYACTIONTYPE yy_find_shift_action(
+  YYCODETYPE iLookAhead,    /* The look-ahead token */
+  YYACTIONTYPE stateno      /* Current state number */
+){
+  int i;
+
+  if( stateno>YY_MAX_SHIFT ) return stateno;
+  assert( stateno <= YY_SHIFT_COUNT );
+#if defined(YYCOVERAGE)
+  yycoverage[stateno][iLookAhead] = 1;
+#endif
+  do{
+    i = yy_shift_ofst[stateno];
+    assert( i>=0 );
+    assert( i<=YY_ACTTAB_COUNT );
+    assert( i+YYNTOKEN<=(int)YY_NLOOKAHEAD );
+    assert( iLookAhead!=YYNOCODE );
+    assert( iLookAhead < YYNTOKEN );
+    i += iLookAhead;
+    assert( i<(int)YY_NLOOKAHEAD );
+    if( yy_lookahead[i]!=iLookAhead ){
+#ifdef YYFALLBACK
+      YYCODETYPE iFallback;            /* Fallback token */
+      assert( iLookAhead<sizeof(yyFallback)/sizeof(yyFallback[0]) );
+      iFallback = yyFallback[iLookAhead];
+      if( iFallback!=0 ){
+#ifndef NDEBUG
+        if( yyTraceFILE ){
+          fprintf(yyTraceFILE, "%sFALLBACK %s => %s\n",
+             yyTracePrompt, yyTokenName[iLookAhead], yyTokenName[iFallback]);
+        }
+#endif
+        assert( yyFallback[iFallback]==0 ); /* Fallback loop must terminate */
+        iLookAhead = iFallback;
+        continue;
+      }
+#endif
+#ifdef YYWILDCARD
+      {
+        int j = i - iLookAhead + YYWILDCARD;
+        assert( j<(int)(sizeof(yy_lookahead)/sizeof(yy_lookahead[0])) );
+        if( yy_lookahead[j]==YYWILDCARD && iLookAhead>0 ){
+#ifndef NDEBUG
+          if( yyTraceFILE ){
+            fprintf(yyTraceFILE, "%sWILDCARD %s => %s\n",
+               yyTracePrompt, yyTokenName[iLookAhead],
+               yyTokenName[YYWILDCARD]);
+          }
+#endif /* NDEBUG */
+          return yy_action[j];
+        }
+      }
+#endif /* YYWILDCARD */
+      return yy_default[stateno];
+    }else{
+      assert( i>=0 && i<(int)(sizeof(yy_action)/sizeof(yy_action[0])) );
+      return yy_action[i];
+    }
+  }while(1);
+}
+
+/*
+** Find the appropriate action for a parser given the non-terminal
+** look-ahead token iLookAhead.
+*/
+static YYACTIONTYPE yy_find_reduce_action(
+  YYACTIONTYPE stateno,     /* Current state number */
+  YYCODETYPE iLookAhead     /* The look-ahead token */
+){
+  int i;
+#ifdef YYERRORSYMBOL
+  if( stateno>YY_REDUCE_COUNT ){
+    return yy_default[stateno];
+  }
+#else
+  assert( stateno<=YY_REDUCE_COUNT );
+#endif
+  i = yy_reduce_ofst[stateno];
+  assert( iLookAhead!=YYNOCODE );
+  i += iLookAhead;
+#ifdef YYERRORSYMBOL
+  if( i<0 || i>=YY_ACTTAB_COUNT || yy_lookahead[i]!=iLookAhead ){
+    return yy_default[stateno];
+  }
+#else
+  assert( i>=0 && i<YY_ACTTAB_COUNT );
+  assert( yy_lookahead[i]==iLookAhead );
+#endif
+  return yy_action[i];
+}
+
+/*
+** The following routine is called if the stack overflows.
+*/
+static void yyStackOverflow(yyParser *yypParser){
+   PerfettoSqlParserARG_FETCH
+   PerfettoSqlParserCTX_FETCH
+#ifndef NDEBUG
+   if( yyTraceFILE ){
+     fprintf(yyTraceFILE,"%sStack Overflow!\n",yyTracePrompt);
+   }
+#endif
+   while( yypParser->yytos>yypParser->yystack ) yy_pop_parser_stack(yypParser);
+   /* Here code is inserted which will execute if the parser
+   ** stack every overflows */
+/******** Begin %stack_overflow code ******************************************/
+/******** End %stack_overflow code ********************************************/
+   PerfettoSqlParserARG_STORE /* Suppress warning about unused %extra_argument var */
+   PerfettoSqlParserCTX_STORE
+}
+
+/*
+** Print tracing information for a SHIFT action
+*/
+#ifndef NDEBUG
+static void yyTraceShift(yyParser *yypParser, int yyNewState, const char *zTag){
+  if( yyTraceFILE ){
+    if( yyNewState<YYNSTATE ){
+      fprintf(yyTraceFILE,"%s%s '%s', go to state %d\n",
+         yyTracePrompt, zTag, yyTokenName[yypParser->yytos->major],
+         yyNewState);
+    }else{
+      fprintf(yyTraceFILE,"%s%s '%s', pending reduce %d\n",
+         yyTracePrompt, zTag, yyTokenName[yypParser->yytos->major],
+         yyNewState - YY_MIN_REDUCE);
+    }
+  }
+}
+#else
+# define yyTraceShift(X,Y,Z)
+#endif
+
+/*
+** Perform a shift action.
+*/
+static void yy_shift(
+  yyParser *yypParser,          /* The parser to be shifted */
+  YYACTIONTYPE yyNewState,      /* The new state to shift in */
+  YYCODETYPE yyMajor,           /* The major token to shift in */
+  PerfettoSqlParserTOKENTYPE yyMinor        /* The minor token to shift in */
+){
+  yyStackEntry *yytos;
+  yypParser->yytos++;
+#ifdef YYTRACKMAXSTACKDEPTH
+  if( (int)(yypParser->yytos - yypParser->yystack)>yypParser->yyhwm ){
+    yypParser->yyhwm++;
+    assert( yypParser->yyhwm == (int)(yypParser->yytos - yypParser->yystack) );
+  }
+#endif
+#if YYSTACKDEPTH>0 
+  if( yypParser->yytos>yypParser->yystackEnd ){
+    yypParser->yytos--;
+    yyStackOverflow(yypParser);
+    return;
+  }
+#else
+  if( yypParser->yytos>=&yypParser->yystack[yypParser->yystksz] ){
+    if( yyGrowStack(yypParser) ){
+      yypParser->yytos--;
+      yyStackOverflow(yypParser);
+      return;
+    }
+  }
+#endif
+  if( yyNewState > YY_MAX_SHIFT ){
+    yyNewState += YY_MIN_REDUCE - YY_MIN_SHIFTREDUCE;
+  }
+  yytos = yypParser->yytos;
+  yytos->stateno = yyNewState;
+  yytos->major = yyMajor;
+  yytos->minor.yy0 = yyMinor;
+  yyTraceShift(yypParser, yyNewState, "Shift");
+}
+
+/* For rule J, yyRuleInfoLhs[J] contains the symbol on the left-hand side
+** of that rule */
+static const YYCODETYPE yyRuleInfoLhs[] = {
+   176,  /* (0) explain ::= EXPLAIN */
+   176,  /* (1) explain ::= EXPLAIN QUERY PLAN */
+   177,  /* (2) cmdx ::= cmd */
+   178,  /* (3) cmd ::= BEGIN transtype trans_opt */
+   179,  /* (4) transtype ::= */
+   179,  /* (5) transtype ::= DEFERRED */
+   179,  /* (6) transtype ::= IMMEDIATE */
+   179,  /* (7) transtype ::= EXCLUSIVE */
+   178,  /* (8) cmd ::= COMMIT|END trans_opt */
+   178,  /* (9) cmd ::= ROLLBACK trans_opt */
+   178,  /* (10) cmd ::= SAVEPOINT nm */
+   178,  /* (11) cmd ::= RELEASE savepoint_opt nm */
+   178,  /* (12) cmd ::= ROLLBACK trans_opt TO savepoint_opt nm */
+   183,  /* (13) create_table ::= createkw temp TABLE ifnotexists nm dbnm */
+   184,  /* (14) createkw ::= CREATE */
+   186,  /* (15) ifnotexists ::= */
+   186,  /* (16) ifnotexists ::= IF NOT EXISTS */
+   185,  /* (17) temp ::= TEMP */
+   185,  /* (18) temp ::= */
+   188,  /* (19) create_table_args ::= LP columnlist conslist_opt RP table_option_set */
+   188,  /* (20) create_table_args ::= AS select */
+   191,  /* (21) table_option_set ::= */
+   191,  /* (22) table_option_set ::= table_option_set COMMA table_option */
+   193,  /* (23) table_option ::= WITHOUT nm */
+   193,  /* (24) table_option ::= nm */
+   194,  /* (25) columnname ::= nm typetoken */
+   195,  /* (26) typetoken ::= */
+   195,  /* (27) typetoken ::= typename LP signed RP */
+   195,  /* (28) typetoken ::= typename LP signed COMMA signed RP */
+   196,  /* (29) typename ::= typename ID|STRING */
+   198,  /* (30) scanpt ::= */
+   199,  /* (31) scantok ::= */
+   200,  /* (32) ccons ::= CONSTRAINT nm */
+   200,  /* (33) ccons ::= DEFAULT scantok term */
+   200,  /* (34) ccons ::= DEFAULT LP expr RP */
+   200,  /* (35) ccons ::= DEFAULT PLUS scantok term */
+   200,  /* (36) ccons ::= DEFAULT MINUS scantok term */
+   200,  /* (37) ccons ::= DEFAULT scantok ID|INDEXED */
+   200,  /* (38) ccons ::= NOT NULL onconf */
+   200,  /* (39) ccons ::= PRIMARY KEY sortorder onconf autoinc */
+   200,  /* (40) ccons ::= UNIQUE onconf */
+   200,  /* (41) ccons ::= CHECK LP expr RP */
+   200,  /* (42) ccons ::= REFERENCES nm eidlist_opt refargs */
+   200,  /* (43) ccons ::= defer_subclause */
+   200,  /* (44) ccons ::= COLLATE ID|STRING */
+   209,  /* (45) generated ::= LP expr RP */
+   209,  /* (46) generated ::= LP expr RP ID */
+   205,  /* (47) autoinc ::= */
+   205,  /* (48) autoinc ::= AUTOINCR */
+   207,  /* (49) refargs ::= */
+   207,  /* (50) refargs ::= refargs refarg */
+   210,  /* (51) refarg ::= MATCH nm */
+   210,  /* (52) refarg ::= ON INSERT refact */
+   210,  /* (53) refarg ::= ON DELETE refact */
+   210,  /* (54) refarg ::= ON UPDATE refact */
+   211,  /* (55) refact ::= SET NULL */
+   211,  /* (56) refact ::= SET DEFAULT */
+   211,  /* (57) refact ::= CASCADE */
+   211,  /* (58) refact ::= RESTRICT */
+   211,  /* (59) refact ::= NO ACTION */
+   208,  /* (60) defer_subclause ::= NOT DEFERRABLE init_deferred_pred_opt */
+   208,  /* (61) defer_subclause ::= DEFERRABLE init_deferred_pred_opt */
+   212,  /* (62) init_deferred_pred_opt ::= */
+   212,  /* (63) init_deferred_pred_opt ::= INITIALLY DEFERRED */
+   212,  /* (64) init_deferred_pred_opt ::= INITIALLY IMMEDIATE */
+   190,  /* (65) conslist_opt ::= */
+   213,  /* (66) tconscomma ::= COMMA */
+   214,  /* (67) tcons ::= CONSTRAINT nm */
+   214,  /* (68) tcons ::= PRIMARY KEY LP sortlist autoinc RP onconf */
+   214,  /* (69) tcons ::= UNIQUE LP sortlist RP onconf */
+   214,  /* (70) tcons ::= CHECK LP expr RP onconf */
+   214,  /* (71) tcons ::= FOREIGN KEY LP eidlist RP REFERENCES nm eidlist_opt refargs defer_subclause_opt */
+   217,  /* (72) defer_subclause_opt ::= */
+   203,  /* (73) onconf ::= */
+   203,  /* (74) onconf ::= ON CONFLICT resolvetype */
+   219,  /* (75) orconf ::= */
+   219,  /* (76) orconf ::= OR resolvetype */
+   218,  /* (77) resolvetype ::= IGNORE */
+   218,  /* (78) resolvetype ::= REPLACE */
+   178,  /* (79) cmd ::= DROP TABLE ifexists fullname */
+   220,  /* (80) ifexists ::= IF EXISTS */
+   220,  /* (81) ifexists ::= */
+   178,  /* (82) cmd ::= createkw temp VIEW ifnotexists nm dbnm eidlist_opt AS select */
+   178,  /* (83) cmd ::= DROP VIEW ifexists fullname */
+   178,  /* (84) cmd ::= select */
+   192,  /* (85) select ::= WITH wqlist selectnowith */
+   192,  /* (86) select ::= WITH RECURSIVE wqlist selectnowith */
+   192,  /* (87) select ::= selectnowith */
+   223,  /* (88) selectnowith ::= selectnowith multiselect_op oneselect */
+   224,  /* (89) multiselect_op ::= UNION */
+   224,  /* (90) multiselect_op ::= UNION ALL */
+   224,  /* (91) multiselect_op ::= EXCEPT|INTERSECT */
+   225,  /* (92) oneselect ::= SELECT distinct selcollist from where_opt groupby_opt having_opt orderby_opt limit_opt */
+   225,  /* (93) oneselect ::= SELECT distinct selcollist from where_opt groupby_opt having_opt window_clause orderby_opt limit_opt */
+   235,  /* (94) values ::= VALUES LP nexprlist RP */
+   235,  /* (95) values ::= values COMMA LP nexprlist RP */
+   226,  /* (96) distinct ::= DISTINCT */
+   226,  /* (97) distinct ::= ALL */
+   226,  /* (98) distinct ::= */
+   237,  /* (99) sclp ::= */
+   227,  /* (100) selcollist ::= sclp scanpt expr scanpt as */
+   227,  /* (101) selcollist ::= sclp scanpt STAR */
+   227,  /* (102) selcollist ::= sclp scanpt nm DOT STAR */
+   238,  /* (103) as ::= AS nm */
+   238,  /* (104) as ::= */
+   228,  /* (105) from ::= */
+   228,  /* (106) from ::= FROM seltablist */
+   240,  /* (107) stl_prefix ::= seltablist joinop */
+   240,  /* (108) stl_prefix ::= */
+   239,  /* (109) seltablist ::= stl_prefix nm dbnm as on_using */
+   239,  /* (110) seltablist ::= stl_prefix nm dbnm as indexed_by on_using */
+   239,  /* (111) seltablist ::= stl_prefix nm dbnm LP exprlist RP as on_using */
+   239,  /* (112) seltablist ::= stl_prefix LP select RP as on_using */
+   239,  /* (113) seltablist ::= stl_prefix LP seltablist RP as on_using */
+   187,  /* (114) dbnm ::= */
+   187,  /* (115) dbnm ::= DOT nm */
+   221,  /* (116) fullname ::= nm */
+   221,  /* (117) fullname ::= nm DOT nm */
+   245,  /* (118) xfullname ::= nm */
+   245,  /* (119) xfullname ::= nm DOT nm */
+   245,  /* (120) xfullname ::= nm DOT nm AS nm */
+   245,  /* (121) xfullname ::= nm AS nm */
+   241,  /* (122) joinop ::= COMMA|JOIN */
+   241,  /* (123) joinop ::= JOIN_KW JOIN */
+   241,  /* (124) joinop ::= JOIN_KW nm JOIN */
+   241,  /* (125) joinop ::= JOIN_KW nm nm JOIN */
+   242,  /* (126) on_using ::= ON expr */
+   242,  /* (127) on_using ::= USING LP idlist RP */
+   242,  /* (128) on_using ::= */
+   247,  /* (129) indexed_opt ::= */
+   243,  /* (130) indexed_by ::= INDEXED BY nm */
+   243,  /* (131) indexed_by ::= NOT INDEXED */
+   232,  /* (132) orderby_opt ::= */
+   232,  /* (133) orderby_opt ::= ORDER BY sortlist */
+   215,  /* (134) sortlist ::= sortlist COMMA expr sortorder nulls */
+   215,  /* (135) sortlist ::= expr sortorder nulls */
+   204,  /* (136) sortorder ::= ASC */
+   204,  /* (137) sortorder ::= DESC */
+   204,  /* (138) sortorder ::= */
+   248,  /* (139) nulls ::= NULLS FIRST */
+   248,  /* (140) nulls ::= NULLS LAST */
+   248,  /* (141) nulls ::= */
+   230,  /* (142) groupby_opt ::= */
+   230,  /* (143) groupby_opt ::= GROUP BY nexprlist */
+   231,  /* (144) having_opt ::= */
+   231,  /* (145) having_opt ::= HAVING expr */
+   233,  /* (146) limit_opt ::= */
+   233,  /* (147) limit_opt ::= LIMIT expr */
+   233,  /* (148) limit_opt ::= LIMIT expr OFFSET expr */
+   233,  /* (149) limit_opt ::= LIMIT expr COMMA expr */
+   178,  /* (150) cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret */
+   229,  /* (151) where_opt ::= */
+   229,  /* (152) where_opt ::= WHERE expr */
+   250,  /* (153) where_opt_ret ::= */
+   250,  /* (154) where_opt_ret ::= WHERE expr */
+   250,  /* (155) where_opt_ret ::= RETURNING selcollist */
+   250,  /* (156) where_opt_ret ::= WHERE expr RETURNING selcollist */
+   178,  /* (157) cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret */
+   251,  /* (158) setlist ::= setlist COMMA nm EQ expr */
+   251,  /* (159) setlist ::= setlist COMMA LP idlist RP EQ expr */
+   251,  /* (160) setlist ::= nm EQ expr */
+   251,  /* (161) setlist ::= LP idlist RP EQ expr */
+   178,  /* (162) cmd ::= with insert_cmd INTO xfullname idlist_opt select upsert */
+   178,  /* (163) cmd ::= with insert_cmd INTO xfullname idlist_opt DEFAULT VALUES returning */
+   254,  /* (164) upsert ::= */
+   254,  /* (165) upsert ::= RETURNING selcollist */
+   254,  /* (166) upsert ::= ON CONFLICT LP sortlist RP where_opt DO UPDATE SET setlist where_opt upsert */
+   254,  /* (167) upsert ::= ON CONFLICT LP sortlist RP where_opt DO NOTHING upsert */
+   254,  /* (168) upsert ::= ON CONFLICT DO NOTHING returning */
+   254,  /* (169) upsert ::= ON CONFLICT DO UPDATE SET setlist where_opt returning */
+   255,  /* (170) returning ::= RETURNING selcollist */
+   252,  /* (171) insert_cmd ::= INSERT orconf */
+   252,  /* (172) insert_cmd ::= REPLACE */
+   253,  /* (173) idlist_opt ::= */
+   253,  /* (174) idlist_opt ::= LP idlist RP */
+   246,  /* (175) idlist ::= idlist COMMA nm */
+   246,  /* (176) idlist ::= nm */
+   202,  /* (177) expr ::= LP expr RP */
+   202,  /* (178) expr ::= ID|INDEXED|JOIN_KW */
+   202,  /* (179) expr ::= nm DOT nm */
+   202,  /* (180) expr ::= nm DOT nm DOT nm */
+   201,  /* (181) term ::= NULL|FLOAT|BLOB */
+   201,  /* (182) term ::= STRING */
+   201,  /* (183) term ::= INTEGER */
+   202,  /* (184) expr ::= VARIABLE */
+   202,  /* (185) expr ::= expr COLLATE ID|STRING */
+   202,  /* (186) expr ::= CAST LP expr AS typetoken RP */
+   202,  /* (187) expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist RP */
+   202,  /* (188) expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist ORDER BY sortlist RP */
+   202,  /* (189) expr ::= ID|INDEXED|JOIN_KW LP STAR RP */
+   202,  /* (190) expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist RP filter_over */
+   202,  /* (191) expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist ORDER BY sortlist RP filter_over */
+   202,  /* (192) expr ::= ID|INDEXED|JOIN_KW LP STAR RP filter_over */
+   201,  /* (193) term ::= CTIME_KW */
+   202,  /* (194) expr ::= LP nexprlist COMMA expr RP */
+   202,  /* (195) expr ::= expr AND expr */
+   202,  /* (196) expr ::= expr OR expr */
+   202,  /* (197) expr ::= expr LT|GT|GE|LE expr */
+   202,  /* (198) expr ::= expr EQ|NE expr */
+   202,  /* (199) expr ::= expr BITAND|BITOR|LSHIFT|RSHIFT expr */
+   202,  /* (200) expr ::= expr PLUS|MINUS expr */
+   202,  /* (201) expr ::= expr STAR|SLASH|REM expr */
+   202,  /* (202) expr ::= expr CONCAT expr */
+   257,  /* (203) likeop ::= NOT LIKE_KW|MATCH */
+   202,  /* (204) expr ::= expr likeop expr */
+   202,  /* (205) expr ::= expr likeop expr ESCAPE expr */
+   202,  /* (206) expr ::= expr ISNULL|NOTNULL */
+   202,  /* (207) expr ::= expr NOT NULL */
+   202,  /* (208) expr ::= expr IS expr */
+   202,  /* (209) expr ::= expr IS NOT expr */
+   202,  /* (210) expr ::= expr IS NOT DISTINCT FROM expr */
+   202,  /* (211) expr ::= expr IS DISTINCT FROM expr */
+   202,  /* (212) expr ::= NOT expr */
+   202,  /* (213) expr ::= BITNOT expr */
+   202,  /* (214) expr ::= PLUS|MINUS expr */
+   202,  /* (215) expr ::= expr PTR expr */
+   258,  /* (216) between_op ::= BETWEEN */
+   258,  /* (217) between_op ::= NOT BETWEEN */
+   202,  /* (218) expr ::= expr between_op expr AND expr */
+   259,  /* (219) in_op ::= IN */
+   259,  /* (220) in_op ::= NOT IN */
+   202,  /* (221) expr ::= expr in_op LP exprlist RP */
+   202,  /* (222) expr ::= LP select RP */
+   202,  /* (223) expr ::= expr in_op LP select RP */
+   202,  /* (224) expr ::= expr in_op nm dbnm paren_exprlist */
+   202,  /* (225) expr ::= EXISTS LP select RP */
+   202,  /* (226) expr ::= CASE case_operand case_exprlist case_else END */
+   262,  /* (227) case_exprlist ::= case_exprlist WHEN expr THEN expr */
+   262,  /* (228) case_exprlist ::= WHEN expr THEN expr */
+   263,  /* (229) case_else ::= ELSE expr */
+   263,  /* (230) case_else ::= */
+   261,  /* (231) case_operand ::= */
+   244,  /* (232) exprlist ::= */
+   236,  /* (233) nexprlist ::= nexprlist COMMA expr */
+   236,  /* (234) nexprlist ::= expr */
+   260,  /* (235) paren_exprlist ::= */
+   260,  /* (236) paren_exprlist ::= LP exprlist RP */
+   178,  /* (237) cmd ::= createkw uniqueflag INDEX ifnotexists nm dbnm ON nm LP sortlist RP where_opt */
+   264,  /* (238) uniqueflag ::= UNIQUE */
+   264,  /* (239) uniqueflag ::= */
+   206,  /* (240) eidlist_opt ::= */
+   206,  /* (241) eidlist_opt ::= LP eidlist RP */
+   216,  /* (242) eidlist ::= eidlist COMMA nm collate sortorder */
+   216,  /* (243) eidlist ::= nm collate sortorder */
+   265,  /* (244) collate ::= */
+   265,  /* (245) collate ::= COLLATE ID|STRING */
+   178,  /* (246) cmd ::= DROP INDEX ifexists fullname */
+   178,  /* (247) cmd ::= VACUUM vinto */
+   178,  /* (248) cmd ::= VACUUM nm vinto */
+   266,  /* (249) vinto ::= INTO expr */
+   266,  /* (250) vinto ::= */
+   178,  /* (251) cmd ::= PRAGMA nm dbnm */
+   178,  /* (252) cmd ::= PRAGMA nm dbnm EQ nmnum */
+   178,  /* (253) cmd ::= PRAGMA nm dbnm LP nmnum RP */
+   178,  /* (254) cmd ::= PRAGMA nm dbnm EQ minus_num */
+   178,  /* (255) cmd ::= PRAGMA nm dbnm LP minus_num RP */
+   269,  /* (256) plus_num ::= PLUS INTEGER|FLOAT */
+   268,  /* (257) minus_num ::= MINUS INTEGER|FLOAT */
+   178,  /* (258) cmd ::= createkw trigger_decl BEGIN trigger_cmd_list END */
+   270,  /* (259) trigger_decl ::= temp TRIGGER ifnotexists nm dbnm trigger_time trigger_event ON fullname foreach_clause when_clause */
+   272,  /* (260) trigger_time ::= BEFORE|AFTER */
+   272,  /* (261) trigger_time ::= INSTEAD OF */
+   272,  /* (262) trigger_time ::= */
+   273,  /* (263) trigger_event ::= DELETE|INSERT */
+   273,  /* (264) trigger_event ::= UPDATE */
+   273,  /* (265) trigger_event ::= UPDATE OF idlist */
+   275,  /* (266) when_clause ::= */
+   275,  /* (267) when_clause ::= WHEN expr */
+   271,  /* (268) trigger_cmd_list ::= trigger_cmd_list trigger_cmd SEMI */
+   271,  /* (269) trigger_cmd_list ::= trigger_cmd SEMI */
+   277,  /* (270) trnm ::= nm DOT nm */
+   278,  /* (271) tridxby ::= INDEXED BY nm */
+   278,  /* (272) tridxby ::= NOT INDEXED */
+   276,  /* (273) trigger_cmd ::= UPDATE orconf trnm tridxby SET setlist from where_opt scanpt */
+   276,  /* (274) trigger_cmd ::= scanpt insert_cmd INTO trnm idlist_opt select upsert scanpt */
+   276,  /* (275) trigger_cmd ::= DELETE FROM trnm tridxby where_opt scanpt */
+   276,  /* (276) trigger_cmd ::= scanpt select scanpt */
+   202,  /* (277) expr ::= RAISE LP IGNORE RP */
+   202,  /* (278) expr ::= RAISE LP raisetype COMMA nm RP */
+   279,  /* (279) raisetype ::= ROLLBACK */
+   279,  /* (280) raisetype ::= ABORT */
+   279,  /* (281) raisetype ::= FAIL */
+   178,  /* (282) cmd ::= DROP TRIGGER ifexists fullname */
+   178,  /* (283) cmd ::= ATTACH database_kw_opt expr AS expr key_opt */
+   178,  /* (284) cmd ::= DETACH database_kw_opt expr */
+   281,  /* (285) key_opt ::= */
+   281,  /* (286) key_opt ::= KEY expr */
+   178,  /* (287) cmd ::= REINDEX */
+   178,  /* (288) cmd ::= REINDEX nm dbnm */
+   178,  /* (289) cmd ::= ANALYZE */
+   178,  /* (290) cmd ::= ANALYZE nm dbnm */
+   178,  /* (291) cmd ::= ALTER TABLE fullname RENAME TO nm */
+   178,  /* (292) cmd ::= ALTER TABLE add_column_fullname ADD kwcolumn_opt columnname carglist */
+   178,  /* (293) cmd ::= ALTER TABLE fullname DROP kwcolumn_opt nm */
+   282,  /* (294) add_column_fullname ::= fullname */
+   178,  /* (295) cmd ::= ALTER TABLE fullname RENAME kwcolumn_opt nm TO nm */
+   178,  /* (296) cmd ::= create_vtab */
+   178,  /* (297) cmd ::= create_vtab LP vtabarglist RP */
+   285,  /* (298) create_vtab ::= createkw VIRTUAL TABLE ifnotexists nm dbnm USING nm */
+   287,  /* (299) vtabarg ::= */
+   288,  /* (300) vtabargtoken ::= ANY */
+   288,  /* (301) vtabargtoken ::= lp anylist RP */
+   289,  /* (302) lp ::= LP */
+   249,  /* (303) with ::= WITH wqlist */
+   249,  /* (304) with ::= WITH RECURSIVE wqlist */
+   291,  /* (305) wqas ::= AS */
+   291,  /* (306) wqas ::= AS MATERIALIZED */
+   291,  /* (307) wqas ::= AS NOT MATERIALIZED */
+   292,  /* (308) wqitem ::= nm eidlist_opt wqas LP select RP */
+   222,  /* (309) wqlist ::= wqitem */
+   222,  /* (310) wqlist ::= wqlist COMMA wqitem */
+   293,  /* (311) windowdefn_list ::= windowdefn_list COMMA windowdefn */
+   294,  /* (312) windowdefn ::= nm AS LP window RP */
+   295,  /* (313) window ::= PARTITION BY nexprlist orderby_opt frame_opt */
+   295,  /* (314) window ::= nm PARTITION BY nexprlist orderby_opt frame_opt */
+   295,  /* (315) window ::= ORDER BY sortlist frame_opt */
+   295,  /* (316) window ::= nm ORDER BY sortlist frame_opt */
+   295,  /* (317) window ::= nm frame_opt */
+   296,  /* (318) frame_opt ::= */
+   296,  /* (319) frame_opt ::= range_or_rows frame_bound_s frame_exclude_opt */
+   296,  /* (320) frame_opt ::= range_or_rows BETWEEN frame_bound_s AND frame_bound_e frame_exclude_opt */
+   297,  /* (321) range_or_rows ::= RANGE|ROWS|GROUPS */
+   298,  /* (322) frame_bound_s ::= frame_bound */
+   298,  /* (323) frame_bound_s ::= UNBOUNDED PRECEDING */
+   300,  /* (324) frame_bound_e ::= frame_bound */
+   300,  /* (325) frame_bound_e ::= UNBOUNDED FOLLOWING */
+   301,  /* (326) frame_bound ::= expr PRECEDING|FOLLOWING */
+   301,  /* (327) frame_bound ::= CURRENT ROW */
+   299,  /* (328) frame_exclude_opt ::= */
+   299,  /* (329) frame_exclude_opt ::= EXCLUDE frame_exclude */
+   302,  /* (330) frame_exclude ::= NO OTHERS */
+   302,  /* (331) frame_exclude ::= CURRENT ROW */
+   302,  /* (332) frame_exclude ::= GROUP|TIES */
+   234,  /* (333) window_clause ::= WINDOW windowdefn_list */
+   256,  /* (334) filter_over ::= filter_clause over_clause */
+   256,  /* (335) filter_over ::= over_clause */
+   256,  /* (336) filter_over ::= filter_clause */
+   304,  /* (337) over_clause ::= OVER LP window RP */
+   304,  /* (338) over_clause ::= OVER nm */
+   303,  /* (339) filter_clause ::= FILTER LP WHERE expr RP */
+   305,  /* (340) input ::= cmdlist */
+   306,  /* (341) cmdlist ::= cmdlist ecmd */
+   306,  /* (342) cmdlist ::= ecmd */
+   307,  /* (343) ecmd ::= SEMI */
+   307,  /* (344) ecmd ::= cmdx SEMI */
+   307,  /* (345) ecmd ::= explain cmdx SEMI */
+   180,  /* (346) trans_opt ::= */
+   180,  /* (347) trans_opt ::= TRANSACTION */
+   180,  /* (348) trans_opt ::= TRANSACTION nm */
+   182,  /* (349) savepoint_opt ::= SAVEPOINT */
+   182,  /* (350) savepoint_opt ::= */
+   178,  /* (351) cmd ::= create_table create_table_args */
+   191,  /* (352) table_option_set ::= table_option */
+   189,  /* (353) columnlist ::= columnlist COMMA columnname carglist */
+   189,  /* (354) columnlist ::= columnname carglist */
+   181,  /* (355) nm ::= ID|INDEXED|JOIN_KW */
+   181,  /* (356) nm ::= STRING */
+   195,  /* (357) typetoken ::= typename */
+   196,  /* (358) typename ::= ID|STRING */
+   197,  /* (359) signed ::= plus_num */
+   197,  /* (360) signed ::= minus_num */
+   284,  /* (361) carglist ::= carglist ccons */
+   284,  /* (362) carglist ::= */
+   200,  /* (363) ccons ::= NULL onconf */
+   200,  /* (364) ccons ::= GENERATED ALWAYS AS generated */
+   200,  /* (365) ccons ::= AS generated */
+   190,  /* (366) conslist_opt ::= COMMA conslist */
+   308,  /* (367) conslist ::= conslist tconscomma tcons */
+   308,  /* (368) conslist ::= tcons */
+   213,  /* (369) tconscomma ::= */
+   217,  /* (370) defer_subclause_opt ::= defer_subclause */
+   218,  /* (371) resolvetype ::= raisetype */
+   223,  /* (372) selectnowith ::= oneselect */
+   225,  /* (373) oneselect ::= values */
+   237,  /* (374) sclp ::= selcollist COMMA */
+   238,  /* (375) as ::= ID|STRING */
+   247,  /* (376) indexed_opt ::= indexed_by */
+   255,  /* (377) returning ::= */
+   202,  /* (378) expr ::= term */
+   257,  /* (379) likeop ::= LIKE_KW|MATCH */
+   261,  /* (380) case_operand ::= expr */
+   244,  /* (381) exprlist ::= nexprlist */
+   267,  /* (382) nmnum ::= plus_num */
+   267,  /* (383) nmnum ::= nm */
+   267,  /* (384) nmnum ::= ON */
+   267,  /* (385) nmnum ::= DELETE */
+   267,  /* (386) nmnum ::= DEFAULT */
+   269,  /* (387) plus_num ::= INTEGER|FLOAT */
+   274,  /* (388) foreach_clause ::= */
+   274,  /* (389) foreach_clause ::= FOR EACH ROW */
+   277,  /* (390) trnm ::= nm */
+   278,  /* (391) tridxby ::= */
+   280,  /* (392) database_kw_opt ::= DATABASE */
+   280,  /* (393) database_kw_opt ::= */
+   283,  /* (394) kwcolumn_opt ::= */
+   283,  /* (395) kwcolumn_opt ::= COLUMNKW */
+   286,  /* (396) vtabarglist ::= vtabarg */
+   286,  /* (397) vtabarglist ::= vtabarglist COMMA vtabarg */
+   287,  /* (398) vtabarg ::= vtabarg vtabargtoken */
+   290,  /* (399) anylist ::= */
+   290,  /* (400) anylist ::= anylist LP anylist RP */
+   290,  /* (401) anylist ::= anylist ANY */
+   249,  /* (402) with ::= */
+   293,  /* (403) windowdefn_list ::= windowdefn */
+   295,  /* (404) window ::= frame_opt */
+};
+
+/* For rule J, yyRuleInfoNRhs[J] contains the negative of the number
+** of symbols on the right-hand side of that rule. */
+static const signed char yyRuleInfoNRhs[] = {
+   -1,  /* (0) explain ::= EXPLAIN */
+   -3,  /* (1) explain ::= EXPLAIN QUERY PLAN */
+   -1,  /* (2) cmdx ::= cmd */
+   -3,  /* (3) cmd ::= BEGIN transtype trans_opt */
+    0,  /* (4) transtype ::= */
+   -1,  /* (5) transtype ::= DEFERRED */
+   -1,  /* (6) transtype ::= IMMEDIATE */
+   -1,  /* (7) transtype ::= EXCLUSIVE */
+   -2,  /* (8) cmd ::= COMMIT|END trans_opt */
+   -2,  /* (9) cmd ::= ROLLBACK trans_opt */
+   -2,  /* (10) cmd ::= SAVEPOINT nm */
+   -3,  /* (11) cmd ::= RELEASE savepoint_opt nm */
+   -5,  /* (12) cmd ::= ROLLBACK trans_opt TO savepoint_opt nm */
+   -6,  /* (13) create_table ::= createkw temp TABLE ifnotexists nm dbnm */
+   -1,  /* (14) createkw ::= CREATE */
+    0,  /* (15) ifnotexists ::= */
+   -3,  /* (16) ifnotexists ::= IF NOT EXISTS */
+   -1,  /* (17) temp ::= TEMP */
+    0,  /* (18) temp ::= */
+   -5,  /* (19) create_table_args ::= LP columnlist conslist_opt RP table_option_set */
+   -2,  /* (20) create_table_args ::= AS select */
+    0,  /* (21) table_option_set ::= */
+   -3,  /* (22) table_option_set ::= table_option_set COMMA table_option */
+   -2,  /* (23) table_option ::= WITHOUT nm */
+   -1,  /* (24) table_option ::= nm */
+   -2,  /* (25) columnname ::= nm typetoken */
+    0,  /* (26) typetoken ::= */
+   -4,  /* (27) typetoken ::= typename LP signed RP */
+   -6,  /* (28) typetoken ::= typename LP signed COMMA signed RP */
+   -2,  /* (29) typename ::= typename ID|STRING */
+    0,  /* (30) scanpt ::= */
+    0,  /* (31) scantok ::= */
+   -2,  /* (32) ccons ::= CONSTRAINT nm */
+   -3,  /* (33) ccons ::= DEFAULT scantok term */
+   -4,  /* (34) ccons ::= DEFAULT LP expr RP */
+   -4,  /* (35) ccons ::= DEFAULT PLUS scantok term */
+   -4,  /* (36) ccons ::= DEFAULT MINUS scantok term */
+   -3,  /* (37) ccons ::= DEFAULT scantok ID|INDEXED */
+   -3,  /* (38) ccons ::= NOT NULL onconf */
+   -5,  /* (39) ccons ::= PRIMARY KEY sortorder onconf autoinc */
+   -2,  /* (40) ccons ::= UNIQUE onconf */
+   -4,  /* (41) ccons ::= CHECK LP expr RP */
+   -4,  /* (42) ccons ::= REFERENCES nm eidlist_opt refargs */
+   -1,  /* (43) ccons ::= defer_subclause */
+   -2,  /* (44) ccons ::= COLLATE ID|STRING */
+   -3,  /* (45) generated ::= LP expr RP */
+   -4,  /* (46) generated ::= LP expr RP ID */
+    0,  /* (47) autoinc ::= */
+   -1,  /* (48) autoinc ::= AUTOINCR */
+    0,  /* (49) refargs ::= */
+   -2,  /* (50) refargs ::= refargs refarg */
+   -2,  /* (51) refarg ::= MATCH nm */
+   -3,  /* (52) refarg ::= ON INSERT refact */
+   -3,  /* (53) refarg ::= ON DELETE refact */
+   -3,  /* (54) refarg ::= ON UPDATE refact */
+   -2,  /* (55) refact ::= SET NULL */
+   -2,  /* (56) refact ::= SET DEFAULT */
+   -1,  /* (57) refact ::= CASCADE */
+   -1,  /* (58) refact ::= RESTRICT */
+   -2,  /* (59) refact ::= NO ACTION */
+   -3,  /* (60) defer_subclause ::= NOT DEFERRABLE init_deferred_pred_opt */
+   -2,  /* (61) defer_subclause ::= DEFERRABLE init_deferred_pred_opt */
+    0,  /* (62) init_deferred_pred_opt ::= */
+   -2,  /* (63) init_deferred_pred_opt ::= INITIALLY DEFERRED */
+   -2,  /* (64) init_deferred_pred_opt ::= INITIALLY IMMEDIATE */
+    0,  /* (65) conslist_opt ::= */
+   -1,  /* (66) tconscomma ::= COMMA */
+   -2,  /* (67) tcons ::= CONSTRAINT nm */
+   -7,  /* (68) tcons ::= PRIMARY KEY LP sortlist autoinc RP onconf */
+   -5,  /* (69) tcons ::= UNIQUE LP sortlist RP onconf */
+   -5,  /* (70) tcons ::= CHECK LP expr RP onconf */
+  -10,  /* (71) tcons ::= FOREIGN KEY LP eidlist RP REFERENCES nm eidlist_opt refargs defer_subclause_opt */
+    0,  /* (72) defer_subclause_opt ::= */
+    0,  /* (73) onconf ::= */
+   -3,  /* (74) onconf ::= ON CONFLICT resolvetype */
+    0,  /* (75) orconf ::= */
+   -2,  /* (76) orconf ::= OR resolvetype */
+   -1,  /* (77) resolvetype ::= IGNORE */
+   -1,  /* (78) resolvetype ::= REPLACE */
+   -4,  /* (79) cmd ::= DROP TABLE ifexists fullname */
+   -2,  /* (80) ifexists ::= IF EXISTS */
+    0,  /* (81) ifexists ::= */
+   -9,  /* (82) cmd ::= createkw temp VIEW ifnotexists nm dbnm eidlist_opt AS select */
+   -4,  /* (83) cmd ::= DROP VIEW ifexists fullname */
+   -1,  /* (84) cmd ::= select */
+   -3,  /* (85) select ::= WITH wqlist selectnowith */
+   -4,  /* (86) select ::= WITH RECURSIVE wqlist selectnowith */
+   -1,  /* (87) select ::= selectnowith */
+   -3,  /* (88) selectnowith ::= selectnowith multiselect_op oneselect */
+   -1,  /* (89) multiselect_op ::= UNION */
+   -2,  /* (90) multiselect_op ::= UNION ALL */
+   -1,  /* (91) multiselect_op ::= EXCEPT|INTERSECT */
+   -9,  /* (92) oneselect ::= SELECT distinct selcollist from where_opt groupby_opt having_opt orderby_opt limit_opt */
+  -10,  /* (93) oneselect ::= SELECT distinct selcollist from where_opt groupby_opt having_opt window_clause orderby_opt limit_opt */
+   -4,  /* (94) values ::= VALUES LP nexprlist RP */
+   -5,  /* (95) values ::= values COMMA LP nexprlist RP */
+   -1,  /* (96) distinct ::= DISTINCT */
+   -1,  /* (97) distinct ::= ALL */
+    0,  /* (98) distinct ::= */
+    0,  /* (99) sclp ::= */
+   -5,  /* (100) selcollist ::= sclp scanpt expr scanpt as */
+   -3,  /* (101) selcollist ::= sclp scanpt STAR */
+   -5,  /* (102) selcollist ::= sclp scanpt nm DOT STAR */
+   -2,  /* (103) as ::= AS nm */
+    0,  /* (104) as ::= */
+    0,  /* (105) from ::= */
+   -2,  /* (106) from ::= FROM seltablist */
+   -2,  /* (107) stl_prefix ::= seltablist joinop */
+    0,  /* (108) stl_prefix ::= */
+   -5,  /* (109) seltablist ::= stl_prefix nm dbnm as on_using */
+   -6,  /* (110) seltablist ::= stl_prefix nm dbnm as indexed_by on_using */
+   -8,  /* (111) seltablist ::= stl_prefix nm dbnm LP exprlist RP as on_using */
+   -6,  /* (112) seltablist ::= stl_prefix LP select RP as on_using */
+   -6,  /* (113) seltablist ::= stl_prefix LP seltablist RP as on_using */
+    0,  /* (114) dbnm ::= */
+   -2,  /* (115) dbnm ::= DOT nm */
+   -1,  /* (116) fullname ::= nm */
+   -3,  /* (117) fullname ::= nm DOT nm */
+   -1,  /* (118) xfullname ::= nm */
+   -3,  /* (119) xfullname ::= nm DOT nm */
+   -5,  /* (120) xfullname ::= nm DOT nm AS nm */
+   -3,  /* (121) xfullname ::= nm AS nm */
+   -1,  /* (122) joinop ::= COMMA|JOIN */
+   -2,  /* (123) joinop ::= JOIN_KW JOIN */
+   -3,  /* (124) joinop ::= JOIN_KW nm JOIN */
+   -4,  /* (125) joinop ::= JOIN_KW nm nm JOIN */
+   -2,  /* (126) on_using ::= ON expr */
+   -4,  /* (127) on_using ::= USING LP idlist RP */
+    0,  /* (128) on_using ::= */
+    0,  /* (129) indexed_opt ::= */
+   -3,  /* (130) indexed_by ::= INDEXED BY nm */
+   -2,  /* (131) indexed_by ::= NOT INDEXED */
+    0,  /* (132) orderby_opt ::= */
+   -3,  /* (133) orderby_opt ::= ORDER BY sortlist */
+   -5,  /* (134) sortlist ::= sortlist COMMA expr sortorder nulls */
+   -3,  /* (135) sortlist ::= expr sortorder nulls */
+   -1,  /* (136) sortorder ::= ASC */
+   -1,  /* (137) sortorder ::= DESC */
+    0,  /* (138) sortorder ::= */
+   -2,  /* (139) nulls ::= NULLS FIRST */
+   -2,  /* (140) nulls ::= NULLS LAST */
+    0,  /* (141) nulls ::= */
+    0,  /* (142) groupby_opt ::= */
+   -3,  /* (143) groupby_opt ::= GROUP BY nexprlist */
+    0,  /* (144) having_opt ::= */
+   -2,  /* (145) having_opt ::= HAVING expr */
+    0,  /* (146) limit_opt ::= */
+   -2,  /* (147) limit_opt ::= LIMIT expr */
+   -4,  /* (148) limit_opt ::= LIMIT expr OFFSET expr */
+   -4,  /* (149) limit_opt ::= LIMIT expr COMMA expr */
+   -6,  /* (150) cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret */
+    0,  /* (151) where_opt ::= */
+   -2,  /* (152) where_opt ::= WHERE expr */
+    0,  /* (153) where_opt_ret ::= */
+   -2,  /* (154) where_opt_ret ::= WHERE expr */
+   -2,  /* (155) where_opt_ret ::= RETURNING selcollist */
+   -4,  /* (156) where_opt_ret ::= WHERE expr RETURNING selcollist */
+   -9,  /* (157) cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret */
+   -5,  /* (158) setlist ::= setlist COMMA nm EQ expr */
+   -7,  /* (159) setlist ::= setlist COMMA LP idlist RP EQ expr */
+   -3,  /* (160) setlist ::= nm EQ expr */
+   -5,  /* (161) setlist ::= LP idlist RP EQ expr */
+   -7,  /* (162) cmd ::= with insert_cmd INTO xfullname idlist_opt select upsert */
+   -8,  /* (163) cmd ::= with insert_cmd INTO xfullname idlist_opt DEFAULT VALUES returning */
+    0,  /* (164) upsert ::= */
+   -2,  /* (165) upsert ::= RETURNING selcollist */
+  -12,  /* (166) upsert ::= ON CONFLICT LP sortlist RP where_opt DO UPDATE SET setlist where_opt upsert */
+   -9,  /* (167) upsert ::= ON CONFLICT LP sortlist RP where_opt DO NOTHING upsert */
+   -5,  /* (168) upsert ::= ON CONFLICT DO NOTHING returning */
+   -8,  /* (169) upsert ::= ON CONFLICT DO UPDATE SET setlist where_opt returning */
+   -2,  /* (170) returning ::= RETURNING selcollist */
+   -2,  /* (171) insert_cmd ::= INSERT orconf */
+   -1,  /* (172) insert_cmd ::= REPLACE */
+    0,  /* (173) idlist_opt ::= */
+   -3,  /* (174) idlist_opt ::= LP idlist RP */
+   -3,  /* (175) idlist ::= idlist COMMA nm */
+   -1,  /* (176) idlist ::= nm */
+   -3,  /* (177) expr ::= LP expr RP */
+   -1,  /* (178) expr ::= ID|INDEXED|JOIN_KW */
+   -3,  /* (179) expr ::= nm DOT nm */
+   -5,  /* (180) expr ::= nm DOT nm DOT nm */
+   -1,  /* (181) term ::= NULL|FLOAT|BLOB */
+   -1,  /* (182) term ::= STRING */
+   -1,  /* (183) term ::= INTEGER */
+   -1,  /* (184) expr ::= VARIABLE */
+   -3,  /* (185) expr ::= expr COLLATE ID|STRING */
+   -6,  /* (186) expr ::= CAST LP expr AS typetoken RP */
+   -5,  /* (187) expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist RP */
+   -8,  /* (188) expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist ORDER BY sortlist RP */
+   -4,  /* (189) expr ::= ID|INDEXED|JOIN_KW LP STAR RP */
+   -6,  /* (190) expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist RP filter_over */
+   -9,  /* (191) expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist ORDER BY sortlist RP filter_over */
+   -5,  /* (192) expr ::= ID|INDEXED|JOIN_KW LP STAR RP filter_over */
+   -1,  /* (193) term ::= CTIME_KW */
+   -5,  /* (194) expr ::= LP nexprlist COMMA expr RP */
+   -3,  /* (195) expr ::= expr AND expr */
+   -3,  /* (196) expr ::= expr OR expr */
+   -3,  /* (197) expr ::= expr LT|GT|GE|LE expr */
+   -3,  /* (198) expr ::= expr EQ|NE expr */
+   -3,  /* (199) expr ::= expr BITAND|BITOR|LSHIFT|RSHIFT expr */
+   -3,  /* (200) expr ::= expr PLUS|MINUS expr */
+   -3,  /* (201) expr ::= expr STAR|SLASH|REM expr */
+   -3,  /* (202) expr ::= expr CONCAT expr */
+   -2,  /* (203) likeop ::= NOT LIKE_KW|MATCH */
+   -3,  /* (204) expr ::= expr likeop expr */
+   -5,  /* (205) expr ::= expr likeop expr ESCAPE expr */
+   -2,  /* (206) expr ::= expr ISNULL|NOTNULL */
+   -3,  /* (207) expr ::= expr NOT NULL */
+   -3,  /* (208) expr ::= expr IS expr */
+   -4,  /* (209) expr ::= expr IS NOT expr */
+   -6,  /* (210) expr ::= expr IS NOT DISTINCT FROM expr */
+   -5,  /* (211) expr ::= expr IS DISTINCT FROM expr */
+   -2,  /* (212) expr ::= NOT expr */
+   -2,  /* (213) expr ::= BITNOT expr */
+   -2,  /* (214) expr ::= PLUS|MINUS expr */
+   -3,  /* (215) expr ::= expr PTR expr */
+   -1,  /* (216) between_op ::= BETWEEN */
+   -2,  /* (217) between_op ::= NOT BETWEEN */
+   -5,  /* (218) expr ::= expr between_op expr AND expr */
+   -1,  /* (219) in_op ::= IN */
+   -2,  /* (220) in_op ::= NOT IN */
+   -5,  /* (221) expr ::= expr in_op LP exprlist RP */
+   -3,  /* (222) expr ::= LP select RP */
+   -5,  /* (223) expr ::= expr in_op LP select RP */
+   -5,  /* (224) expr ::= expr in_op nm dbnm paren_exprlist */
+   -4,  /* (225) expr ::= EXISTS LP select RP */
+   -5,  /* (226) expr ::= CASE case_operand case_exprlist case_else END */
+   -5,  /* (227) case_exprlist ::= case_exprlist WHEN expr THEN expr */
+   -4,  /* (228) case_exprlist ::= WHEN expr THEN expr */
+   -2,  /* (229) case_else ::= ELSE expr */
+    0,  /* (230) case_else ::= */
+    0,  /* (231) case_operand ::= */
+    0,  /* (232) exprlist ::= */
+   -3,  /* (233) nexprlist ::= nexprlist COMMA expr */
+   -1,  /* (234) nexprlist ::= expr */
+    0,  /* (235) paren_exprlist ::= */
+   -3,  /* (236) paren_exprlist ::= LP exprlist RP */
+  -12,  /* (237) cmd ::= createkw uniqueflag INDEX ifnotexists nm dbnm ON nm LP sortlist RP where_opt */
+   -1,  /* (238) uniqueflag ::= UNIQUE */
+    0,  /* (239) uniqueflag ::= */
+    0,  /* (240) eidlist_opt ::= */
+   -3,  /* (241) eidlist_opt ::= LP eidlist RP */
+   -5,  /* (242) eidlist ::= eidlist COMMA nm collate sortorder */
+   -3,  /* (243) eidlist ::= nm collate sortorder */
+    0,  /* (244) collate ::= */
+   -2,  /* (245) collate ::= COLLATE ID|STRING */
+   -4,  /* (246) cmd ::= DROP INDEX ifexists fullname */
+   -2,  /* (247) cmd ::= VACUUM vinto */
+   -3,  /* (248) cmd ::= VACUUM nm vinto */
+   -2,  /* (249) vinto ::= INTO expr */
+    0,  /* (250) vinto ::= */
+   -3,  /* (251) cmd ::= PRAGMA nm dbnm */
+   -5,  /* (252) cmd ::= PRAGMA nm dbnm EQ nmnum */
+   -6,  /* (253) cmd ::= PRAGMA nm dbnm LP nmnum RP */
+   -5,  /* (254) cmd ::= PRAGMA nm dbnm EQ minus_num */
+   -6,  /* (255) cmd ::= PRAGMA nm dbnm LP minus_num RP */
+   -2,  /* (256) plus_num ::= PLUS INTEGER|FLOAT */
+   -2,  /* (257) minus_num ::= MINUS INTEGER|FLOAT */
+   -5,  /* (258) cmd ::= createkw trigger_decl BEGIN trigger_cmd_list END */
+  -11,  /* (259) trigger_decl ::= temp TRIGGER ifnotexists nm dbnm trigger_time trigger_event ON fullname foreach_clause when_clause */
+   -1,  /* (260) trigger_time ::= BEFORE|AFTER */
+   -2,  /* (261) trigger_time ::= INSTEAD OF */
+    0,  /* (262) trigger_time ::= */
+   -1,  /* (263) trigger_event ::= DELETE|INSERT */
+   -1,  /* (264) trigger_event ::= UPDATE */
+   -3,  /* (265) trigger_event ::= UPDATE OF idlist */
+    0,  /* (266) when_clause ::= */
+   -2,  /* (267) when_clause ::= WHEN expr */
+   -3,  /* (268) trigger_cmd_list ::= trigger_cmd_list trigger_cmd SEMI */
+   -2,  /* (269) trigger_cmd_list ::= trigger_cmd SEMI */
+   -3,  /* (270) trnm ::= nm DOT nm */
+   -3,  /* (271) tridxby ::= INDEXED BY nm */
+   -2,  /* (272) tridxby ::= NOT INDEXED */
+   -9,  /* (273) trigger_cmd ::= UPDATE orconf trnm tridxby SET setlist from where_opt scanpt */
+   -8,  /* (274) trigger_cmd ::= scanpt insert_cmd INTO trnm idlist_opt select upsert scanpt */
+   -6,  /* (275) trigger_cmd ::= DELETE FROM trnm tridxby where_opt scanpt */
+   -3,  /* (276) trigger_cmd ::= scanpt select scanpt */
+   -4,  /* (277) expr ::= RAISE LP IGNORE RP */
+   -6,  /* (278) expr ::= RAISE LP raisetype COMMA nm RP */
+   -1,  /* (279) raisetype ::= ROLLBACK */
+   -1,  /* (280) raisetype ::= ABORT */
+   -1,  /* (281) raisetype ::= FAIL */
+   -4,  /* (282) cmd ::= DROP TRIGGER ifexists fullname */
+   -6,  /* (283) cmd ::= ATTACH database_kw_opt expr AS expr key_opt */
+   -3,  /* (284) cmd ::= DETACH database_kw_opt expr */
+    0,  /* (285) key_opt ::= */
+   -2,  /* (286) key_opt ::= KEY expr */
+   -1,  /* (287) cmd ::= REINDEX */
+   -3,  /* (288) cmd ::= REINDEX nm dbnm */
+   -1,  /* (289) cmd ::= ANALYZE */
+   -3,  /* (290) cmd ::= ANALYZE nm dbnm */
+   -6,  /* (291) cmd ::= ALTER TABLE fullname RENAME TO nm */
+   -7,  /* (292) cmd ::= ALTER TABLE add_column_fullname ADD kwcolumn_opt columnname carglist */
+   -6,  /* (293) cmd ::= ALTER TABLE fullname DROP kwcolumn_opt nm */
+   -1,  /* (294) add_column_fullname ::= fullname */
+   -8,  /* (295) cmd ::= ALTER TABLE fullname RENAME kwcolumn_opt nm TO nm */
+   -1,  /* (296) cmd ::= create_vtab */
+   -4,  /* (297) cmd ::= create_vtab LP vtabarglist RP */
+   -8,  /* (298) create_vtab ::= createkw VIRTUAL TABLE ifnotexists nm dbnm USING nm */
+    0,  /* (299) vtabarg ::= */
+   -1,  /* (300) vtabargtoken ::= ANY */
+   -3,  /* (301) vtabargtoken ::= lp anylist RP */
+   -1,  /* (302) lp ::= LP */
+   -2,  /* (303) with ::= WITH wqlist */
+   -3,  /* (304) with ::= WITH RECURSIVE wqlist */
+   -1,  /* (305) wqas ::= AS */
+   -2,  /* (306) wqas ::= AS MATERIALIZED */
+   -3,  /* (307) wqas ::= AS NOT MATERIALIZED */
+   -6,  /* (308) wqitem ::= nm eidlist_opt wqas LP select RP */
+   -1,  /* (309) wqlist ::= wqitem */
+   -3,  /* (310) wqlist ::= wqlist COMMA wqitem */
+   -3,  /* (311) windowdefn_list ::= windowdefn_list COMMA windowdefn */
+   -5,  /* (312) windowdefn ::= nm AS LP window RP */
+   -5,  /* (313) window ::= PARTITION BY nexprlist orderby_opt frame_opt */
+   -6,  /* (314) window ::= nm PARTITION BY nexprlist orderby_opt frame_opt */
+   -4,  /* (315) window ::= ORDER BY sortlist frame_opt */
+   -5,  /* (316) window ::= nm ORDER BY sortlist frame_opt */
+   -2,  /* (317) window ::= nm frame_opt */
+    0,  /* (318) frame_opt ::= */
+   -3,  /* (319) frame_opt ::= range_or_rows frame_bound_s frame_exclude_opt */
+   -6,  /* (320) frame_opt ::= range_or_rows BETWEEN frame_bound_s AND frame_bound_e frame_exclude_opt */
+   -1,  /* (321) range_or_rows ::= RANGE|ROWS|GROUPS */
+   -1,  /* (322) frame_bound_s ::= frame_bound */
+   -2,  /* (323) frame_bound_s ::= UNBOUNDED PRECEDING */
+   -1,  /* (324) frame_bound_e ::= frame_bound */
+   -2,  /* (325) frame_bound_e ::= UNBOUNDED FOLLOWING */
+   -2,  /* (326) frame_bound ::= expr PRECEDING|FOLLOWING */
+   -2,  /* (327) frame_bound ::= CURRENT ROW */
+    0,  /* (328) frame_exclude_opt ::= */
+   -2,  /* (329) frame_exclude_opt ::= EXCLUDE frame_exclude */
+   -2,  /* (330) frame_exclude ::= NO OTHERS */
+   -2,  /* (331) frame_exclude ::= CURRENT ROW */
+   -1,  /* (332) frame_exclude ::= GROUP|TIES */
+   -2,  /* (333) window_clause ::= WINDOW windowdefn_list */
+   -2,  /* (334) filter_over ::= filter_clause over_clause */
+   -1,  /* (335) filter_over ::= over_clause */
+   -1,  /* (336) filter_over ::= filter_clause */
+   -4,  /* (337) over_clause ::= OVER LP window RP */
+   -2,  /* (338) over_clause ::= OVER nm */
+   -5,  /* (339) filter_clause ::= FILTER LP WHERE expr RP */
+   -1,  /* (340) input ::= cmdlist */
+   -2,  /* (341) cmdlist ::= cmdlist ecmd */
+   -1,  /* (342) cmdlist ::= ecmd */
+   -1,  /* (343) ecmd ::= SEMI */
+   -2,  /* (344) ecmd ::= cmdx SEMI */
+   -3,  /* (345) ecmd ::= explain cmdx SEMI */
+    0,  /* (346) trans_opt ::= */
+   -1,  /* (347) trans_opt ::= TRANSACTION */
+   -2,  /* (348) trans_opt ::= TRANSACTION nm */
+   -1,  /* (349) savepoint_opt ::= SAVEPOINT */
+    0,  /* (350) savepoint_opt ::= */
+   -2,  /* (351) cmd ::= create_table create_table_args */
+   -1,  /* (352) table_option_set ::= table_option */
+   -4,  /* (353) columnlist ::= columnlist COMMA columnname carglist */
+   -2,  /* (354) columnlist ::= columnname carglist */
+   -1,  /* (355) nm ::= ID|INDEXED|JOIN_KW */
+   -1,  /* (356) nm ::= STRING */
+   -1,  /* (357) typetoken ::= typename */
+   -1,  /* (358) typename ::= ID|STRING */
+   -1,  /* (359) signed ::= plus_num */
+   -1,  /* (360) signed ::= minus_num */
+   -2,  /* (361) carglist ::= carglist ccons */
+    0,  /* (362) carglist ::= */
+   -2,  /* (363) ccons ::= NULL onconf */
+   -4,  /* (364) ccons ::= GENERATED ALWAYS AS generated */
+   -2,  /* (365) ccons ::= AS generated */
+   -2,  /* (366) conslist_opt ::= COMMA conslist */
+   -3,  /* (367) conslist ::= conslist tconscomma tcons */
+   -1,  /* (368) conslist ::= tcons */
+    0,  /* (369) tconscomma ::= */
+   -1,  /* (370) defer_subclause_opt ::= defer_subclause */
+   -1,  /* (371) resolvetype ::= raisetype */
+   -1,  /* (372) selectnowith ::= oneselect */
+   -1,  /* (373) oneselect ::= values */
+   -2,  /* (374) sclp ::= selcollist COMMA */
+   -1,  /* (375) as ::= ID|STRING */
+   -1,  /* (376) indexed_opt ::= indexed_by */
+    0,  /* (377) returning ::= */
+   -1,  /* (378) expr ::= term */
+   -1,  /* (379) likeop ::= LIKE_KW|MATCH */
+   -1,  /* (380) case_operand ::= expr */
+   -1,  /* (381) exprlist ::= nexprlist */
+   -1,  /* (382) nmnum ::= plus_num */
+   -1,  /* (383) nmnum ::= nm */
+   -1,  /* (384) nmnum ::= ON */
+   -1,  /* (385) nmnum ::= DELETE */
+   -1,  /* (386) nmnum ::= DEFAULT */
+   -1,  /* (387) plus_num ::= INTEGER|FLOAT */
+    0,  /* (388) foreach_clause ::= */
+   -3,  /* (389) foreach_clause ::= FOR EACH ROW */
+   -1,  /* (390) trnm ::= nm */
+    0,  /* (391) tridxby ::= */
+   -1,  /* (392) database_kw_opt ::= DATABASE */
+    0,  /* (393) database_kw_opt ::= */
+    0,  /* (394) kwcolumn_opt ::= */
+   -1,  /* (395) kwcolumn_opt ::= COLUMNKW */
+   -1,  /* (396) vtabarglist ::= vtabarg */
+   -3,  /* (397) vtabarglist ::= vtabarglist COMMA vtabarg */
+   -2,  /* (398) vtabarg ::= vtabarg vtabargtoken */
+    0,  /* (399) anylist ::= */
+   -4,  /* (400) anylist ::= anylist LP anylist RP */
+   -2,  /* (401) anylist ::= anylist ANY */
+    0,  /* (402) with ::= */
+   -1,  /* (403) windowdefn_list ::= windowdefn */
+   -1,  /* (404) window ::= frame_opt */
+};
+
+static void yy_accept(yyParser*);  /* Forward Declaration */
+
+/*
+** Perform a reduce action and the shift that must immediately
+** follow the reduce.
+**
+** The yyLookahead and yyLookaheadToken parameters provide reduce actions
+** access to the lookahead token (if any).  The yyLookahead will be YYNOCODE
+** if the lookahead token has already been consumed.  As this procedure is
+** only called from one place, optimizing compilers will in-line it, which
+** means that the extra parameters have no performance impact.
+*/
+static YYACTIONTYPE yy_reduce(
+  yyParser *yypParser,         /* The parser */
+  unsigned int yyruleno,       /* Number of the rule by which to reduce */
+  int yyLookahead,             /* Lookahead token, or YYNOCODE if none */
+  PerfettoSqlParserTOKENTYPE yyLookaheadToken  /* Value of the lookahead token */
+  PerfettoSqlParserCTX_PDECL                   /* %extra_context */
+){
+  int yygoto;                     /* The next state */
+  YYACTIONTYPE yyact;             /* The next action */
+  yyStackEntry *yymsp;            /* The top of the parser's stack */
+  int yysize;                     /* Amount to pop the stack */
+  PerfettoSqlParserARG_FETCH
+  (void)yyLookahead;
+  (void)yyLookaheadToken;
+  yymsp = yypParser->yytos;
+
+  switch( yyruleno ){
+  /* Beginning here are the reduction cases.  A typical example
+  ** follows:
+  **   case 0:
+  **  #line <lineno> <grammarfile>
+  **     { ... }           // User supplied code
+  **  #line <lineno> <thisfile>
+  **     break;
+  */
+/********** Begin reduce actions **********************************************/
+      default:
+      /* (0) explain ::= EXPLAIN */ yytestcase(yyruleno==0);
+      /* (1) explain ::= EXPLAIN QUERY PLAN */ yytestcase(yyruleno==1);
+      /* (2) cmdx ::= cmd (OPTIMIZED OUT) */ assert(yyruleno!=2);
+      /* (3) cmd ::= BEGIN transtype trans_opt */ yytestcase(yyruleno==3);
+      /* (4) transtype ::= */ yytestcase(yyruleno==4);
+      /* (5) transtype ::= DEFERRED */ yytestcase(yyruleno==5);
+      /* (6) transtype ::= IMMEDIATE */ yytestcase(yyruleno==6);
+      /* (7) transtype ::= EXCLUSIVE */ yytestcase(yyruleno==7);
+      /* (8) cmd ::= COMMIT|END trans_opt */ yytestcase(yyruleno==8);
+      /* (9) cmd ::= ROLLBACK trans_opt */ yytestcase(yyruleno==9);
+      /* (10) cmd ::= SAVEPOINT nm */ yytestcase(yyruleno==10);
+      /* (11) cmd ::= RELEASE savepoint_opt nm */ yytestcase(yyruleno==11);
+      /* (12) cmd ::= ROLLBACK trans_opt TO savepoint_opt nm */ yytestcase(yyruleno==12);
+      /* (13) create_table ::= createkw temp TABLE ifnotexists nm dbnm */ yytestcase(yyruleno==13);
+      /* (14) createkw ::= CREATE */ yytestcase(yyruleno==14);
+      /* (15) ifnotexists ::= */ yytestcase(yyruleno==15);
+      /* (16) ifnotexists ::= IF NOT EXISTS */ yytestcase(yyruleno==16);
+      /* (17) temp ::= TEMP */ yytestcase(yyruleno==17);
+      /* (18) temp ::= */ yytestcase(yyruleno==18);
+      /* (19) create_table_args ::= LP columnlist conslist_opt RP table_option_set */ yytestcase(yyruleno==19);
+      /* (20) create_table_args ::= AS select */ yytestcase(yyruleno==20);
+      /* (21) table_option_set ::= */ yytestcase(yyruleno==21);
+      /* (22) table_option_set ::= table_option_set COMMA table_option */ yytestcase(yyruleno==22);
+      /* (23) table_option ::= WITHOUT nm */ yytestcase(yyruleno==23);
+      /* (24) table_option ::= nm (OPTIMIZED OUT) */ assert(yyruleno!=24);
+      /* (25) columnname ::= nm typetoken */ yytestcase(yyruleno==25);
+      /* (26) typetoken ::= */ yytestcase(yyruleno==26);
+      /* (27) typetoken ::= typename LP signed RP */ yytestcase(yyruleno==27);
+      /* (28) typetoken ::= typename LP signed COMMA signed RP */ yytestcase(yyruleno==28);
+      /* (29) typename ::= typename ID|STRING */ yytestcase(yyruleno==29);
+      /* (30) scanpt ::= */ yytestcase(yyruleno==30);
+      /* (31) scantok ::= */ yytestcase(yyruleno==31);
+      /* (32) ccons ::= CONSTRAINT nm */ yytestcase(yyruleno==32);
+      /* (33) ccons ::= DEFAULT scantok term */ yytestcase(yyruleno==33);
+      /* (34) ccons ::= DEFAULT LP expr RP */ yytestcase(yyruleno==34);
+      /* (35) ccons ::= DEFAULT PLUS scantok term */ yytestcase(yyruleno==35);
+      /* (36) ccons ::= DEFAULT MINUS scantok term */ yytestcase(yyruleno==36);
+      /* (37) ccons ::= DEFAULT scantok ID|INDEXED */ yytestcase(yyruleno==37);
+      /* (38) ccons ::= NOT NULL onconf */ yytestcase(yyruleno==38);
+      /* (39) ccons ::= PRIMARY KEY sortorder onconf autoinc */ yytestcase(yyruleno==39);
+      /* (40) ccons ::= UNIQUE onconf */ yytestcase(yyruleno==40);
+      /* (41) ccons ::= CHECK LP expr RP */ yytestcase(yyruleno==41);
+      /* (42) ccons ::= REFERENCES nm eidlist_opt refargs */ yytestcase(yyruleno==42);
+      /* (43) ccons ::= defer_subclause (OPTIMIZED OUT) */ assert(yyruleno!=43);
+      /* (44) ccons ::= COLLATE ID|STRING */ yytestcase(yyruleno==44);
+      /* (45) generated ::= LP expr RP */ yytestcase(yyruleno==45);
+      /* (46) generated ::= LP expr RP ID */ yytestcase(yyruleno==46);
+      /* (47) autoinc ::= */ yytestcase(yyruleno==47);
+      /* (48) autoinc ::= AUTOINCR */ yytestcase(yyruleno==48);
+      /* (49) refargs ::= */ yytestcase(yyruleno==49);
+      /* (50) refargs ::= refargs refarg */ yytestcase(yyruleno==50);
+      /* (51) refarg ::= MATCH nm */ yytestcase(yyruleno==51);
+      /* (52) refarg ::= ON INSERT refact */ yytestcase(yyruleno==52);
+      /* (53) refarg ::= ON DELETE refact */ yytestcase(yyruleno==53);
+      /* (54) refarg ::= ON UPDATE refact */ yytestcase(yyruleno==54);
+      /* (55) refact ::= SET NULL */ yytestcase(yyruleno==55);
+      /* (56) refact ::= SET DEFAULT */ yytestcase(yyruleno==56);
+      /* (57) refact ::= CASCADE */ yytestcase(yyruleno==57);
+      /* (58) refact ::= RESTRICT */ yytestcase(yyruleno==58);
+      /* (59) refact ::= NO ACTION */ yytestcase(yyruleno==59);
+      /* (60) defer_subclause ::= NOT DEFERRABLE init_deferred_pred_opt */ yytestcase(yyruleno==60);
+      /* (61) defer_subclause ::= DEFERRABLE init_deferred_pred_opt */ yytestcase(yyruleno==61);
+      /* (62) init_deferred_pred_opt ::= */ yytestcase(yyruleno==62);
+      /* (63) init_deferred_pred_opt ::= INITIALLY DEFERRED */ yytestcase(yyruleno==63);
+      /* (64) init_deferred_pred_opt ::= INITIALLY IMMEDIATE */ yytestcase(yyruleno==64);
+      /* (65) conslist_opt ::= */ yytestcase(yyruleno==65);
+      /* (66) tconscomma ::= COMMA */ yytestcase(yyruleno==66);
+      /* (67) tcons ::= CONSTRAINT nm */ yytestcase(yyruleno==67);
+      /* (68) tcons ::= PRIMARY KEY LP sortlist autoinc RP onconf */ yytestcase(yyruleno==68);
+      /* (69) tcons ::= UNIQUE LP sortlist RP onconf */ yytestcase(yyruleno==69);
+      /* (70) tcons ::= CHECK LP expr RP onconf */ yytestcase(yyruleno==70);
+      /* (71) tcons ::= FOREIGN KEY LP eidlist RP REFERENCES nm eidlist_opt refargs defer_subclause_opt */ yytestcase(yyruleno==71);
+      /* (72) defer_subclause_opt ::= */ yytestcase(yyruleno==72);
+      /* (73) onconf ::= */ yytestcase(yyruleno==73);
+      /* (74) onconf ::= ON CONFLICT resolvetype */ yytestcase(yyruleno==74);
+      /* (75) orconf ::= */ yytestcase(yyruleno==75);
+      /* (76) orconf ::= OR resolvetype */ yytestcase(yyruleno==76);
+      /* (77) resolvetype ::= IGNORE */ yytestcase(yyruleno==77);
+      /* (78) resolvetype ::= REPLACE */ yytestcase(yyruleno==78);
+      /* (79) cmd ::= DROP TABLE ifexists fullname */ yytestcase(yyruleno==79);
+      /* (80) ifexists ::= IF EXISTS */ yytestcase(yyruleno==80);
+      /* (81) ifexists ::= */ yytestcase(yyruleno==81);
+      /* (82) cmd ::= createkw temp VIEW ifnotexists nm dbnm eidlist_opt AS select */ yytestcase(yyruleno==82);
+      /* (83) cmd ::= DROP VIEW ifexists fullname */ yytestcase(yyruleno==83);
+      /* (84) cmd ::= select (OPTIMIZED OUT) */ assert(yyruleno!=84);
+      /* (85) select ::= WITH wqlist selectnowith */ yytestcase(yyruleno==85);
+      /* (86) select ::= WITH RECURSIVE wqlist selectnowith */ yytestcase(yyruleno==86);
+      /* (87) select ::= selectnowith */ yytestcase(yyruleno==87);
+      /* (88) selectnowith ::= selectnowith multiselect_op oneselect */ yytestcase(yyruleno==88);
+      /* (89) multiselect_op ::= UNION */ yytestcase(yyruleno==89);
+      /* (90) multiselect_op ::= UNION ALL */ yytestcase(yyruleno==90);
+      /* (91) multiselect_op ::= EXCEPT|INTERSECT */ yytestcase(yyruleno==91);
+      /* (92) oneselect ::= SELECT distinct selcollist from where_opt groupby_opt having_opt orderby_opt limit_opt */ yytestcase(yyruleno==92);
+      /* (93) oneselect ::= SELECT distinct selcollist from where_opt groupby_opt having_opt window_clause orderby_opt limit_opt */ yytestcase(yyruleno==93);
+      /* (94) values ::= VALUES LP nexprlist RP */ yytestcase(yyruleno==94);
+      /* (95) values ::= values COMMA LP nexprlist RP */ yytestcase(yyruleno==95);
+      /* (96) distinct ::= DISTINCT */ yytestcase(yyruleno==96);
+      /* (97) distinct ::= ALL */ yytestcase(yyruleno==97);
+      /* (98) distinct ::= */ yytestcase(yyruleno==98);
+      /* (99) sclp ::= */ yytestcase(yyruleno==99);
+      /* (100) selcollist ::= sclp scanpt expr scanpt as */ yytestcase(yyruleno==100);
+      /* (101) selcollist ::= sclp scanpt STAR */ yytestcase(yyruleno==101);
+      /* (102) selcollist ::= sclp scanpt nm DOT STAR */ yytestcase(yyruleno==102);
+      /* (103) as ::= AS nm */ yytestcase(yyruleno==103);
+      /* (104) as ::= */ yytestcase(yyruleno==104);
+      /* (105) from ::= */ yytestcase(yyruleno==105);
+      /* (106) from ::= FROM seltablist */ yytestcase(yyruleno==106);
+      /* (107) stl_prefix ::= seltablist joinop */ yytestcase(yyruleno==107);
+      /* (108) stl_prefix ::= */ yytestcase(yyruleno==108);
+      /* (109) seltablist ::= stl_prefix nm dbnm as on_using */ yytestcase(yyruleno==109);
+      /* (110) seltablist ::= stl_prefix nm dbnm as indexed_by on_using */ yytestcase(yyruleno==110);
+      /* (111) seltablist ::= stl_prefix nm dbnm LP exprlist RP as on_using */ yytestcase(yyruleno==111);
+      /* (112) seltablist ::= stl_prefix LP select RP as on_using */ yytestcase(yyruleno==112);
+      /* (113) seltablist ::= stl_prefix LP seltablist RP as on_using */ yytestcase(yyruleno==113);
+      /* (114) dbnm ::= */ yytestcase(yyruleno==114);
+      /* (115) dbnm ::= DOT nm */ yytestcase(yyruleno==115);
+      /* (116) fullname ::= nm */ yytestcase(yyruleno==116);
+      /* (117) fullname ::= nm DOT nm */ yytestcase(yyruleno==117);
+      /* (118) xfullname ::= nm */ yytestcase(yyruleno==118);
+      /* (119) xfullname ::= nm DOT nm */ yytestcase(yyruleno==119);
+      /* (120) xfullname ::= nm DOT nm AS nm */ yytestcase(yyruleno==120);
+      /* (121) xfullname ::= nm AS nm */ yytestcase(yyruleno==121);
+      /* (122) joinop ::= COMMA|JOIN */ yytestcase(yyruleno==122);
+      /* (123) joinop ::= JOIN_KW JOIN */ yytestcase(yyruleno==123);
+      /* (124) joinop ::= JOIN_KW nm JOIN */ yytestcase(yyruleno==124);
+      /* (125) joinop ::= JOIN_KW nm nm JOIN */ yytestcase(yyruleno==125);
+      /* (126) on_using ::= ON expr */ yytestcase(yyruleno==126);
+      /* (127) on_using ::= USING LP idlist RP */ yytestcase(yyruleno==127);
+      /* (128) on_using ::= */ yytestcase(yyruleno==128);
+      /* (129) indexed_opt ::= */ yytestcase(yyruleno==129);
+      /* (130) indexed_by ::= INDEXED BY nm */ yytestcase(yyruleno==130);
+      /* (131) indexed_by ::= NOT INDEXED */ yytestcase(yyruleno==131);
+      /* (132) orderby_opt ::= */ yytestcase(yyruleno==132);
+      /* (133) orderby_opt ::= ORDER BY sortlist */ yytestcase(yyruleno==133);
+      /* (134) sortlist ::= sortlist COMMA expr sortorder nulls */ yytestcase(yyruleno==134);
+      /* (135) sortlist ::= expr sortorder nulls */ yytestcase(yyruleno==135);
+      /* (136) sortorder ::= ASC */ yytestcase(yyruleno==136);
+      /* (137) sortorder ::= DESC */ yytestcase(yyruleno==137);
+      /* (138) sortorder ::= */ yytestcase(yyruleno==138);
+      /* (139) nulls ::= NULLS FIRST */ yytestcase(yyruleno==139);
+      /* (140) nulls ::= NULLS LAST */ yytestcase(yyruleno==140);
+      /* (141) nulls ::= */ yytestcase(yyruleno==141);
+      /* (142) groupby_opt ::= */ yytestcase(yyruleno==142);
+      /* (143) groupby_opt ::= GROUP BY nexprlist */ yytestcase(yyruleno==143);
+      /* (144) having_opt ::= */ yytestcase(yyruleno==144);
+      /* (145) having_opt ::= HAVING expr */ yytestcase(yyruleno==145);
+      /* (146) limit_opt ::= */ yytestcase(yyruleno==146);
+      /* (147) limit_opt ::= LIMIT expr */ yytestcase(yyruleno==147);
+      /* (148) limit_opt ::= LIMIT expr OFFSET expr */ yytestcase(yyruleno==148);
+      /* (149) limit_opt ::= LIMIT expr COMMA expr */ yytestcase(yyruleno==149);
+      /* (150) cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret */ yytestcase(yyruleno==150);
+      /* (151) where_opt ::= */ yytestcase(yyruleno==151);
+      /* (152) where_opt ::= WHERE expr */ yytestcase(yyruleno==152);
+      /* (153) where_opt_ret ::= */ yytestcase(yyruleno==153);
+      /* (154) where_opt_ret ::= WHERE expr */ yytestcase(yyruleno==154);
+      /* (155) where_opt_ret ::= RETURNING selcollist */ yytestcase(yyruleno==155);
+      /* (156) where_opt_ret ::= WHERE expr RETURNING selcollist */ yytestcase(yyruleno==156);
+      /* (157) cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret */ yytestcase(yyruleno==157);
+      /* (158) setlist ::= setlist COMMA nm EQ expr */ yytestcase(yyruleno==158);
+      /* (159) setlist ::= setlist COMMA LP idlist RP EQ expr */ yytestcase(yyruleno==159);
+      /* (160) setlist ::= nm EQ expr */ yytestcase(yyruleno==160);
+      /* (161) setlist ::= LP idlist RP EQ expr */ yytestcase(yyruleno==161);
+      /* (162) cmd ::= with insert_cmd INTO xfullname idlist_opt select upsert */ yytestcase(yyruleno==162);
+      /* (163) cmd ::= with insert_cmd INTO xfullname idlist_opt DEFAULT VALUES returning */ yytestcase(yyruleno==163);
+      /* (164) upsert ::= */ yytestcase(yyruleno==164);
+      /* (165) upsert ::= RETURNING selcollist */ yytestcase(yyruleno==165);
+      /* (166) upsert ::= ON CONFLICT LP sortlist RP where_opt DO UPDATE SET setlist where_opt upsert */ yytestcase(yyruleno==166);
+      /* (167) upsert ::= ON CONFLICT LP sortlist RP where_opt DO NOTHING upsert */ yytestcase(yyruleno==167);
+      /* (168) upsert ::= ON CONFLICT DO NOTHING returning */ yytestcase(yyruleno==168);
+      /* (169) upsert ::= ON CONFLICT DO UPDATE SET setlist where_opt returning */ yytestcase(yyruleno==169);
+      /* (170) returning ::= RETURNING selcollist */ yytestcase(yyruleno==170);
+      /* (171) insert_cmd ::= INSERT orconf */ yytestcase(yyruleno==171);
+      /* (172) insert_cmd ::= REPLACE */ yytestcase(yyruleno==172);
+      /* (173) idlist_opt ::= */ yytestcase(yyruleno==173);
+      /* (174) idlist_opt ::= LP idlist RP */ yytestcase(yyruleno==174);
+      /* (175) idlist ::= idlist COMMA nm */ yytestcase(yyruleno==175);
+      /* (176) idlist ::= nm (OPTIMIZED OUT) */ assert(yyruleno!=176);
+      /* (177) expr ::= LP expr RP */ yytestcase(yyruleno==177);
+      /* (178) expr ::= ID|INDEXED|JOIN_KW */ yytestcase(yyruleno==178);
+      /* (179) expr ::= nm DOT nm */ yytestcase(yyruleno==179);
+      /* (180) expr ::= nm DOT nm DOT nm */ yytestcase(yyruleno==180);
+      /* (181) term ::= NULL|FLOAT|BLOB */ yytestcase(yyruleno==181);
+      /* (182) term ::= STRING */ yytestcase(yyruleno==182);
+      /* (183) term ::= INTEGER */ yytestcase(yyruleno==183);
+      /* (184) expr ::= VARIABLE */ yytestcase(yyruleno==184);
+      /* (185) expr ::= expr COLLATE ID|STRING */ yytestcase(yyruleno==185);
+      /* (186) expr ::= CAST LP expr AS typetoken RP */ yytestcase(yyruleno==186);
+      /* (187) expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist RP */ yytestcase(yyruleno==187);
+      /* (188) expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist ORDER BY sortlist RP */ yytestcase(yyruleno==188);
+      /* (189) expr ::= ID|INDEXED|JOIN_KW LP STAR RP */ yytestcase(yyruleno==189);
+      /* (190) expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist RP filter_over */ yytestcase(yyruleno==190);
+      /* (191) expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist ORDER BY sortlist RP filter_over */ yytestcase(yyruleno==191);
+      /* (192) expr ::= ID|INDEXED|JOIN_KW LP STAR RP filter_over */ yytestcase(yyruleno==192);
+      /* (193) term ::= CTIME_KW */ yytestcase(yyruleno==193);
+      /* (194) expr ::= LP nexprlist COMMA expr RP */ yytestcase(yyruleno==194);
+      /* (195) expr ::= expr AND expr */ yytestcase(yyruleno==195);
+      /* (196) expr ::= expr OR expr */ yytestcase(yyruleno==196);
+      /* (197) expr ::= expr LT|GT|GE|LE expr */ yytestcase(yyruleno==197);
+      /* (198) expr ::= expr EQ|NE expr */ yytestcase(yyruleno==198);
+      /* (199) expr ::= expr BITAND|BITOR|LSHIFT|RSHIFT expr */ yytestcase(yyruleno==199);
+      /* (200) expr ::= expr PLUS|MINUS expr */ yytestcase(yyruleno==200);
+      /* (201) expr ::= expr STAR|SLASH|REM expr */ yytestcase(yyruleno==201);
+      /* (202) expr ::= expr CONCAT expr */ yytestcase(yyruleno==202);
+      /* (203) likeop ::= NOT LIKE_KW|MATCH */ yytestcase(yyruleno==203);
+      /* (204) expr ::= expr likeop expr */ yytestcase(yyruleno==204);
+      /* (205) expr ::= expr likeop expr ESCAPE expr */ yytestcase(yyruleno==205);
+      /* (206) expr ::= expr ISNULL|NOTNULL */ yytestcase(yyruleno==206);
+      /* (207) expr ::= expr NOT NULL */ yytestcase(yyruleno==207);
+      /* (208) expr ::= expr IS expr */ yytestcase(yyruleno==208);
+      /* (209) expr ::= expr IS NOT expr */ yytestcase(yyruleno==209);
+      /* (210) expr ::= expr IS NOT DISTINCT FROM expr */ yytestcase(yyruleno==210);
+      /* (211) expr ::= expr IS DISTINCT FROM expr */ yytestcase(yyruleno==211);
+      /* (212) expr ::= NOT expr */ yytestcase(yyruleno==212);
+      /* (213) expr ::= BITNOT expr */ yytestcase(yyruleno==213);
+      /* (214) expr ::= PLUS|MINUS expr */ yytestcase(yyruleno==214);
+      /* (215) expr ::= expr PTR expr */ yytestcase(yyruleno==215);
+      /* (216) between_op ::= BETWEEN */ yytestcase(yyruleno==216);
+      /* (217) between_op ::= NOT BETWEEN */ yytestcase(yyruleno==217);
+      /* (218) expr ::= expr between_op expr AND expr */ yytestcase(yyruleno==218);
+      /* (219) in_op ::= IN */ yytestcase(yyruleno==219);
+      /* (220) in_op ::= NOT IN */ yytestcase(yyruleno==220);
+      /* (221) expr ::= expr in_op LP exprlist RP */ yytestcase(yyruleno==221);
+      /* (222) expr ::= LP select RP */ yytestcase(yyruleno==222);
+      /* (223) expr ::= expr in_op LP select RP */ yytestcase(yyruleno==223);
+      /* (224) expr ::= expr in_op nm dbnm paren_exprlist */ yytestcase(yyruleno==224);
+      /* (225) expr ::= EXISTS LP select RP */ yytestcase(yyruleno==225);
+      /* (226) expr ::= CASE case_operand case_exprlist case_else END */ yytestcase(yyruleno==226);
+      /* (227) case_exprlist ::= case_exprlist WHEN expr THEN expr */ yytestcase(yyruleno==227);
+      /* (228) case_exprlist ::= WHEN expr THEN expr */ yytestcase(yyruleno==228);
+      /* (229) case_else ::= ELSE expr */ yytestcase(yyruleno==229);
+      /* (230) case_else ::= */ yytestcase(yyruleno==230);
+      /* (231) case_operand ::= */ yytestcase(yyruleno==231);
+      /* (232) exprlist ::= */ yytestcase(yyruleno==232);
+      /* (233) nexprlist ::= nexprlist COMMA expr */ yytestcase(yyruleno==233);
+      /* (234) nexprlist ::= expr */ yytestcase(yyruleno==234);
+      /* (235) paren_exprlist ::= */ yytestcase(yyruleno==235);
+      /* (236) paren_exprlist ::= LP exprlist RP */ yytestcase(yyruleno==236);
+      /* (237) cmd ::= createkw uniqueflag INDEX ifnotexists nm dbnm ON nm LP sortlist RP where_opt */ yytestcase(yyruleno==237);
+      /* (238) uniqueflag ::= UNIQUE */ yytestcase(yyruleno==238);
+      /* (239) uniqueflag ::= */ yytestcase(yyruleno==239);
+      /* (240) eidlist_opt ::= */ yytestcase(yyruleno==240);
+      /* (241) eidlist_opt ::= LP eidlist RP */ yytestcase(yyruleno==241);
+      /* (242) eidlist ::= eidlist COMMA nm collate sortorder */ yytestcase(yyruleno==242);
+      /* (243) eidlist ::= nm collate sortorder */ yytestcase(yyruleno==243);
+      /* (244) collate ::= */ yytestcase(yyruleno==244);
+      /* (245) collate ::= COLLATE ID|STRING */ yytestcase(yyruleno==245);
+      /* (246) cmd ::= DROP INDEX ifexists fullname */ yytestcase(yyruleno==246);
+      /* (247) cmd ::= VACUUM vinto */ yytestcase(yyruleno==247);
+      /* (248) cmd ::= VACUUM nm vinto */ yytestcase(yyruleno==248);
+      /* (249) vinto ::= INTO expr */ yytestcase(yyruleno==249);
+      /* (250) vinto ::= */ yytestcase(yyruleno==250);
+      /* (251) cmd ::= PRAGMA nm dbnm */ yytestcase(yyruleno==251);
+      /* (252) cmd ::= PRAGMA nm dbnm EQ nmnum */ yytestcase(yyruleno==252);
+      /* (253) cmd ::= PRAGMA nm dbnm LP nmnum RP */ yytestcase(yyruleno==253);
+      /* (254) cmd ::= PRAGMA nm dbnm EQ minus_num */ yytestcase(yyruleno==254);
+      /* (255) cmd ::= PRAGMA nm dbnm LP minus_num RP */ yytestcase(yyruleno==255);
+      /* (256) plus_num ::= PLUS INTEGER|FLOAT */ yytestcase(yyruleno==256);
+      /* (257) minus_num ::= MINUS INTEGER|FLOAT */ yytestcase(yyruleno==257);
+      /* (258) cmd ::= createkw trigger_decl BEGIN trigger_cmd_list END */ yytestcase(yyruleno==258);
+      /* (259) trigger_decl ::= temp TRIGGER ifnotexists nm dbnm trigger_time trigger_event ON fullname foreach_clause when_clause */ yytestcase(yyruleno==259);
+      /* (260) trigger_time ::= BEFORE|AFTER */ yytestcase(yyruleno==260);
+      /* (261) trigger_time ::= INSTEAD OF */ yytestcase(yyruleno==261);
+      /* (262) trigger_time ::= */ yytestcase(yyruleno==262);
+      /* (263) trigger_event ::= DELETE|INSERT */ yytestcase(yyruleno==263);
+      /* (264) trigger_event ::= UPDATE */ yytestcase(yyruleno==264);
+      /* (265) trigger_event ::= UPDATE OF idlist */ yytestcase(yyruleno==265);
+      /* (266) when_clause ::= */ yytestcase(yyruleno==266);
+      /* (267) when_clause ::= WHEN expr */ yytestcase(yyruleno==267);
+      /* (268) trigger_cmd_list ::= trigger_cmd_list trigger_cmd SEMI */ yytestcase(yyruleno==268);
+      /* (269) trigger_cmd_list ::= trigger_cmd SEMI */ yytestcase(yyruleno==269);
+      /* (270) trnm ::= nm DOT nm */ yytestcase(yyruleno==270);
+      /* (271) tridxby ::= INDEXED BY nm */ yytestcase(yyruleno==271);
+      /* (272) tridxby ::= NOT INDEXED */ yytestcase(yyruleno==272);
+      /* (273) trigger_cmd ::= UPDATE orconf trnm tridxby SET setlist from where_opt scanpt */ yytestcase(yyruleno==273);
+      /* (274) trigger_cmd ::= scanpt insert_cmd INTO trnm idlist_opt select upsert scanpt */ yytestcase(yyruleno==274);
+      /* (275) trigger_cmd ::= DELETE FROM trnm tridxby where_opt scanpt */ yytestcase(yyruleno==275);
+      /* (276) trigger_cmd ::= scanpt select scanpt */ yytestcase(yyruleno==276);
+      /* (277) expr ::= RAISE LP IGNORE RP */ yytestcase(yyruleno==277);
+      /* (278) expr ::= RAISE LP raisetype COMMA nm RP */ yytestcase(yyruleno==278);
+      /* (279) raisetype ::= ROLLBACK */ yytestcase(yyruleno==279);
+      /* (280) raisetype ::= ABORT */ yytestcase(yyruleno==280);
+      /* (281) raisetype ::= FAIL */ yytestcase(yyruleno==281);
+      /* (282) cmd ::= DROP TRIGGER ifexists fullname */ yytestcase(yyruleno==282);
+      /* (283) cmd ::= ATTACH database_kw_opt expr AS expr key_opt */ yytestcase(yyruleno==283);
+      /* (284) cmd ::= DETACH database_kw_opt expr */ yytestcase(yyruleno==284);
+      /* (285) key_opt ::= */ yytestcase(yyruleno==285);
+      /* (286) key_opt ::= KEY expr */ yytestcase(yyruleno==286);
+      /* (287) cmd ::= REINDEX */ yytestcase(yyruleno==287);
+      /* (288) cmd ::= REINDEX nm dbnm */ yytestcase(yyruleno==288);
+      /* (289) cmd ::= ANALYZE */ yytestcase(yyruleno==289);
+      /* (290) cmd ::= ANALYZE nm dbnm */ yytestcase(yyruleno==290);
+      /* (291) cmd ::= ALTER TABLE fullname RENAME TO nm */ yytestcase(yyruleno==291);
+      /* (292) cmd ::= ALTER TABLE add_column_fullname ADD kwcolumn_opt columnname carglist */ yytestcase(yyruleno==292);
+      /* (293) cmd ::= ALTER TABLE fullname DROP kwcolumn_opt nm */ yytestcase(yyruleno==293);
+      /* (294) add_column_fullname ::= fullname */ yytestcase(yyruleno==294);
+      /* (295) cmd ::= ALTER TABLE fullname RENAME kwcolumn_opt nm TO nm */ yytestcase(yyruleno==295);
+      /* (296) cmd ::= create_vtab */ yytestcase(yyruleno==296);
+      /* (297) cmd ::= create_vtab LP vtabarglist RP */ yytestcase(yyruleno==297);
+      /* (298) create_vtab ::= createkw VIRTUAL TABLE ifnotexists nm dbnm USING nm */ yytestcase(yyruleno==298);
+      /* (299) vtabarg ::= */ yytestcase(yyruleno==299);
+      /* (300) vtabargtoken ::= ANY */ yytestcase(yyruleno==300);
+      /* (301) vtabargtoken ::= lp anylist RP */ yytestcase(yyruleno==301);
+      /* (302) lp ::= LP */ yytestcase(yyruleno==302);
+      /* (303) with ::= WITH wqlist */ yytestcase(yyruleno==303);
+      /* (304) with ::= WITH RECURSIVE wqlist */ yytestcase(yyruleno==304);
+      /* (305) wqas ::= AS */ yytestcase(yyruleno==305);
+      /* (306) wqas ::= AS MATERIALIZED */ yytestcase(yyruleno==306);
+      /* (307) wqas ::= AS NOT MATERIALIZED */ yytestcase(yyruleno==307);
+      /* (308) wqitem ::= nm eidlist_opt wqas LP select RP */ yytestcase(yyruleno==308);
+      /* (309) wqlist ::= wqitem (OPTIMIZED OUT) */ assert(yyruleno!=309);
+      /* (310) wqlist ::= wqlist COMMA wqitem */ yytestcase(yyruleno==310);
+      /* (311) windowdefn_list ::= windowdefn_list COMMA windowdefn */ yytestcase(yyruleno==311);
+      /* (312) windowdefn ::= nm AS LP window RP */ yytestcase(yyruleno==312);
+      /* (313) window ::= PARTITION BY nexprlist orderby_opt frame_opt */ yytestcase(yyruleno==313);
+      /* (314) window ::= nm PARTITION BY nexprlist orderby_opt frame_opt */ yytestcase(yyruleno==314);
+      /* (315) window ::= ORDER BY sortlist frame_opt */ yytestcase(yyruleno==315);
+      /* (316) window ::= nm ORDER BY sortlist frame_opt */ yytestcase(yyruleno==316);
+      /* (317) window ::= nm frame_opt */ yytestcase(yyruleno==317);
+      /* (318) frame_opt ::= */ yytestcase(yyruleno==318);
+      /* (319) frame_opt ::= range_or_rows frame_bound_s frame_exclude_opt */ yytestcase(yyruleno==319);
+      /* (320) frame_opt ::= range_or_rows BETWEEN frame_bound_s AND frame_bound_e frame_exclude_opt */ yytestcase(yyruleno==320);
+      /* (321) range_or_rows ::= RANGE|ROWS|GROUPS */ yytestcase(yyruleno==321);
+      /* (322) frame_bound_s ::= frame_bound (OPTIMIZED OUT) */ assert(yyruleno!=322);
+      /* (323) frame_bound_s ::= UNBOUNDED PRECEDING */ yytestcase(yyruleno==323);
+      /* (324) frame_bound_e ::= frame_bound (OPTIMIZED OUT) */ assert(yyruleno!=324);
+      /* (325) frame_bound_e ::= UNBOUNDED FOLLOWING */ yytestcase(yyruleno==325);
+      /* (326) frame_bound ::= expr PRECEDING|FOLLOWING */ yytestcase(yyruleno==326);
+      /* (327) frame_bound ::= CURRENT ROW */ yytestcase(yyruleno==327);
+      /* (328) frame_exclude_opt ::= */ yytestcase(yyruleno==328);
+      /* (329) frame_exclude_opt ::= EXCLUDE frame_exclude */ yytestcase(yyruleno==329);
+      /* (330) frame_exclude ::= NO OTHERS */ yytestcase(yyruleno==330);
+      /* (331) frame_exclude ::= CURRENT ROW */ yytestcase(yyruleno==331);
+      /* (332) frame_exclude ::= GROUP|TIES */ yytestcase(yyruleno==332);
+      /* (333) window_clause ::= WINDOW windowdefn_list */ yytestcase(yyruleno==333);
+      /* (334) filter_over ::= filter_clause over_clause */ yytestcase(yyruleno==334);
+      /* (335) filter_over ::= over_clause (OPTIMIZED OUT) */ assert(yyruleno!=335);
+      /* (336) filter_over ::= filter_clause */ yytestcase(yyruleno==336);
+      /* (337) over_clause ::= OVER LP window RP */ yytestcase(yyruleno==337);
+      /* (338) over_clause ::= OVER nm */ yytestcase(yyruleno==338);
+      /* (339) filter_clause ::= FILTER LP WHERE expr RP */ yytestcase(yyruleno==339);
+      /* (340) input ::= cmdlist */ yytestcase(yyruleno==340);
+      /* (341) cmdlist ::= cmdlist ecmd */ yytestcase(yyruleno==341);
+      /* (342) cmdlist ::= ecmd (OPTIMIZED OUT) */ assert(yyruleno!=342);
+      /* (343) ecmd ::= SEMI */ yytestcase(yyruleno==343);
+      /* (344) ecmd ::= cmdx SEMI */ yytestcase(yyruleno==344);
+      /* (345) ecmd ::= explain cmdx SEMI */ yytestcase(yyruleno==345);
+      /* (346) trans_opt ::= */ yytestcase(yyruleno==346);
+      /* (347) trans_opt ::= TRANSACTION */ yytestcase(yyruleno==347);
+      /* (348) trans_opt ::= TRANSACTION nm */ yytestcase(yyruleno==348);
+      /* (349) savepoint_opt ::= SAVEPOINT */ yytestcase(yyruleno==349);
+      /* (350) savepoint_opt ::= */ yytestcase(yyruleno==350);
+      /* (351) cmd ::= create_table create_table_args */ yytestcase(yyruleno==351);
+      /* (352) table_option_set ::= table_option (OPTIMIZED OUT) */ assert(yyruleno!=352);
+      /* (353) columnlist ::= columnlist COMMA columnname carglist */ yytestcase(yyruleno==353);
+      /* (354) columnlist ::= columnname carglist */ yytestcase(yyruleno==354);
+      /* (355) nm ::= ID|INDEXED|JOIN_KW */ yytestcase(yyruleno==355);
+      /* (356) nm ::= STRING */ yytestcase(yyruleno==356);
+      /* (357) typetoken ::= typename */ yytestcase(yyruleno==357);
+      /* (358) typename ::= ID|STRING */ yytestcase(yyruleno==358);
+      /* (359) signed ::= plus_num (OPTIMIZED OUT) */ assert(yyruleno!=359);
+      /* (360) signed ::= minus_num (OPTIMIZED OUT) */ assert(yyruleno!=360);
+      /* (361) carglist ::= carglist ccons */ yytestcase(yyruleno==361);
+      /* (362) carglist ::= */ yytestcase(yyruleno==362);
+      /* (363) ccons ::= NULL onconf */ yytestcase(yyruleno==363);
+      /* (364) ccons ::= GENERATED ALWAYS AS generated */ yytestcase(yyruleno==364);
+      /* (365) ccons ::= AS generated */ yytestcase(yyruleno==365);
+      /* (366) conslist_opt ::= COMMA conslist */ yytestcase(yyruleno==366);
+      /* (367) conslist ::= conslist tconscomma tcons */ yytestcase(yyruleno==367);
+      /* (368) conslist ::= tcons (OPTIMIZED OUT) */ assert(yyruleno!=368);
+      /* (369) tconscomma ::= */ yytestcase(yyruleno==369);
+      /* (370) defer_subclause_opt ::= defer_subclause (OPTIMIZED OUT) */ assert(yyruleno!=370);
+      /* (371) resolvetype ::= raisetype (OPTIMIZED OUT) */ assert(yyruleno!=371);
+      /* (372) selectnowith ::= oneselect (OPTIMIZED OUT) */ assert(yyruleno!=372);
+      /* (373) oneselect ::= values */ yytestcase(yyruleno==373);
+      /* (374) sclp ::= selcollist COMMA */ yytestcase(yyruleno==374);
+      /* (375) as ::= ID|STRING */ yytestcase(yyruleno==375);
+      /* (376) indexed_opt ::= indexed_by (OPTIMIZED OUT) */ assert(yyruleno!=376);
+      /* (377) returning ::= */ yytestcase(yyruleno==377);
+      /* (378) expr ::= term (OPTIMIZED OUT) */ assert(yyruleno!=378);
+      /* (379) likeop ::= LIKE_KW|MATCH */ yytestcase(yyruleno==379);
+      /* (380) case_operand ::= expr */ yytestcase(yyruleno==380);
+      /* (381) exprlist ::= nexprlist */ yytestcase(yyruleno==381);
+      /* (382) nmnum ::= plus_num (OPTIMIZED OUT) */ assert(yyruleno!=382);
+      /* (383) nmnum ::= nm (OPTIMIZED OUT) */ assert(yyruleno!=383);
+      /* (384) nmnum ::= ON */ yytestcase(yyruleno==384);
+      /* (385) nmnum ::= DELETE */ yytestcase(yyruleno==385);
+      /* (386) nmnum ::= DEFAULT */ yytestcase(yyruleno==386);
+      /* (387) plus_num ::= INTEGER|FLOAT */ yytestcase(yyruleno==387);
+      /* (388) foreach_clause ::= */ yytestcase(yyruleno==388);
+      /* (389) foreach_clause ::= FOR EACH ROW */ yytestcase(yyruleno==389);
+      /* (390) trnm ::= nm */ yytestcase(yyruleno==390);
+      /* (391) tridxby ::= */ yytestcase(yyruleno==391);
+      /* (392) database_kw_opt ::= DATABASE */ yytestcase(yyruleno==392);
+      /* (393) database_kw_opt ::= */ yytestcase(yyruleno==393);
+      /* (394) kwcolumn_opt ::= */ yytestcase(yyruleno==394);
+      /* (395) kwcolumn_opt ::= COLUMNKW */ yytestcase(yyruleno==395);
+      /* (396) vtabarglist ::= vtabarg */ yytestcase(yyruleno==396);
+      /* (397) vtabarglist ::= vtabarglist COMMA vtabarg */ yytestcase(yyruleno==397);
+      /* (398) vtabarg ::= vtabarg vtabargtoken */ yytestcase(yyruleno==398);
+      /* (399) anylist ::= */ yytestcase(yyruleno==399);
+      /* (400) anylist ::= anylist LP anylist RP */ yytestcase(yyruleno==400);
+      /* (401) anylist ::= anylist ANY */ yytestcase(yyruleno==401);
+      /* (402) with ::= */ yytestcase(yyruleno==402);
+      /* (403) windowdefn_list ::= windowdefn (OPTIMIZED OUT) */ assert(yyruleno!=403);
+      /* (404) window ::= frame_opt (OPTIMIZED OUT) */ assert(yyruleno!=404);
+        break;
+/********** End reduce actions ************************************************/
+  };
+  assert( yyruleno<sizeof(yyRuleInfoLhs)/sizeof(yyRuleInfoLhs[0]) );
+  yygoto = yyRuleInfoLhs[yyruleno];
+  yysize = yyRuleInfoNRhs[yyruleno];
+  yyact = yy_find_reduce_action(yymsp[yysize].stateno,(YYCODETYPE)yygoto);
+
+  /* There are no SHIFTREDUCE actions on nonterminals because the table
+  ** generator has simplified them to pure REDUCE actions. */
+  assert( !(yyact>YY_MAX_SHIFT && yyact<=YY_MAX_SHIFTREDUCE) );
+
+  /* It is not possible for a REDUCE to be followed by an error */
+  assert( yyact!=YY_ERROR_ACTION );
+
+  yymsp += yysize+1;
+  yypParser->yytos = yymsp;
+  yymsp->stateno = (YYACTIONTYPE)yyact;
+  yymsp->major = (YYCODETYPE)yygoto;
+  yyTraceShift(yypParser, yyact, "... then shift");
+  return yyact;
+}
+
+/*
+** The following code executes when the parse fails
+*/
+#ifndef YYNOERRORRECOVERY
+static void yy_parse_failed(
+  yyParser *yypParser           /* The parser */
+){
+  PerfettoSqlParserARG_FETCH
+  PerfettoSqlParserCTX_FETCH
+#ifndef NDEBUG
+  if( yyTraceFILE ){
+    fprintf(yyTraceFILE,"%sFail!\n",yyTracePrompt);
+  }
+#endif
+  while( yypParser->yytos>yypParser->yystack ) yy_pop_parser_stack(yypParser);
+  /* Here code is inserted which will be executed whenever the
+  ** parser fails */
+/************ Begin %parse_failure code ***************************************/
+/************ End %parse_failure code *****************************************/
+  PerfettoSqlParserARG_STORE /* Suppress warning about unused %extra_argument variable */
+  PerfettoSqlParserCTX_STORE
+}
+#endif /* YYNOERRORRECOVERY */
+
+/*
+** The following code executes when a syntax error first occurs.
+*/
+static void yy_syntax_error(
+  yyParser *yypParser,           /* The parser */
+  int yymajor,                   /* The major type of the error token */
+  PerfettoSqlParserTOKENTYPE yyminor         /* The minor type of the error token */
+){
+  PerfettoSqlParserARG_FETCH
+  PerfettoSqlParserCTX_FETCH
+#define TOKEN yyminor
+/************ Begin %syntax_error code ****************************************/
+/************ End %syntax_error code ******************************************/
+  PerfettoSqlParserARG_STORE /* Suppress warning about unused %extra_argument variable */
+  PerfettoSqlParserCTX_STORE
+}
+
+/*
+** The following is executed when the parser accepts
+*/
+static void yy_accept(
+  yyParser *yypParser           /* The parser */
+){
+  PerfettoSqlParserARG_FETCH
+  PerfettoSqlParserCTX_FETCH
+#ifndef NDEBUG
+  if( yyTraceFILE ){
+    fprintf(yyTraceFILE,"%sAccept!\n",yyTracePrompt);
+  }
+#endif
+#ifndef YYNOERRORRECOVERY
+  yypParser->yyerrcnt = -1;
+#endif
+  assert( yypParser->yytos==yypParser->yystack );
+  /* Here code is inserted which will be executed whenever the
+  ** parser accepts */
+/*********** Begin %parse_accept code *****************************************/
+/*********** End %parse_accept code *******************************************/
+  PerfettoSqlParserARG_STORE /* Suppress warning about unused %extra_argument variable */
+  PerfettoSqlParserCTX_STORE
+}
+
+/* The main parser program.
+** The first argument is a pointer to a structure obtained from
+** "PerfettoSqlParserAlloc" which describes the current state of the parser.
+** The second argument is the major token number.  The third is
+** the minor token.  The fourth optional argument is whatever the
+** user wants (and specified in the grammar) and is available for
+** use by the action routines.
+**
+** Inputs:
+** <ul>
+** <li> A pointer to the parser (an opaque structure.)
+** <li> The major token number.
+** <li> The minor token number.
+** <li> An option argument of a grammar-specified type.
+** </ul>
+**
+** Outputs:
+** None.
+*/
+void PerfettoSqlParser(
+  void *yyp,                   /* The parser */
+  int yymajor,                 /* The major token code number */
+  PerfettoSqlParserTOKENTYPE yyminor       /* The value for the token */
+  PerfettoSqlParserARG_PDECL               /* Optional %extra_argument parameter */
+){
+  YYMINORTYPE yyminorunion;
+  YYACTIONTYPE yyact;   /* The parser action. */
+#if !defined(YYERRORSYMBOL) && !defined(YYNOERRORRECOVERY)
+  int yyendofinput;     /* True if we are at the end of input */
+#endif
+#ifdef YYERRORSYMBOL
+  int yyerrorhit = 0;   /* True if yymajor has invoked an error */
+#endif
+  yyParser *yypParser = (yyParser*)yyp;  /* The parser */
+  PerfettoSqlParserCTX_FETCH
+  PerfettoSqlParserARG_STORE
+
+  assert( yypParser->yytos!=0 );
+#if !defined(YYERRORSYMBOL) && !defined(YYNOERRORRECOVERY)
+  yyendofinput = (yymajor==0);
+#endif
+
+  yyact = yypParser->yytos->stateno;
+#ifndef NDEBUG
+  if( yyTraceFILE ){
+    if( yyact < YY_MIN_REDUCE ){
+      fprintf(yyTraceFILE,"%sInput '%s' in state %d\n",
+              yyTracePrompt,yyTokenName[yymajor],yyact);
+    }else{
+      fprintf(yyTraceFILE,"%sInput '%s' with pending reduce %d\n",
+              yyTracePrompt,yyTokenName[yymajor],yyact-YY_MIN_REDUCE);
+    }
+  }
+#endif
+
+  while(1){ /* Exit by "break" */
+    assert( yypParser->yytos>=yypParser->yystack );
+    assert( yyact==yypParser->yytos->stateno );
+    yyact = yy_find_shift_action((YYCODETYPE)yymajor,yyact);
+    if( yyact >= YY_MIN_REDUCE ){
+      unsigned int yyruleno = yyact - YY_MIN_REDUCE; /* Reduce by this rule */
+#ifndef NDEBUG
+      assert( yyruleno<(int)(sizeof(yyRuleName)/sizeof(yyRuleName[0])) );
+      if( yyTraceFILE ){
+        int yysize = yyRuleInfoNRhs[yyruleno];
+        if( yysize ){
+          fprintf(yyTraceFILE, "%sReduce %d [%s]%s, pop back to state %d.\n",
+            yyTracePrompt,
+            yyruleno, yyRuleName[yyruleno],
+            yyruleno<YYNRULE_WITH_ACTION ? "" : " without external action",
+            yypParser->yytos[yysize].stateno);
+        }else{
+          fprintf(yyTraceFILE, "%sReduce %d [%s]%s.\n",
+            yyTracePrompt, yyruleno, yyRuleName[yyruleno],
+            yyruleno<YYNRULE_WITH_ACTION ? "" : " without external action");
+        }
+      }
+#endif /* NDEBUG */
+
+      /* Check that the stack is large enough to grow by a single entry
+      ** if the RHS of the rule is empty.  This ensures that there is room
+      ** enough on the stack to push the LHS value */
+      if( yyRuleInfoNRhs[yyruleno]==0 ){
+#ifdef YYTRACKMAXSTACKDEPTH
+        if( (int)(yypParser->yytos - yypParser->yystack)>yypParser->yyhwm ){
+          yypParser->yyhwm++;
+          assert( yypParser->yyhwm ==
+                  (int)(yypParser->yytos - yypParser->yystack));
+        }
+#endif
+#if YYSTACKDEPTH>0 
+        if( yypParser->yytos>=yypParser->yystackEnd ){
+          yyStackOverflow(yypParser);
+          break;
+        }
+#else
+        if( yypParser->yytos>=&yypParser->yystack[yypParser->yystksz-1] ){
+          if( yyGrowStack(yypParser) ){
+            yyStackOverflow(yypParser);
+            break;
+          }
+        }
+#endif
+      }
+      yyact = yy_reduce(yypParser,yyruleno,yymajor,yyminor PerfettoSqlParserCTX_PARAM);
+    }else if( yyact <= YY_MAX_SHIFTREDUCE ){
+      yy_shift(yypParser,yyact,(YYCODETYPE)yymajor,yyminor);
+#ifndef YYNOERRORRECOVERY
+      yypParser->yyerrcnt--;
+#endif
+      break;
+    }else if( yyact==YY_ACCEPT_ACTION ){
+      yypParser->yytos--;
+      yy_accept(yypParser);
+      return;
+    }else{
+      assert( yyact == YY_ERROR_ACTION );
+      yyminorunion.yy0 = yyminor;
+#ifdef YYERRORSYMBOL
+      int yymx;
+#endif
+#ifndef NDEBUG
+      if( yyTraceFILE ){
+        fprintf(yyTraceFILE,"%sSyntax Error!\n",yyTracePrompt);
+      }
+#endif
+#ifdef YYERRORSYMBOL
+      /* A syntax error has occurred.
+      ** The response to an error depends upon whether or not the
+      ** grammar defines an error token "ERROR".  
+      **
+      ** This is what we do if the grammar does define ERROR:
+      **
+      **  * Call the %syntax_error function.
+      **
+      **  * Begin popping the stack until we enter a state where
+      **    it is legal to shift the error symbol, then shift
+      **    the error symbol.
+      **
+      **  * Set the error count to three.
+      **
+      **  * Begin accepting and shifting new tokens.  No new error
+      **    processing will occur until three tokens have been
+      **    shifted successfully.
+      **
+      */
+      if( yypParser->yyerrcnt<0 ){
+        yy_syntax_error(yypParser,yymajor,yyminor);
+      }
+      yymx = yypParser->yytos->major;
+      if( yymx==YYERRORSYMBOL || yyerrorhit ){
+#ifndef NDEBUG
+        if( yyTraceFILE ){
+          fprintf(yyTraceFILE,"%sDiscard input token %s\n",
+             yyTracePrompt,yyTokenName[yymajor]);
+        }
+#endif
+        yy_destructor(yypParser, (YYCODETYPE)yymajor, &yyminorunion);
+        yymajor = YYNOCODE;
+      }else{
+        while( yypParser->yytos > yypParser->yystack ){
+          yyact = yy_find_reduce_action(yypParser->yytos->stateno,
+                                        YYERRORSYMBOL);
+          if( yyact<=YY_MAX_SHIFTREDUCE ) break;
+          yy_pop_parser_stack(yypParser);
+        }
+        if( yypParser->yytos <= yypParser->yystack || yymajor==0 ){
+          yy_destructor(yypParser,(YYCODETYPE)yymajor,&yyminorunion);
+          yy_parse_failed(yypParser);
+#ifndef YYNOERRORRECOVERY
+          yypParser->yyerrcnt = -1;
+#endif
+          yymajor = YYNOCODE;
+        }else if( yymx!=YYERRORSYMBOL ){
+          yy_shift(yypParser,yyact,YYERRORSYMBOL,yyminor);
+        }
+      }
+      yypParser->yyerrcnt = 3;
+      yyerrorhit = 1;
+      if( yymajor==YYNOCODE ) break;
+      yyact = yypParser->yytos->stateno;
+#elif defined(YYNOERRORRECOVERY)
+      /* If the YYNOERRORRECOVERY macro is defined, then do not attempt to
+      ** do any kind of error recovery.  Instead, simply invoke the syntax
+      ** error routine and continue going as if nothing had happened.
+      **
+      ** Applications can set this macro (for example inside %include) if
+      ** they intend to abandon the parse upon the first syntax error seen.
+      */
+      yy_syntax_error(yypParser,yymajor, yyminor);
+      yy_destructor(yypParser,(YYCODETYPE)yymajor,&yyminorunion);
+      break;
+#else  /* YYERRORSYMBOL is not defined */
+      /* This is what we do if the grammar does not define ERROR:
+      **
+      **  * Report an error message, and throw away the input token.
+      **
+      **  * If the input token is $, then fail the parse.
+      **
+      ** As before, subsequent error messages are suppressed until
+      ** three input tokens have been successfully shifted.
+      */
+      if( yypParser->yyerrcnt<=0 ){
+        yy_syntax_error(yypParser,yymajor, yyminor);
+      }
+      yypParser->yyerrcnt = 3;
+      yy_destructor(yypParser,(YYCODETYPE)yymajor,&yyminorunion);
+      if( yyendofinput ){
+        yy_parse_failed(yypParser);
+#ifndef YYNOERRORRECOVERY
+        yypParser->yyerrcnt = -1;
+#endif
+      }
+      break;
+#endif
+    }
+  }
+#ifndef NDEBUG
+  if( yyTraceFILE ){
+    yyStackEntry *i;
+    char cDiv = '[';
+    fprintf(yyTraceFILE,"%sReturn. Stack=",yyTracePrompt);
+    for(i=&yypParser->yystack[1]; i<=yypParser->yytos; i++){
+      fprintf(yyTraceFILE,"%c%s", cDiv, yyTokenName[i->major]);
+      cDiv = ' ';
+    }
+    fprintf(yyTraceFILE,"]\n");
+  }
+#endif
+  return;
+}
+
+/*
+** Return the fallback token corresponding to canonical token iToken, or
+** 0 if iToken has no fallback.
+*/
+int PerfettoSqlParserFallback(int iToken){
+#ifdef YYFALLBACK
+  assert( iToken<(int)(sizeof(yyFallback)/sizeof(yyFallback[0])) );
+  return yyFallback[iToken];
+#else
+  (void)iToken;
+  return 0;
+#endif
+}
diff --git a/src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.h b/src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.h
new file mode 100644
index 0000000..3d7555f
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.h
@@ -0,0 +1,175 @@
+#define TK_CREATE 1
+#define TK_REPLACE 2
+#define TK_PERFETTO 3
+#define TK_MACRO 4
+#define TK_INCLUDE 5
+#define TK_MODULE 6
+#define TK_RETURNS 7
+#define TK_FUNCTION 8
+#define TK_OR 9
+#define TK_AND 10
+#define TK_NOT 11
+#define TK_IS 12
+#define TK_MATCH 13
+#define TK_LIKE_KW 14
+#define TK_BETWEEN 15
+#define TK_IN 16
+#define TK_ISNULL 17
+#define TK_NOTNULL 18
+#define TK_NE 19
+#define TK_EQ 20
+#define TK_GT 21
+#define TK_LE 22
+#define TK_LT 23
+#define TK_GE 24
+#define TK_ESCAPE 25
+#define TK_BITAND 26
+#define TK_BITOR 27
+#define TK_LSHIFT 28
+#define TK_RSHIFT 29
+#define TK_PLUS 30
+#define TK_MINUS 31
+#define TK_STAR 32
+#define TK_SLASH 33
+#define TK_REM 34
+#define TK_CONCAT 35
+#define TK_PTR 36
+#define TK_COLLATE 37
+#define TK_BITNOT 38
+#define TK_ON 39
+#define TK_ID 40
+#define TK_ABORT 41
+#define TK_ACTION 42
+#define TK_AFTER 43
+#define TK_ANALYZE 44
+#define TK_ASC 45
+#define TK_ATTACH 46
+#define TK_BEFORE 47
+#define TK_BEGIN 48
+#define TK_BY 49
+#define TK_CASCADE 50
+#define TK_CAST 51
+#define TK_COLUMNKW 52
+#define TK_CONFLICT 53
+#define TK_DATABASE 54
+#define TK_DEFERRED 55
+#define TK_DESC 56
+#define TK_DETACH 57
+#define TK_DO 58
+#define TK_EACH 59
+#define TK_END 60
+#define TK_EXCLUSIVE 61
+#define TK_EXPLAIN 62
+#define TK_FAIL 63
+#define TK_FOR 64
+#define TK_IGNORE 65
+#define TK_IMMEDIATE 66
+#define TK_INITIALLY 67
+#define TK_INSTEAD 68
+#define TK_NO 69
+#define TK_PLAN 70
+#define TK_QUERY 71
+#define TK_KEY 72
+#define TK_OF 73
+#define TK_OFFSET 74
+#define TK_PRAGMA 75
+#define TK_RAISE 76
+#define TK_RECURSIVE 77
+#define TK_RELEASE 78
+#define TK_RESTRICT 79
+#define TK_ROW 80
+#define TK_ROWS 81
+#define TK_ROLLBACK 82
+#define TK_SAVEPOINT 83
+#define TK_TEMP 84
+#define TK_TRIGGER 85
+#define TK_VACUUM 86
+#define TK_VIEW 87
+#define TK_VIRTUAL 88
+#define TK_WITH 89
+#define TK_WITHOUT 90
+#define TK_NULLS 91
+#define TK_FIRST 92
+#define TK_LAST 93
+#define TK_EXCEPT 94
+#define TK_INTERSECT 95
+#define TK_UNION 96
+#define TK_CURRENT 97
+#define TK_FOLLOWING 98
+#define TK_PARTITION 99
+#define TK_PRECEDING 100
+#define TK_RANGE 101
+#define TK_UNBOUNDED 102
+#define TK_EXCLUDE 103
+#define TK_GROUPS 104
+#define TK_OTHERS 105
+#define TK_TIES 106
+#define TK_WITHIN 107
+#define TK_GENERATED 108
+#define TK_ALWAYS 109
+#define TK_MATERIALIZED 110
+#define TK_REINDEX 111
+#define TK_RENAME 112
+#define TK_CTIME_KW 113
+#define TK_IF 114
+#define TK_ANY 115
+#define TK_COMMIT 116
+#define TK_TO 117
+#define TK_TABLE 118
+#define TK_EXISTS 119
+#define TK_LP 120
+#define TK_RP 121
+#define TK_AS 122
+#define TK_COMMA 123
+#define TK_STRING 124
+#define TK_CONSTRAINT 125
+#define TK_DEFAULT 126
+#define TK_INDEXED 127
+#define TK_NULL 128
+#define TK_PRIMARY 129
+#define TK_UNIQUE 130
+#define TK_CHECK 131
+#define TK_REFERENCES 132
+#define TK_AUTOINCR 133
+#define TK_INSERT 134
+#define TK_DELETE 135
+#define TK_UPDATE 136
+#define TK_SET 137
+#define TK_DEFERRABLE 138
+#define TK_FOREIGN 139
+#define TK_DROP 140
+#define TK_ALL 141
+#define TK_SELECT 142
+#define TK_VALUES 143
+#define TK_DISTINCT 144
+#define TK_DOT 145
+#define TK_FROM 146
+#define TK_JOIN 147
+#define TK_JOIN_KW 148
+#define TK_USING 149
+#define TK_ORDER 150
+#define TK_GROUP 151
+#define TK_HAVING 152
+#define TK_LIMIT 153
+#define TK_WHERE 154
+#define TK_RETURNING 155
+#define TK_INTO 156
+#define TK_NOTHING 157
+#define TK_FLOAT 158
+#define TK_BLOB 159
+#define TK_INTEGER 160
+#define TK_VARIABLE 161
+#define TK_CASE 162
+#define TK_WHEN 163
+#define TK_THEN 164
+#define TK_ELSE 165
+#define TK_INDEX 166
+#define TK_SEMI 167
+#define TK_ALTER 168
+#define TK_ADD 169
+#define TK_WINDOW 170
+#define TK_OVER 171
+#define TK_FILTER 172
+#define TK_TRANSACTION 173
+#define TK_SPACE 174
+#define TK_ILLEGAL 175
diff --git a/src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.y b/src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.y
new file mode 100644
index 0000000..a7e5924
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.y
@@ -0,0 +1,613 @@
+%name PerfettoSqlParser
+%token_prefix TK_
+%start_symbol input
+
+%include {
+#include <stddef.h>
+
+#define YYNOERRORRECOVERY 1
+}
+
+%token CREATE REPLACE PERFETTO MACRO INCLUDE MODULE RETURNS FUNCTION.
+
+%left OR.
+%left AND.
+%right NOT.
+%left IS MATCH LIKE_KW BETWEEN IN ISNULL NOTNULL NE EQ.
+%left GT LE LT GE.
+%right ESCAPE.
+%left BITAND BITOR LSHIFT RSHIFT.
+%left PLUS MINUS.
+%left STAR SLASH REM.
+%left CONCAT PTR.
+%left COLLATE.
+%right BITNOT.
+%nonassoc ON.
+
+%fallback ID
+  ABORT ACTION AFTER ANALYZE ASC ATTACH BEFORE BEGIN BY CASCADE CAST COLUMNKW
+  CONFLICT DATABASE DEFERRED DESC DETACH DO
+  EACH END EXCLUSIVE EXPLAIN FAIL FOR
+  IGNORE IMMEDIATE INITIALLY INSTEAD LIKE_KW MATCH NO PLAN
+  QUERY KEY OF OFFSET PRAGMA RAISE RECURSIVE RELEASE REPLACE RESTRICT ROW ROWS
+  ROLLBACK SAVEPOINT TEMP TRIGGER VACUUM VIEW VIRTUAL WITH WITHOUT
+  NULLS FIRST LAST
+  EXCEPT INTERSECT UNION
+  CURRENT FOLLOWING PARTITION PRECEDING RANGE UNBOUNDED
+  EXCLUDE GROUPS OTHERS TIES
+  WITHIN
+  GENERATED ALWAYS
+  MATERIALIZED
+  REINDEX RENAME CTIME_KW IF
+  .
+%wildcard ANY.
+
+// Reprint of input file "buildtools/sqlite_src/src/parse.y".
+// Symbols:
+//   0 $                      160 ELSE                  
+//   1 SEMI                   161 INDEX                 
+//   2 EXPLAIN                162 ALTER                 
+//   3 QUERY                  163 ADD                   
+//   4 PLAN                   164 WINDOW                
+//   5 BEGIN                  165 OVER                  
+//   6 TRANSACTION            166 FILTER                
+//   7 DEFERRED               167 COLUMN                
+//   8 IMMEDIATE              168 AGG_FUNCTION          
+//   9 EXCLUSIVE              169 AGG_COLUMN            
+//  10 COMMIT                 170 TRUEFALSE             
+//  11 END                    171 ISNOT                 
+//  12 ROLLBACK               172 FUNCTION              
+//  13 SAVEPOINT              173 UMINUS                
+//  14 RELEASE                174 UPLUS                 
+//  15 TO                     175 TRUTH                 
+//  16 TABLE                  176 REGISTER              
+//  17 CREATE                 177 VECTOR                
+//  18 IF                     178 SELECT_COLUMN         
+//  19 NOT                    179 IF_NULL_ROW           
+//  20 EXISTS                 180 ASTERISK              
+//  21 TEMP                   181 SPAN                  
+//  22 LP                     182 ERROR                 
+//  23 RP                     183 SPACE                 
+//  24 AS                     184 ILLEGAL               
+//  25 COMMA                  185 input                 
+//  26 WITHOUT                186 cmdlist               
+//  27 ABORT                  187 ecmd                  
+//  28 ACTION                 188 cmdx                  
+//  29 AFTER                  189 explain               
+//  30 ANALYZE                190 cmd                   
+//  31 ASC                    191 transtype             
+//  32 ATTACH                 192 trans_opt             
+//  33 BEFORE                 193 nm                    
+//  34 BY                     194 savepoint_opt         
+//  35 CASCADE                195 create_table          
+//  36 CAST                   196 create_table_args     
+//  37 CONFLICT               197 createkw              
+//  38 DATABASE               198 temp                  
+//  39 DESC                   199 ifnotexists           
+//  40 DETACH                 200 dbnm                  
+//  41 EACH                   201 columnlist            
+//  42 FAIL                   202 conslist_opt          
+//  43 OR                     203 table_option_set      
+//  44 AND                    204 select                
+//  45 IS                     205 table_option          
+//  46 MATCH                  206 columnname            
+//  47 LIKE_KW                207 carglist              
+//  48 BETWEEN                208 typetoken             
+//  49 IN                     209 typename              
+//  50 ISNULL                 210 signed                
+//  51 NOTNULL                211 plus_num              
+//  52 NE                     212 minus_num             
+//  53 EQ                     213 scanpt                
+//  54 GT                     214 scantok               
+//  55 LE                     215 ccons                 
+//  56 LT                     216 term                  
+//  57 GE                     217 expr                  
+//  58 ESCAPE                 218 onconf                
+//  59 ID                     219 sortorder             
+//  60 COLUMNKW               220 autoinc               
+//  61 DO                     221 eidlist_opt           
+//  62 FOR                    222 refargs               
+//  63 IGNORE                 223 defer_subclause       
+//  64 INITIALLY              224 generated             
+//  65 INSTEAD                225 refarg                
+//  66 NO                     226 refact                
+//  67 KEY                    227 init_deferred_pred_opt
+//  68 OF                     228 conslist              
+//  69 OFFSET                 229 tconscomma            
+//  70 PRAGMA                 230 tcons                 
+//  71 RAISE                  231 sortlist              
+//  72 RECURSIVE              232 eidlist               
+//  73 REPLACE                233 defer_subclause_opt   
+//  74 RESTRICT               234 orconf                
+//  75 ROW                    235 resolvetype           
+//  76 ROWS                   236 raisetype             
+//  77 TRIGGER                237 ifexists              
+//  78 VACUUM                 238 fullname              
+//  79 VIEW                   239 selectnowith          
+//  80 VIRTUAL                240 oneselect             
+//  81 WITH                   241 wqlist                
+//  82 NULLS                  242 multiselect_op        
+//  83 FIRST                  243 distinct              
+//  84 LAST                   244 selcollist            
+//  85 CURRENT                245 from                  
+//  86 FOLLOWING              246 where_opt             
+//  87 PARTITION              247 groupby_opt           
+//  88 PRECEDING              248 having_opt            
+//  89 RANGE                  249 orderby_opt           
+//  90 UNBOUNDED              250 limit_opt             
+//  91 EXCLUDE                251 window_clause         
+//  92 GROUPS                 252 values                
+//  93 OTHERS                 253 nexprlist             
+//  94 TIES                   254 sclp                  
+//  95 GENERATED              255 as                    
+//  96 ALWAYS                 256 seltablist            
+//  97 MATERIALIZED           257 stl_prefix            
+//  98 REINDEX                258 joinop                
+//  99 RENAME                 259 on_using              
+// 100 CTIME_KW               260 indexed_by            
+// 101 ANY                    261 exprlist              
+// 102 BITAND                 262 xfullname             
+// 103 BITOR                  263 idlist                
+// 104 LSHIFT                 264 indexed_opt           
+// 105 RSHIFT                 265 nulls                 
+// 106 PLUS                   266 with                  
+// 107 MINUS                  267 where_opt_ret         
+// 108 STAR                   268 setlist               
+// 109 SLASH                  269 insert_cmd            
+// 110 REM                    270 idlist_opt            
+// 111 CONCAT                 271 upsert                
+// 112 PTR                    272 returning             
+// 113 COLLATE                273 filter_over           
+// 114 BITNOT                 274 likeop                
+// 115 ON                     275 between_op            
+// 116 INDEXED                276 in_op                 
+// 117 STRING                 277 paren_exprlist        
+// 118 JOIN_KW                278 case_operand          
+// 119 CONSTRAINT             279 case_exprlist         
+// 120 DEFAULT                280 case_else             
+// 121 NULL                   281 uniqueflag            
+// 122 PRIMARY                282 collate               
+// 123 UNIQUE                 283 vinto                 
+// 124 CHECK                  284 nmnum                 
+// 125 REFERENCES             285 trigger_decl          
+// 126 AUTOINCR               286 trigger_cmd_list      
+// 127 INSERT                 287 trigger_time          
+// 128 DELETE                 288 trigger_event         
+// 129 UPDATE                 289 foreach_clause        
+// 130 SET                    290 when_clause           
+// 131 DEFERRABLE             291 trigger_cmd           
+// 132 FOREIGN                292 trnm                  
+// 133 DROP                   293 tridxby               
+// 134 UNION                  294 database_kw_opt       
+// 135 ALL                    295 key_opt               
+// 136 EXCEPT                 296 add_column_fullname   
+// 137 INTERSECT              297 kwcolumn_opt          
+// 138 SELECT                 298 create_vtab           
+// 139 VALUES                 299 vtabarglist           
+// 140 DISTINCT               300 vtabarg               
+// 141 DOT                    301 vtabargtoken          
+// 142 FROM                   302 lp                    
+// 143 JOIN                   303 anylist               
+// 144 USING                  304 wqitem                
+// 145 ORDER                  305 wqas                  
+// 146 GROUP                  306 windowdefn_list       
+// 147 HAVING                 307 windowdefn            
+// 148 LIMIT                  308 window                
+// 149 WHERE                  309 frame_opt             
+// 150 RETURNING              310 part_opt              
+// 151 INTO                   311 filter_clause         
+// 152 NOTHING                312 over_clause           
+// 153 FLOAT                  313 range_or_rows         
+// 154 BLOB                   314 frame_bound           
+// 155 INTEGER                315 frame_bound_s         
+// 156 VARIABLE               316 frame_bound_e         
+// 157 CASE                   317 frame_exclude_opt     
+// 158 WHEN                   318 frame_exclude         
+// 159 THEN                  
+explain ::= EXPLAIN.
+explain ::= EXPLAIN QUERY PLAN.
+cmdx ::= cmd.
+cmd ::= BEGIN transtype trans_opt.
+transtype ::=.
+transtype ::= DEFERRED.
+transtype ::= IMMEDIATE.
+transtype ::= EXCLUSIVE.
+cmd ::= COMMIT|END trans_opt.
+cmd ::= ROLLBACK trans_opt.
+cmd ::= SAVEPOINT nm.
+cmd ::= RELEASE savepoint_opt nm.
+cmd ::= ROLLBACK trans_opt TO savepoint_opt nm.
+create_table ::= createkw temp TABLE ifnotexists nm dbnm.
+createkw ::= CREATE.
+ifnotexists ::=.
+ifnotexists ::= IF NOT EXISTS.
+temp ::= TEMP.
+temp ::=.
+create_table_args ::= LP columnlist conslist_opt RP table_option_set.
+create_table_args ::= AS select.
+table_option_set ::=.
+table_option_set ::= table_option_set COMMA table_option.
+table_option ::= WITHOUT nm.
+table_option ::= nm.
+columnname ::= nm typetoken.
+typetoken ::=.
+typetoken ::= typename LP signed RP.
+typetoken ::= typename LP signed COMMA signed RP.
+typename ::= typename ID|STRING.
+scanpt ::=.
+scantok ::=.
+ccons ::= CONSTRAINT nm.
+ccons ::= DEFAULT scantok term.
+ccons ::= DEFAULT LP expr RP.
+ccons ::= DEFAULT PLUS scantok term.
+ccons ::= DEFAULT MINUS scantok term.
+ccons ::= DEFAULT scantok ID|INDEXED.
+ccons ::= NOT NULL onconf.
+ccons ::= PRIMARY KEY sortorder onconf autoinc.
+ccons ::= UNIQUE onconf.
+ccons ::= CHECK LP expr RP.
+ccons ::= REFERENCES nm eidlist_opt refargs.
+ccons ::= defer_subclause.
+ccons ::= COLLATE ID|STRING.
+generated ::= LP expr RP.
+generated ::= LP expr RP ID.
+autoinc ::=.
+autoinc ::= AUTOINCR.
+refargs ::=.
+refargs ::= refargs refarg.
+refarg ::= MATCH nm.
+refarg ::= ON INSERT refact.
+refarg ::= ON DELETE refact.
+refarg ::= ON UPDATE refact.
+refact ::= SET NULL.
+refact ::= SET DEFAULT.
+refact ::= CASCADE.
+refact ::= RESTRICT.
+refact ::= NO ACTION.
+defer_subclause ::= NOT DEFERRABLE init_deferred_pred_opt.
+defer_subclause ::= DEFERRABLE init_deferred_pred_opt.
+init_deferred_pred_opt ::=.
+init_deferred_pred_opt ::= INITIALLY DEFERRED.
+init_deferred_pred_opt ::= INITIALLY IMMEDIATE.
+conslist_opt ::=.
+tconscomma ::= COMMA.
+tcons ::= CONSTRAINT nm.
+tcons ::= PRIMARY KEY LP sortlist autoinc RP onconf.
+tcons ::= UNIQUE LP sortlist RP onconf.
+tcons ::= CHECK LP expr RP onconf.
+tcons ::= FOREIGN KEY LP eidlist RP REFERENCES nm eidlist_opt refargs defer_subclause_opt.
+defer_subclause_opt ::=.
+onconf ::=.
+onconf ::= ON CONFLICT resolvetype.
+orconf ::=.
+orconf ::= OR resolvetype.
+resolvetype ::= IGNORE.
+resolvetype ::= REPLACE.
+cmd ::= DROP TABLE ifexists fullname.
+ifexists ::= IF EXISTS.
+ifexists ::=.
+cmd ::= createkw temp VIEW ifnotexists nm dbnm eidlist_opt AS select.
+cmd ::= DROP VIEW ifexists fullname.
+cmd ::= select.
+select ::= WITH wqlist selectnowith.
+select ::= WITH RECURSIVE wqlist selectnowith.
+select ::= selectnowith.
+selectnowith ::= selectnowith multiselect_op oneselect.
+multiselect_op ::= UNION.
+multiselect_op ::= UNION ALL.
+multiselect_op ::= EXCEPT|INTERSECT.
+oneselect ::= SELECT distinct selcollist from where_opt groupby_opt having_opt orderby_opt limit_opt.
+oneselect ::= SELECT distinct selcollist from where_opt groupby_opt having_opt window_clause orderby_opt limit_opt.
+values ::= VALUES LP nexprlist RP.
+values ::= values COMMA LP nexprlist RP.
+distinct ::= DISTINCT.
+distinct ::= ALL.
+distinct ::=.
+sclp ::=.
+selcollist ::= sclp scanpt expr scanpt as.
+selcollist ::= sclp scanpt STAR.
+selcollist ::= sclp scanpt nm DOT STAR.
+as ::= AS nm.
+as ::=.
+from ::=.
+from ::= FROM seltablist.
+stl_prefix ::= seltablist joinop.
+stl_prefix ::=.
+seltablist ::= stl_prefix nm dbnm as on_using.
+seltablist ::= stl_prefix nm dbnm as indexed_by on_using.
+seltablist ::= stl_prefix nm dbnm LP exprlist RP as on_using.
+seltablist ::= stl_prefix LP select RP as on_using.
+seltablist ::= stl_prefix LP seltablist RP as on_using.
+dbnm ::=.
+dbnm ::= DOT nm.
+fullname ::= nm.
+fullname ::= nm DOT nm.
+xfullname ::= nm.
+xfullname ::= nm DOT nm.
+xfullname ::= nm DOT nm AS nm.
+xfullname ::= nm AS nm.
+joinop ::= COMMA|JOIN.
+joinop ::= JOIN_KW JOIN.
+joinop ::= JOIN_KW nm JOIN.
+joinop ::= JOIN_KW nm nm JOIN.
+on_using ::= ON expr.
+on_using ::= USING LP idlist RP.
+on_using ::=. [OR]
+indexed_opt ::=.
+indexed_by ::= INDEXED BY nm.
+indexed_by ::= NOT INDEXED.
+orderby_opt ::=.
+orderby_opt ::= ORDER BY sortlist.
+sortlist ::= sortlist COMMA expr sortorder nulls.
+sortlist ::= expr sortorder nulls.
+sortorder ::= ASC.
+sortorder ::= DESC.
+sortorder ::=.
+nulls ::= NULLS FIRST.
+nulls ::= NULLS LAST.
+nulls ::=.
+groupby_opt ::=.
+groupby_opt ::= GROUP BY nexprlist.
+having_opt ::=.
+having_opt ::= HAVING expr.
+limit_opt ::=.
+limit_opt ::= LIMIT expr.
+limit_opt ::= LIMIT expr OFFSET expr.
+limit_opt ::= LIMIT expr COMMA expr.
+cmd ::= with DELETE FROM xfullname indexed_opt where_opt_ret.
+where_opt ::=.
+where_opt ::= WHERE expr.
+where_opt_ret ::=.
+where_opt_ret ::= WHERE expr.
+where_opt_ret ::= RETURNING selcollist.
+where_opt_ret ::= WHERE expr RETURNING selcollist.
+cmd ::= with UPDATE orconf xfullname indexed_opt SET setlist from where_opt_ret.
+setlist ::= setlist COMMA nm EQ expr.
+setlist ::= setlist COMMA LP idlist RP EQ expr.
+setlist ::= nm EQ expr.
+setlist ::= LP idlist RP EQ expr.
+cmd ::= with insert_cmd INTO xfullname idlist_opt select upsert.
+cmd ::= with insert_cmd INTO xfullname idlist_opt DEFAULT VALUES returning.
+upsert ::=.
+upsert ::= RETURNING selcollist.
+upsert ::= ON CONFLICT LP sortlist RP where_opt DO UPDATE SET setlist where_opt upsert.
+upsert ::= ON CONFLICT LP sortlist RP where_opt DO NOTHING upsert.
+upsert ::= ON CONFLICT DO NOTHING returning.
+upsert ::= ON CONFLICT DO UPDATE SET setlist where_opt returning.
+returning ::= RETURNING selcollist.
+insert_cmd ::= INSERT orconf.
+insert_cmd ::= REPLACE.
+idlist_opt ::=.
+idlist_opt ::= LP idlist RP.
+idlist ::= idlist COMMA nm.
+idlist ::= nm.
+expr ::= LP expr RP.
+expr ::= ID|INDEXED|JOIN_KW.
+expr ::= nm DOT nm.
+expr ::= nm DOT nm DOT nm.
+term ::= NULL|FLOAT|BLOB.
+term ::= STRING.
+term ::= INTEGER.
+expr ::= VARIABLE.
+expr ::= expr COLLATE ID|STRING.
+expr ::= CAST LP expr AS typetoken RP.
+expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist RP.
+expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist ORDER BY sortlist RP.
+expr ::= ID|INDEXED|JOIN_KW LP STAR RP.
+expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist RP filter_over.
+expr ::= ID|INDEXED|JOIN_KW LP distinct exprlist ORDER BY sortlist RP filter_over.
+expr ::= ID|INDEXED|JOIN_KW LP STAR RP filter_over.
+term ::= CTIME_KW.
+expr ::= LP nexprlist COMMA expr RP.
+expr ::= expr AND expr.
+expr ::= expr OR expr.
+expr ::= expr LT|GT|GE|LE expr.
+expr ::= expr EQ|NE expr.
+expr ::= expr BITAND|BITOR|LSHIFT|RSHIFT expr.
+expr ::= expr PLUS|MINUS expr.
+expr ::= expr STAR|SLASH|REM expr.
+expr ::= expr CONCAT expr.
+likeop ::= NOT LIKE_KW|MATCH.
+expr ::= expr likeop expr. [LIKE_KW]
+expr ::= expr likeop expr ESCAPE expr. [LIKE_KW]
+expr ::= expr ISNULL|NOTNULL.
+expr ::= expr NOT NULL.
+expr ::= expr IS expr.
+expr ::= expr IS NOT expr.
+expr ::= expr IS NOT DISTINCT FROM expr.
+expr ::= expr IS DISTINCT FROM expr.
+expr ::= NOT expr.
+expr ::= BITNOT expr.
+expr ::= PLUS|MINUS expr. [BITNOT]
+expr ::= expr PTR expr.
+between_op ::= BETWEEN.
+between_op ::= NOT BETWEEN.
+expr ::= expr between_op expr AND expr. [BETWEEN]
+in_op ::= IN.
+in_op ::= NOT IN.
+expr ::= expr in_op LP exprlist RP. [IN]
+expr ::= LP select RP.
+expr ::= expr in_op LP select RP. [IN]
+expr ::= expr in_op nm dbnm paren_exprlist. [IN]
+expr ::= EXISTS LP select RP.
+expr ::= CASE case_operand case_exprlist case_else END.
+case_exprlist ::= case_exprlist WHEN expr THEN expr.
+case_exprlist ::= WHEN expr THEN expr.
+case_else ::= ELSE expr.
+case_else ::=.
+case_operand ::=.
+exprlist ::=.
+nexprlist ::= nexprlist COMMA expr.
+nexprlist ::= expr.
+paren_exprlist ::=.
+paren_exprlist ::= LP exprlist RP.
+cmd ::= createkw uniqueflag INDEX ifnotexists nm dbnm ON nm LP sortlist RP where_opt.
+uniqueflag ::= UNIQUE.
+uniqueflag ::=.
+eidlist_opt ::=.
+eidlist_opt ::= LP eidlist RP.
+eidlist ::= eidlist COMMA nm collate sortorder.
+eidlist ::= nm collate sortorder.
+collate ::=.
+collate ::= COLLATE ID|STRING.
+cmd ::= DROP INDEX ifexists fullname.
+cmd ::= VACUUM vinto.
+cmd ::= VACUUM nm vinto.
+vinto ::= INTO expr.
+vinto ::=.
+cmd ::= PRAGMA nm dbnm.
+cmd ::= PRAGMA nm dbnm EQ nmnum.
+cmd ::= PRAGMA nm dbnm LP nmnum RP.
+cmd ::= PRAGMA nm dbnm EQ minus_num.
+cmd ::= PRAGMA nm dbnm LP minus_num RP.
+plus_num ::= PLUS INTEGER|FLOAT.
+minus_num ::= MINUS INTEGER|FLOAT.
+cmd ::= createkw trigger_decl BEGIN trigger_cmd_list END.
+trigger_decl ::= temp TRIGGER ifnotexists nm dbnm trigger_time trigger_event ON fullname foreach_clause when_clause.
+trigger_time ::= BEFORE|AFTER.
+trigger_time ::= INSTEAD OF.
+trigger_time ::=.
+trigger_event ::= DELETE|INSERT.
+trigger_event ::= UPDATE.
+trigger_event ::= UPDATE OF idlist.
+when_clause ::=.
+when_clause ::= WHEN expr.
+trigger_cmd_list ::= trigger_cmd_list trigger_cmd SEMI.
+trigger_cmd_list ::= trigger_cmd SEMI.
+trnm ::= nm DOT nm.
+tridxby ::= INDEXED BY nm.
+tridxby ::= NOT INDEXED.
+trigger_cmd ::= UPDATE orconf trnm tridxby SET setlist from where_opt scanpt.
+trigger_cmd ::= scanpt insert_cmd INTO trnm idlist_opt select upsert scanpt.
+trigger_cmd ::= DELETE FROM trnm tridxby where_opt scanpt.
+trigger_cmd ::= scanpt select scanpt.
+expr ::= RAISE LP IGNORE RP.
+expr ::= RAISE LP raisetype COMMA nm RP.
+raisetype ::= ROLLBACK.
+raisetype ::= ABORT.
+raisetype ::= FAIL.
+cmd ::= DROP TRIGGER ifexists fullname.
+cmd ::= ATTACH database_kw_opt expr AS expr key_opt.
+cmd ::= DETACH database_kw_opt expr.
+key_opt ::=.
+key_opt ::= KEY expr.
+cmd ::= REINDEX.
+cmd ::= REINDEX nm dbnm.
+cmd ::= ANALYZE.
+cmd ::= ANALYZE nm dbnm.
+cmd ::= ALTER TABLE fullname RENAME TO nm.
+cmd ::= ALTER TABLE add_column_fullname ADD kwcolumn_opt columnname carglist.
+cmd ::= ALTER TABLE fullname DROP kwcolumn_opt nm.
+add_column_fullname ::= fullname.
+cmd ::= ALTER TABLE fullname RENAME kwcolumn_opt nm TO nm.
+cmd ::= create_vtab.
+cmd ::= create_vtab LP vtabarglist RP.
+create_vtab ::= createkw VIRTUAL TABLE ifnotexists nm dbnm USING nm.
+vtabarg ::=.
+vtabargtoken ::= ANY.
+vtabargtoken ::= lp anylist RP.
+lp ::= LP.
+with ::= WITH wqlist.
+with ::= WITH RECURSIVE wqlist.
+wqas ::= AS.
+wqas ::= AS MATERIALIZED.
+wqas ::= AS NOT MATERIALIZED.
+wqitem ::= nm eidlist_opt wqas LP select RP.
+wqlist ::= wqitem.
+wqlist ::= wqlist COMMA wqitem.
+windowdefn_list ::= windowdefn_list COMMA windowdefn.
+windowdefn ::= nm AS LP window RP.
+window ::= PARTITION BY nexprlist orderby_opt frame_opt.
+window ::= nm PARTITION BY nexprlist orderby_opt frame_opt.
+window ::= ORDER BY sortlist frame_opt.
+window ::= nm ORDER BY sortlist frame_opt.
+window ::= nm frame_opt.
+frame_opt ::=.
+frame_opt ::= range_or_rows frame_bound_s frame_exclude_opt.
+frame_opt ::= range_or_rows BETWEEN frame_bound_s AND frame_bound_e frame_exclude_opt.
+range_or_rows ::= RANGE|ROWS|GROUPS.
+frame_bound_s ::= frame_bound.
+frame_bound_s ::= UNBOUNDED PRECEDING.
+frame_bound_e ::= frame_bound.
+frame_bound_e ::= UNBOUNDED FOLLOWING.
+frame_bound ::= expr PRECEDING|FOLLOWING.
+frame_bound ::= CURRENT ROW.
+frame_exclude_opt ::=.
+frame_exclude_opt ::= EXCLUDE frame_exclude.
+frame_exclude ::= NO OTHERS.
+frame_exclude ::= CURRENT ROW.
+frame_exclude ::= GROUP|TIES.
+window_clause ::= WINDOW windowdefn_list.
+filter_over ::= filter_clause over_clause.
+filter_over ::= over_clause.
+filter_over ::= filter_clause.
+over_clause ::= OVER LP window RP.
+over_clause ::= OVER nm.
+filter_clause ::= FILTER LP WHERE expr RP.
+input ::= cmdlist.
+cmdlist ::= cmdlist ecmd.
+cmdlist ::= ecmd.
+ecmd ::= SEMI.
+ecmd ::= cmdx SEMI.
+ecmd ::= explain cmdx SEMI.
+trans_opt ::=.
+trans_opt ::= TRANSACTION.
+trans_opt ::= TRANSACTION nm.
+savepoint_opt ::= SAVEPOINT.
+savepoint_opt ::=.
+cmd ::= create_table create_table_args.
+table_option_set ::= table_option.
+columnlist ::= columnlist COMMA columnname carglist.
+columnlist ::= columnname carglist.
+nm ::= ID|INDEXED|JOIN_KW.
+nm ::= STRING.
+typetoken ::= typename.
+typename ::= ID|STRING.
+signed ::= plus_num.
+signed ::= minus_num.
+carglist ::= carglist ccons.
+carglist ::=.
+ccons ::= NULL onconf.
+ccons ::= GENERATED ALWAYS AS generated.
+ccons ::= AS generated.
+conslist_opt ::= COMMA conslist.
+conslist ::= conslist tconscomma tcons.
+conslist ::= tcons.
+tconscomma ::=.
+defer_subclause_opt ::= defer_subclause.
+resolvetype ::= raisetype.
+selectnowith ::= oneselect.
+oneselect ::= values.
+sclp ::= selcollist COMMA.
+as ::= ID|STRING.
+indexed_opt ::= indexed_by.
+returning ::=.
+expr ::= term.
+likeop ::= LIKE_KW|MATCH.
+case_operand ::= expr.
+exprlist ::= nexprlist.
+nmnum ::= plus_num.
+nmnum ::= nm.
+nmnum ::= ON.
+nmnum ::= DELETE.
+nmnum ::= DEFAULT.
+plus_num ::= INTEGER|FLOAT.
+foreach_clause ::=.
+foreach_clause ::= FOR EACH ROW.
+trnm ::= nm.
+tridxby ::=.
+database_kw_opt ::= DATABASE.
+database_kw_opt ::=.
+kwcolumn_opt ::=.
+kwcolumn_opt ::= COLUMNKW.
+vtabarglist ::= vtabarg.
+vtabarglist ::= vtabarglist COMMA vtabarg.
+vtabarg ::= vtabarg vtabargtoken.
+anylist ::=.
+anylist ::= anylist LP anylist RP.
+anylist ::= anylist ANY.
+with ::=.
+windowdefn_list ::= windowdefn.
+window ::= frame_opt.
+
+%token SPACE ILLEGAL.
diff --git a/src/trace_processor/perfetto_sql/grammar/perfettosql_include.y b/src/trace_processor/perfetto_sql/grammar/perfettosql_include.y
new file mode 100644
index 0000000..f86e4f8
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/grammar/perfettosql_include.y
@@ -0,0 +1,44 @@
+%name PerfettoSqlParser
+%token_prefix TK_
+%start_symbol input
+
+%include {
+#include <stddef.h>
+
+#define YYNOERRORRECOVERY 1
+}
+
+%token CREATE REPLACE PERFETTO MACRO INCLUDE MODULE RETURNS FUNCTION.
+
+%left OR.
+%left AND.
+%right NOT.
+%left IS MATCH LIKE_KW BETWEEN IN ISNULL NOTNULL NE EQ.
+%left GT LE LT GE.
+%right ESCAPE.
+%left BITAND BITOR LSHIFT RSHIFT.
+%left PLUS MINUS.
+%left STAR SLASH REM.
+%left CONCAT PTR.
+%left COLLATE.
+%right BITNOT.
+%nonassoc ON.
+
+%fallback ID
+  ABORT ACTION AFTER ANALYZE ASC ATTACH BEFORE BEGIN BY CASCADE CAST COLUMNKW
+  CONFLICT DATABASE DEFERRED DESC DETACH DO
+  EACH END EXCLUSIVE EXPLAIN FAIL FOR
+  IGNORE IMMEDIATE INITIALLY INSTEAD LIKE_KW MATCH NO PLAN
+  QUERY KEY OF OFFSET PRAGMA RAISE RECURSIVE RELEASE REPLACE RESTRICT ROW ROWS
+  ROLLBACK SAVEPOINT TEMP TRIGGER VACUUM VIEW VIRTUAL WITH WITHOUT
+  NULLS FIRST LAST
+  EXCEPT INTERSECT UNION
+  CURRENT FOLLOWING PARTITION PRECEDING RANGE UNBOUNDED
+  EXCLUDE GROUPS OTHERS TIES
+  WITHIN
+  GENERATED ALWAYS
+  MATERIALIZED
+  REINDEX RENAME CTIME_KW IF
+  .
+%wildcard ANY.
+
diff --git a/src/trace_processor/perfetto_sql/grammar/perfettosql_keywordhash.h b/src/trace_processor/perfetto_sql/grammar/perfettosql_keywordhash.h
new file mode 100644
index 0000000..54bb825
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/grammar/perfettosql_keywordhash.h
@@ -0,0 +1,614 @@
+
+#include "src/trace_processor/perfetto_sql/grammar/perfettosql_keywordhash_helper.h"
+/***** This file contains automatically generated code ******
+**
+** The code in this file has been automatically generated by
+**
+**   sqlite/tool/mkkeywordhash.c
+**
+** The code in this file implements a function that determines whether
+** or not a given identifier is really an SQL keyword.  The same thing
+** might be implemented more directly using a hand-written hash table.
+** But by using this automatically generated code, the size of the code
+** is substantially reduced.  This is important for embedded applications
+** on platforms with limited memory.
+*/
+/* Hash score: 241 */
+/* zKWText[] encodes 1054 bytes of keyword text in 700 bytes */
+/*   REINDEXEDESCAPERFETTOFFSETABLEFTHENDATABASELECTIESAVEPOINT         */
+/*   ERSECTRANSACTIONOTNULLSBEFOREIGNOREGEXPLAINCLUDEFERRABLEACHECK     */
+/*   EYISNULLIKELSEXCLUDELETEMPORARYCONSTRAINTORDERAISEXCEPTRIGGER      */
+/*   ANGENERATEDETACHAVINGLOBEGINSTEADDEFAULTMACROSSUNIQUERYWITHOUT     */
+/*   EREFERENCESATTACHBETWEENATURALTERELEASEXCLUSIVEXISTSCASCADE        */
+/*   FERREDISTINCTCASECOLLATECREATECURRENT_DATEIMMEDIATEJOINNER         */
+/*   ECURSIVEMATCHMODULEPLANALYZEPRAGMATERIALIZEDROPARTITIONOTHING      */
+/*   ROUPSUPDATEVALUESVIRTUALWAYSWHENWHERENAMEABORTAFTEREPLACEAND       */
+/*   AUTOINCREMENTCASTCOLUMNCOMMITCONFLICTCURRENT_TIMESTAMPRECEDING     */
+/*   FAILASTFILTERESTRICTFIRSTFOLLOWINGFROMFULLIMITFUNCTIONIFINSERT     */
+/*   OTHERSOVERETURNINGRETURNSRIGHTROLLBACKROWSUNBOUNDEDUNIONUSING      */
+/*   VACUUMVIEWINDOWBYINITIALLYPRIMARY                                  */
+static const char zKWText[699] = {
+    'R', 'E', 'I', 'N', 'D', 'E', 'X', 'E', 'D', 'E', 'S', 'C', 'A', 'P', 'E',
+    'R', 'F', 'E', 'T', 'T', 'O', 'F', 'F', 'S', 'E', 'T', 'A', 'B', 'L', 'E',
+    'F', 'T', 'H', 'E', 'N', 'D', 'A', 'T', 'A', 'B', 'A', 'S', 'E', 'L', 'E',
+    'C', 'T', 'I', 'E', 'S', 'A', 'V', 'E', 'P', 'O', 'I', 'N', 'T', 'E', 'R',
+    'S', 'E', 'C', 'T', 'R', 'A', 'N', 'S', 'A', 'C', 'T', 'I', 'O', 'N', 'O',
+    'T', 'N', 'U', 'L', 'L', 'S', 'B', 'E', 'F', 'O', 'R', 'E', 'I', 'G', 'N',
+    'O', 'R', 'E', 'G', 'E', 'X', 'P', 'L', 'A', 'I', 'N', 'C', 'L', 'U', 'D',
+    'E', 'F', 'E', 'R', 'R', 'A', 'B', 'L', 'E', 'A', 'C', 'H', 'E', 'C', 'K',
+    'E', 'Y', 'I', 'S', 'N', 'U', 'L', 'L', 'I', 'K', 'E', 'L', 'S', 'E', 'X',
+    'C', 'L', 'U', 'D', 'E', 'L', 'E', 'T', 'E', 'M', 'P', 'O', 'R', 'A', 'R',
+    'Y', 'C', 'O', 'N', 'S', 'T', 'R', 'A', 'I', 'N', 'T', 'O', 'R', 'D', 'E',
+    'R', 'A', 'I', 'S', 'E', 'X', 'C', 'E', 'P', 'T', 'R', 'I', 'G', 'G', 'E',
+    'R', 'A', 'N', 'G', 'E', 'N', 'E', 'R', 'A', 'T', 'E', 'D', 'E', 'T', 'A',
+    'C', 'H', 'A', 'V', 'I', 'N', 'G', 'L', 'O', 'B', 'E', 'G', 'I', 'N', 'S',
+    'T', 'E', 'A', 'D', 'D', 'E', 'F', 'A', 'U', 'L', 'T', 'M', 'A', 'C', 'R',
+    'O', 'S', 'S', 'U', 'N', 'I', 'Q', 'U', 'E', 'R', 'Y', 'W', 'I', 'T', 'H',
+    'O', 'U', 'T', 'E', 'R', 'E', 'F', 'E', 'R', 'E', 'N', 'C', 'E', 'S', 'A',
+    'T', 'T', 'A', 'C', 'H', 'B', 'E', 'T', 'W', 'E', 'E', 'N', 'A', 'T', 'U',
+    'R', 'A', 'L', 'T', 'E', 'R', 'E', 'L', 'E', 'A', 'S', 'E', 'X', 'C', 'L',
+    'U', 'S', 'I', 'V', 'E', 'X', 'I', 'S', 'T', 'S', 'C', 'A', 'S', 'C', 'A',
+    'D', 'E', 'F', 'E', 'R', 'R', 'E', 'D', 'I', 'S', 'T', 'I', 'N', 'C', 'T',
+    'C', 'A', 'S', 'E', 'C', 'O', 'L', 'L', 'A', 'T', 'E', 'C', 'R', 'E', 'A',
+    'T', 'E', 'C', 'U', 'R', 'R', 'E', 'N', 'T', '_', 'D', 'A', 'T', 'E', 'I',
+    'M', 'M', 'E', 'D', 'I', 'A', 'T', 'E', 'J', 'O', 'I', 'N', 'N', 'E', 'R',
+    'E', 'C', 'U', 'R', 'S', 'I', 'V', 'E', 'M', 'A', 'T', 'C', 'H', 'M', 'O',
+    'D', 'U', 'L', 'E', 'P', 'L', 'A', 'N', 'A', 'L', 'Y', 'Z', 'E', 'P', 'R',
+    'A', 'G', 'M', 'A', 'T', 'E', 'R', 'I', 'A', 'L', 'I', 'Z', 'E', 'D', 'R',
+    'O', 'P', 'A', 'R', 'T', 'I', 'T', 'I', 'O', 'N', 'O', 'T', 'H', 'I', 'N',
+    'G', 'R', 'O', 'U', 'P', 'S', 'U', 'P', 'D', 'A', 'T', 'E', 'V', 'A', 'L',
+    'U', 'E', 'S', 'V', 'I', 'R', 'T', 'U', 'A', 'L', 'W', 'A', 'Y', 'S', 'W',
+    'H', 'E', 'N', 'W', 'H', 'E', 'R', 'E', 'N', 'A', 'M', 'E', 'A', 'B', 'O',
+    'R', 'T', 'A', 'F', 'T', 'E', 'R', 'E', 'P', 'L', 'A', 'C', 'E', 'A', 'N',
+    'D', 'A', 'U', 'T', 'O', 'I', 'N', 'C', 'R', 'E', 'M', 'E', 'N', 'T', 'C',
+    'A', 'S', 'T', 'C', 'O', 'L', 'U', 'M', 'N', 'C', 'O', 'M', 'M', 'I', 'T',
+    'C', 'O', 'N', 'F', 'L', 'I', 'C', 'T', 'C', 'U', 'R', 'R', 'E', 'N', 'T',
+    '_', 'T', 'I', 'M', 'E', 'S', 'T', 'A', 'M', 'P', 'R', 'E', 'C', 'E', 'D',
+    'I', 'N', 'G', 'F', 'A', 'I', 'L', 'A', 'S', 'T', 'F', 'I', 'L', 'T', 'E',
+    'R', 'E', 'S', 'T', 'R', 'I', 'C', 'T', 'F', 'I', 'R', 'S', 'T', 'F', 'O',
+    'L', 'L', 'O', 'W', 'I', 'N', 'G', 'F', 'R', 'O', 'M', 'F', 'U', 'L', 'L',
+    'I', 'M', 'I', 'T', 'F', 'U', 'N', 'C', 'T', 'I', 'O', 'N', 'I', 'F', 'I',
+    'N', 'S', 'E', 'R', 'T', 'O', 'T', 'H', 'E', 'R', 'S', 'O', 'V', 'E', 'R',
+    'E', 'T', 'U', 'R', 'N', 'I', 'N', 'G', 'R', 'E', 'T', 'U', 'R', 'N', 'S',
+    'R', 'I', 'G', 'H', 'T', 'R', 'O', 'L', 'L', 'B', 'A', 'C', 'K', 'R', 'O',
+    'W', 'S', 'U', 'N', 'B', 'O', 'U', 'N', 'D', 'E', 'D', 'U', 'N', 'I', 'O',
+    'N', 'U', 'S', 'I', 'N', 'G', 'V', 'A', 'C', 'U', 'U', 'M', 'V', 'I', 'E',
+    'W', 'I', 'N', 'D', 'O', 'W', 'B', 'Y', 'I', 'N', 'I', 'T', 'I', 'A', 'L',
+    'L', 'Y', 'P', 'R', 'I', 'M', 'A', 'R', 'Y',
+};
+/* aKWHash[i] is the hash value for the i-th keyword */
+static const unsigned char aKWHash[127] = {
+    134, 81,  140, 86,  97,  45,  6,   0,   102, 0,  90,  98,  0,   8,   17,
+    92,  59,  0,   20,  105, 9,   95,  141, 16,  0,  0,   146, 0,   40,  126,
+    91,  32,  113, 0,   28,  0,   0,   128, 84,  0,  82,  36,  0,   65,  111,
+    153, 0,   142, 120, 0,   0,   75,  0,   79,  35, 0,   14,  0,   43,  70,
+    13,  42,  5,   57,  148, 116, 127, 0,   99,  80, 71,  151, 58,  125, 100,
+    0,   76,  0,   30,  51,  0,   63,  0,   0,   0,  115, 29,  117, 121, 130,
+    33,  132, 129, 0,   108, 0,   15,  110, 150, 53, 135, 145, 94,  87,  24,
+    46,  131, 0,   0,   114, 48,  136, 49,  0,   19, 0,   0,   137, 0,   106,
+    25,  27,  0,   10,  72,  122, 101,
+};
+/* aKWNext[] forms the hash collision chain.  If aKWHash[i]==0
+** then the i-th keyword has no more hash collisions.  Otherwise,
+** the next keyword with the same hash is aKWHash[i]-1. */
+static const unsigned char aKWNext[154] = {
+    0,   0,   0,   0,   0,   4,  0,   0,  34,  56,  0,  0,   0,  0,   0,   149,
+    138, 31,  0,   143, 139, 0,  0,   0,  0,   88,  0,  0,   0,  112, 119, 0,
+    12,  0,   0,   0,   0,   21, 0,   7,  144, 0,   0,  147, 0,  0,   124, 0,
+    0,   68,  0,   0,   0,   50, 0,   0,  0,   0,   0,  2,   0,  0,   0,   0,
+    0,   0,   0,   74,  0,   0,  0,   0,  23,  0,   0,  44,  61, 0,   55,  0,
+    96,  0,   1,   77,  0,   0,  0,   39, 0,   0,   0,  0,   0,  0,   0,   133,
+    38,  0,   0,   152, 3,   64, 66,  69, 0,   0,   0,  0,   0,  73,  67,  60,
+    0,   0,   0,   0,   0,   0,  0,   0,  85,  109, 62, 118, 11, 37,  0,   0,
+    83,  104, 123, 0,   47,  0,  0,   0,  89,  22,  0,  0,   52, 0,   78,  0,
+    103, 26,  18,  54,  41,  0,  107, 0,  0,   93,
+};
+/* aKWLen[i] is the length (in bytes) of the i-th keyword */
+static const unsigned char aKWLen[154] = {
+    0, 7, 7,  5,  4, 6, 8, 6,  2, 3,  5,  4, 4, 3,  8, 2, 6, 4, 9, 9,  11, 6,
+    2, 7, 3,  2,  5, 4, 6, 7,  3, 6,  6,  7, 7, 10, 4, 5, 3, 6, 4, 4,  7,  6,
+    9, 4, 2,  10, 4, 5, 5, 6,  7, 5,  9,  6, 6, 4,  5, 7, 3, 7, 5, 5,  6,  5,
+    7, 4, 5,  10, 6, 7, 7, 5,  7, 9,  6,  7, 3, 8,  8, 2, 4, 7, 6, 12, 9,  4,
+    5, 9, 5,  6,  4, 7, 6, 12, 4, 9,  7,  6, 5, 6,  6, 7, 6, 4, 5, 6,  5,  5,
+    7, 3, 13, 2,  2, 4, 6, 6,  8, 17, 12, 7, 9, 4,  4, 6, 8, 5, 9, 4,  4,  5,
+    8, 2, 6,  6,  4, 9, 7, 5,  8, 4,  3,  9, 5, 5,  6, 4, 6, 2, 2, 9,  3,  7,
+};
+/* aKWOffset[i] is the index into zKWText[] of the start of
+** the text for the i-th keyword. */
+static const unsigned short int aKWOffset[154] = {
+    0,   0,   2,   2,   8,   9,   13,  20,  20,  23,  25,  28,  31,  33,
+    35,  40,  41,  46,  49,  55,  63,  68,  72,  73,  73,  73,  76,  76,
+    81,  83,  83,  87,  91,  94,  99,  104, 113, 115, 119, 122, 127, 130,
+    133, 138, 142, 142, 146, 151, 158, 161, 165, 169, 174, 180, 183, 191,
+    196, 201, 204, 207, 212, 214, 221, 223, 228, 231, 236, 236, 240, 244,
+    254, 260, 266, 271, 275, 281, 289, 295, 296, 300, 307, 308, 315, 319,
+    326, 332, 344, 353, 355, 359, 368, 373, 379, 381, 388, 392, 403, 406,
+    414, 420, 420, 426, 432, 438, 443, 449, 453, 456, 462, 467, 471, 478,
+    481, 483, 485, 494, 498, 504, 510, 518, 518, 518, 534, 543, 546, 550,
+    555, 563, 568, 577, 581, 584, 589, 597, 599, 605, 611, 614, 623, 630,
+    635, 643, 643, 647, 656, 661, 666, 672, 675, 678, 681, 683, 688, 692,
+};
+/* aKWCode[i] is the parser symbol code for the i-th keyword */
+static const unsigned char aKWCode[154] = {
+    0,
+    TK_REINDEX,
+    TK_INDEXED,
+    TK_INDEX,
+    TK_DESC,
+    TK_ESCAPE,
+    TK_PERFETTO,
+    TK_OFFSET,
+    TK_OF,
+    TK_SET,
+    TK_TABLE,
+    TK_JOIN_KW,
+    TK_THEN,
+    TK_END,
+    TK_DATABASE,
+    TK_AS,
+    TK_SELECT,
+    TK_TIES,
+    TK_SAVEPOINT,
+    TK_INTERSECT,
+    TK_TRANSACTION,
+    TK_ACTION,
+    TK_ON,
+    TK_NOTNULL,
+    TK_NOT,
+    TK_NO,
+    TK_NULLS,
+    TK_NULL,
+    TK_BEFORE,
+    TK_FOREIGN,
+    TK_FOR,
+    TK_IGNORE,
+    TK_LIKE_KW,
+    TK_EXPLAIN,
+    TK_INCLUDE,
+    TK_DEFERRABLE,
+    TK_EACH,
+    TK_CHECK,
+    TK_KEY,
+    TK_ISNULL,
+    TK_LIKE_KW,
+    TK_ELSE,
+    TK_EXCLUDE,
+    TK_DELETE,
+    TK_TEMP,
+    TK_TEMP,
+    TK_OR,
+    TK_CONSTRAINT,
+    TK_INTO,
+    TK_ORDER,
+    TK_RAISE,
+    TK_EXCEPT,
+    TK_TRIGGER,
+    TK_RANGE,
+    TK_GENERATED,
+    TK_DETACH,
+    TK_HAVING,
+    TK_LIKE_KW,
+    TK_BEGIN,
+    TK_INSTEAD,
+    TK_ADD,
+    TK_DEFAULT,
+    TK_MACRO,
+    TK_JOIN_KW,
+    TK_UNIQUE,
+    TK_QUERY,
+    TK_WITHOUT,
+    TK_WITH,
+    TK_JOIN_KW,
+    TK_REFERENCES,
+    TK_ATTACH,
+    TK_BETWEEN,
+    TK_JOIN_KW,
+    TK_ALTER,
+    TK_RELEASE,
+    TK_EXCLUSIVE,
+    TK_EXISTS,
+    TK_CASCADE,
+    TK_ASC,
+    TK_DEFERRED,
+    TK_DISTINCT,
+    TK_IS,
+    TK_CASE,
+    TK_COLLATE,
+    TK_CREATE,
+    TK_CTIME_KW,
+    TK_IMMEDIATE,
+    TK_JOIN,
+    TK_JOIN_KW,
+    TK_RECURSIVE,
+    TK_MATCH,
+    TK_MODULE,
+    TK_PLAN,
+    TK_ANALYZE,
+    TK_PRAGMA,
+    TK_MATERIALIZED,
+    TK_DROP,
+    TK_PARTITION,
+    TK_NOTHING,
+    TK_GROUPS,
+    TK_GROUP,
+    TK_UPDATE,
+    TK_VALUES,
+    TK_VIRTUAL,
+    TK_ALWAYS,
+    TK_WHEN,
+    TK_WHERE,
+    TK_RENAME,
+    TK_ABORT,
+    TK_AFTER,
+    TK_REPLACE,
+    TK_AND,
+    TK_AUTOINCR,
+    TK_TO,
+    TK_IN,
+    TK_CAST,
+    TK_COLUMNKW,
+    TK_COMMIT,
+    TK_CONFLICT,
+    TK_CTIME_KW,
+    TK_CTIME_KW,
+    TK_CURRENT,
+    TK_PRECEDING,
+    TK_FAIL,
+    TK_LAST,
+    TK_FILTER,
+    TK_RESTRICT,
+    TK_FIRST,
+    TK_FOLLOWING,
+    TK_FROM,
+    TK_JOIN_KW,
+    TK_LIMIT,
+    TK_FUNCTION,
+    TK_IF,
+    TK_INSERT,
+    TK_OTHERS,
+    TK_OVER,
+    TK_RETURNING,
+    TK_RETURNS,
+    TK_JOIN_KW,
+    TK_ROLLBACK,
+    TK_ROWS,
+    TK_ROW,
+    TK_UNBOUNDED,
+    TK_UNION,
+    TK_USING,
+    TK_VACUUM,
+    TK_VIEW,
+    TK_WINDOW,
+    TK_DO,
+    TK_BY,
+    TK_INITIALLY,
+    TK_ALL,
+    TK_PRIMARY,
+};
+/* Hash table decoded:
+**   0: INSERT
+**   1: IS
+**   2: ROLLBACK TRIGGER
+**   3: IMMEDIATE
+**   4: PARTITION
+**   5: TEMP
+**   6: PERFETTO
+**   7:
+**   8: VALUES WITHOUT
+**   9:
+**  10: MATCH
+**  11: NOTHING
+**  12:
+**  13: OF INCLUDE
+**  14: TIES IGNORE
+**  15: PLAN
+**  16: INSTEAD INDEXED
+**  17:
+**  18: TRANSACTION RIGHT
+**  19: WHEN
+**  20: SET HAVING
+**  21: MATERIALIZED IF
+**  22: ROWS
+**  23: SELECT RETURNS
+**  24:
+**  25:
+**  26: VACUUM SAVEPOINT
+**  27:
+**  28: LIKE UNION VIRTUAL REFERENCES
+**  29: RESTRICT
+**  30: MODULE
+**  31: REGEXP THEN
+**  32: TO
+**  33:
+**  34: BEFORE
+**  35:
+**  36:
+**  37: FOLLOWING COLLATE CASCADE
+**  38: CREATE
+**  39:
+**  40: CASE REINDEX
+**  41: EACH
+**  42:
+**  43: QUERY
+**  44: AND ADD
+**  45: PRIMARY ANALYZE
+**  46:
+**  47: ROW ASC DETACH
+**  48: CURRENT_TIME CURRENT_DATE
+**  49:
+**  50:
+**  51: EXCLUSIVE TEMPORARY
+**  52:
+**  53: DEFERRED
+**  54: DEFERRABLE
+**  55:
+**  56: DATABASE
+**  57:
+**  58: DELETE VIEW GENERATED
+**  59: ATTACH
+**  60: END
+**  61: EXCLUDE
+**  62: ESCAPE DESC
+**  63: GLOB
+**  64: WINDOW ELSE
+**  65: COLUMN
+**  66: FIRST
+**  67:
+**  68: GROUPS ALL
+**  69: DISTINCT DROP KEY
+**  70: BETWEEN
+**  71: INITIALLY
+**  72: BEGIN
+**  73: FILTER CHECK ACTION
+**  74: GROUP INDEX
+**  75:
+**  76: EXISTS DEFAULT
+**  77:
+**  78: FOR CURRENT_TIMESTAMP
+**  79: EXCEPT
+**  80:
+**  81: CROSS
+**  82:
+**  83:
+**  84:
+**  85: CAST
+**  86: FOREIGN AUTOINCREMENT
+**  87: COMMIT
+**  88: CURRENT AFTER ALTER
+**  89: FULL FAIL CONFLICT
+**  90: EXPLAIN
+**  91: FUNCTION CONSTRAINT
+**  92: FROM ALWAYS
+**  93:
+**  94: ABORT
+**  95:
+**  96: AS DO
+**  97: REPLACE WITH RELEASE
+**  98: BY RENAME
+**  99: RANGE RAISE
+** 100: OTHERS
+** 101: USING NULLS
+** 102: PRAGMA
+** 103: JOIN ISNULL OFFSET
+** 104: NOT
+** 105: OR LAST LEFT
+** 106: LIMIT
+** 107:
+** 108:
+** 109: IN
+** 110: INTO
+** 111: OVER RECURSIVE
+** 112: ORDER OUTER
+** 113:
+** 114: INTERSECT UNBOUNDED
+** 115:
+** 116:
+** 117: RETURNING ON
+** 118:
+** 119: WHERE
+** 120: NO INNER
+** 121: NULL
+** 122:
+** 123: TABLE
+** 124: NATURAL NOTNULL
+** 125: PRECEDING MACRO
+** 126: UPDATE UNIQUE
+*/
+/* Check to see if z[0..n-1] is a keyword. If it is, write the
+** parser symbol code for that keyword into *pType.  Always
+** return the integer n (the length of the token). */
+static int keywordCode(const char* z, int n, int* pType) {
+  int i, j;
+  const char* zKW;
+  assert(n >= 2);
+  i = ((charMap(z[0]) * 4) ^ (charMap(z[n - 1]) * 3) ^ n * 1) % 127;
+  for (i = (int)aKWHash[i]; i > 0; i = aKWNext[i]) {
+    if (aKWLen[i] != n)
+      continue;
+    zKW = &zKWText[aKWOffset[i]];
+#ifdef SQLITE_ASCII
+    if ((z[0] & ~0x20) != zKW[0])
+      continue;
+    if ((z[1] & ~0x20) != zKW[1])
+      continue;
+    j = 2;
+    while (j < n && (z[j] & ~0x20) == zKW[j]) {
+      j++;
+    }
+#endif
+#ifdef SQLITE_EBCDIC
+    if (toupper(z[0]) != zKW[0])
+      continue;
+    if (toupper(z[1]) != zKW[1])
+      continue;
+    j = 2;
+    while (j < n && toupper(z[j]) == zKW[j]) {
+      j++;
+    }
+#endif
+    if (j < n)
+      continue;
+    testcase(i == 1);   /* REINDEX */
+    testcase(i == 2);   /* INDEXED */
+    testcase(i == 3);   /* INDEX */
+    testcase(i == 4);   /* DESC */
+    testcase(i == 5);   /* ESCAPE */
+    testcase(i == 6);   /* PERFETTO */
+    testcase(i == 7);   /* OFFSET */
+    testcase(i == 8);   /* OF */
+    testcase(i == 9);   /* SET */
+    testcase(i == 10);  /* TABLE */
+    testcase(i == 11);  /* LEFT */
+    testcase(i == 12);  /* THEN */
+    testcase(i == 13);  /* END */
+    testcase(i == 14);  /* DATABASE */
+    testcase(i == 15);  /* AS */
+    testcase(i == 16);  /* SELECT */
+    testcase(i == 17);  /* TIES */
+    testcase(i == 18);  /* SAVEPOINT */
+    testcase(i == 19);  /* INTERSECT */
+    testcase(i == 20);  /* TRANSACTION */
+    testcase(i == 21);  /* ACTION */
+    testcase(i == 22);  /* ON */
+    testcase(i == 23);  /* NOTNULL */
+    testcase(i == 24);  /* NOT */
+    testcase(i == 25);  /* NO */
+    testcase(i == 26);  /* NULLS */
+    testcase(i == 27);  /* NULL */
+    testcase(i == 28);  /* BEFORE */
+    testcase(i == 29);  /* FOREIGN */
+    testcase(i == 30);  /* FOR */
+    testcase(i == 31);  /* IGNORE */
+    testcase(i == 32);  /* REGEXP */
+    testcase(i == 33);  /* EXPLAIN */
+    testcase(i == 34);  /* INCLUDE */
+    testcase(i == 35);  /* DEFERRABLE */
+    testcase(i == 36);  /* EACH */
+    testcase(i == 37);  /* CHECK */
+    testcase(i == 38);  /* KEY */
+    testcase(i == 39);  /* ISNULL */
+    testcase(i == 40);  /* LIKE */
+    testcase(i == 41);  /* ELSE */
+    testcase(i == 42);  /* EXCLUDE */
+    testcase(i == 43);  /* DELETE */
+    testcase(i == 44);  /* TEMPORARY */
+    testcase(i == 45);  /* TEMP */
+    testcase(i == 46);  /* OR */
+    testcase(i == 47);  /* CONSTRAINT */
+    testcase(i == 48);  /* INTO */
+    testcase(i == 49);  /* ORDER */
+    testcase(i == 50);  /* RAISE */
+    testcase(i == 51);  /* EXCEPT */
+    testcase(i == 52);  /* TRIGGER */
+    testcase(i == 53);  /* RANGE */
+    testcase(i == 54);  /* GENERATED */
+    testcase(i == 55);  /* DETACH */
+    testcase(i == 56);  /* HAVING */
+    testcase(i == 57);  /* GLOB */
+    testcase(i == 58);  /* BEGIN */
+    testcase(i == 59);  /* INSTEAD */
+    testcase(i == 60);  /* ADD */
+    testcase(i == 61);  /* DEFAULT */
+    testcase(i == 62);  /* MACRO */
+    testcase(i == 63);  /* CROSS */
+    testcase(i == 64);  /* UNIQUE */
+    testcase(i == 65);  /* QUERY */
+    testcase(i == 66);  /* WITHOUT */
+    testcase(i == 67);  /* WITH */
+    testcase(i == 68);  /* OUTER */
+    testcase(i == 69);  /* REFERENCES */
+    testcase(i == 70);  /* ATTACH */
+    testcase(i == 71);  /* BETWEEN */
+    testcase(i == 72);  /* NATURAL */
+    testcase(i == 73);  /* ALTER */
+    testcase(i == 74);  /* RELEASE */
+    testcase(i == 75);  /* EXCLUSIVE */
+    testcase(i == 76);  /* EXISTS */
+    testcase(i == 77);  /* CASCADE */
+    testcase(i == 78);  /* ASC */
+    testcase(i == 79);  /* DEFERRED */
+    testcase(i == 80);  /* DISTINCT */
+    testcase(i == 81);  /* IS */
+    testcase(i == 82);  /* CASE */
+    testcase(i == 83);  /* COLLATE */
+    testcase(i == 84);  /* CREATE */
+    testcase(i == 85);  /* CURRENT_DATE */
+    testcase(i == 86);  /* IMMEDIATE */
+    testcase(i == 87);  /* JOIN */
+    testcase(i == 88);  /* INNER */
+    testcase(i == 89);  /* RECURSIVE */
+    testcase(i == 90);  /* MATCH */
+    testcase(i == 91);  /* MODULE */
+    testcase(i == 92);  /* PLAN */
+    testcase(i == 93);  /* ANALYZE */
+    testcase(i == 94);  /* PRAGMA */
+    testcase(i == 95);  /* MATERIALIZED */
+    testcase(i == 96);  /* DROP */
+    testcase(i == 97);  /* PARTITION */
+    testcase(i == 98);  /* NOTHING */
+    testcase(i == 99);  /* GROUPS */
+    testcase(i == 100); /* GROUP */
+    testcase(i == 101); /* UPDATE */
+    testcase(i == 102); /* VALUES */
+    testcase(i == 103); /* VIRTUAL */
+    testcase(i == 104); /* ALWAYS */
+    testcase(i == 105); /* WHEN */
+    testcase(i == 106); /* WHERE */
+    testcase(i == 107); /* RENAME */
+    testcase(i == 108); /* ABORT */
+    testcase(i == 109); /* AFTER */
+    testcase(i == 110); /* REPLACE */
+    testcase(i == 111); /* AND */
+    testcase(i == 112); /* AUTOINCREMENT */
+    testcase(i == 113); /* TO */
+    testcase(i == 114); /* IN */
+    testcase(i == 115); /* CAST */
+    testcase(i == 116); /* COLUMN */
+    testcase(i == 117); /* COMMIT */
+    testcase(i == 118); /* CONFLICT */
+    testcase(i == 119); /* CURRENT_TIMESTAMP */
+    testcase(i == 120); /* CURRENT_TIME */
+    testcase(i == 121); /* CURRENT */
+    testcase(i == 122); /* PRECEDING */
+    testcase(i == 123); /* FAIL */
+    testcase(i == 124); /* LAST */
+    testcase(i == 125); /* FILTER */
+    testcase(i == 126); /* RESTRICT */
+    testcase(i == 127); /* FIRST */
+    testcase(i == 128); /* FOLLOWING */
+    testcase(i == 129); /* FROM */
+    testcase(i == 130); /* FULL */
+    testcase(i == 131); /* LIMIT */
+    testcase(i == 132); /* FUNCTION */
+    testcase(i == 133); /* IF */
+    testcase(i == 134); /* INSERT */
+    testcase(i == 135); /* OTHERS */
+    testcase(i == 136); /* OVER */
+    testcase(i == 137); /* RETURNING */
+    testcase(i == 138); /* RETURNS */
+    testcase(i == 139); /* RIGHT */
+    testcase(i == 140); /* ROLLBACK */
+    testcase(i == 141); /* ROWS */
+    testcase(i == 142); /* ROW */
+    testcase(i == 143); /* UNBOUNDED */
+    testcase(i == 144); /* UNION */
+    testcase(i == 145); /* USING */
+    testcase(i == 146); /* VACUUM */
+    testcase(i == 147); /* VIEW */
+    testcase(i == 148); /* WINDOW */
+    testcase(i == 149); /* DO */
+    testcase(i == 150); /* BY */
+    testcase(i == 151); /* INITIALLY */
+    testcase(i == 152); /* ALL */
+    testcase(i == 153); /* PRIMARY */
+    *pType = aKWCode[i];
+    break;
+  }
+  return n;
+}
+int sqlite3KeywordCode(const unsigned char* z, int n) {
+  int id = TK_ID;
+  if (n >= 2)
+    keywordCode((char*)z, n, &id);
+  return id;
+}
diff --git a/src/trace_processor/perfetto_sql/grammar/perfettosql_keywordhash_helper.h b/src/trace_processor/perfetto_sql/grammar/perfettosql_keywordhash_helper.h
new file mode 100644
index 0000000..3b60abe
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/grammar/perfettosql_keywordhash_helper.h
@@ -0,0 +1,45 @@
+/*
+ * 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_GRAMMAR_PERFETTOSQL_KEYWORDHASH_HELPER_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_GRAMMAR_PERFETTOSQL_KEYWORDHASH_HELPER_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <assert.h>
+#include <ctype.h>
+
+#include "src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.h"
+
+typedef unsigned char u8;
+
+#define SQLITE_OK 0
+#define SQLITE_ERROR 1
+#define SQLITE_ASCII 1
+
+static inline int charMap(char c) {
+  return tolower(c);
+}
+
+static inline void testcase(int X) {}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_GRAMMAR_PERFETTOSQL_KEYWORDHASH_HELPER_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/BUILD.gn b/src/trace_processor/perfetto_sql/intrinsics/functions/BUILD.gn
index 863c2e6..58d1ae6 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/BUILD.gn
@@ -22,6 +22,8 @@
     "base64.cc",
     "base64.h",
     "clock_functions.h",
+    "counter_intervals.cc",
+    "counter_intervals.h",
     "create_function.cc",
     "create_function.h",
     "create_view_function.cc",
@@ -84,6 +86,7 @@
     "../../../util:sql_argument",
     "../../../util:stdlib",
     "../../engine",
+    "../../parser",
     "../types",
   ]
   public_deps = [ ":interface" ]
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/counter_intervals.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/counter_intervals.cc
new file mode 100644
index 0000000..97a6218
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/counter_intervals.cc
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/perfetto_sql/intrinsics/functions/counter_intervals.h"
+
+#include <algorithm>
+#include <cinttypes>
+#include <cstdint>
+#include <iterator>
+#include <memory>
+#include <numeric>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <variant>
+#include <vector>
+
+#include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/trace_processor/basic_types.h"
+#include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/db/runtime_table.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/types/counter.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/types/partitioned_intervals.h"
+#include "src/trace_processor/perfetto_sql/parser/function_util.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_bind.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_column.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_function.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_stmt.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_type.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_value.h"
+#include "src/trace_processor/sqlite/sqlite_utils.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto::trace_processor::perfetto_sql {
+namespace {
+
+struct CounterIntervals : public SqliteFunction<CounterIntervals> {
+  static constexpr char kName[] = "__intrinsic_counter_intervals";
+  static constexpr int kArgCount = 3;
+
+  struct UserDataContext {
+    PerfettoSqlEngine* engine;
+    StringPool* pool;
+  };
+
+  static void Step(sqlite3_context* ctx, int argc, sqlite3_value** argv) {
+    PERFETTO_DCHECK(argc == kArgCount);
+    const char* leading_str = sqlite::value::Text(argv[0]);
+    if (!leading_str) {
+      return sqlite::result::Error(
+          ctx, "interval intersect: column list cannot be null");
+    }
+
+    // TODO(mayzner): Support 'lagging'.
+    if (base::CaseInsensitiveEqual("lagging", leading_str)) {
+      return sqlite::result::Error(
+          ctx, "interval intersect: 'lagging' is not implemented");
+    }
+    if (!base::CaseInsensitiveEqual("leading", leading_str)) {
+      return sqlite::result::Error(ctx,
+                                   "interval intersect: second argument has to "
+                                   "be either 'leading' or 'lagging");
+    }
+
+    int64_t trace_end = sqlite::value::Int64(argv[1]);
+
+    // Get column names of return columns.
+    std::vector<std::string> ret_col_names{
+        "id", "ts", "dur", "track_id", "value", "next_value", "delta_value"};
+    std::vector<RuntimeTable::BuilderColumnType> col_types{
+        RuntimeTable::kInt,         // id
+        RuntimeTable::kInt,         // ts,
+        RuntimeTable::kInt,         // dur
+        RuntimeTable::kInt,         // track_id
+        RuntimeTable::kDouble,      // value
+        RuntimeTable::kNullDouble,  // next_value
+        RuntimeTable::kNullDouble,  // delta_value
+    };
+
+    auto partitioned_counter = sqlite::value::Pointer<PartitionedCounter>(
+        argv[2], PartitionedCounter::kName);
+    if (!partitioned_counter) {
+      SQLITE_ASSIGN_OR_RETURN(
+          ctx, std::unique_ptr<RuntimeTable> ret_table,
+          RuntimeTable::Builder(GetUserData(ctx)->pool, ret_col_names)
+              .Build(0));
+      return sqlite::result::UniquePointer(ctx, std::move(ret_table), "TABLE");
+    }
+
+    RuntimeTable::Builder builder(GetUserData(ctx)->pool, ret_col_names,
+                                  col_types);
+
+    uint32_t rows_count = 0;
+    for (auto track_counter = partitioned_counter->partitions_map.GetIterator();
+         track_counter; ++track_counter) {
+      int64_t track_id = track_counter.key();
+      const auto& cols = track_counter.value();
+      size_t r_count = cols.id.size();
+      rows_count += r_count;
+
+      // Id
+      builder.AddNonNullIntegersUnchecked(0, cols.id);
+      // Ts
+      builder.AddNonNullIntegersUnchecked(1, cols.ts);
+
+      // Dur
+      std::vector<int64_t> dur(r_count);
+      for (size_t i = 0; i < r_count - 1; i++) {
+        dur[i] = cols.ts[i + 1] - cols.ts[i];
+      }
+      dur[r_count - 1] = trace_end - cols.ts.back();
+      builder.AddNonNullIntegersUnchecked(2, dur);
+
+      // Track id
+      builder.AddIntegers(3, track_id, static_cast<uint32_t>(r_count));
+      // Value
+      builder.AddNonNullDoublesUnchecked(4, cols.val);
+
+      // Next value
+      std::vector<double> next_vals(cols.val.begin() + 1, cols.val.end());
+      builder.AddNullDoublesUnchecked(5, next_vals);
+      builder.AddNull(5);
+
+      // Delta value
+      std::vector<double> deltas(r_count - 1);
+      for (size_t i = 0; i < r_count - 1; i++) {
+        deltas[i] = cols.val[i + 1] - cols.val[i];
+      }
+      builder.AddNull(6);
+      builder.AddNullDoublesUnchecked(6, deltas);
+    }
+
+    SQLITE_ASSIGN_OR_RETURN(ctx, std::unique_ptr<RuntimeTable> ret_tab,
+                            std::move(builder).Build(rows_count));
+
+    return sqlite::result::UniquePointer(ctx, std::move(ret_tab), "TABLE");
+  }
+};
+
+}  // namespace
+
+base::Status RegisterCounterIntervalsFunctions(PerfettoSqlEngine& engine,
+                                               StringPool* pool) {
+  return engine.RegisterSqliteFunction<CounterIntervals>(
+      std::make_unique<CounterIntervals::UserDataContext>(
+          CounterIntervals::UserDataContext{&engine, pool}));
+}
+
+}  // namespace perfetto::trace_processor::perfetto_sql
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/counter_intervals.h b/src/trace_processor/perfetto_sql/intrinsics/functions/counter_intervals.h
new file mode 100644
index 0000000..dc0249a
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/counter_intervals.h
@@ -0,0 +1,32 @@
+/*
+ * 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_FUNCTIONS_COUNTER_INTERVALS_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_FUNCTIONS_COUNTER_INTERVALS_H_
+
+#include "perfetto/base/status.h"
+#include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
+
+namespace perfetto::trace_processor::perfetto_sql {
+
+// Registers all interval intersect related functions with |engine|.
+base::Status RegisterCounterIntervalsFunctions(PerfettoSqlEngine& engine,
+                                               StringPool* pool);
+
+}  // namespace perfetto::trace_processor::perfetto_sql
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_FUNCTIONS_COUNTER_INTERVALS_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/create_function.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/create_function.cc
index a7fc1d4..934ed4e 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/create_function.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/create_function.cc
@@ -21,8 +21,8 @@
 
 #include "perfetto/base/status.h"
 #include "perfetto/trace_processor/basic_types.h"
-#include "src/trace_processor/perfetto_sql/engine/function_util.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
+#include "src/trace_processor/perfetto_sql/parser/function_util.h"
 #include "src/trace_processor/sqlite/scoped_db.h"
 #include "src/trace_processor/sqlite/sql_source.h"
 #include "src/trace_processor/sqlite/sqlite_engine.h"
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/create_view_function.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/create_view_function.cc
index f1483ea..9270e06 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/create_view_function.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/create_view_function.cc
@@ -24,8 +24,8 @@
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/trace_processor/basic_types.h"
 #include "src/trace_processor/containers/null_term_string_view.h"
-#include "src/trace_processor/perfetto_sql/engine/function_util.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
+#include "src/trace_processor/perfetto_sql/parser/function_util.h"
 #include "src/trace_processor/sqlite/sql_source.h"
 #include "src/trace_processor/sqlite/sqlite_utils.h"
 #include "src/trace_processor/util/status_macros.h"
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/graph_scan.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/graph_scan.cc
index 1df93ad..62aad8b 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/graph_scan.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/graph_scan.cc
@@ -32,12 +32,12 @@
 #include "perfetto/ext/base/string_utils.h"
 #include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/db/runtime_table.h"
-#include "src/trace_processor/perfetto_sql/engine/function_util.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/types/array.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/types/node.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/types/row_dataframe.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/types/value.h"
+#include "src/trace_processor/perfetto_sql/parser/function_util.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_bind.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_column.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_function.h"
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/interval_intersect.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/interval_intersect.cc
index 96ee292..93e1f60 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/interval_intersect.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/interval_intersect.cc
@@ -38,9 +38,9 @@
 #include "src/trace_processor/containers/interval_intersector.h"
 #include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/db/runtime_table.h"
-#include "src/trace_processor/perfetto_sql/engine/function_util.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/types/partitioned_intervals.h"
+#include "src/trace_processor/perfetto_sql/parser/function_util.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_bind.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_column.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_function.h"
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/layout_functions.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/layout_functions.cc
index 4029384..45df5ca 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/layout_functions.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/layout_functions.cc
@@ -147,15 +147,19 @@
       GetOrCreateAggregationContext(ctx);
   RETURN_IF_ERROR(slice_packer.status());
 
-  base::StatusOr<SqlValue> ts =
-      sqlite::utils::ExtractArgument(argc, argv, "ts", 0, SqlValue::kLong);
-  RETURN_IF_ERROR(ts.status());
+  ASSIGN_OR_RETURN(SqlValue ts, sqlite::utils::ExtractArgument(
+                                    argc, argv, "ts", 0, SqlValue::kLong));
+  if (ts.AsLong() < 0) {
+    return base::ErrStatus("ts cannot be negative.");
+  }
 
-  base::StatusOr<SqlValue> dur =
-      sqlite::utils::ExtractArgument(argc, argv, "dur", 1, SqlValue::kLong);
-  RETURN_IF_ERROR(dur.status());
+  ASSIGN_OR_RETURN(SqlValue dur, sqlite::utils::ExtractArgument(
+                                     argc, argv, "dur", 1, SqlValue::kLong));
+  if (dur.AsLong() < -1) {
+    return base::ErrStatus("dur cannot be < -1.");
+  }
 
-  return slice_packer.value()->AddSlice(ts->AsLong(), dur.value().AsLong());
+  return slice_packer.value()->AddSlice(ts.AsLong(), dur.AsLong());
 }
 
 struct InternalLayout : public SqliteWindowFunction {
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/type_builders.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/type_builders.cc
index 4ce84d5..93ce52d 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/type_builders.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/type_builders.cc
@@ -19,9 +19,11 @@
 #include <algorithm>
 #include <cstddef>
 #include <cstdint>
+#include <functional>
 #include <limits>
 #include <memory>
 #include <optional>
+#include <queue>
 #include <string>
 #include <utility>
 #include <variant>
@@ -31,11 +33,13 @@
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/hash.h"
+#include "perfetto/ext/base/small_vector.h"
 #include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/basic_types.h"
 #include "src/trace_processor/containers/interval_intersector.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/types/array.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/types/counter.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/types/node.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/types/partitioned_intervals.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/types/row_dataframe.h"
@@ -304,25 +308,29 @@
   struct AggCtx : SqliteAggregateContext<AggCtx> {
     perfetto_sql::PartitionedTable partitions;
     std::vector<SqlValue> tmp_vals;
-    uint64_t max_ts = std::numeric_limits<uint32_t>::min();
+    uint64_t last_interval_start = 0;
   };
 
   static void Step(sqlite3_context* ctx, int rargc, sqlite3_value** argv) {
     auto argc = static_cast<uint32_t>(rargc);
     PERFETTO_DCHECK(argc >= kMinArgCount);
     auto& agg_ctx = AggCtx::GetOrCreateContextForStep(ctx);
-    auto& parts = AggCtx::GetOrCreateContextForStep(ctx).partitions;
 
     // Fetch and validate the interval.
     Interval interval;
     interval.id = static_cast<uint32_t>(sqlite::value::Int64(argv[0]));
     interval.start = static_cast<uint64_t>(sqlite::value::Int64(argv[1]));
-    if (interval.start < agg_ctx.max_ts) {
+    if (interval.start < agg_ctx.last_interval_start) {
+      if (sqlite::value::Int64(argv[1]) < 0) {
+        sqlite::result::Error(
+            ctx, "Interval intersect only accepts positive `ts` values.");
+        return;
+      }
       sqlite::result::Error(
           ctx, "Interval intersect requires intervals to be sorted by ts.");
       return;
     }
-    agg_ctx.max_ts = interval.start;
+    agg_ctx.last_interval_start = interval.start;
     int64_t dur = sqlite::value::Int64(argv[2]);
     if (dur < 1) {
       sqlite::result::Error(
@@ -332,6 +340,7 @@
     interval.end = interval.start + static_cast<uint64_t>(dur);
 
     // Fast path for no partitions.
+    auto& parts = agg_ctx.partitions;
     if (argc == kMinArgCount) {
       auto& part = parts.partitions_map[0];
       part.intervals.push_back(interval);
@@ -407,6 +416,55 @@
   }
 };
 
+struct CounterPerTrackAgg
+    : public SqliteAggregateFunction<perfetto_sql::PartitionedCounter> {
+  static constexpr char kName[] = "__intrinsic_counter_per_track_agg";
+  static constexpr int kArgCount = 4;
+  struct AggCtx : SqliteAggregateContext<AggCtx> {
+    perfetto_sql::PartitionedCounter tracks;
+  };
+
+  static void Step(sqlite3_context* ctx, int rargc, sqlite3_value** argv) {
+    auto argc = static_cast<uint32_t>(rargc);
+    PERFETTO_DCHECK(argc == kArgCount);
+    auto& tracks = AggCtx::GetOrCreateContextForStep(ctx).tracks;
+
+    // Fetch columns.
+    int64_t id = sqlite::value::Int64(argv[0]);
+    int64_t ts = sqlite::value::Int64(argv[1]);
+    int64_t track_id = static_cast<uint32_t>(sqlite::value::Int64(argv[2]));
+    double val = sqlite::value::Double(argv[3]);
+
+    auto* new_rows_track = tracks.partitions_map.Find(track_id);
+    if (!new_rows_track) {
+      new_rows_track = tracks.partitions_map.Insert(track_id, {}).first;
+    } else if (std::equal_to<double>()(new_rows_track->val.back(), val)) {
+      // TODO(mayzner): This algorithm is focused on "leading" counters - if the
+      // counter before had the same value we can safely remove the new one as
+      // it adds no value. In the future we should also support "lagging" - if
+      // the next one has the same value as the previous, we should remove the
+      // previous.
+      return;
+    }
+
+    new_rows_track->id.push_back(id);
+    new_rows_track->ts.push_back(ts);
+    new_rows_track->val.push_back(val);
+  }
+
+  static void Final(sqlite3_context* ctx) {
+    auto raw_agg_ctx = AggCtx::GetContextOrNullForFinal(ctx);
+    if (!raw_agg_ctx) {
+      return sqlite::result::Null(ctx);
+    }
+    return sqlite::result::UniquePointer(
+        ctx,
+        std::make_unique<perfetto_sql::PartitionedCounter>(
+            std::move(raw_agg_ctx.get()->tracks)),
+        perfetto_sql::PartitionedCounter::kName);
+  }
+};
+
 }  // namespace
 
 base::Status RegisterTypeBuilderFunctions(PerfettoSqlEngine& engine) {
@@ -417,6 +475,8 @@
   RETURN_IF_ERROR(
       engine.RegisterSqliteAggregateFunction<IntervalTreeIntervalsAgg>(
           nullptr));
+  RETURN_IF_ERROR(
+      engine.RegisterSqliteAggregateFunction<CounterPerTrackAgg>(nullptr));
   return engine.RegisterSqliteAggregateFunction<NodeAgg>(nullptr);
 }
 
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn b/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
index 57ab611..49a4a81 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
@@ -20,8 +20,6 @@
   sources = [
     "counter_mipmap_operator.cc",
     "counter_mipmap_operator.h",
-    "interval_intersect_operator.cc",
-    "interval_intersect_operator.h",
     "slice_mipmap_operator.cc",
     "slice_mipmap_operator.h",
     "span_join_operator.cc",
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/interval_intersect_operator.cc b/src/trace_processor/perfetto_sql/intrinsics/operators/interval_intersect_operator.cc
deleted file mode 100644
index 4d0bb02..0000000
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/interval_intersect_operator.cc
+++ /dev/null
@@ -1,567 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/perfetto_sql/intrinsics/operators/interval_intersect_operator.h"
-
-#include <sqlite3.h>
-#include <algorithm>
-#include <cerrno>
-#include <cstddef>
-#include <cstdint>
-#include <iterator>
-#include <memory>
-#include <optional>
-#include <string>
-#include <utility>
-
-#include "perfetto/base/logging.h"
-#include "perfetto/base/status.h"
-#include "perfetto/ext/base/flat_hash_map.h"
-#include "perfetto/ext/base/hash.h"
-#include "perfetto/ext/base/status_or.h"
-#include "perfetto/ext/base/string_utils.h"
-#include "perfetto/trace_processor/basic_types.h"
-#include "perfetto/trace_processor/status.h"
-#include "src/trace_processor/containers/interval_tree.h"
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
-#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
-#include "src/trace_processor/sqlite/module_lifecycle_manager.h"
-#include "src/trace_processor/sqlite/sqlite_utils.h"
-#include "src/trace_processor/util/status_macros.h"
-
-namespace perfetto::trace_processor {
-namespace {
-
-using Op = IntervalIntersectOperator;
-using Cursor = Op::Cursor;
-using Manager = sqlite::ModuleStateManager<Op>;
-using ColumnsMap = Op::SchemaToTableColumnMap;
-
-constexpr char kSliceSchema[] = R"(
-  CREATE TABLE x(
-    tab TEXT HIDDEN,
-    exposed_cols_str TEXT HIDDEN,
-    ts BIGINT,
-    ts_end BIGINT,
-    id BIGINT,
-    c0 ANY,
-    c1 ANY,
-    c2 ANY,
-    c3 ANY,
-    c4 ANY,
-    c5 ANY,
-    c6 ANY,
-    c7 ANY,
-    c8 ANY,
-    PRIMARY KEY(id)
-  ) WITHOUT ROWID
-)";
-
-enum SchemaColumnIds {
-  kTableName = 0,
-  kExposedCols = 1,
-  kTs = 2,
-  kTsEnd = 3,
-  kId = 4,
-  kAdditional = 5,
-  kMaxCol = 13
-};
-
-constexpr uint32_t kArgsCount = 2;
-
-inline void HashSqlValue(base::Hasher& h, const SqlValue& v) {
-  switch (v.type) {
-    case SqlValue::Type::kString:
-      h.Update(v.AsString());
-      break;
-    case SqlValue::Type::kDouble:
-      h.Update(v.AsDouble());
-      break;
-    case SqlValue::Type::kLong:
-      h.Update(v.AsLong());
-      break;
-    case SqlValue::Type::kBytes:
-      PERFETTO_FATAL("Wrong type");
-      break;
-    case SqlValue::Type::kNull:
-      h.Update(nullptr);
-      break;
-  }
-  return;
-}
-
-base::StatusOr<uint32_t> ColIdForName(const Table* t,
-                                      const std::string& col_name,
-                                      const std::string& table_name) {
-  auto x = t->ColumnIdxFromName(col_name);
-  if (!x.has_value()) {
-    return base::ErrStatus("interval_intersect: No column '%s' in table '%s'",
-                           col_name.c_str(), table_name.c_str());
-  }
-  return *x;
-}
-
-base::StatusOr<Cursor::TreesMap> CreateIntervalTrees(
-    const Table* t,
-    const std::string& table_name,
-    const ColumnsMap& cols) {
-  uint32_t ts_col_idx = 0;
-  ASSIGN_OR_RETURN(ts_col_idx, ColIdForName(t, "ts", table_name));
-  uint32_t ts_end_col_idx = 0;
-  ASSIGN_OR_RETURN(ts_end_col_idx, ColIdForName(t, "ts_end", table_name));
-  uint32_t id_col_idx = 0;
-  ASSIGN_OR_RETURN(id_col_idx, ColIdForName(t, "id", table_name));
-
-  std::vector<Op::SchemaCol> cols_for_tree;
-  for (const auto& c : cols) {
-    if (c) {
-      cols_for_tree.push_back(*c);
-    }
-  }
-
-  base::FlatHashMap<Cursor::TreesKey, std::vector<Interval>> sorted_intervals;
-  for (Table::Iterator it = t->IterateRows(); it; ++it) {
-    Interval i;
-    i.start = static_cast<uint64_t>(it.Get(ts_col_idx).AsLong());
-    i.end = static_cast<uint64_t>(it.Get(ts_end_col_idx).AsLong());
-    i.id = static_cast<uint32_t>(it.Get(id_col_idx).AsLong());
-
-    base::Hasher h;
-    for (const auto& c : cols_for_tree) {
-      SqlValue v = it.Get(c);
-      HashSqlValue(h, v);
-    }
-    sorted_intervals[h.digest()].push_back(i);
-  }
-
-  Cursor::TreesMap ret;
-  for (auto it = sorted_intervals.GetIterator(); it; ++it) {
-    IntervalTree x(it.value());
-    ret[it.key()] = std::make_unique<IntervalTree>(std::move(x));
-  }
-  return std::move(ret);
-}
-
-base::StatusOr<SqlValue> GetRhsValue(sqlite3_index_info* info,
-                                     SchemaColumnIds col) {
-  sqlite3_value* val = nullptr;
-
-  int ret = -1;
-  for (int i = 0; i < info->nConstraint; ++i) {
-    auto c = info->aConstraint[i];
-    if (sqlite::utils::IsOpEq(c.op) && c.iColumn == col)
-      ret = sqlite3_vtab_rhs_value(info, i, &val);
-  }
-  if (ret != SQLITE_OK) {
-    return base::ErrStatus("Invalid RHS value.");
-  }
-
-  return sqlite::utils::SqliteValueToSqlValue(val);
-}
-
-base::StatusOr<const Table*> GetTableFromRhsValue(PerfettoSqlEngine* engine,
-                                                  sqlite3_index_info* info) {
-  ASSIGN_OR_RETURN(SqlValue table_name_val, GetRhsValue(info, kTableName));
-  if (table_name_val.type != SqlValue::kString) {
-    return base::ErrStatus("Table name is not a string");
-  }
-
-  const std::string table_name = table_name_val.AsString();
-  const Table* t = engine->GetTableOrNull(table_name);
-  if (!t) {
-    return base::ErrStatus("Table not registered");
-  }
-  return t;
-}
-
-base::StatusOr<ColumnsMap> GetExposedColumns(
-    const std::string& exposed_cols_str,
-    const Table* tab) {
-  ColumnsMap ret;
-  for (const std::string& col : base::SplitString(exposed_cols_str, ",")) {
-    std::string col_name = base::TrimWhitespace(col);
-    auto table_i = tab->ColumnIdxFromName(col_name);
-    if (!table_i) {
-      return base::ErrStatus("Didn't find column '%s'", col_name.c_str());
-    }
-    uint32_t schema_idx =
-        *base::CStringToUInt32(
-            std::string(col_name.begin() + 1, col_name.end()).c_str()) +
-        kAdditional;
-    ret[schema_idx] = static_cast<uint16_t>(*table_i);
-  }
-  return ret;
-}
-
-base::Status CreateCursorInnerData(Cursor::InnerData* inner,
-                                   PerfettoSqlEngine* engine,
-                                   const std::string& table_name,
-                                   const ColumnsMap& cols) {
-  // Build the tree for the runtime table if possible
-  const Table* t = engine->GetTableOrNull(table_name);
-  if (!t) {
-    return base::ErrStatus("interval_intersect operator: table not found");
-  }
-  ASSIGN_OR_RETURN(inner->trees, CreateIntervalTrees(t, table_name, cols));
-  return base::OkStatus();
-}
-
-base::Status CreateCursorOuterData(const Table* t,
-                                   Cursor::OuterData* outer,
-                                   const std::string& table_name) {
-  outer->it = std::make_unique<Table::Iterator>(t->IterateRows());
-
-  ASSIGN_OR_RETURN(outer->additional_cols[kId],
-                   ColIdForName(t, "id", table_name));
-  ASSIGN_OR_RETURN(outer->additional_cols[kTs],
-                   ColIdForName(t, "ts", table_name));
-  ASSIGN_OR_RETURN(outer->additional_cols[kTsEnd],
-                   ColIdForName(t, "ts_end", table_name));
-
-  return base::OkStatus();
-}
-
-}  // namespace
-
-int IntervalIntersectOperator::Connect(sqlite3* db,
-                                       void* raw_ctx,
-                                       int,
-                                       const char* const* argv,
-                                       sqlite3_vtab** vtab,
-                                       char**) {
-  // No args because we are not creating vtab, not like mipmap op.
-  if (int ret = sqlite3_declare_vtab(db, kSliceSchema); ret != SQLITE_OK) {
-    return ret;
-  }
-
-  // Create the state to access the engine in Filter.
-  auto ctx = GetContext(raw_ctx);
-  auto state = std::make_unique<State>();
-  state->engine = ctx->engine;
-
-  std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
-  res->state = ctx->manager.OnCreate(argv, std::move(state));
-  *vtab = res.release();
-  return SQLITE_OK;
-}
-
-int IntervalIntersectOperator::Disconnect(sqlite3_vtab* vtab) {
-  std::unique_ptr<Vtab> tab(GetVtab(vtab));
-  sqlite::ModuleStateManager<IntervalIntersectOperator>::OnDestroy(tab->state);
-  return SQLITE_OK;
-}
-
-int IntervalIntersectOperator::BestIndex(sqlite3_vtab* t,
-                                         sqlite3_index_info* info) {
-  int n = info->nConstraint;
-
-  // Validate `table_name` constraint. We expect it to be a constraint on
-  // equality and on the kTableName column.
-  base::Status args_status =
-      sqlite::utils::ValidateFunctionArguments(info, kArgsCount, [](int c) {
-        return c == SchemaColumnIds::kTableName ||
-               c == SchemaColumnIds::kExposedCols;
-      });
-
-  PERFETTO_CHECK(args_status.ok());
-  if (!args_status.ok()) {
-    return SQLITE_CONSTRAINT;
-  }
-
-  // Find real rows count
-  PerfettoSqlEngine* engine = Manager::GetState(GetVtab(t)->state)->engine;
-  SQLITE_ASSIGN_OR_RETURN(t, const Table* tab,
-                          GetTableFromRhsValue(engine, info));
-  if (!t) {
-    return sqlite::utils::SetError(t, "Table not registered");
-  }
-  uint32_t rows_count = tab->row_count();
-  info->estimatedRows = rows_count;
-
-  // Count usable constraints among args and required schema.
-  uint32_t count_usable = 0;
-  for (int i = 0; i < n; ++i) {
-    auto c = info->aConstraint[i];
-    if (c.iColumn < kAdditional) {
-      count_usable += c.usable;
-    }
-  }
-
-  // There is nothing more to do for only args constraints, which happens for
-  // the kOuter operator.
-  if (count_usable == kArgsCount) {
-    info->idxNum = kOuter;
-    info->estimatedCost = rows_count;
-    return SQLITE_OK;
-  }
-
-  // For inner we expect all constraints to be usable.
-  PERFETTO_CHECK(count_usable == 4);
-  if (count_usable != 4) {
-    return SQLITE_CONSTRAINT;
-  }
-
-  info->idxNum = kInner;
-
-  // Cost of querying centered interval tree.
-  info->estimatedCost = log2(rows_count);
-
-  // We are now doing BestIndex of kInner.
-
-  auto ts_found = false;
-  auto ts_end_found = false;
-  int argv_index = kAdditional;
-  auto* s = Manager::GetState(GetVtab(t)->state);
-
-  for (int i = 0; i < n; ++i) {
-    const auto& c = info->aConstraint[i];
-
-    // Ignore table_name constraints as we validated it before.
-    if (c.iColumn == kTableName || c.iColumn == kExposedCols) {
-      continue;
-    }
-
-    // We should omit all constraints.
-    // TODO(mayzner): Remove after we support handling other columns.
-    auto& usage = info->aConstraintUsage[i];
-    usage.omit = true;
-
-    // The constraints we are looking for is `A.ts < B.ts_end AND A.ts_end >
-    // B.ts`. That is why for `ts` column we can only have `kLt` operator and
-    // for `ts_end` only `kGt`.
-
-    // Add `ts` constraint.
-    if (c.iColumn == kTs && !ts_found) {
-      ts_found = true;
-      if (!sqlite::utils::IsOpLt(c.op)) {
-        return sqlite::utils::SetError(
-            t, "interval_intersect operator: `ts` columns has wrong operation");
-      }
-      // The index is moved by one.
-      usage.argvIndex = kTs + 1;
-      continue;
-    }
-
-    // Add `ts_end` constraint.
-    if (c.iColumn == kTsEnd && !ts_end_found) {
-      ts_end_found = true;
-      if (!sqlite::utils::IsOpGt(c.op)) {
-        return sqlite::utils::SetError(t,
-                                       "interval_intersect operator: `ts_end` "
-                                       "columns has wrong operation");
-      }
-      usage.argvIndex = kTsEnd + 1;
-      continue;
-    }
-
-    if (c.iColumn >= kAdditional) {
-      if (!sqlite::utils::IsOpEq(c.op)) {
-        return sqlite::utils::SetError(t,
-                                       "interval_intersect operator: `ts_end` "
-                                       "columns has wrong operation");
-      }
-      usage.argvIndex = argv_index++;
-      s->argv_to_col_map[static_cast<size_t>(c.iColumn)] = usage.argvIndex;
-      continue;
-    }
-
-    return sqlite::utils::SetError(
-        t, "interval_intersect operator: wrong constraint");
-  }
-
-  return SQLITE_OK;
-}
-
-int IntervalIntersectOperator::Open(sqlite3_vtab*,
-                                    sqlite3_vtab_cursor** cursor) {
-  std::unique_ptr<Cursor> c = std::make_unique<Cursor>();
-  *cursor = c.release();
-  return SQLITE_OK;
-}
-
-int IntervalIntersectOperator::Close(sqlite3_vtab_cursor* cursor) {
-  std::unique_ptr<Cursor> c(GetCursor(cursor));
-  return SQLITE_OK;
-}
-
-int IntervalIntersectOperator::Filter(sqlite3_vtab_cursor* cursor,
-                                      int idxNum,
-                                      const char*,
-                                      int,
-                                      sqlite3_value** argv) {
-  auto* c = GetCursor(cursor);
-  c->type = static_cast<OperatorType>(idxNum);
-
-  auto* t = GetVtab(c->pVtab);
-  PerfettoSqlEngine* engine = Manager::GetState(t->state)->engine;
-
-  // Table name constraint.
-  auto table_name_sql_val = sqlite::utils::SqliteValueToSqlValue(argv[0]);
-  if (table_name_sql_val.type != SqlValue::kString) {
-    return sqlite::utils::SetError(
-        t, "interval_intersect operator: table name is not a string");
-  }
-  std::string table_name = table_name_sql_val.AsString();
-
-  // Exposed columns constraint.
-  auto exposed_cols_sql_val = sqlite::utils::SqliteValueToSqlValue(argv[1]);
-  if (exposed_cols_sql_val.type != SqlValue::kString) {
-    return sqlite::utils::SetError(
-        t, "interval_intersect operator: exposed columns is not a string");
-  }
-  std::string exposed_cols_str = exposed_cols_sql_val.AsString();
-
-  // If the cursor has different table cached or differenct cols reset the
-  // cursor.
-  if (c->table_name != table_name || exposed_cols_str != c->exposed_cols_str) {
-    c->inner.trees.Clear();
-    c->outer.it.reset();
-  }
-  c->exposed_cols_str = exposed_cols_str;
-
-  if (c->type == kOuter) {
-    // We expect this function to be called only once per table, so recreate
-    // this if needed.
-    c->table = engine->GetTableOrNull(table_name);
-    c->table_name = table_name;
-    SQLITE_ASSIGN_OR_RETURN(t, c->outer.additional_cols,
-                            GetExposedColumns(c->exposed_cols_str, c->table));
-    SQLITE_RETURN_IF_ERROR(
-        t, CreateCursorOuterData(c->table, &c->outer, table_name));
-    return SQLITE_OK;
-  }
-
-  PERFETTO_DCHECK(c->type == kInner);
-  const auto argv_map = Manager::GetState(GetVtab(t)->state)->argv_to_col_map;
-
-  // Create inner cursor if tree doesn't exist.
-  if (c->inner.trees.size() == 0) {
-    c->table = engine->GetTableOrNull(table_name);
-    c->table_name = table_name;
-    Op::SchemaToTableColumnMap exposed_cols_map;
-    SQLITE_ASSIGN_OR_RETURN(t, exposed_cols_map,
-                            GetExposedColumns(c->exposed_cols_str, c->table));
-    SchemaToTableColumnMap new_map;
-    for (uint32_t i = 0; i < Op::kSchemaColumnsCount; i++) {
-      if (argv_map[i]) {
-        new_map[i] = exposed_cols_map[i];
-      }
-    }
-
-    SQLITE_RETURN_IF_ERROR(
-        c->pVtab,
-        CreateCursorInnerData(&c->inner, engine, table_name, new_map));
-  }
-
-  // Query |c.tree| on the interval and materialize the results.
-  auto ts_constraint = sqlite::utils::SqliteValueToSqlValue(argv[kTs]);
-  if (ts_constraint.type != SqlValue::kLong) {
-    return sqlite::utils::SetError(
-        t, "interval_intersect operator: `ts` constraint has to be a number");
-  }
-
-  auto ts_end_constraint = sqlite::utils::SqliteValueToSqlValue(argv[kTsEnd]);
-  if (ts_end_constraint.type != SqlValue::kLong) {
-    return sqlite::utils::SetError(
-        t,
-        "interval_intersect operator: `ts_end` constraint has to be a number");
-  }
-
-  uint64_t end = static_cast<uint64_t>(ts_constraint.AsLong());
-  uint64_t start = static_cast<uint64_t>(ts_end_constraint.AsLong());
-
-  base::Hasher h;
-  for (uint32_t i = 0; i < argv_map.size(); i++) {
-    if (argv_map[i]) {
-      uint32_t x = *argv_map[i];
-      HashSqlValue(h, sqlite::utils::SqliteValueToSqlValue(argv[x - 1]));
-    }
-  }
-
-  c->inner.Query(start, end, h.digest());
-
-  return SQLITE_OK;
-}
-
-int IntervalIntersectOperator::Next(sqlite3_vtab_cursor* cursor) {
-  auto* c = GetCursor(cursor);
-
-  switch (c->type) {
-    case kInner:
-      c->inner.index++;
-      break;
-    case kOuter:
-      ++(*c->outer.it);
-      break;
-  }
-
-  return SQLITE_OK;
-}
-
-int IntervalIntersectOperator::Eof(sqlite3_vtab_cursor* cursor) {
-  auto* c = GetCursor(cursor);
-
-  switch (c->type) {
-    case kInner:
-      return c->inner.index >= c->inner.query_results.size();
-    case kOuter:
-      return !(*c->outer.it);
-  }
-  PERFETTO_FATAL("For GCC");
-}
-
-int IntervalIntersectOperator::Column(sqlite3_vtab_cursor* cursor,
-                                      sqlite3_context* ctx,
-                                      int N) {
-  auto* c = GetCursor(cursor);
-
-  if (c->type == kInner) {
-    PERFETTO_DCHECK(N == kId);
-    sqlite::result::Long(ctx, c->inner.GetResultId());
-    return SQLITE_OK;
-  }
-
-  PERFETTO_CHECK(c->type == kOuter);
-
-  switch (N) {
-    case kTs:
-      sqlite::result::Long(ctx, c->outer.Get(kTs).AsLong());
-      break;
-    case kTsEnd:
-      sqlite::result::Long(ctx, c->outer.Get(kTsEnd).AsLong());
-      break;
-    case kId:
-      sqlite::result::Long(ctx, c->outer.Get(kId).AsLong());
-      break;
-    case kExposedCols:
-    case kTableName:
-      return sqlite::utils::SetError(
-          GetVtab(cursor->pVtab),
-          "interval_intersect operator: invalid column");
-    default:
-      PERFETTO_DCHECK(N >= kAdditional && N <= kMaxCol);
-      sqlite::utils::ReportSqlValue(ctx, c->outer.Get(N));
-      break;
-  }
-
-  return SQLITE_OK;
-}
-
-int IntervalIntersectOperator::Rowid(sqlite3_vtab_cursor*, sqlite_int64*) {
-  return SQLITE_ERROR;
-}
-
-}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/interval_intersect_operator.h b/src/trace_processor/perfetto_sql/intrinsics/operators/interval_intersect_operator.h
deleted file mode 100644
index 896d1a7..0000000
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/interval_intersect_operator.h
+++ /dev/null
@@ -1,139 +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.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_INTERVAL_INTERSECT_OPERATOR_H_
-#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_INTERVAL_INTERSECT_OPERATOR_H_
-
-#include <sqlite3.h>
-#include <array>
-#include <cstdint>
-#include <memory>
-#include <vector>
-
-#include "perfetto/ext/base/hash.h"
-#include "perfetto/trace_processor/basic_types.h"
-#include "src/trace_processor/containers/bit_vector.h"
-#include "src/trace_processor/containers/interval_tree.h"
-#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
-#include "src/trace_processor/sqlite/bindings/sqlite_module.h"
-#include "src/trace_processor/sqlite/module_lifecycle_manager.h"
-
-namespace perfetto::trace_processor {
-
-struct IntervalIntersectOperator : sqlite::Module<IntervalIntersectOperator> {
-  static constexpr uint16_t kSchemaColumnsCount = 16;
-  using SchemaCol = uint16_t;
-  using SchemaToTableColumnMap =
-      std::array<std::optional<SchemaCol>, kSchemaColumnsCount>;
-
-  enum OperatorType { kInner = 0, kOuter = 1 };
-  struct State {
-    PerfettoSqlEngine* engine;
-    std::array<std::optional<uint16_t>, kSchemaColumnsCount> argv_to_col_map{};
-  };
-
-  struct Context {
-    explicit Context(PerfettoSqlEngine* _engine) : engine(_engine) {}
-    PerfettoSqlEngine* engine;
-    sqlite::ModuleStateManager<IntervalIntersectOperator> manager;
-  };
-
-  struct Vtab : sqlite::Module<IntervalIntersectOperator>::Vtab {
-    sqlite::ModuleStateManager<IntervalIntersectOperator>::PerVtabState* state;
-  };
-
-  struct Cursor : sqlite::Module<IntervalIntersectOperator>::Cursor {
-    using TreesKey = uint64_t;
-    using TreesMap = base::FlatHashMap<TreesKey,
-                                       std::unique_ptr<IntervalTree>,
-                                       base::AlreadyHashed<TreesKey>>;
-
-    struct InnerData {
-      TreesMap trees;
-      SchemaToTableColumnMap additional_cols;
-
-      std::vector<uint32_t> query_results;
-      uint32_t index = 0;
-
-      inline uint32_t GetResultId() const { return query_results[index]; }
-      inline void Query(uint64_t start,
-                        uint64_t end,
-                        const TreesKey& tree_key) {
-        query_results.clear();
-        index = 0;
-        auto* tree_ptr = trees.Find(tree_key);
-        if (!tree_ptr) {
-          return;
-        }
-        (*tree_ptr)->FindOverlaps(start, end, query_results);
-      }
-    };
-
-    struct OuterData {
-      std::unique_ptr<Table::Iterator> it;
-      SchemaToTableColumnMap additional_cols;
-
-      inline SqlValue Get(int col) {
-        return it->Get(*additional_cols[static_cast<size_t>(col)]);
-      }
-    };
-
-    OperatorType type;
-    std::string table_name;
-    std::string exposed_cols_str;
-    const Table* table = nullptr;
-
-    // Only one of those can be non null.
-    InnerData inner;
-    OuterData outer;
-  };
-
-  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
-
-#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_INTERVAL_INTERSECT_OPERATOR_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc b/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc
index 2a6063e..fcfd7f0 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc
@@ -226,14 +226,6 @@
     auto col = cols[i];
     if (IsRequiredColumn(col.second)) {
       ++required_columns_found;
-      if (col.first != SqlValue::Type::kLong &&
-          col.first != SqlValue::Type::kNull) {
-        return base::ErrStatus(
-            "SPAN_JOIN: Invalid type for column '%s' in table %s: expect LONG "
-            "or NULL, but %s found",
-            col.second.c_str(), desc.name.c_str(),
-            sqlite::utils::SqlValueTypeToString(col.first));
-      }
     }
     if (base::Contains(col.second, ",")) {
       return base::ErrStatus("SPAN_JOIN: column '%s' cannot contain any ','",
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 7be5b26..a73c7d9 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn
@@ -41,6 +41,8 @@
     "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",
   ]
@@ -62,6 +64,9 @@
     "../../../tables",
     "../../../types",
     "../../../util",
+    "../../../util:descriptors",
+    "../../../util:proto_to_args_parser",
+    "../../../util:winscope_proto_mapping",
     "../../engine",
   ]
   public_deps = [ ":interface" ]
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 aa882dd..22999ee 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
@@ -80,6 +80,21 @@
     ],
     parent=FLOW_TABLE)
 
+ARGS_WITH_DEFAULTS_TABLE = Table(
+    python_module=__file__,
+    class_name='WinscopeArgsWithDefaultsTable',
+    sql_name='__intrinsic_winscope_proto_to_args_with_defaults',
+    columns=[
+        C("table_name", CppString(), flags=ColumnFlag.HIDDEN),
+        C('base64_proto_id', CppUint32()),
+        C('flat_key', CppString()),
+        C('key', CppString()),
+        C('int_value', CppOptional(CppInt64())),
+        C('string_value', CppOptional(CppString())),
+        C('real_value', CppOptional(CppDouble())),
+        C('value_type', CppString()),
+    ])
+
 DESCENDANT_SLICE_TABLE = Table(
     python_module=__file__,
     class_name="DescendantSliceTable",
@@ -169,6 +184,7 @@
     ANCESTOR_SLICE_TABLE,
     ANCESTOR_STACK_PROFILE_CALLSITE_TABLE,
     CONNECTED_FLOW_TABLE,
+    ARGS_WITH_DEFAULTS_TABLE,
     DESCENDANT_SLICE_BY_STACK_TABLE,
     DESCENDANT_SLICE_TABLE,
     DFS_WEIGHT_BOUNDED_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
new file mode 100644
index 0000000..ee8e600
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.cc
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h"
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/base64.h"
+#include "perfetto/ext/base/status_or.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"
+#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/tables_py.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/descriptors.h"
+#include "src/trace_processor/util/proto_to_args_parser.h"
+#include "src/trace_processor/util/status_macros.h"
+#include "src/trace_processor/util/winscope_proto_mapping.h"
+
+namespace perfetto::trace_processor {
+namespace tables {
+WinscopeArgsWithDefaultsTable::~WinscopeArgsWithDefaultsTable() = default;
+}  // namespace tables
+
+namespace {
+using Row = tables::WinscopeArgsWithDefaultsTable::Row;
+
+class Delegate : public util::ProtoToArgsParser::Delegate {
+ public:
+  using Key = util::ProtoToArgsParser::Key;
+  explicit Delegate(StringPool* pool,
+                    const uint32_t base64_proto_id,
+                    tables::WinscopeArgsWithDefaultsTable* table)
+      : pool_(pool), base64_proto_id_(base64_proto_id), table_(table) {}
+
+  void AddInteger(const Key& key, int64_t res) override {
+    Row r;
+    r.int_value = res;
+    SetColumnsAndInsertRow(key, r);
+  }
+  void AddUnsignedInteger(const Key& key, uint64_t res) override {
+    Row r;
+    r.int_value = res;
+    SetColumnsAndInsertRow(key, r);
+  }
+  void AddString(const Key& key, const protozero::ConstChars& res) override {
+    Row r;
+    r.string_value = pool_->InternString(base::StringView((res.ToStdString())));
+    SetColumnsAndInsertRow(key, r);
+  }
+  void AddString(const Key& key, const std::string& res) override {
+    Row r;
+    r.string_value = pool_->InternString(base::StringView(res));
+    SetColumnsAndInsertRow(key, r);
+  }
+  void AddDouble(const Key& key, double res) override {
+    Row r;
+    r.real_value = res;
+    SetColumnsAndInsertRow(key, r);
+  }
+  void AddBoolean(const Key& key, bool res) override {
+    Row r;
+    r.int_value = res;
+    SetColumnsAndInsertRow(key, r);
+  }
+  void AddBytes(const Key& key, const protozero::ConstBytes& res) override {
+    Row r;
+    r.string_value = pool_->InternString(base::StringView((res.ToStdString())));
+    SetColumnsAndInsertRow(key, r);
+  }
+  void AddNull(const Key& key) override {
+    Row r;
+    SetColumnsAndInsertRow(key, r);
+  }
+  void AddPointer(const Key&, const void*) override {
+    PERFETTO_FATAL("Unsupported");
+  }
+  bool AddJson(const Key&, const protozero::ConstChars&) override {
+    PERFETTO_FATAL("Unsupported");
+  }
+  size_t GetArrayEntryIndex(const std::string&) override {
+    PERFETTO_FATAL("Unsupported");
+  }
+  size_t IncrementArrayEntryIndex(const std::string&) override {
+    PERFETTO_FATAL("Unsupported");
+  }
+  PacketSequenceStateGeneration* seq_state() override { return nullptr; }
+
+ private:
+  InternedMessageView* GetInternedMessageView(uint32_t, uint64_t) override {
+    return nullptr;
+  }
+
+  void SetColumnsAndInsertRow(const Key& key, Row& row) {
+    row.key = pool_->InternString(base::StringView(key.key));
+    row.flat_key = pool_->InternString(base::StringView(key.flat_key));
+    row.base64_proto_id = base64_proto_id_;
+    table_->Insert(row);
+  }
+
+  StringPool* pool_;
+  const uint32_t base64_proto_id_;
+  tables::WinscopeArgsWithDefaultsTable* table_;
+};
+
+base::Status InsertRows(
+    const Table& static_table,
+    tables::WinscopeArgsWithDefaultsTable* inflated_args_table,
+    const std::string& proto_name,
+    const std::vector<uint32_t>* allowed_fields,
+    DescriptorPool& descriptor_pool,
+    StringPool* string_pool) {
+  util::ProtoToArgsParser args_parser{descriptor_pool};
+  const auto base64_proto_id_col_idx =
+      static_table.ColumnIdxFromName("base64_proto_id").value();
+  const auto base_64_proto_col_idx =
+      static_table.ColumnIdxFromName("base64_proto").value();
+
+  std::unordered_set<uint32_t> inflated_protos;
+  for (auto it = static_table.IterateRows(); it; ++it) {
+    const auto base64_proto_id =
+        static_cast<uint32_t>(it.Get(base64_proto_id_col_idx).AsLong());
+    if (inflated_protos.count(base64_proto_id) > 0) {
+      continue;
+    }
+    inflated_protos.insert(base64_proto_id);
+    const auto* raw_proto = it.Get(base_64_proto_col_idx).AsString();
+    const auto blob = *base::Base64Decode(raw_proto);
+    const auto cb = protozero::ConstBytes{
+        reinterpret_cast<const uint8_t*>(blob.data()), blob.size()};
+    Delegate delegate(string_pool, base64_proto_id, inflated_args_table);
+    RETURN_IF_ERROR(args_parser.ParseMessage(cb, proto_name, allowed_fields,
+                                             delegate, nullptr, true));
+  }
+  return base::OkStatus();
+}
+}  // namespace
+
+WinscopeProtoToArgsWithDefaults::WinscopeProtoToArgsWithDefaults(
+    StringPool* string_pool,
+    PerfettoSqlEngine* engine,
+    TraceProcessorContext* context)
+    : string_pool_(string_pool), engine_(engine), context_(context) {}
+
+base::StatusOr<std::unique_ptr<Table>>
+WinscopeProtoToArgsWithDefaults::ComputeTable(
+    const std::vector<SqlValue>& arguments) {
+  PERFETTO_CHECK(arguments.size() == 1);
+  if (arguments[0].type != SqlValue::kString) {
+    return base::ErrStatus(
+        "__intrinsic_winscope_proto_to_args_with_defaults takes table name as "
+        "a string.");
+  }
+  std::string table_name = arguments[0].AsString();
+
+  const Table* static_table = engine_->GetStaticTableOrNull(table_name);
+  if (!static_table) {
+    return base::ErrStatus("Failed to find %s table.", table_name.c_str());
+  }
+
+  std::string proto_name;
+  ASSIGN_OR_RETURN(proto_name,
+                   util::winscope_proto_mapping::GetProtoName(table_name));
+
+  auto table =
+      std::make_unique<tables::WinscopeArgsWithDefaultsTable>(string_pool_);
+
+  auto allowed_fields =
+      util::winscope_proto_mapping::GetAllowedFields(table_name);
+  RETURN_IF_ERROR(InsertRows(*static_table, table.get(), proto_name,
+                             allowed_fields ? &allowed_fields.value() : nullptr,
+                             *context_->descriptor_pool_, string_pool_));
+
+  return std::unique_ptr<Table>(std::move(table));
+}
+
+Table::Schema WinscopeProtoToArgsWithDefaults::CreateSchema() {
+  return tables::WinscopeArgsWithDefaultsTable::ComputeStaticSchema();
+}
+
+std::string WinscopeProtoToArgsWithDefaults::TableName() {
+  return tables::WinscopeArgsWithDefaultsTable::Name();
+}
+
+uint32_t WinscopeProtoToArgsWithDefaults::EstimateRowCount() {
+  // 100 inflated args per 100 elements per 100 entries
+  return 1000000;
+}
+}  // namespace perfetto::trace_processor
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
new file mode 100644
index 0000000..91ab8c8
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h
@@ -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.
+ */
+
+#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 "perfetto/ext/base/status_or.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"
+#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
+
+namespace perfetto::trace_processor {
+
+class TraceProcessorContext;
+
+class WinscopeProtoToArgsWithDefaults : public StaticTableFunction {
+ public:
+  explicit WinscopeProtoToArgsWithDefaults(StringPool*,
+                                           PerfettoSqlEngine*,
+                                           TraceProcessorContext* context);
+
+  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:
+  StringPool* string_pool_ = nullptr;
+  PerfettoSqlEngine* engine_ = nullptr;
+  TraceProcessorContext* context_ = nullptr;
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_WINSCOPE_PROTO_TO_ARGS_WITH_DEFAULTS_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/types/BUILD.gn b/src/trace_processor/perfetto_sql/intrinsics/types/BUILD.gn
index 628f05f..26486e83 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/types/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/intrinsics/types/BUILD.gn
@@ -20,6 +20,7 @@
 source_set("types") {
   sources = [
     "array.h",
+    "counter.h",
     "node.h",
     "partitioned_intervals.h",
     "row_dataframe.h",
diff --git a/src/trace_processor/perfetto_sql/intrinsics/types/counter.h b/src/trace_processor/perfetto_sql/intrinsics/types/counter.h
new file mode 100644
index 0000000..863bbc3
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/types/counter.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TYPES_COUNTER_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TYPES_COUNTER_H_
+
+#include <cstdint>
+#include <limits>
+#include <string>
+#include <vector>
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/trace_processor/basic_types.h"
+
+namespace perfetto::trace_processor::perfetto_sql {
+
+struct CounterTrackPartition {
+  std::vector<int64_t> id;
+  std::vector<int64_t> ts;
+  std::vector<double> val;
+};
+
+struct PartitionedCounter {
+  static constexpr char kName[] = "COUNTER_TRACK_PARTITIONS";
+  base::
+      FlatHashMap<int64_t, CounterTrackPartition, base::AlreadyHashed<int64_t>>
+          partitions_map;
+};
+
+}  // namespace perfetto::trace_processor::perfetto_sql
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TYPES_COUNTER_H_
diff --git a/src/trace_processor/perfetto_sql/parser/BUILD.gn b/src/trace_processor/perfetto_sql/parser/BUILD.gn
new file mode 100644
index 0000000..f31cae2
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/parser/BUILD.gn
@@ -0,0 +1,64 @@
+# Copyright (C) 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
+#
+#      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/test.gni")
+
+assert(enable_perfetto_trace_processor_sqlite)
+
+source_set("parser") {
+  sources = [
+    "function_util.cc",
+    "function_util.h",
+    "perfetto_sql_parser.cc",
+    "perfetto_sql_parser.h",
+  ]
+  deps = [
+    "../..:metatrace",
+    "../../../../gn:default_deps",
+    "../../../../gn:sqlite",
+    "../../../base",
+    "../../sqlite",
+    "../../util",
+    "../../util:sql_argument",
+    "../preprocessor",
+    "../tokenizer",
+  ]
+}
+
+perfetto_unittest_source_set("unittests") {
+  testonly = true
+  sources = [ "perfetto_sql_parser_unittest.cc" ]
+  deps = [
+    ":parser",
+    ":test_utils",
+    "../../../../gn:default_deps",
+    "../../../../gn:gtest_and_gmock",
+    "../../../../gn:sqlite",
+    "../../../base",
+    "../../sqlite",
+  ]
+}
+
+perfetto_unittest_source_set("test_utils") {
+  testonly = true
+  sources = [ "perfetto_sql_test_utils.h" ]
+  deps = [
+    ":parser",
+    "../../../../gn:default_deps",
+    "../../../../gn:gtest_and_gmock",
+    "../../../../gn:sqlite",
+    "../../../base",
+    "../../sqlite",
+  ]
+}
diff --git a/src/trace_processor/perfetto_sql/parser/function_util.cc b/src/trace_processor/perfetto_sql/parser/function_util.cc
new file mode 100644
index 0000000..cc0b2ee
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/parser/function_util.cc
@@ -0,0 +1,121 @@
+/*
+ * 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/parser/function_util.h"
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/string_view.h"
+#include "src/trace_processor/sqlite/sqlite_utils.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+std::string FunctionPrototype::ToString() const {
+  return function_name + "(" + SerializeArguments(arguments) + ")";
+}
+
+base::Status ParseFunctionName(base::StringView raw, base::StringView& out) {
+  size_t function_name_end = raw.find('(');
+  if (function_name_end == base::StringView::npos)
+    return base::ErrStatus("unable to find bracket starting argument list");
+
+  base::StringView function_name = raw.substr(0, function_name_end);
+  if (!sql_argument::IsValidName(function_name)) {
+    return base::ErrStatus("function name %s is not alphanumeric",
+                           function_name.ToStdString().c_str());
+  }
+  out = function_name;
+  return base::OkStatus();
+}
+
+base::Status ParsePrototype(base::StringView raw, FunctionPrototype& out) {
+  // Examples of function prototypes:
+  // ANDROID_SDK_LEVEL()
+  // STARTUP_SLICE(dur_ns INT)
+  // FIND_NEXT_SLICE_WITH_NAME(ts INT, name STRING)
+
+  base::StringView function_name;
+  RETURN_IF_ERROR(ParseFunctionName(raw, function_name));
+
+  size_t function_name_end = function_name.size();
+  size_t args_start = function_name_end + 1;
+  size_t args_end = raw.find(')', args_start);
+  if (args_end == base::StringView::npos)
+    return base::ErrStatus("unable to find bracket ending argument list");
+
+  base::StringView args_str = raw.substr(args_start, args_end - args_start);
+  RETURN_IF_ERROR(sql_argument::ParseArgumentDefinitions(args_str.ToStdString(),
+                                                         out.arguments));
+
+  out.function_name = function_name.ToStdString();
+  return base::OkStatus();
+}
+
+base::Status SqliteRetToStatus(sqlite3* db,
+                               const std::string& function_name,
+                               int ret) {
+  if (ret != SQLITE_ROW && ret != SQLITE_DONE) {
+    return base::ErrStatus("%s: SQLite error while executing function body: %s",
+                           function_name.c_str(), sqlite3_errmsg(db));
+  }
+  return base::OkStatus();
+}
+
+base::Status MaybeBindArgument(sqlite3_stmt* stmt,
+                               const std::string& function_name,
+                               const sql_argument::ArgumentDefinition& arg,
+                               sqlite3_value* value) {
+  int index = sqlite3_bind_parameter_index(stmt, arg.dollar_name().c_str());
+
+  // If the argument is not in the query, this just means its an unused
+  // argument which we can just ignore.
+  if (index == 0)
+    return base::Status();
+
+  int ret = sqlite3_bind_value(stmt, index, value);
+  if (ret != SQLITE_OK) {
+    return base::ErrStatus(
+        "%s: SQLite error while binding value to argument %s: %s",
+        function_name.c_str(), arg.name().c_str(),
+        sqlite3_errmsg(sqlite3_db_handle(stmt)));
+  }
+  return base::OkStatus();
+}
+
+base::Status MaybeBindIntArgument(sqlite3_stmt* stmt,
+                                  const std::string& function_name,
+                                  const sql_argument::ArgumentDefinition& arg,
+                                  int64_t value) {
+  int index = sqlite3_bind_parameter_index(stmt, arg.dollar_name().c_str());
+
+  // If the argument is not in the query, this just means its an unused
+  // argument which we can just ignore.
+  if (index == 0)
+    return base::Status();
+
+  int ret = sqlite3_bind_int64(stmt, index, value);
+  if (ret != SQLITE_OK) {
+    return base::ErrStatus(
+        "%s: SQLite error while binding value to argument %s: %s",
+        function_name.c_str(), arg.name().c_str(),
+        sqlite3_errmsg(sqlite3_db_handle(stmt)));
+  }
+  return base::OkStatus();
+}
+
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/perfetto_sql/parser/function_util.h b/src/trace_processor/perfetto_sql/parser/function_util.h
new file mode 100644
index 0000000..129bc5f
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/parser/function_util.h
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_PARSER_FUNCTION_UTIL_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_PARSER_FUNCTION_UTIL_H_
+
+#include <sqlite3.h>
+#include <optional>
+#include <string>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/string_view.h"
+#include "src/trace_processor/util/sql_argument.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+struct FunctionPrototype {
+  std::string function_name;
+  std::vector<sql_argument::ArgumentDefinition> arguments;
+
+  std::string ToString() const;
+
+  bool operator==(const FunctionPrototype& other) const {
+    return function_name == other.function_name && arguments == other.arguments;
+  }
+  bool operator!=(const FunctionPrototype& other) const {
+    return !(*this == other);
+  }
+};
+
+base::Status ParseFunctionName(base::StringView raw,
+                               base::StringView& function_name);
+
+base::Status ParsePrototype(base::StringView raw, FunctionPrototype& out);
+
+base::Status SqliteRetToStatus(sqlite3* db,
+                               const std::string& function_name,
+                               int ret);
+
+base::Status MaybeBindArgument(sqlite3_stmt*,
+                               const std::string& function_name,
+                               const sql_argument::ArgumentDefinition&,
+                               sqlite3_value*);
+
+base::Status MaybeBindIntArgument(sqlite3_stmt*,
+                                  const std::string& function_name,
+                                  const sql_argument::ArgumentDefinition&,
+                                  int64_t);
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_PARSER_FUNCTION_UTIL_H_
diff --git a/src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.cc b/src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.cc
new file mode 100644
index 0000000..b0c4e6e
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.cc
@@ -0,0 +1,631 @@
+/*
+ * 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.
+ */
+
+#include "src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.h"
+
+#include <algorithm>
+#include <cctype>
+#include <functional>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "src/trace_processor/perfetto_sql/parser/function_util.h"
+#include "src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.h"
+#include "src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+#include "src/trace_processor/util/sql_argument.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace {
+
+using Token = SqliteTokenizer::Token;
+using Statement = PerfettoSqlParser::Statement;
+
+enum class State {
+  kDrop,
+  kDropPerfetto,
+  kCreate,
+  kCreateOr,
+  kCreateOrReplace,
+  kCreateOrReplacePerfetto,
+  kCreatePerfetto,
+  kInclude,
+  kIncludePerfetto,
+  kPassthrough,
+  kStmtStart,
+};
+
+bool IsValidModuleWord(const std::string& word) {
+  for (const char& c : word) {
+    if (!std::isalnum(c) && (c != '_') && !std::islower(c)) {
+      return false;
+    }
+  }
+  return true;
+}
+
+bool ValidateModuleName(const std::string& name) {
+  if (name.empty()) {
+    return false;
+  }
+
+  std::vector<std::string> packages = base::SplitString(name, ".");
+
+  // The last part of the path can be a wildcard.
+  if (!packages.empty() && packages.back() == "*") {
+    packages.pop_back();
+  }
+
+  // The rest of the path must be valid words.
+  return std::find_if(packages.begin(), packages.end(),
+                      std::not_fn(IsValidModuleWord)) == packages.end();
+}
+
+}  // namespace
+
+PerfettoSqlParser::PerfettoSqlParser(
+    SqlSource source,
+    const base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro>&
+        macros)
+    : preprocessor_(std::move(source), macros),
+      tokenizer_(SqlSource::FromTraceProcessorImplementation("")) {}
+
+bool PerfettoSqlParser::Next() {
+  PERFETTO_CHECK(status_.ok());
+
+  if (!preprocessor_.NextStatement()) {
+    status_ = preprocessor_.status();
+    return false;
+  }
+  tokenizer_.Reset(preprocessor_.statement());
+
+  State state = State::kStmtStart;
+  std::optional<Token> first_non_space_token;
+  for (Token token = tokenizer_.Next();; token = tokenizer_.Next()) {
+    // Space should always be completely ignored by any logic below as it will
+    // never change the current state in the state machine.
+    if (token.token_type == TK_SPACE) {
+      continue;
+    }
+
+    if (token.IsTerminal()) {
+      // If we have a non-space character we've seen, just return all the stuff
+      // after that point.
+      if (first_non_space_token) {
+        statement_ = SqliteSql{};
+        statement_sql_ = tokenizer_.Substr(*first_non_space_token, token);
+        return true;
+      }
+      // This means we've seen a semi-colon without any non-space content. Just
+      // try and find the next statement as this "statement" is a noop.
+      if (token.token_type == TK_SEMI) {
+        continue;
+      }
+      // This means we've reached the end of the SQL.
+      PERFETTO_DCHECK(token.str.empty());
+      return false;
+    }
+
+    // If we've not seen a space character, keep track of the current position.
+    if (!first_non_space_token) {
+      first_non_space_token = token;
+    }
+
+    switch (state) {
+      case State::kPassthrough:
+        statement_ = SqliteSql{};
+        statement_sql_ = preprocessor_.statement();
+        return true;
+      case State::kStmtStart:
+        if (token.token_type == TK_CREATE) {
+          state = State::kCreate;
+        } else if (token.token_type == TK_INCLUDE) {
+          state = State::kInclude;
+        } else if (token.token_type == TK_DROP) {
+          state = State::kDrop;
+        } else {
+          state = State::kPassthrough;
+        }
+        break;
+      case State::kInclude:
+        if (token.token_type == TK_PERFETTO) {
+          state = State::kIncludePerfetto;
+        } else {
+          return ErrorAtToken(token,
+                              "Use 'INCLUDE PERFETTO MODULE {include_key}'.");
+        }
+        break;
+      case State::kIncludePerfetto:
+        if (token.token_type == TK_MODULE) {
+          return ParseIncludePerfettoModule(*first_non_space_token);
+        } else {
+          return ErrorAtToken(token,
+                              "Use 'INCLUDE PERFETTO MODULE {include_key}'.");
+        }
+      case State::kDrop:
+        if (token.token_type == TK_PERFETTO) {
+          state = State::kDropPerfetto;
+        } else {
+          state = State::kPassthrough;
+        }
+        break;
+      case State::kDropPerfetto:
+        if (token.token_type == TK_INDEX) {
+          return ParseDropPerfettoIndex(*first_non_space_token);
+        } else {
+          return ErrorAtToken(token, "Only Perfetto index can be dropped");
+        }
+      case State::kCreate:
+        if (token.token_type == TK_TRIGGER) {
+          // TODO(lalitm): add this to the "errors" documentation page
+          // explaining why this is the case.
+          return ErrorAtToken(
+              token, "Creating triggers is not supported in PerfettoSQL.");
+        }
+        if (token.token_type == TK_PERFETTO) {
+          state = State::kCreatePerfetto;
+        } else if (token.token_type == TK_OR) {
+          state = State::kCreateOr;
+        } else {
+          state = State::kPassthrough;
+        }
+        break;
+      case State::kCreateOr:
+        state = token.token_type == TK_REPLACE ? State::kCreateOrReplace
+                                               : State::kPassthrough;
+        break;
+      case State::kCreateOrReplace:
+        state = token.token_type == TK_PERFETTO
+                    ? State::kCreateOrReplacePerfetto
+                    : State::kPassthrough;
+        break;
+      case State::kCreateOrReplacePerfetto:
+      case State::kCreatePerfetto:
+        bool replace = state == State::kCreateOrReplacePerfetto;
+        if (token.token_type == TK_FUNCTION) {
+          return ParseCreatePerfettoFunction(replace, *first_non_space_token);
+        }
+        if (token.token_type == TK_TABLE) {
+          return ParseCreatePerfettoTableOrView(replace, *first_non_space_token,
+                                                TableOrView::kTable);
+        }
+        if (token.token_type == TK_VIEW) {
+          return ParseCreatePerfettoTableOrView(replace, *first_non_space_token,
+                                                TableOrView::kView);
+        }
+        if (token.token_type == TK_MACRO) {
+          return ParseCreatePerfettoMacro(replace);
+        }
+        if (token.token_type == TK_INDEX) {
+          return ParseCreatePerfettoIndex(replace, *first_non_space_token);
+        }
+        base::StackString<1024> err(
+            "Expected 'FUNCTION', 'TABLE', 'MACRO' OR 'INDEX' after 'CREATE "
+            "PERFETTO', received '%*s'.",
+            static_cast<int>(token.str.size()), token.str.data());
+        return ErrorAtToken(token, err.c_str());
+    }
+  }
+}
+
+bool PerfettoSqlParser::ParseIncludePerfettoModule(
+    Token first_non_space_token) {
+  auto tok = tokenizer_.NextNonWhitespace();
+  auto terminal = tokenizer_.NextTerminal();
+  std::string key = tokenizer_.Substr(tok, terminal).sql();
+
+  if (!ValidateModuleName(key)) {
+    base::StackString<1024> err(
+        "Include key should be a dot-separated list of module names, with the "
+        "last name optionally being a wildcard: '%s'",
+        key.c_str());
+    return ErrorAtToken(tok, err.c_str());
+  }
+
+  statement_ = Include{key};
+  statement_sql_ = tokenizer_.Substr(first_non_space_token, terminal);
+  return true;
+}
+
+bool PerfettoSqlParser::ParseCreatePerfettoTableOrView(
+    bool replace,
+    Token first_non_space_token,
+    TableOrView table_or_view) {
+  Token table_name = tokenizer_.NextNonWhitespace();
+  if (table_name.token_type != TK_ID) {
+    base::StackString<1024> err("Invalid table name %.*s",
+                                static_cast<int>(table_name.str.size()),
+                                table_name.str.data());
+    return ErrorAtToken(table_name, err.c_str());
+  }
+  std::string name(table_name.str);
+  std::vector<sql_argument::ArgumentDefinition> schema;
+
+  auto token = tokenizer_.NextNonWhitespace();
+
+  // If the next token is a left parenthesis, then the table or view have a
+  // schema.
+  if (token.token_type == TK_LP) {
+    if (!ParseArguments(schema)) {
+      return false;
+    }
+    token = tokenizer_.NextNonWhitespace();
+  }
+
+  if (token.token_type != TK_AS) {
+    base::StackString<1024> err(
+        "Expected 'AS' after table_name, received "
+        "%*s.",
+        static_cast<int>(token.str.size()), token.str.data());
+    return ErrorAtToken(token, err.c_str());
+  }
+
+  Token first = tokenizer_.NextNonWhitespace();
+  Token terminal = tokenizer_.NextTerminal();
+  switch (table_or_view) {
+    case TableOrView::kTable:
+      statement_ = CreateTable{replace, std::move(name),
+                               tokenizer_.Substr(first, terminal), schema};
+      break;
+    case TableOrView::kView:
+      SqlSource original_statement =
+          tokenizer_.Substr(first_non_space_token, terminal);
+      SqlSource header = SqlSource::FromTraceProcessorImplementation(
+          "CREATE VIEW " + name + " AS ");
+      SqlSource::Rewriter rewriter(original_statement);
+      tokenizer_.Rewrite(rewriter, first_non_space_token, first, header,
+                         SqliteTokenizer::EndToken::kExclusive);
+      statement_ = CreateView{replace, std::move(name),
+                              tokenizer_.Substr(first, terminal),
+                              std::move(rewriter).Build(), schema};
+      break;
+  }
+  statement_sql_ = tokenizer_.Substr(first_non_space_token, terminal);
+  return true;
+}
+
+bool PerfettoSqlParser::ParseCreatePerfettoIndex(bool replace,
+                                                 Token first_non_space_token) {
+  Token index_name_tok = tokenizer_.NextNonWhitespace();
+  if (index_name_tok.token_type != TK_ID) {
+    base::StackString<1024> err("Invalid index name %.*s",
+                                static_cast<int>(index_name_tok.str.size()),
+                                index_name_tok.str.data());
+    return ErrorAtToken(index_name_tok, err.c_str());
+  }
+  std::string index_name(index_name_tok.str);
+
+  auto token = tokenizer_.NextNonWhitespace();
+  if (token.token_type != TK_ON) {
+    base::StackString<1024> err("Expected 'ON' after index name, received %*s.",
+                                static_cast<int>(token.str.size()),
+                                token.str.data());
+    return ErrorAtToken(token, err.c_str());
+  }
+
+  Token table_name_tok = tokenizer_.NextNonWhitespace();
+  if (table_name_tok.token_type != TK_ID) {
+    base::StackString<1024> err("Invalid table name %.*s",
+                                static_cast<int>(table_name_tok.str.size()),
+                                table_name_tok.str.data());
+    return ErrorAtToken(table_name_tok, err.c_str());
+  }
+  std::string table_name(table_name_tok.str);
+
+  token = tokenizer_.NextNonWhitespace();
+  if (token.token_type != TK_LP) {
+    base::StackString<1024> err(
+        "Expected parenthesis after table name, received '%*s'.",
+        static_cast<int>(token.str.size()), token.str.data());
+    return ErrorAtToken(token, err.c_str());
+  }
+
+  std::vector<std::string> cols;
+
+  do {
+    Token col_name_tok = tokenizer_.NextNonWhitespace();
+    cols.push_back(std::string(col_name_tok.str));
+    token = tokenizer_.NextNonWhitespace();
+  } while (token.token_type == TK_COMMA);
+
+  if (token.token_type != TK_RP) {
+    base::StackString<1024> err("Expected closed parenthesis, received '%*s'.",
+                                static_cast<int>(token.str.size()),
+                                token.str.data());
+    return ErrorAtToken(token, err.c_str());
+  }
+
+  token = tokenizer_.NextNonWhitespace();
+  if (!token.IsTerminal()) {
+    return ErrorAtToken(
+        token,
+        "Expected semicolon after columns list in CREATE PERFETTO INDEX.");
+  }
+
+  statement_sql_ = tokenizer_.Substr(first_non_space_token, token);
+  statement_ = CreateIndex{replace, index_name, table_name, cols};
+  return true;
+}
+
+bool PerfettoSqlParser::ParseDropPerfettoIndex(
+    SqliteTokenizer::Token first_non_space_token) {
+  Token index_name_tok = tokenizer_.NextNonWhitespace();
+  if (index_name_tok.token_type != TK_ID) {
+    base::StackString<1024> err("Invalid index name %.*s",
+                                static_cast<int>(index_name_tok.str.size()),
+                                index_name_tok.str.data());
+    return ErrorAtToken(index_name_tok, err.c_str());
+  }
+  std::string index_name(index_name_tok.str);
+
+  auto token = tokenizer_.NextNonWhitespace();
+  if (token.token_type != TK_ON) {
+    base::StackString<1024> err("Expected 'ON' after index name, received %*s.",
+                                static_cast<int>(token.str.size()),
+                                token.str.data());
+    return ErrorAtToken(token, err.c_str());
+  }
+
+  Token table_name_tok = tokenizer_.NextNonWhitespace();
+  if (table_name_tok.token_type != TK_ID) {
+    base::StackString<1024> err("Invalid table name %.*s",
+                                static_cast<int>(table_name_tok.str.size()),
+                                table_name_tok.str.data());
+    return ErrorAtToken(table_name_tok, err.c_str());
+  }
+  std::string table_name(table_name_tok.str);
+
+  token = tokenizer_.NextNonWhitespace();
+  if (!token.IsTerminal()) {
+    return ErrorAtToken(
+        token, "Nothing is allowed after table name in DROP PERFETTO INDEX");
+  }
+  statement_sql_ = tokenizer_.Substr(first_non_space_token, token);
+  statement_ = DropIndex{index_name, table_name};
+  return true;
+}
+
+bool PerfettoSqlParser::ParseCreatePerfettoFunction(
+    bool replace,
+    Token first_non_space_token) {
+  Token function_name = tokenizer_.NextNonWhitespace();
+  if (function_name.token_type != TK_ID) {
+    // TODO(lalitm): add a link to create function documentation.
+    base::StackString<1024> err("Invalid function name %.*s",
+                                static_cast<int>(function_name.str.size()),
+                                function_name.str.data());
+    return ErrorAtToken(function_name, err.c_str());
+  }
+
+  // TK_LP == '(' (i.e. left parenthesis).
+  if (Token lp = tokenizer_.NextNonWhitespace(); lp.token_type != TK_LP) {
+    // TODO(lalitm): add a link to create function documentation.
+    return ErrorAtToken(lp, "Malformed function prototype: '(' expected");
+  }
+
+  std::vector<sql_argument::ArgumentDefinition> args;
+  if (!ParseArguments(args)) {
+    return false;
+  }
+
+  if (Token returns = tokenizer_.NextNonWhitespace();
+      returns.token_type != TK_RETURNS) {
+    // TODO(lalitm): add a link to create function documentation.
+    return ErrorAtToken(returns, "Expected keyword 'returns'");
+  }
+
+  Token ret_token = tokenizer_.NextNonWhitespace();
+  std::string ret;
+  bool table_return = ret_token.token_type == TK_TABLE;
+  if (table_return) {
+    if (Token lp = tokenizer_.NextNonWhitespace(); lp.token_type != TK_LP) {
+      // TODO(lalitm): add a link to create function documentation.
+      return ErrorAtToken(lp, "Malformed table return: '(' expected");
+    }
+    // Table function return.
+    std::vector<sql_argument::ArgumentDefinition> ret_args;
+    if (!ParseArguments(ret_args)) {
+      return false;
+    }
+    ret = sql_argument::SerializeArguments(ret_args);
+  } else if (ret_token.token_type != TK_ID) {
+    // TODO(lalitm): add a link to create function documentation.
+    return ErrorAtToken(ret_token, "Invalid return type");
+  } else {
+    // Scalar function return.
+    ret = ret_token.str;
+  }
+
+  if (Token as_token = tokenizer_.NextNonWhitespace();
+      as_token.token_type != TK_AS) {
+    // TODO(lalitm): add a link to create function documentation.
+    return ErrorAtToken(as_token, "Expected keyword 'as'");
+  }
+
+  Token first = tokenizer_.NextNonWhitespace();
+  Token terminal = tokenizer_.NextTerminal();
+  statement_ = CreateFunction{
+      replace,
+      FunctionPrototype{std::string(function_name.str), std::move(args)},
+      std::move(ret), tokenizer_.Substr(first, terminal), table_return};
+  statement_sql_ = tokenizer_.Substr(first_non_space_token, terminal);
+  return true;
+}
+
+bool PerfettoSqlParser::ParseCreatePerfettoMacro(bool replace) {
+  Token name = tokenizer_.NextNonWhitespace();
+  if (name.token_type != TK_ID) {
+    // TODO(lalitm): add a link to create macro documentation.
+    base::StackString<1024> err("Invalid macro name %.*s",
+                                static_cast<int>(name.str.size()),
+                                name.str.data());
+    return ErrorAtToken(name, err.c_str());
+  }
+
+  // TK_LP == '(' (i.e. left parenthesis).
+  if (Token lp = tokenizer_.NextNonWhitespace(); lp.token_type != TK_LP) {
+    // TODO(lalitm): add a link to create macro documentation.
+    return ErrorAtToken(lp, "Malformed macro prototype: '(' expected");
+  }
+
+  std::vector<RawArgument> raw_args;
+  std::vector<std::pair<SqlSource, SqlSource>> args;
+  if (!ParseRawArguments(raw_args)) {
+    return false;
+  }
+  for (const auto& arg : raw_args) {
+    args.emplace_back(tokenizer_.SubstrToken(arg.name),
+                      tokenizer_.SubstrToken(arg.type));
+  }
+
+  if (Token returns = tokenizer_.NextNonWhitespace();
+      returns.token_type != TK_RETURNS) {
+    // TODO(lalitm): add a link to create macro documentation.
+    return ErrorAtToken(returns, "Expected keyword 'returns'");
+  }
+
+  Token returns_value = tokenizer_.NextNonWhitespace();
+  if (returns_value.token_type != TK_ID) {
+    // TODO(lalitm): add a link to create function documentation.
+    return ErrorAtToken(returns_value, "Expected return type");
+  }
+
+  if (Token as_token = tokenizer_.NextNonWhitespace();
+      as_token.token_type != TK_AS) {
+    // TODO(lalitm): add a link to create macro documentation.
+    return ErrorAtToken(as_token, "Expected keyword 'as'");
+  }
+
+  Token first = tokenizer_.NextNonWhitespace();
+  Token tok = tokenizer_.NextTerminal();
+  statement_ = CreateMacro{
+      replace, tokenizer_.SubstrToken(name), std::move(args),
+      tokenizer_.SubstrToken(returns_value), tokenizer_.Substr(first, tok)};
+  return true;
+}
+
+bool PerfettoSqlParser::ParseRawArguments(std::vector<RawArgument>& args) {
+  enum TokenType {
+    kIdOrRp,
+    kId,
+    kType,
+    kCommaOrRp,
+  };
+
+  std::optional<Token> id = std::nullopt;
+  TokenType expected = kIdOrRp;
+  for (Token tok = tokenizer_.NextNonWhitespace();;
+       tok = tokenizer_.NextNonWhitespace()) {
+    if (expected == kCommaOrRp) {
+      PERFETTO_CHECK(expected == kCommaOrRp);
+      if (tok.token_type == TK_RP) {
+        return true;
+      }
+      if (tok.token_type == TK_COMMA) {
+        expected = kId;
+        continue;
+      }
+      return ErrorAtToken(tok, "')' or ',' expected");
+    }
+    if (expected == kType) {
+      if (tok.token_type != TK_ID) {
+        // TODO(lalitm): add a link to documentation.
+        base::StackString<1024> err("%.*s is not a valid argument type",
+                                    static_cast<int>(tok.str.size()),
+                                    tok.str.data());
+        return ErrorAtToken(tok, err.c_str());
+      }
+      PERFETTO_CHECK(id);
+      args.push_back({*id, tok});
+      id = std::nullopt;
+      expected = kCommaOrRp;
+      continue;
+    }
+
+    // kIdOrRp only happens on the very first token.
+    if (tok.token_type == TK_RP && expected == kIdOrRp) {
+      return true;
+    }
+
+    if (tok.token_type != TK_ID && tok.token_type != TK_KEY &&
+        tok.token_type != TK_FUNCTION) {
+      // TODO(lalitm): add a link to documentation.
+      base::StackString<1024> err("%.*s is not a valid argument name",
+                                  static_cast<int>(tok.str.size()),
+                                  tok.str.data());
+      return ErrorAtToken(tok, err.c_str());
+    }
+    id = tok;
+    expected = kType;
+    continue;
+  }
+}
+
+bool PerfettoSqlParser::ParseArguments(
+    std::vector<sql_argument::ArgumentDefinition>& args) {
+  std::vector<RawArgument> raw_args;
+  if (!ParseRawArguments(raw_args)) {
+    return false;
+  }
+  for (const auto& raw_arg : raw_args) {
+    std::optional<sql_argument::ArgumentDefinition> arg =
+        ResolveRawArgument(raw_arg);
+    if (!arg) {
+      return false;
+    }
+    args.emplace_back(std::move(*arg));
+  }
+  return true;
+}
+
+std::optional<sql_argument::ArgumentDefinition>
+PerfettoSqlParser::ResolveRawArgument(RawArgument arg) {
+  std::string arg_name = tokenizer_.SubstrToken(arg.name).sql();
+  std::string arg_type = tokenizer_.SubstrToken(arg.type).sql();
+  if (!sql_argument::IsValidName(base::StringView(arg_name))) {
+    base::StackString<1024> err("Name %s is not alphanumeric",
+                                arg_name.c_str());
+    ErrorAtToken(arg.name, err.c_str());
+    return std::nullopt;
+  }
+  std::optional<sql_argument::Type> parsed_arg_type =
+      sql_argument::ParseType(base::StringView(arg_type));
+  if (!parsed_arg_type) {
+    base::StackString<1024> err("Invalid type %s", arg_type.c_str());
+    ErrorAtToken(arg.type, err.c_str());
+    return std::nullopt;
+  }
+  return sql_argument::ArgumentDefinition("$" + arg_name, *parsed_arg_type);
+}
+
+bool PerfettoSqlParser::ErrorAtToken(const SqliteTokenizer::Token& token,
+                                     const char* error,
+                                     ...) {
+  std::string traceback = tokenizer_.AsTraceback(token);
+  status_ = base::ErrStatus("%s%s", traceback.c_str(), error);
+  return false;
+}
+
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.h b/src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.h
new file mode 100644
index 0000000..8321bb6
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.h
@@ -0,0 +1,210 @@
+/*
+ * 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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_PARSER_PERFETTO_SQL_PARSER_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_PARSER_PERFETTO_SQL_PARSER_H_
+
+#include <optional>
+#include <string>
+#include <utility>
+#include <variant>
+#include <vector>
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "src/trace_processor/perfetto_sql/parser/function_util.h"
+#include "src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.h"
+#include "src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+#include "src/trace_processor/util/sql_argument.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+// Parser for PerfettoSQL statements. This class provides an iterator-style
+// interface for reading all PerfettoSQL statements from a block of SQL.
+//
+// Usage:
+// PerfettoSqlParser parser(my_sql_string.c_str());
+// while (parser.Next()) {
+//   auto& stmt = parser.statement();
+//   // Handle |stmt| here
+// }
+// RETURN_IF_ERROR(r.status());
+class PerfettoSqlParser {
+ public:
+  // Indicates that the specified SQLite SQL was extracted directly from a
+  // PerfettoSQL statement and should be directly executed with SQLite.
+  struct SqliteSql {};
+  // Indicates that the specified SQL was a CREATE PERFETTO FUNCTION statement
+  // with the following parameters.
+  struct CreateFunction {
+    bool replace;
+    FunctionPrototype prototype;
+    std::string returns;
+    SqlSource sql;
+    bool is_table;
+  };
+  // Indicates that the specified SQL was a CREATE PERFETTO TABLE statement
+  // with the following parameters.
+  struct CreateTable {
+    bool replace;
+    std::string name;
+    // SQL source for the select statement.
+    SqlSource sql;
+    std::vector<sql_argument::ArgumentDefinition> schema;
+  };
+  // Indicates that the specified SQL was a CREATE PERFETTO VIEW statement
+  // with the following parameters.
+  struct CreateView {
+    bool replace;
+    std::string name;
+    // SQL source for the select statement.
+    SqlSource select_sql;
+    // SQL source corresponding to the rewritten statement creating the
+    // underlying view.
+    SqlSource create_view_sql;
+    std::vector<sql_argument::ArgumentDefinition> schema;
+  };
+  // Indicates that the specified SQL was a CREATE PERFETTO INDEX statement
+  // with the following parameters.
+  struct CreateIndex {
+    bool replace = false;
+    std::string name;
+    std::string table_name;
+    std::vector<std::string> col_names;
+  };
+  // Indicates that the specified SQL was a DROP PERFETTO INDEX statement
+  // with the following parameters.
+  struct DropIndex {
+    std::string name;
+    std::string table_name;
+  };
+  // Indicates that the specified SQL was a INCLUDE PERFETTO MODULE statement
+  // with the following parameter.
+  struct Include {
+    std::string key;
+  };
+  // Indicates that the specified SQL was a CREATE PERFETTO MACRO statement
+  // with the following parameter.
+  struct CreateMacro {
+    bool replace;
+    SqlSource name;
+    std::vector<std::pair<SqlSource, SqlSource>> args;
+    SqlSource returns;
+    SqlSource sql;
+  };
+  using Statement = std::variant<CreateFunction,
+                                 CreateIndex,
+                                 CreateMacro,
+                                 CreateTable,
+                                 CreateView,
+                                 DropIndex,
+                                 Include,
+                                 SqliteSql>;
+
+  // Creates a new SQL parser with the a block of PerfettoSQL statements.
+  // Concretely, the passed string can contain >1 statement.
+  explicit PerfettoSqlParser(
+      SqlSource,
+      const base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro>&);
+
+  // Attempts to parse to the next statement in the SQL. Returns true if
+  // a statement was successfully parsed and false if EOF was reached or the
+  // statement was not parsed correctly.
+  //
+  // Note: if this function returns false, callers *must* call |status()|: it
+  // is undefined behaviour to not do so.
+  bool Next();
+
+  // Returns the current statement which was parsed. This function *must not* be
+  // called unless |Next()| returned true.
+  Statement& statement() {
+    PERFETTO_DCHECK(statement_.has_value());
+    return statement_.value();
+  }
+
+  // Returns the full statement which was parsed. This should return
+  // |statement()| and Perfetto SQL code that's in front. This function *must
+  // not* be called unless |Next()| returned true.
+  const SqlSource& statement_sql() const {
+    PERFETTO_CHECK(statement_sql_);
+    return *statement_sql_;
+  }
+
+  // Returns the error status for the parser. This will be |base::OkStatus()|
+  // until an unrecoverable error is encountered.
+  const base::Status& status() const { return status_; }
+
+ private:
+  // This cannot be moved because we keep pointers into |sql_| in
+  // |preprocessor_|.
+  PerfettoSqlParser(PerfettoSqlParser&&) = delete;
+  PerfettoSqlParser& operator=(PerfettoSqlParser&&) = delete;
+
+  // Most of the code needs sql_argument::ArgumentDefinition, but we explcitly
+  // track raw arguments separately, as macro implementations need access to
+  // the underlying tokens.
+  struct RawArgument {
+    SqliteTokenizer::Token name;
+    SqliteTokenizer::Token type;
+  };
+
+  bool ParseCreatePerfettoFunction(
+      bool replace,
+      SqliteTokenizer::Token first_non_space_token);
+
+  enum class TableOrView {
+    kTable,
+    kView,
+  };
+  bool ParseCreatePerfettoTableOrView(
+      bool replace,
+      SqliteTokenizer::Token first_non_space_token,
+      TableOrView table_or_view);
+
+  bool ParseIncludePerfettoModule(SqliteTokenizer::Token first_non_space_token);
+
+  bool ParseCreatePerfettoMacro(bool replace);
+
+  bool ParseCreatePerfettoIndex(bool replace,
+                                SqliteTokenizer::Token first_non_space_token);
+
+  bool ParseDropPerfettoIndex(SqliteTokenizer::Token first_non_space_token);
+
+  // Convert a "raw" argument (i.e. one that points to specific tokens) to the
+  // argument definition consumed by the rest of the SQL code.
+  // Guarantees to call ErrorAtToken if std::nullopt is returned.
+  std::optional<sql_argument::ArgumentDefinition> ResolveRawArgument(
+      RawArgument arg);
+  // Parse the arguments in their raw token form.
+  bool ParseRawArguments(std::vector<RawArgument>&);
+  // Same as above, but also convert the raw tokens into argument definitions.
+  bool ParseArguments(std::vector<sql_argument::ArgumentDefinition>&);
+
+  bool ErrorAtToken(const SqliteTokenizer::Token&, const char* error, ...);
+
+  PerfettoSqlPreprocessor preprocessor_;
+  SqliteTokenizer tokenizer_;
+
+  base::Status status_;
+  std::optional<SqlSource> statement_sql_;
+  std::optional<Statement> statement_;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_PARSER_PERFETTO_SQL_PARSER_H_
diff --git a/src/trace_processor/perfetto_sql/parser/perfetto_sql_parser_unittest.cc b/src/trace_processor/perfetto_sql/parser/perfetto_sql_parser_unittest.cc
new file mode 100644
index 0000000..15ac7c4
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/parser/perfetto_sql_parser_unittest.cc
@@ -0,0 +1,368 @@
+/*
+ * 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.
+ */
+
+#include "src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.h"
+
+#include <cstdint>
+#include <variant>
+#include <vector>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/perfetto_sql/parser/perfetto_sql_test_utils.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+using Result = PerfettoSqlParser::Statement;
+using Statement = PerfettoSqlParser::Statement;
+using SqliteSql = PerfettoSqlParser::SqliteSql;
+using CreateFn = PerfettoSqlParser::CreateFunction;
+using CreateTable = PerfettoSqlParser::CreateTable;
+using CreateView = PerfettoSqlParser::CreateView;
+using Include = PerfettoSqlParser::Include;
+using CreateMacro = PerfettoSqlParser::CreateMacro;
+using CreateIndex = PerfettoSqlParser::CreateIndex;
+
+namespace {
+
+class PerfettoSqlParserTest : public ::testing::Test {
+ protected:
+  base::StatusOr<std::vector<PerfettoSqlParser::Statement>> Parse(
+      SqlSource sql) {
+    PerfettoSqlParser parser(sql, macros_);
+    std::vector<PerfettoSqlParser::Statement> results;
+    while (parser.Next()) {
+      results.push_back(std::move(parser.statement()));
+    }
+    if (!parser.status().ok()) {
+      return parser.status();
+    }
+    return results;
+  }
+
+  base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro> macros_;
+};
+
+TEST_F(PerfettoSqlParserTest, Empty) {
+  ASSERT_THAT(*Parse(SqlSource::FromExecuteQuery("")), testing::IsEmpty());
+}
+
+TEST_F(PerfettoSqlParserTest, SemiColonTerminatedStatement) {
+  SqlSource res = SqlSource::FromExecuteQuery("SELECT * FROM slice;");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(), Statement{SqliteSql{}});
+  ASSERT_EQ(parser.statement_sql(), FindSubstr(res, "SELECT * FROM slice;"));
+}
+
+TEST_F(PerfettoSqlParserTest, MultipleStmts) {
+  auto res =
+      SqlSource::FromExecuteQuery("SELECT * FROM slice; SELECT * FROM s");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(), Statement{SqliteSql{}});
+  ASSERT_EQ(parser.statement_sql().sql(),
+            FindSubstr(res, "SELECT * FROM slice;").sql());
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(), Statement{SqliteSql{}});
+  ASSERT_EQ(parser.statement_sql().sql(),
+            FindSubstr(res, "SELECT * FROM s").sql());
+}
+
+TEST_F(PerfettoSqlParserTest, IgnoreOnlySpace) {
+  auto res = SqlSource::FromExecuteQuery(" ; SELECT * FROM s; ; ;");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(), Statement{SqliteSql{}});
+  ASSERT_EQ(parser.statement_sql().sql(),
+            FindSubstr(res, "SELECT * FROM s;").sql());
+}
+
+TEST_F(PerfettoSqlParserTest, CreatePerfettoFunctionScalar) {
+  auto res = SqlSource::FromExecuteQuery(
+      "create perfetto function foo() returns INT as select 1");
+  ASSERT_THAT(*Parse(res), testing::ElementsAre(CreateFn{
+                               false, FunctionPrototype{"foo", {}}, "INT",
+                               FindSubstr(res, "select 1"), false}));
+
+  res = SqlSource::FromExecuteQuery(
+      "create perfetto function bar(x INT, y LONG) returns STRING as "
+      "select 'foo'");
+  ASSERT_THAT(*Parse(res),
+              testing::ElementsAre(
+                  CreateFn{false,
+                           FunctionPrototype{
+                               "bar",
+                               {
+                                   {"$x", sql_argument::Type::kInt},
+                                   {"$y", sql_argument::Type::kLong},
+                               },
+                           },
+                           "STRING", FindSubstr(res, "select 'foo'"), false}));
+
+  res = SqlSource::FromExecuteQuery(
+      "CREATE perfetto FuNcTiOn bar(x INT, y LONG) returnS STRING As "
+      "select 'foo'");
+  ASSERT_THAT(*Parse(res),
+              testing::ElementsAre(
+                  CreateFn{false,
+                           FunctionPrototype{
+                               "bar",
+                               {
+                                   {"$x", sql_argument::Type::kInt},
+                                   {"$y", sql_argument::Type::kLong},
+                               },
+                           },
+                           "STRING", FindSubstr(res, "select 'foo'"), false}));
+}
+
+TEST_F(PerfettoSqlParserTest, CreateOrReplacePerfettoFunctionScalar) {
+  auto res = SqlSource::FromExecuteQuery(
+      "create or replace perfetto function foo() returns INT as select 1");
+  ASSERT_THAT(*Parse(res), testing::ElementsAre(CreateFn{
+                               true, FunctionPrototype{"foo", {}}, "INT",
+                               FindSubstr(res, "select 1"), false}));
+}
+
+TEST_F(PerfettoSqlParserTest, CreatePerfettoFunctionScalarError) {
+  auto res = SqlSource::FromExecuteQuery(
+      "create perfetto function foo( returns INT as select 1");
+  ASSERT_FALSE(Parse(res).status().ok());
+
+  res = SqlSource::FromExecuteQuery(
+      "create perfetto function foo(x INT) as select 1");
+  ASSERT_FALSE(Parse(res).status().ok());
+
+  res = SqlSource::FromExecuteQuery(
+      "create perfetto function foo(x INT) returns INT");
+  ASSERT_FALSE(Parse(res).status().ok());
+}
+
+TEST_F(PerfettoSqlParserTest, CreatePerfettoFunctionAndOther) {
+  auto res = SqlSource::FromExecuteQuery(
+      "create perfetto function foo() returns INT as select 1; select foo()");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  CreateFn fn{false, FunctionPrototype{"foo", {}}, "INT",
+              FindSubstr(res, "select 1"), false};
+  ASSERT_EQ(parser.statement(), Statement{fn});
+  ASSERT_EQ(
+      parser.statement_sql().sql(),
+      FindSubstr(res, "create perfetto function foo() returns INT as select 1")
+          .sql());
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(), Statement{SqliteSql{}});
+  ASSERT_EQ(parser.statement_sql().sql(),
+            FindSubstr(res, "select foo()").sql());
+}
+
+TEST_F(PerfettoSqlParserTest, IncludePerfettoTrivial) {
+  auto res =
+      SqlSource::FromExecuteQuery("include perfetto module cheese.bre_ad;");
+  ASSERT_THAT(*Parse(res), testing::ElementsAre(Include{"cheese.bre_ad"}));
+}
+
+TEST_F(PerfettoSqlParserTest, IncludePerfettoErrorAdditionalChars) {
+  auto res = SqlSource::FromExecuteQuery(
+      "include perfetto module cheese.bre_ad blabla;");
+  ASSERT_FALSE(Parse(res).status().ok());
+}
+
+TEST_F(PerfettoSqlParserTest, IncludePerfettoErrorWrongModuleName) {
+  auto res =
+      SqlSource::FromExecuteQuery("include perfetto module chees*e.bre_ad;");
+  ASSERT_FALSE(Parse(res).status().ok());
+}
+
+TEST_F(PerfettoSqlParserTest, CreatePerfettoMacro) {
+  auto res = SqlSource::FromExecuteQuery(
+      "create perfetto macro foo(a1 Expr, b1 TableOrSubquery,c3_d "
+      "TableOrSubquery2 ) returns TableOrSubquery3 as random sql snippet");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(
+      parser.statement(),
+      Statement(CreateMacro{
+          false,
+          FindSubstr(res, "foo"),
+          {
+              {FindSubstr(res, "a1"), FindSubstr(res, "Expr")},
+              {FindSubstr(res, "b1"), FindSubstr(res, "TableOrSubquery")},
+              {FindSubstr(res, "c3_d"), FindSubstr(res, "TableOrSubquery2")},
+          },
+          FindSubstr(res, "TableOrSubquery3"),
+          FindSubstr(res, "random sql snippet")}));
+  ASSERT_FALSE(parser.Next());
+}
+
+TEST_F(PerfettoSqlParserTest, CreateOrReplacePerfettoMacro) {
+  auto res = SqlSource::FromExecuteQuery(
+      "create or replace perfetto macro foo() returns Expr as 1");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(), Statement(CreateMacro{true,
+                                                      FindSubstr(res, "foo"),
+                                                      {},
+                                                      FindSubstr(res, "Expr"),
+                                                      FindSubstr(res, "1")}));
+  ASSERT_FALSE(parser.Next());
+}
+
+TEST_F(PerfettoSqlParserTest, CreatePerfettoMacroAndOther) {
+  auto res = SqlSource::FromExecuteQuery(
+      "create perfetto macro foo() returns sql1 as random sql snippet; "
+      "select 1");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(), Statement(CreateMacro{
+                                    false,
+                                    FindSubstr(res, "foo"),
+                                    {},
+                                    FindSubstr(res, "sql1"),
+                                    FindSubstr(res, "random sql snippet"),
+                                }));
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(), Statement(SqliteSql{}));
+  ASSERT_EQ(parser.statement_sql(), FindSubstr(res, "select 1"));
+  ASSERT_FALSE(parser.Next());
+}
+
+TEST_F(PerfettoSqlParserTest, CreatePerfettoTable) {
+  auto res = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO TABLE foo AS SELECT 42 AS bar");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(),
+            Statement(CreateTable{
+                false, "foo", FindSubstr(res, "SELECT 42 AS bar"), {}}));
+  ASSERT_FALSE(parser.Next());
+}
+
+TEST_F(PerfettoSqlParserTest, CreateOrReplacePerfettoTable) {
+  auto res = SqlSource::FromExecuteQuery(
+      "CREATE OR REPLACE PERFETTO TABLE foo AS SELECT 42 AS bar");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(),
+            Statement(CreateTable{
+                true, "foo", FindSubstr(res, "SELECT 42 AS bar"), {}}));
+  ASSERT_FALSE(parser.Next());
+}
+
+TEST_F(PerfettoSqlParserTest, CreatePerfettoTableWithSchema) {
+  auto res = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO TABLE foo(bar INT) AS SELECT 42 AS bar");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(), Statement(CreateTable{
+                                    false,
+                                    "foo",
+                                    FindSubstr(res, "SELECT 42 AS bar"),
+                                    {{"$bar", sql_argument::Type::kInt}},
+                                }));
+  ASSERT_FALSE(parser.Next());
+}
+
+TEST_F(PerfettoSqlParserTest, CreatePerfettoTableAndOther) {
+  auto res = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO TABLE foo AS SELECT 42 AS bar; select 1");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(),
+            Statement(CreateTable{
+                false, "foo", FindSubstr(res, "SELECT 42 AS bar"), {}}));
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(), Statement(SqliteSql{}));
+  ASSERT_EQ(parser.statement_sql(), FindSubstr(res, "select 1"));
+  ASSERT_FALSE(parser.Next());
+}
+
+TEST_F(PerfettoSqlParserTest, CreatePerfettoView) {
+  auto res = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO VIEW foo AS SELECT 42 AS bar");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(
+      parser.statement(),
+      Statement(CreateView{
+          false,
+          "foo",
+          SqlSource::FromExecuteQuery("SELECT 42 AS bar"),
+          SqlSource::FromExecuteQuery("CREATE VIEW foo AS SELECT 42 AS bar"),
+          {}}));
+  ASSERT_FALSE(parser.Next());
+}
+
+TEST_F(PerfettoSqlParserTest, CreateOrReplacePerfettoView) {
+  auto res = SqlSource::FromExecuteQuery(
+      "CREATE OR REPLACE PERFETTO VIEW foo AS SELECT 42 AS bar");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(
+      parser.statement(),
+      Statement(CreateView{
+          true,
+          "foo",
+          SqlSource::FromExecuteQuery("SELECT 42 AS bar"),
+          SqlSource::FromExecuteQuery("CREATE VIEW foo AS SELECT 42 AS bar"),
+          {}}));
+  ASSERT_FALSE(parser.Next());
+}
+
+TEST_F(PerfettoSqlParserTest, CreatePerfettoViewAndOther) {
+  auto res = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO VIEW foo AS SELECT 42 AS bar; select 1");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(
+      parser.statement(),
+      Statement(CreateView{
+          false,
+          "foo",
+          SqlSource::FromExecuteQuery("SELECT 42 AS bar"),
+          SqlSource::FromExecuteQuery("CREATE VIEW foo AS SELECT 42 AS bar"),
+          {}}));
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(), Statement(SqliteSql{}));
+  ASSERT_EQ(parser.statement_sql(), FindSubstr(res, "select 1"));
+  ASSERT_FALSE(parser.Next());
+}
+
+TEST_F(PerfettoSqlParserTest, CreatePerfettoViewWithSchema) {
+  auto res = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO VIEW foo(foo STRING, bar INT) AS SELECT 'a' as foo, 42 "
+      "AS bar");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(),
+            Statement(CreateView{
+                false,
+                "foo",
+                SqlSource::FromExecuteQuery("SELECT 'a' as foo, 42 AS bar"),
+                SqlSource::FromExecuteQuery(
+                    "CREATE VIEW foo AS SELECT 'a' as foo, 42 AS bar"),
+                {{"$foo", sql_argument::Type::kString},
+                 {"$bar", sql_argument::Type::kInt}},
+            }));
+  ASSERT_FALSE(parser.Next());
+}
+
+}  // namespace
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/perfetto_sql/parser/perfetto_sql_test_utils.h b/src/trace_processor/perfetto_sql/parser/perfetto_sql_test_utils.h
new file mode 100644
index 0000000..5a776e0
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/parser/perfetto_sql_test_utils.h
@@ -0,0 +1,149 @@
+/*
+ * 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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_PARSER_PERFETTO_SQL_TEST_UTILS_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_PARSER_PERFETTO_SQL_TEST_UTILS_H_
+
+#include <cstddef>
+#include <cstdint>
+#include <ostream>
+#include <string>
+#include <tuple>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+inline bool operator==(const SqlSource& a, const SqlSource& b) {
+  return a.sql() == b.sql();
+}
+
+inline bool operator==(const PerfettoSqlParser::SqliteSql&,
+                       const PerfettoSqlParser::SqliteSql&) {
+  return true;
+}
+
+inline bool operator==(const PerfettoSqlParser::CreateFunction& a,
+                       const PerfettoSqlParser::CreateFunction& b) {
+  return std::tie(a.returns, a.is_table, a.prototype, a.replace, a.sql) ==
+         std::tie(b.returns, b.is_table, b.prototype, b.replace, b.sql);
+}
+
+inline bool operator==(const PerfettoSqlParser::CreateTable& a,
+                       const PerfettoSqlParser::CreateTable& b) {
+  return std::tie(a.name, a.sql) == std::tie(b.name, b.sql);
+}
+
+inline bool operator==(const PerfettoSqlParser::CreateView& a,
+                       const PerfettoSqlParser::CreateView& b) {
+  return std::tie(a.name, a.create_view_sql) ==
+         std::tie(b.name, b.create_view_sql);
+}
+
+inline bool operator==(const PerfettoSqlParser::Include& a,
+                       const PerfettoSqlParser::Include& b) {
+  return std::tie(a.key) == std::tie(b.key);
+}
+
+constexpr bool operator==(const PerfettoSqlParser::CreateMacro& a,
+                          const PerfettoSqlParser::CreateMacro& b) {
+  return std::tie(a.replace, a.name, a.sql, a.args) ==
+         std::tie(b.replace, b.name, b.sql, b.args);
+}
+
+constexpr bool operator==(const PerfettoSqlParser::CreateIndex& a,
+                          const PerfettoSqlParser::CreateIndex& b) {
+  return std::tie(a.replace, a.name, a.table_name, a.col_names) ==
+         std::tie(b.replace, b.name, b.table_name, b.col_names);
+}
+
+constexpr bool operator==(const PerfettoSqlParser::DropIndex& a,
+                          const PerfettoSqlParser::DropIndex& b) {
+  return std::tie(a.name, a.table_name) == std::tie(b.name, b.table_name);
+}
+
+inline std::ostream& operator<<(std::ostream& stream, const SqlSource& sql) {
+  return stream << "SqlSource(sql=" << testing::PrintToString(sql.sql()) << ")";
+}
+
+inline std::ostream& operator<<(std::ostream& stream,
+                                const PerfettoSqlParser::Statement& line) {
+  if (std::get_if<PerfettoSqlParser::SqliteSql>(&line)) {
+    return stream << "SqliteSql()";
+  }
+  if (auto* fn = std::get_if<PerfettoSqlParser::CreateFunction>(&line)) {
+    return stream << "CreateFn(sql=" << testing::PrintToString(fn->sql)
+                  << ", prototype=" << testing::PrintToString(fn->prototype)
+                  << ", returns=" << testing::PrintToString(fn->returns)
+                  << ", is_table=" << testing::PrintToString(fn->is_table)
+                  << ", replace=" << testing::PrintToString(fn->replace) << ")";
+  }
+  if (auto* tab = std::get_if<PerfettoSqlParser::CreateTable>(&line)) {
+    return stream << "CreateTable(name=" << testing::PrintToString(tab->name)
+                  << ", sql=" << testing::PrintToString(tab->sql) << ")";
+  }
+  if (auto* tab = std::get_if<PerfettoSqlParser::CreateView>(&line)) {
+    return stream << "CreateView(name=" << testing::PrintToString(tab->name)
+                  << ", sql=" << testing::PrintToString(tab->create_view_sql)
+                  << ")";
+  }
+  if (auto* macro = std::get_if<PerfettoSqlParser::CreateMacro>(&line)) {
+    return stream << "CreateTable(name=" << testing::PrintToString(macro->name)
+                  << ", args=" << testing::PrintToString(macro->args)
+                  << ", replace=" << testing::PrintToString(macro->replace)
+                  << ", sql=" << testing::PrintToString(macro->sql) << ")";
+  }
+  PERFETTO_FATAL("Unknown type");
+}
+
+template <typename T>
+inline bool operator==(const base::StatusOr<T>& a, const base::StatusOr<T>& b) {
+  return a.status().ok() == b.ok() &&
+         a.status().message() == b.status().message() &&
+         (!a.ok() || a.value() == b.value());
+}
+
+inline std::ostream& operator<<(std::ostream& stream, const base::Status& a) {
+  return stream << "base::Status(ok=" << a.ok()
+                << ", message=" << testing::PrintToString(a.message()) << ")";
+}
+
+template <typename T>
+inline std::ostream& operator<<(std::ostream& stream,
+                                const base::StatusOr<T>& a) {
+  std::string val = a.ok() ? testing::PrintToString(a.value()) : "";
+  return stream << "base::StatusOr(status="
+                << testing::PrintToString(a.status()) << ", value=" << val
+                << ")";
+}
+
+inline SqlSource FindSubstr(const SqlSource& source,
+                            const std::string& needle) {
+  size_t off = source.sql().find(needle);
+  PERFETTO_CHECK(off != std::string::npos);
+  return source.Substr(static_cast<uint32_t>(off),
+                       static_cast<uint32_t>(needle.size()));
+}
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_PARSER_PERFETTO_SQL_TEST_UTILS_H_
diff --git a/src/trace_processor/perfetto_sql/preprocessor/BUILD.gn b/src/trace_processor/perfetto_sql/preprocessor/BUILD.gn
new file mode 100644
index 0000000..f27699d
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/preprocessor/BUILD.gn
@@ -0,0 +1,64 @@
+# 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("../../../../gn/test.gni")
+
+assert(enable_perfetto_trace_processor_sqlite)
+
+source_set("preprocessor") {
+  sources = [
+    "perfetto_sql_preprocessor.cc",
+    "perfetto_sql_preprocessor.h",
+  ]
+  deps = [
+    ":grammar",
+    "../../../../gn:default_deps",
+    "../../../base",
+    "../../sqlite",
+    "../../util",
+    "../tokenizer",
+  ]
+}
+
+source_set("grammar") {
+  sources = [
+    "preprocessor_grammar.c",
+    "preprocessor_grammar.h",
+    "preprocessor_grammar_interface.h",
+  ]
+  deps = [ "../../../../gn:default_deps" ]
+  visibility = [ ":preprocessor" ]
+  if (perfetto_build_standalone) {
+    configs -= [ "//gn/standalone:extra_warnings" ]  # nogncheck
+  } else {
+    cflags_c = [
+      "-Wno-unused-parameter",
+      "-Wno-unreachable-code",
+    ]
+  }
+}
+
+perfetto_unittest_source_set("unittests") {
+  testonly = true
+  sources = [ "perfetto_sql_preprocessor_unittest.cc" ]
+  deps = [
+    ":preprocessor",
+    "../../../../gn:default_deps",
+    "../../../../gn:gtest_and_gmock",
+    "../../../../gn:sqlite",
+    "../../../base",
+    "../../sqlite",
+    "../parser:test_utils",
+  ]
+}
diff --git a/src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.cc b/src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.cc
new file mode 100644
index 0000000..5570d5d
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.cc
@@ -0,0 +1,553 @@
+/*
+ * 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.
+ */
+
+#include "src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <cstdlib>
+#include <list>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <unordered_set>
+#include <utility>
+#include <variant>
+#include <vector>
+
+#include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar_interface.h"
+#include "src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+
+namespace perfetto::trace_processor {
+namespace {
+
+using State = PreprocessorGrammarState;
+
+struct Preprocessor {
+ public:
+  explicit Preprocessor(State* state)
+      : parser_(PreprocessorGrammarParseAlloc(malloc, state)) {}
+  ~Preprocessor() { PreprocessorGrammarParseFree(parser_, free); }
+
+  void Parse(int token_type, PreprocessorGrammarToken token) {
+    PreprocessorGrammarParse(parser_, token_type, token);
+  }
+
+ private:
+  void* parser_;
+};
+
+struct Stringify {
+  bool ignore_table;
+};
+struct Apply {
+  int join_token;
+  int prefix_token;
+};
+using MacroImpl =
+    std::variant<PerfettoSqlPreprocessor::Macro*, Stringify, Apply>;
+
+// Synthetic "stackframe" representing the processing of a single piece of SQL.
+struct Frame {
+  struct Root {};
+  struct Rewrite {
+    SqliteTokenizer& tokenizer;
+    SqlSource::Rewriter& rewriter;
+    SqliteTokenizer::Token start;
+    SqliteTokenizer::Token end;
+  };
+  struct Append {
+    std::vector<SqlSource>& result;
+  };
+  using Type = std::variant<Root, Rewrite, Append>;
+  struct ActiveMacro {
+    std::string name;
+    MacroImpl impl;
+    std::vector<SqlSource> args;
+    uint32_t nested_macro_count;
+    std::unordered_set<std::string> seen_variables;
+    std::unordered_set<std::string> expanded_variables;
+  };
+  enum VariableHandling { kLookup, kLookupOrIgnore, kIgnore };
+
+  explicit Frame(Type _type,
+                 VariableHandling _var_handling,
+                 State* s,
+                 const SqlSource& source)
+      : type(_type),
+        var_handling(_var_handling),
+        preprocessor(s),
+        tokenizer(source),
+        rewriter(source),
+        substituitions(&owned_substituitions) {}
+  Frame(const Frame&) = delete;
+  Frame& operator=(const Frame&) = delete;
+  Frame(Frame&&) = delete;
+  Frame& operator=(Frame&&) = delete;
+
+  Type type;
+  VariableHandling var_handling;
+  Preprocessor preprocessor;
+  SqliteTokenizer tokenizer;
+
+  bool seen_semicolon = false;
+  SqlSource::Rewriter rewriter;
+  bool ignore_rewrite = false;
+
+  std::optional<ActiveMacro> active_macro;
+
+  base::FlatHashMap<std::string, SqlSource> owned_substituitions;
+  base::FlatHashMap<std::string, SqlSource>* substituitions;
+};
+
+struct ErrorToken {
+  SqliteTokenizer::Token token;
+  std::string message;
+};
+
+extern "C" struct PreprocessorGrammarState {
+  std::list<Frame> stack;
+  const base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro>& macros;
+  std::optional<ErrorToken> error;
+};
+
+extern "C" struct PreprocessorGrammarApplyList {
+  std::vector<PreprocessorGrammarTokenBounds> args;
+};
+
+SqliteTokenizer::Token GrammarTokenToTokenizerToken(
+    const PreprocessorGrammarToken& token) {
+  return SqliteTokenizer::Token{std::string_view(token.ptr, token.n),
+                                TK_ILLEGAL};
+}
+
+base::Status ErrorAtToken(const SqliteTokenizer& tokenizer,
+                          const SqliteTokenizer::Token& token,
+                          const char* error) {
+  std::string traceback = tokenizer.AsTraceback(token);
+  return base::ErrStatus("%s%s", traceback.c_str(), error);
+}
+
+std::vector<std::string> SqlSourceVectorToString(
+    const std::vector<SqlSource>& vec) {
+  std::vector<std::string> pieces;
+  pieces.reserve(vec.size());
+  for (const auto& list : vec) {
+    pieces.emplace_back(list.sql());
+  }
+  return pieces;
+}
+
+std::string_view BoundsToStringView(const PreprocessorGrammarTokenBounds& b) {
+  return {b.start.ptr, static_cast<size_t>(b.end.ptr + b.end.n - b.start.ptr)};
+}
+
+void RewriteIntrinsicMacro(Frame& frame,
+                           SqliteTokenizer::Token name,
+                           SqliteTokenizer::Token rp) {
+  const auto& macro = *frame.active_macro;
+  frame.tokenizer.Rewrite(
+      frame.rewriter, name, rp,
+      SqlSource::FromTraceProcessorImplementation(
+          macro.name + "!(" +
+          base::Join(SqlSourceVectorToString(macro.args), ", ") + ")"),
+      SqliteTokenizer::EndToken::kInclusive);
+}
+
+void ExecuteSqlMacro(State* state,
+                     Frame& frame,
+                     Frame::ActiveMacro& macro,
+                     SqliteTokenizer::Token name,
+                     SqliteTokenizer::Token rp) {
+  auto& sql_macro = std::get<PerfettoSqlPreprocessor::Macro*>(macro.impl);
+  if (macro.args.size() != sql_macro->args.size()) {
+    state->error = ErrorToken{
+        name,
+        base::ErrStatus(
+            "wrong number of macro arguments, expected %zu actual %zu",
+            sql_macro->args.size(), macro.args.size())
+            .message(),
+    };
+    return;
+  }
+  // TODO(lalitm): switch back to kLookup once we have proper parser support.
+  state->stack.emplace_back(
+      Frame::Rewrite{frame.tokenizer, frame.rewriter, name, rp},
+      Frame::kLookupOrIgnore, state, sql_macro->sql);
+  auto& macro_frame = state->stack.back();
+  for (uint32_t i = 0; i < sql_macro->args.size(); ++i) {
+    macro_frame.owned_substituitions.Insert(sql_macro->args[i],
+                                            std::move(macro.args[i]));
+  }
+}
+
+void ExecuteStringify(State* state,
+                      Frame& frame,
+                      Frame::ActiveMacro& macro,
+                      SqliteTokenizer::Token name,
+                      SqliteTokenizer::Token rp) {
+  auto& stringify = std::get<Stringify>(macro.impl);
+  if (macro.args.size() != 1) {
+    state->error = ErrorToken{
+        name,
+        base::ErrStatus(
+            "stringify: must specify exactly 1 argument, actual %zu",
+            macro.args.size())
+            .message(),
+    };
+    return;
+  }
+  bool can_stringify_outer =
+      macro.seen_variables.empty() ||
+      (stringify.ignore_table && macro.seen_variables.size() == 1 &&
+       macro.seen_variables.count("table"));
+  if (!can_stringify_outer) {
+    RewriteIntrinsicMacro(frame, name, rp);
+    return;
+  }
+  if (!macro.expanded_variables.empty()) {
+    state->stack.emplace_back(
+        Frame::Rewrite{frame.tokenizer, frame.rewriter, name, rp},
+        Frame::kIgnore, state,
+        SqlSource::FromTraceProcessorImplementation(macro.name + "!(" +
+                                                    macro.args[0].sql() + ")"));
+    auto& expand_frame = state->stack.back();
+    expand_frame.substituitions = frame.substituitions;
+    return;
+  }
+  auto res = SqlSource::FromTraceProcessorImplementation(
+      "'" + macro.args[0].sql() + "'");
+  frame.tokenizer.Rewrite(frame.rewriter, name, rp, std::move(res),
+                          SqliteTokenizer::EndToken::kInclusive);
+}
+
+void ExecuteApply(State* state,
+                  Frame& frame,
+                  Frame::ActiveMacro& macro,
+                  SqliteTokenizer::Token name,
+                  SqliteTokenizer::Token rp) {
+  auto& apply = std::get<Apply>(macro.impl);
+  if (!macro.seen_variables.empty()) {
+    RewriteIntrinsicMacro(frame, name, rp);
+    return;
+  }
+  state->stack.emplace_back(
+      Frame::Rewrite{frame.tokenizer, frame.rewriter, name, rp},
+      Frame::VariableHandling::kIgnore, state,
+      SqlSource::FromTraceProcessorImplementation(
+          base::Join(SqlSourceVectorToString(macro.args), " ")));
+
+  auto& expansion_frame = state->stack.back();
+  expansion_frame.preprocessor.Parse(
+      PPTK_APPLY, PreprocessorGrammarToken{nullptr, 0, PPTK_APPLY});
+  expansion_frame.preprocessor.Parse(
+      apply.join_token, PreprocessorGrammarToken{nullptr, 0, apply.join_token});
+  expansion_frame.preprocessor.Parse(
+      apply.prefix_token,
+      PreprocessorGrammarToken{nullptr, 0, apply.prefix_token});
+  expansion_frame.ignore_rewrite = true;
+}
+
+extern "C" void OnPreprocessorSyntaxError(State* state,
+                                          PreprocessorGrammarToken* token) {
+  state->error = {GrammarTokenToTokenizerToken(*token),
+                  "preprocessor syntax error"};
+}
+
+extern "C" void OnPreprocessorApply(PreprocessorGrammarState* state,
+                                    PreprocessorGrammarToken* name,
+                                    PreprocessorGrammarToken* join,
+                                    PreprocessorGrammarToken* prefix,
+                                    PreprocessorGrammarApplyList* raw_a,
+                                    PreprocessorGrammarApplyList* raw_b) {
+  std::unique_ptr<PreprocessorGrammarApplyList> a(raw_a);
+  std::unique_ptr<PreprocessorGrammarApplyList> b(raw_b);
+  auto& frame = state->stack.back();
+  size_t size = std::min(a->args.size(), b ? b->args.size() : a->args.size());
+  if (size == 0) {
+    auto& rewrite = std::get<Frame::Rewrite>(frame.type);
+    rewrite.tokenizer.Rewrite(rewrite.rewriter, rewrite.start, rewrite.end,
+                              SqlSource::FromTraceProcessorImplementation(""),
+                              SqliteTokenizer::EndToken::kInclusive);
+    return;
+  }
+  std::string macro(name->ptr, name->n);
+  std::vector<std::string> args;
+  for (uint32_t i = 0; i < size; ++i) {
+    std::string arg = macro;
+    arg.append("!(").append(BoundsToStringView(a->args[i]));
+    if (b) {
+      arg.append(",").append(BoundsToStringView(b->args[i]));
+    }
+    arg.append(")");
+    args.emplace_back(std::move(arg));
+  }
+  std::string joiner = join->major == PPTK_AND ? " AND " : " , ";
+  std::string res = prefix->major == PPTK_TRUE ? joiner : "";
+  res.append(base::Join(args, joiner));
+  state->stack.emplace_back(
+      frame.type, Frame::VariableHandling::kLookupOrIgnore, state,
+      SqlSource::FromTraceProcessorImplementation(std::move(res)));
+}
+
+extern "C" void OnPreprocessorVariable(State* state,
+                                       PreprocessorGrammarToken* var) {
+  if (var->n == 0 || var->ptr[0] != '$') {
+    state->error = {GrammarTokenToTokenizerToken(*var),
+                    "variable must start with '$'"};
+    return;
+  }
+  auto& frame = state->stack.back();
+  if (frame.active_macro) {
+    std::string name(var->ptr + 1, var->n - 1);
+    if (frame.substituitions->Find(name)) {
+      frame.active_macro->expanded_variables.insert(name);
+    } else {
+      frame.active_macro->seen_variables.insert(name);
+    }
+    return;
+  }
+  switch (frame.var_handling) {
+    case Frame::kLookup:
+    case Frame::kLookupOrIgnore: {
+      auto* it =
+          frame.substituitions->Find(std::string(var->ptr + 1, var->n - 1));
+      if (!it) {
+        if (frame.var_handling == Frame::kLookup) {
+          state->error = {GrammarTokenToTokenizerToken(*var),
+                          "variable not defined"};
+        }
+        return;
+      }
+      frame.tokenizer.RewriteToken(frame.rewriter,
+                                   GrammarTokenToTokenizerToken(*var), *it);
+      break;
+    }
+    case Frame::kIgnore:
+      break;
+  }
+}
+
+extern "C" void OnPreprocessorMacroId(State* state,
+                                      PreprocessorGrammarToken* name_tok) {
+  auto& invocation = state->stack.back();
+  if (invocation.active_macro) {
+    invocation.active_macro->nested_macro_count++;
+    return;
+  }
+  std::string name(name_tok->ptr, name_tok->n);
+  MacroImpl impl;
+  if (name == "__intrinsic_stringify") {
+    impl = Stringify();
+  } else if (name == "__intrinsic_stringify_ignore_table") {
+    impl = Stringify{true};
+  } else if (name == "__intrinsic_token_apply") {
+    impl = Apply{PPTK_COMMA, PPTK_FALSE};
+  } else if (name == "__intrinsic_token_apply_prefix") {
+    impl = Apply{PPTK_COMMA, PPTK_TRUE};
+  } else if (name == "__intrinsic_token_apply_and") {
+    impl = Apply{PPTK_AND, PPTK_FALSE};
+  } else if (name == "__intrinsic_token_apply_and_prefix") {
+    impl = Apply{PPTK_AND, PPTK_TRUE};
+  } else {
+    auto* sql_macro = state->macros.Find(name);
+    if (!sql_macro) {
+      state->error = {GrammarTokenToTokenizerToken(*name_tok),
+                      "no such macro defined"};
+      return;
+    }
+    impl = sql_macro;
+  }
+  invocation.active_macro =
+      Frame::ActiveMacro{std::move(name), impl, {}, 0, {}, {}};
+}
+
+extern "C" void OnPreprocessorMacroArg(State* state,
+                                       PreprocessorGrammarTokenBounds* arg) {
+  auto& frame = state->stack.back();
+  auto& macro = *frame.active_macro;
+  if (macro.nested_macro_count > 0) {
+    return;
+  }
+  auto start_token = GrammarTokenToTokenizerToken(arg->start);
+  auto end_token = GrammarTokenToTokenizerToken(arg->end);
+  state->stack.emplace_back(
+      Frame::Append{macro.args}, frame.var_handling, state,
+      frame.tokenizer.Substr(start_token, end_token,
+                             SqliteTokenizer::EndToken::kInclusive));
+
+  auto& arg_frame = state->stack.back();
+  arg_frame.substituitions = frame.substituitions;
+}
+
+extern "C" void OnPreprocessorMacroEnd(State* state,
+                                       PreprocessorGrammarToken* name,
+                                       PreprocessorGrammarToken* rp) {
+  auto& frame = state->stack.back();
+  auto& macro = *frame.active_macro;
+  if (macro.nested_macro_count > 0) {
+    --macro.nested_macro_count;
+    return;
+  }
+  switch (macro.impl.index()) {
+    case base::variant_index<MacroImpl, PerfettoSqlPreprocessor::Macro*>():
+      ExecuteSqlMacro(state, frame, macro, GrammarTokenToTokenizerToken(*name),
+                      GrammarTokenToTokenizerToken(*rp));
+      break;
+    case base::variant_index<MacroImpl, Stringify>():
+      ExecuteStringify(state, frame, macro, GrammarTokenToTokenizerToken(*name),
+                       GrammarTokenToTokenizerToken(*rp));
+      break;
+    case base::variant_index<MacroImpl, Apply>():
+      ExecuteApply(state, frame, macro, GrammarTokenToTokenizerToken(*name),
+                   GrammarTokenToTokenizerToken(*rp));
+      break;
+    default:
+      PERFETTO_FATAL("Unknown variant type");
+  }
+  frame.active_macro = std::nullopt;
+}
+
+extern "C" void OnPreprocessorEnd(State* state) {
+  auto& frame = state->stack.back();
+  PERFETTO_CHECK(!frame.active_macro);
+
+  if (frame.ignore_rewrite) {
+    return;
+  }
+  switch (frame.type.index()) {
+    case base::variant_index<Frame::Type, Frame::Append>(): {
+      auto& append = std::get<Frame::Append>(frame.type);
+      append.result.push_back(std::move(frame.rewriter).Build());
+      break;
+    }
+    case base::variant_index<Frame::Type, Frame::Rewrite>(): {
+      auto& rewrite = std::get<Frame::Rewrite>(frame.type);
+      rewrite.tokenizer.Rewrite(rewrite.rewriter, rewrite.start, rewrite.end,
+                                std::move(frame.rewriter).Build(),
+                                SqliteTokenizer::EndToken::kInclusive);
+      break;
+    }
+    case base::variant_index<Frame::Type, Frame::Root>():
+      break;
+    default:
+      PERFETTO_FATAL("Unknown frame type");
+  }
+}
+
+}  // namespace
+
+PerfettoSqlPreprocessor::PerfettoSqlPreprocessor(
+    SqlSource source,
+    const base::FlatHashMap<std::string, Macro>& macros)
+    : global_tokenizer_(std::move(source)), macros_(&macros) {}
+
+bool PerfettoSqlPreprocessor::NextStatement() {
+  PERFETTO_CHECK(status_.ok());
+
+  // Skip through any number of semi-colons (representing empty statements).
+  SqliteTokenizer::Token tok = global_tokenizer_.NextNonWhitespace();
+  while (tok.token_type == TK_SEMI) {
+    tok = global_tokenizer_.NextNonWhitespace();
+  }
+
+  // If we still see a terminal token at this point, we must have hit EOF.
+  if (tok.IsTerminal()) {
+    PERFETTO_DCHECK(tok.token_type != TK_SEMI);
+    return false;
+  }
+
+  SqlSource stmt =
+      global_tokenizer_.Substr(tok, global_tokenizer_.NextTerminal(),
+                               SqliteTokenizer::EndToken::kInclusive);
+
+  State s{{}, *macros_, {}};
+  s.stack.emplace_back(Frame::Root(), Frame::kIgnore, &s, std::move(stmt));
+  for (;;) {
+    auto* frame = &s.stack.back();
+    auto& tk = frame->tokenizer;
+    SqliteTokenizer::Token t = tk.NextNonWhitespace();
+    int token_type;
+    if (t.str.empty()) {
+      token_type = frame->seen_semicolon ? 0 : PPTK_SEMI;
+      frame->seen_semicolon = true;
+    } else if (t.token_type == TK_SEMI) {
+      token_type = PPTK_SEMI;
+      frame->seen_semicolon = true;
+    } else if (t.token_type == TK_ILLEGAL) {
+      if (t.str.size() == 1 && t.str[0] == '!') {
+        token_type = PPTK_EXCLAIM;
+      } else {
+        status_ = ErrorAtToken(tk, t, "illegal token");
+        return false;
+      }
+    } else if (t.token_type == TK_ID) {
+      token_type = PPTK_ID;
+    } else if (t.token_type == TK_LP) {
+      token_type = PPTK_LP;
+    } else if (t.token_type == TK_RP) {
+      token_type = PPTK_RP;
+    } else if (t.token_type == TK_COMMA) {
+      token_type = PPTK_COMMA;
+    } else if (t.token_type == TK_VARIABLE) {
+      token_type = PPTK_VARIABLE;
+    } else {
+      token_type = PPTK_OPAQUE;
+    }
+    frame->preprocessor.Parse(
+        token_type,
+        PreprocessorGrammarToken{t.str.data(), t.str.size(), token_type});
+    if (s.error) {
+      status_ = ErrorAtToken(tk, s.error->token, s.error->message.c_str());
+      return false;
+    }
+    if (token_type == 0) {
+      if (s.stack.size() == 1) {
+        statement_ = std::move(frame->rewriter).Build();
+        return true;
+      }
+      s.stack.pop_back();
+      frame = &s.stack.back();
+    }
+  }
+}
+
+extern "C" PreprocessorGrammarApplyList* OnPreprocessorCreateApplyList() {
+  return std::make_unique<PreprocessorGrammarApplyList>().release();
+}
+
+extern "C" PreprocessorGrammarApplyList* OnPreprocessorAppendApplyList(
+    PreprocessorGrammarApplyList* list,
+    PreprocessorGrammarTokenBounds* bounds) {
+  list->args.push_back(*bounds);
+  return list;
+}
+
+extern "C" void OnPreprocessorFreeApplyList(
+    PreprocessorGrammarState*,
+    PreprocessorGrammarApplyList* list) {
+  delete list;
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.h b/src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.h
new file mode 100644
index 0000000..fdbee3e
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.h
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_PREPROCESSOR_PERFETTO_SQL_PREPROCESSOR_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_PREPROCESSOR_PERFETTO_SQL_PREPROCESSOR_H_
+
+#include <optional>
+#include <string>
+#include <unordered_set>
+#include <vector>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+
+namespace perfetto::trace_processor {
+
+// Preprocessor for PerfettoSQL statements. The main responsiblity of this
+// class is to perform similar functions to the C/C++ preprocessor (e.g.
+// expanding macros). It is also responsible for splitting the given SQL into
+// statements.
+class PerfettoSqlPreprocessor {
+ public:
+  struct Macro {
+    bool replace;
+    std::string name;
+    std::vector<std::string> args;
+    SqlSource sql;
+  };
+
+  // Creates a preprocessor acting on the given SqlSource.
+  explicit PerfettoSqlPreprocessor(
+      SqlSource,
+      const base::FlatHashMap<std::string, Macro>&);
+
+  // Preprocesses the next SQL statement. Returns true if a statement was
+  // successfully preprocessed and false if EOF was reached or the statement was
+  // not preprocessed correctly.
+  //
+  // Note: if this function returns false, callers *must* call |status()|: it
+  // is undefined behaviour to not do so.
+  bool NextStatement();
+
+  // Returns the error status for the parser. This will be |base::OkStatus()|
+  // until an unrecoverable error is encountered.
+  const base::Status& status() const { return status_; }
+
+  // Returns the most-recent preprocessed SQL statement.
+  //
+  // Note: this function must not be called unless |NextStatement()| returned
+  // true.
+  SqlSource& statement() { return *statement_; }
+
+ private:
+  SqliteTokenizer global_tokenizer_;
+  const base::FlatHashMap<std::string, Macro>* macros_ = nullptr;
+  std::unordered_set<std::string> seen_macros_;
+  std::optional<SqlSource> statement_;
+  base::Status status_;
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_PREPROCESSOR_PERFETTO_SQL_PREPROCESSOR_H_
diff --git a/src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor_unittest.cc b/src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor_unittest.cc
new file mode 100644
index 0000000..305064f
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor_unittest.cc
@@ -0,0 +1,237 @@
+/*
+ * 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.
+ */
+
+#include "src/trace_processor/perfetto_sql/preprocessor/perfetto_sql_preprocessor.h"
+
+#include <string>
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "src/trace_processor/perfetto_sql/parser/perfetto_sql_test_utils.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto::trace_processor {
+namespace {
+
+using ::testing::HasSubstr;
+
+using Macro = PerfettoSqlPreprocessor::Macro;
+
+class PerfettoSqlPreprocessorUnittest : public ::testing::Test {
+ protected:
+  base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro> macros_;
+};
+
+TEST_F(PerfettoSqlPreprocessorUnittest, Empty) {
+  PerfettoSqlPreprocessor preprocessor(SqlSource::FromExecuteQuery(""),
+                                       macros_);
+  ASSERT_FALSE(preprocessor.NextStatement());
+  ASSERT_TRUE(preprocessor.status().ok());
+}
+
+TEST_F(PerfettoSqlPreprocessorUnittest, SemiColonTerminatedStatement) {
+  auto source = SqlSource::FromExecuteQuery("SELECT * FROM slice;");
+  PerfettoSqlPreprocessor preprocessor(source, macros_);
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(preprocessor.statement(),
+            FindSubstr(source, "SELECT * FROM slice;"));
+  ASSERT_FALSE(preprocessor.NextStatement());
+  ASSERT_TRUE(preprocessor.status().ok());
+}
+
+TEST_F(PerfettoSqlPreprocessorUnittest, IgnoreOnlySpace) {
+  auto source = SqlSource::FromExecuteQuery(" ; SELECT * FROM s; ; ;");
+  PerfettoSqlPreprocessor preprocessor(source, macros_);
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(preprocessor.statement(), FindSubstr(source, "SELECT * FROM s;"));
+  ASSERT_FALSE(preprocessor.NextStatement());
+  ASSERT_TRUE(preprocessor.status().ok());
+}
+
+TEST_F(PerfettoSqlPreprocessorUnittest, MultipleStmts) {
+  auto source =
+      SqlSource::FromExecuteQuery("SELECT * FROM slice; SELECT * FROM s");
+  PerfettoSqlPreprocessor preprocessor(source, macros_);
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(preprocessor.statement(),
+            FindSubstr(source, "SELECT * FROM slice;"));
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(preprocessor.statement(), FindSubstr(source, "SELECT * FROM s"));
+  ASSERT_FALSE(preprocessor.NextStatement());
+  ASSERT_TRUE(preprocessor.status().ok());
+}
+
+TEST_F(PerfettoSqlPreprocessorUnittest, CreateMacro) {
+  auto source = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO MACRO foo(a, b) AS SELECT $a + $b");
+  PerfettoSqlPreprocessor preprocessor(source, macros_);
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(
+      preprocessor.statement(),
+      FindSubstr(source, "CREATE PERFETTO MACRO foo(a, b) AS SELECT $a + $b"));
+  ASSERT_FALSE(preprocessor.NextStatement());
+  ASSERT_TRUE(preprocessor.status().ok());
+}
+
+TEST_F(PerfettoSqlPreprocessorUnittest, SingleMacro) {
+  auto foo = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO MACRO foo(a Expr, b Expr) Returns Expr AS "
+      "SELECT $a + $b");
+  macros_.Insert(
+      "foo",
+      Macro{false, "foo", {"a", "b"}, FindSubstr(foo, "SELECT $a + $b")});
+
+  auto source = SqlSource::FromExecuteQuery(
+      "foo!((select s.ts + r.dur from s, r), 1234); SELECT 1");
+  PerfettoSqlPreprocessor preprocessor(source, macros_);
+  ASSERT_TRUE(preprocessor.NextStatement()) << preprocessor.status().message();
+  ASSERT_EQ(preprocessor.statement().AsTraceback(0),
+            "Fully expanded statement\n"
+            "  SELECT (select s.ts + r.dur from s, r) + 1234;\n"
+            "  ^\n"
+            "Traceback (most recent call last):\n"
+            "  File \"stdin\" line 1 col 1\n"
+            "    foo!((select s.ts + r.dur from s, r), 1234);\n"
+            "    ^\n"
+            "  File \"stdin\" line 1 col 59\n"
+            "    SELECT $a + $b\n"
+            "    ^\n");
+  ASSERT_EQ(preprocessor.statement().AsTraceback(7),
+            "Fully expanded statement\n"
+            "  SELECT (select s.ts + r.dur from s, r) + 1234;\n"
+            "         ^\n"
+            "Traceback (most recent call last):\n"
+            "  File \"stdin\" line 1 col 1\n"
+            "    foo!((select s.ts + r.dur from s, r), 1234);\n"
+            "    ^\n"
+            "  File \"stdin\" line 1 col 66\n"
+            "    SELECT $a + $b\n"
+            "           ^\n"
+            "  File \"stdin\" line 1 col 6\n"
+            "    (select s.ts + r.dur from s, r)\n"
+            "    ^\n");
+  ASSERT_EQ(preprocessor.statement().sql(),
+            "SELECT (select s.ts + r.dur from s, r) + 1234;");
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(preprocessor.statement(), FindSubstr(source, "SELECT 1"));
+  ASSERT_FALSE(preprocessor.NextStatement());
+  ASSERT_TRUE(preprocessor.status().ok());
+}
+
+TEST_F(PerfettoSqlPreprocessorUnittest, NestedMacro) {
+  auto foo = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO MACRO foo(a Expr, b Expr) Returns Expr AS $a + $b");
+  macros_.Insert("foo", Macro{
+                            false,
+                            "foo",
+                            {"a", "b"},
+                            FindSubstr(foo, "$a + $b"),
+                        });
+
+  auto bar = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO MACRO bar(a, b) Returns Expr AS "
+      "foo!($a, $b) + foo!($b, $a)");
+  macros_.Insert("bar", Macro{
+                            false,
+                            "bar",
+                            {"a", "b"},
+                            FindSubstr(bar, "foo!($a, $b) + foo!($b, $a)"),
+                        });
+
+  auto source = SqlSource::FromExecuteQuery(
+      "SELECT bar!((select s.ts + r.dur from s, r), 1234); SELECT 1");
+  PerfettoSqlPreprocessor preprocessor(source, macros_);
+  ASSERT_TRUE(preprocessor.NextStatement()) << preprocessor.status().message();
+  ASSERT_EQ(preprocessor.statement().sql(),
+            "SELECT (select s.ts + r.dur from s, r) + 1234 + 1234 + "
+            "(select s.ts + r.dur from s, r);");
+  ASSERT_TRUE(preprocessor.NextStatement()) << preprocessor.status().message();
+  ASSERT_EQ(preprocessor.statement().sql(), "SELECT 1");
+}
+
+TEST_F(PerfettoSqlPreprocessorUnittest, Stringify) {
+  auto sf = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO MACRO sf(a Expr, b Expr) Returns Expr AS "
+      "__intrinsic_stringify!($a + $b)");
+  macros_.Insert("sf", Macro{
+                           false,
+                           "sf",
+                           {"a", "b"},
+                           FindSubstr(sf, "__intrinsic_stringify!($a + $b)"),
+                       });
+  auto bar = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO MACRO bar(a Expr, b Expr) Returns Expr AS "
+      "sf!((SELECT $a), (SELECT $b))");
+  macros_.Insert("bar", Macro{
+                            false,
+                            "bar",
+                            {"a", "b"},
+                            FindSubstr(bar, "sf!((SELECT $a), (SELECT $b))"),
+                        });
+  auto baz = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO MACRO baz(a Expr, b Expr) Returns Expr AS "
+      "SELECT bar!((SELECT $a), (SELECT $b))");
+  macros_.Insert("baz", Macro{
+                            false,
+                            "baz",
+                            {"a", "b"},
+                            FindSubstr(baz, "bar!((SELECT $a), (SELECT $b))"),
+                        });
+
+  {
+    auto source =
+        SqlSource::FromExecuteQuery("__intrinsic_stringify!(foo bar baz)");
+    PerfettoSqlPreprocessor preprocessor(source, macros_);
+    ASSERT_TRUE(preprocessor.NextStatement())
+        << preprocessor.status().message();
+    ASSERT_EQ(preprocessor.statement().sql(), "'foo bar baz'");
+    ASSERT_FALSE(preprocessor.NextStatement());
+  }
+
+  {
+    auto source = SqlSource::FromExecuteQuery("sf!(1, 2)");
+    PerfettoSqlPreprocessor preprocessor(source, macros_);
+    ASSERT_TRUE(preprocessor.NextStatement())
+        << preprocessor.status().message();
+    ASSERT_EQ(preprocessor.statement().sql(), "'1 + 2'");
+    ASSERT_FALSE(preprocessor.NextStatement());
+  }
+
+  {
+    auto source = SqlSource::FromExecuteQuery("baz!(1, 2)");
+    PerfettoSqlPreprocessor preprocessor(source, macros_);
+    ASSERT_TRUE(preprocessor.NextStatement())
+        << preprocessor.status().message();
+    ASSERT_EQ(preprocessor.statement().sql(),
+              "'(SELECT (SELECT 1)) + (SELECT (SELECT 2))'");
+    ASSERT_FALSE(preprocessor.NextStatement());
+  }
+
+  {
+    auto source = SqlSource::FromExecuteQuery("__intrinsic_stringify!()");
+    PerfettoSqlPreprocessor preprocessor(source, macros_);
+    ASSERT_FALSE(preprocessor.NextStatement());
+    ASSERT_EQ(preprocessor.status().message(),
+              "Traceback (most recent call last):\n"
+              "  File \"stdin\" line 1 col 1\n"
+              "    __intrinsic_stringify!()\n"
+              "    ^\n"
+              "stringify: must specify exactly 1 argument, actual 0");
+  }
+}
+
+}  // namespace
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.c b/src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.c
new file mode 100644
index 0000000..ae5eefd
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.c
@@ -0,0 +1,1365 @@
+/* This file is automatically generated by Lemon from input grammar
+** source file "src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.y".
+*/
+/*
+** 2000-05-29
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** Driver template for the LEMON parser generator.
+**
+** The "lemon" program processes an LALR(1) input grammar file, then uses
+** this template to construct a parser.  The "lemon" program inserts text
+** at each "%%" line.  Also, any "P-a-r-s-e" identifer prefix (without the
+** interstitial "-" characters) contained in this template is changed into
+** the value of the %name directive from the grammar.  Otherwise, the content
+** of this template is copied straight through into the generate parser
+** source file.
+**
+** The following is the concatenation of all %include directives from the
+** input grammar file:
+*/
+/************ Begin %include sections from the grammar ************************/
+#include "src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar_interface.h"
+
+#define YYNOERRORRECOVERY 1
+#define YYPARSEFREENEVERNULL 1
+/**************** End of %include directives **********************************/
+/* These constants specify the various numeric values for terminal symbols.
+***************** Begin token definitions *************************************/
+#ifndef PPTK_SEMI
+#define PPTK_SEMI                            1
+#define PPTK_APPLY                           2
+#define PPTK_COMMA                           3
+#define PPTK_AND                             4
+#define PPTK_TRUE                            5
+#define PPTK_FALSE                           6
+#define PPTK_ID                              7
+#define PPTK_LP                              8
+#define PPTK_RP                              9
+#define PPTK_OPAQUE                         10
+#define PPTK_EXCLAIM                        11
+#define PPTK_VARIABLE                       12
+#endif
+/**************** End token definitions ***************************************/
+
+/* The next sections is a series of control #defines.
+** various aspects of the generated parser.
+**    YYCODETYPE         is the data type used to store the integer codes
+**                       that represent terminal and non-terminal symbols.
+**                       "unsigned char" is used if there are fewer than
+**                       256 symbols.  Larger types otherwise.
+**    YYNOCODE           is a number of type YYCODETYPE that is not used for
+**                       any terminal or nonterminal symbol.
+**    YYFALLBACK         If defined, this indicates that one or more tokens
+**                       (also known as: "terminal symbols") have fall-back
+**                       values which should be used if the original symbol
+**                       would not parse.  This permits keywords to sometimes
+**                       be used as identifiers, for example.
+**    YYACTIONTYPE       is the data type used for "action codes" - numbers
+**                       that indicate what to do in response to the next
+**                       token.
+**    PreprocessorGrammarParseTOKENTYPE     is the data type used for minor type for terminal
+**                       symbols.  Background: A "minor type" is a semantic
+**                       value associated with a terminal or non-terminal
+**                       symbols.  For example, for an "ID" terminal symbol,
+**                       the minor type might be the name of the identifier.
+**                       Each non-terminal can have a different minor type.
+**                       Terminal symbols all have the same minor type, though.
+**                       This macros defines the minor type for terminal 
+**                       symbols.
+**    YYMINORTYPE        is the data type used for all minor types.
+**                       This is typically a union of many types, one of
+**                       which is PreprocessorGrammarParseTOKENTYPE.  The entry in the union
+**                       for terminal symbols is called "yy0".
+**    YYSTACKDEPTH       is the maximum depth of the parser's stack.  If
+**                       zero the stack is dynamically sized using realloc()
+**    PreprocessorGrammarParseARG_SDECL     A static variable declaration for the %extra_argument
+**    PreprocessorGrammarParseARG_PDECL     A parameter declaration for the %extra_argument
+**    PreprocessorGrammarParseARG_PARAM     Code to pass %extra_argument as a subroutine parameter
+**    PreprocessorGrammarParseARG_STORE     Code to store %extra_argument into yypParser
+**    PreprocessorGrammarParseARG_FETCH     Code to extract %extra_argument from yypParser
+**    PreprocessorGrammarParseCTX_*         As PreprocessorGrammarParseARG_ except for %extra_context
+**    YYERRORSYMBOL      is the code number of the error symbol.  If not
+**                       defined, then do no error processing.
+**    YYNSTATE           the combined number of states.
+**    YYNRULE            the number of rules in the grammar
+**    YYNTOKEN           Number of terminal symbols
+**    YY_MAX_SHIFT       Maximum value for shift actions
+**    YY_MIN_SHIFTREDUCE Minimum value for shift-reduce actions
+**    YY_MAX_SHIFTREDUCE Maximum value for shift-reduce actions
+**    YY_ERROR_ACTION    The yy_action[] code for syntax error
+**    YY_ACCEPT_ACTION   The yy_action[] code for accept
+**    YY_NO_ACTION       The yy_action[] code for no-op
+**    YY_MIN_REDUCE      Minimum value for reduce actions
+**    YY_MAX_REDUCE      Maximum value for reduce actions
+*/
+#ifndef INTERFACE
+# define INTERFACE 1
+#endif
+/************* Begin control #defines *****************************************/
+#define YYCODETYPE unsigned char
+#define YYNOCODE 25
+#define YYACTIONTYPE unsigned char
+#define PreprocessorGrammarParseTOKENTYPE struct PreprocessorGrammarToken
+typedef union {
+  int yyinit;
+  PreprocessorGrammarParseTOKENTYPE yy0;
+  struct PreprocessorGrammarApplyList* yy2;
+  struct PreprocessorGrammarTokenBounds yy32;
+} YYMINORTYPE;
+#ifndef YYSTACKDEPTH
+#define YYSTACKDEPTH 100
+#endif
+#define PreprocessorGrammarParseARG_SDECL
+#define PreprocessorGrammarParseARG_PDECL
+#define PreprocessorGrammarParseARG_PARAM
+#define PreprocessorGrammarParseARG_FETCH
+#define PreprocessorGrammarParseARG_STORE
+#define PreprocessorGrammarParseCTX_SDECL struct PreprocessorGrammarState* state;
+#define PreprocessorGrammarParseCTX_PDECL ,struct PreprocessorGrammarState* state
+#define PreprocessorGrammarParseCTX_PARAM ,state
+#define PreprocessorGrammarParseCTX_FETCH struct PreprocessorGrammarState* state=yypParser->state;
+#define PreprocessorGrammarParseCTX_STORE yypParser->state=state;
+#define YYNSTATE             26
+#define YYNRULE              26
+#define YYNRULE_WITH_ACTION  16
+#define YYNTOKEN             13
+#define YY_MAX_SHIFT         25
+#define YY_MIN_SHIFTREDUCE   42
+#define YY_MAX_SHIFTREDUCE   67
+#define YY_ERROR_ACTION      68
+#define YY_ACCEPT_ACTION     69
+#define YY_NO_ACTION         70
+#define YY_MIN_REDUCE        71
+#define YY_MAX_REDUCE        96
+/************* End control #defines *******************************************/
+#define YY_NLOOKAHEAD ((int)(sizeof(yy_lookahead)/sizeof(yy_lookahead[0])))
+
+/* Define the yytestcase() macro to be a no-op if is not already defined
+** otherwise.
+**
+** Applications can choose to define yytestcase() in the %include section
+** to a macro that can assist in verifying code coverage.  For production
+** code the yytestcase() macro should be turned off.  But it is useful
+** for testing.
+*/
+#ifndef yytestcase
+# define yytestcase(X)
+#endif
+
+
+/* Next are the tables used to determine what action to take based on the
+** current state and lookahead token.  These tables are used to implement
+** functions that take a state number and lookahead value and return an
+** action integer.  
+**
+** Suppose the action integer is N.  Then the action is determined as
+** follows
+**
+**   0 <= N <= YY_MAX_SHIFT             Shift N.  That is, push the lookahead
+**                                      token onto the stack and goto state N.
+**
+**   N between YY_MIN_SHIFTREDUCE       Shift to an arbitrary state then
+**     and YY_MAX_SHIFTREDUCE           reduce by rule N-YY_MIN_SHIFTREDUCE.
+**
+**   N == YY_ERROR_ACTION               A syntax error has occurred.
+**
+**   N == YY_ACCEPT_ACTION              The parser accepts its input.
+**
+**   N == YY_NO_ACTION                  No such action.  Denotes unused
+**                                      slots in the yy_action[] table.
+**
+**   N between YY_MIN_REDUCE            Reduce by rule N-YY_MIN_REDUCE
+**     and YY_MAX_REDUCE
+**
+** The action table is constructed as a single large table named yy_action[].
+** Given state S and lookahead X, the action is computed as either:
+**
+**    (A)   N = yy_action[ yy_shift_ofst[S] + X ]
+**    (B)   N = yy_default[S]
+**
+** The (A) formula is preferred.  The B formula is used instead if
+** yy_lookahead[yy_shift_ofst[S]+X] is not equal to X.
+**
+** The formulas above are for computing the action when the lookahead is
+** a terminal symbol.  If the lookahead is a non-terminal (as occurs after
+** a reduce action) then the yy_reduce_ofst[] array is used in place of
+** the yy_shift_ofst[] array.
+**
+** The following are the tables generated in this section:
+**
+**  yy_action[]        A single table containing all actions.
+**  yy_lookahead[]     A table containing the lookahead for each entry in
+**                     yy_action.  Used to detect hash collisions.
+**  yy_shift_ofst[]    For each state, the offset into yy_action for
+**                     shifting terminals.
+**  yy_reduce_ofst[]   For each state, the offset into yy_action for
+**                     shifting non-terminals after a reduce.
+**  yy_default[]       Default action for each state.
+**
+*********** Begin parsing tables **********************************************/
+#define YY_ACTTAB_COUNT (84)
+static const YYACTIONTYPE yy_action[] = {
+ /*     0 */    69,   25,    7,   19,   15,   62,    7,    7,   24,   21,
+ /*    10 */     2,    3,   48,   11,   53,   86,   24,   22,   20,   20,
+ /*    20 */    62,   85,   24,    4,   21,    2,   51,   48,   62,   53,
+ /*    30 */    14,   14,   21,    2,   50,   48,   58,   53,   62,   89,
+ /*    40 */    89,   24,   21,    2,    5,   48,   10,   53,   86,   24,
+ /*    50 */    21,    2,    6,   48,   17,   53,    6,    6,   24,   59,
+ /*    60 */     8,   12,    9,   83,   86,   24,   44,    1,   13,    9,
+ /*    70 */    49,   86,   24,    8,   11,   23,   86,   24,   71,   16,
+ /*    80 */    95,   70,   18,   18,
+};
+static const YYCODETYPE yy_lookahead[] = {
+ /*     0 */    13,   14,   15,   16,    2,    3,   19,   20,   21,    7,
+ /*    10 */     8,    8,   10,   18,   12,   20,   21,   22,   23,   24,
+ /*    20 */     3,   20,   21,    8,    7,    8,    9,   10,    3,   12,
+ /*    30 */     3,    4,    7,    8,    9,   10,    1,   12,    3,   19,
+ /*    40 */    20,   21,    7,    8,    3,   10,   18,   12,   20,   21,
+ /*    50 */     7,    8,   15,   10,    7,   12,   19,   20,   21,    1,
+ /*    60 */     3,   17,   18,   11,   20,   21,    9,    8,   17,   18,
+ /*    70 */     9,   20,   21,    3,   18,   11,   20,   21,    0,    9,
+ /*    80 */    24,   25,    5,    6,   25,   25,   25,   25,   25,   25,
+ /*    90 */    25,   25,   25,   25,   25,   25,   13,
+};
+#define YY_SHIFT_COUNT    (25)
+#define YY_SHIFT_MIN      (0)
+#define YY_SHIFT_MAX      (78)
+static const unsigned char yy_shift_ofst[] = {
+ /*     0 */     2,   43,   17,   43,   43,   43,   25,   35,   43,   43,
+ /*    10 */    43,   43,   57,   70,   77,   27,    3,   15,   47,   58,
+ /*    20 */    41,   52,   61,   59,   64,   78,
+};
+#define YY_REDUCE_COUNT (11)
+#define YY_REDUCE_MIN   (-13)
+#define YY_REDUCE_MAX   (56)
+static const signed char yy_reduce_ofst[] = {
+ /*     0 */   -13,   -5,   37,   44,   51,   56,   20,   20,   28,    1,
+ /*    10 */     1,    1,
+};
+static const YYACTIONTYPE yy_default[] = {
+ /*     0 */    68,   94,   68,   76,   76,   68,   68,   68,   68,   75,
+ /*    10 */    74,   84,   68,   68,   68,   68,   72,   68,   68,   68,
+ /*    20 */    93,   81,   68,   68,   68,   68,
+};
+/********** End of lemon-generated parsing tables *****************************/
+
+/* The next table maps tokens (terminal symbols) into fallback tokens.  
+** If a construct like the following:
+** 
+**      %fallback ID X Y Z.
+**
+** appears in the grammar, then ID becomes a fallback token for X, Y,
+** and Z.  Whenever one of the tokens X, Y, or Z is input to the parser
+** but it does not parse, the type of the token is changed to ID and
+** the parse is retried before an error is thrown.
+**
+** This feature can be used, for example, to cause some keywords in a language
+** to revert to identifiers if they keyword does not apply in the context where
+** it appears.
+*/
+#ifdef YYFALLBACK
+static const YYCODETYPE yyFallback[] = {
+};
+#endif /* YYFALLBACK */
+
+/* The following structure represents a single element of the
+** parser's stack.  Information stored includes:
+**
+**   +  The state number for the parser at this level of the stack.
+**
+**   +  The value of the token stored at this level of the stack.
+**      (In other words, the "major" token.)
+**
+**   +  The semantic value stored at this level of the stack.  This is
+**      the information used by the action routines in the grammar.
+**      It is sometimes called the "minor" token.
+**
+** After the "shift" half of a SHIFTREDUCE action, the stateno field
+** actually contains the reduce action for the second half of the
+** SHIFTREDUCE.
+*/
+struct yyStackEntry {
+  YYACTIONTYPE stateno;  /* The state-number, or reduce action in SHIFTREDUCE */
+  YYCODETYPE major;      /* The major token value.  This is the code
+                         ** number for the token at this stack level */
+  YYMINORTYPE minor;     /* The user-supplied minor token value.  This
+                         ** is the value of the token  */
+};
+typedef struct yyStackEntry yyStackEntry;
+
+/* The state of the parser is completely contained in an instance of
+** the following structure */
+struct yyParser {
+  yyStackEntry *yytos;          /* Pointer to top element of the stack */
+#ifdef YYTRACKMAXSTACKDEPTH
+  int yyhwm;                    /* High-water mark of the stack */
+#endif
+#ifndef YYNOERRORRECOVERY
+  int yyerrcnt;                 /* Shifts left before out of the error */
+#endif
+  PreprocessorGrammarParseARG_SDECL                /* A place to hold %extra_argument */
+  PreprocessorGrammarParseCTX_SDECL                /* A place to hold %extra_context */
+#if YYSTACKDEPTH<=0
+  int yystksz;                  /* Current side of the stack */
+  yyStackEntry *yystack;        /* The parser's stack */
+  yyStackEntry yystk0;          /* First stack entry */
+#else
+  yyStackEntry yystack[YYSTACKDEPTH];  /* The parser's stack */
+  yyStackEntry *yystackEnd;            /* Last entry in the stack */
+#endif
+};
+typedef struct yyParser yyParser;
+
+#include <assert.h>
+#ifndef NDEBUG
+#include <stdio.h>
+static FILE *yyTraceFILE = 0;
+static char *yyTracePrompt = 0;
+#endif /* NDEBUG */
+
+#ifndef NDEBUG
+/* 
+** Turn parser tracing on by giving a stream to which to write the trace
+** and a prompt to preface each trace message.  Tracing is turned off
+** by making either argument NULL 
+**
+** Inputs:
+** <ul>
+** <li> A FILE* to which trace output should be written.
+**      If NULL, then tracing is turned off.
+** <li> A prefix string written at the beginning of every
+**      line of trace output.  If NULL, then tracing is
+**      turned off.
+** </ul>
+**
+** Outputs:
+** None.
+*/
+void PreprocessorGrammarParseTrace(FILE *TraceFILE, char *zTracePrompt){
+  yyTraceFILE = TraceFILE;
+  yyTracePrompt = zTracePrompt;
+  if( yyTraceFILE==0 ) yyTracePrompt = 0;
+  else if( yyTracePrompt==0 ) yyTraceFILE = 0;
+}
+#endif /* NDEBUG */
+
+#if defined(YYCOVERAGE) || !defined(NDEBUG)
+/* For tracing shifts, the names of all terminals and nonterminals
+** are required.  The following table supplies these names */
+static const char *const yyTokenName[] = { 
+  /*    0 */ "$",
+  /*    1 */ "SEMI",
+  /*    2 */ "APPLY",
+  /*    3 */ "COMMA",
+  /*    4 */ "AND",
+  /*    5 */ "TRUE",
+  /*    6 */ "FALSE",
+  /*    7 */ "ID",
+  /*    8 */ "LP",
+  /*    9 */ "RP",
+  /*   10 */ "OPAQUE",
+  /*   11 */ "EXCLAIM",
+  /*   12 */ "VARIABLE",
+  /*   13 */ "input",
+  /*   14 */ "cmd",
+  /*   15 */ "sql",
+  /*   16 */ "apply",
+  /*   17 */ "applylist",
+  /*   18 */ "tokenlist",
+  /*   19 */ "sqltoken",
+  /*   20 */ "commalesssqltoken",
+  /*   21 */ "minvocationid",
+  /*   22 */ "marglist",
+  /*   23 */ "marglistinner",
+  /*   24 */ "marg",
+};
+#endif /* defined(YYCOVERAGE) || !defined(NDEBUG) */
+
+#ifndef NDEBUG
+/* For tracing reduce actions, the names of all rules are required.
+*/
+static const char *const yyRuleName[] = {
+ /*   0 */ "input ::= cmd",
+ /*   1 */ "apply ::= APPLY COMMA|AND TRUE|FALSE ID LP applylist RP",
+ /*   2 */ "apply ::= APPLY COMMA|AND TRUE|FALSE ID LP applylist RP LP applylist RP",
+ /*   3 */ "applylist ::= applylist COMMA tokenlist",
+ /*   4 */ "applylist ::= tokenlist",
+ /*   5 */ "applylist ::=",
+ /*   6 */ "commalesssqltoken ::= OPAQUE",
+ /*   7 */ "commalesssqltoken ::= minvocationid EXCLAIM LP marglist RP",
+ /*   8 */ "commalesssqltoken ::= LP sql RP",
+ /*   9 */ "commalesssqltoken ::= LP RP",
+ /*  10 */ "commalesssqltoken ::= ID",
+ /*  11 */ "commalesssqltoken ::= VARIABLE",
+ /*  12 */ "minvocationid ::= ID",
+ /*  13 */ "marg ::= tokenlist",
+ /*  14 */ "tokenlist ::= tokenlist commalesssqltoken",
+ /*  15 */ "tokenlist ::= commalesssqltoken",
+ /*  16 */ "cmd ::= sql SEMI",
+ /*  17 */ "cmd ::= apply SEMI",
+ /*  18 */ "sql ::= sql sqltoken",
+ /*  19 */ "sql ::= sqltoken",
+ /*  20 */ "sqltoken ::= COMMA",
+ /*  21 */ "sqltoken ::= commalesssqltoken",
+ /*  22 */ "marglist ::= marglistinner",
+ /*  23 */ "marglist ::=",
+ /*  24 */ "marglistinner ::= marglistinner COMMA marg",
+ /*  25 */ "marglistinner ::= marg",
+};
+#endif /* NDEBUG */
+
+
+#if YYSTACKDEPTH<=0
+/*
+** Try to increase the size of the parser stack.  Return the number
+** of errors.  Return 0 on success.
+*/
+static int yyGrowStack(yyParser *p){
+  int newSize;
+  int idx;
+  yyStackEntry *pNew;
+
+  newSize = p->yystksz*2 + 100;
+  idx = p->yytos ? (int)(p->yytos - p->yystack) : 0;
+  if( p->yystack==&p->yystk0 ){
+    pNew = malloc(newSize*sizeof(pNew[0]));
+    if( pNew ) pNew[0] = p->yystk0;
+  }else{
+    pNew = realloc(p->yystack, newSize*sizeof(pNew[0]));
+  }
+  if( pNew ){
+    p->yystack = pNew;
+    p->yytos = &p->yystack[idx];
+#ifndef NDEBUG
+    if( yyTraceFILE ){
+      fprintf(yyTraceFILE,"%sStack grows from %d to %d entries.\n",
+              yyTracePrompt, p->yystksz, newSize);
+    }
+#endif
+    p->yystksz = newSize;
+  }
+  return pNew==0; 
+}
+#endif
+
+/* Datatype of the argument to the memory allocated passed as the
+** second argument to PreprocessorGrammarParseAlloc() below.  This can be changed by
+** putting an appropriate #define in the %include section of the input
+** grammar.
+*/
+#ifndef YYMALLOCARGTYPE
+# define YYMALLOCARGTYPE size_t
+#endif
+
+/* Initialize a new parser that has already been allocated.
+*/
+void PreprocessorGrammarParseInit(void *yypRawParser PreprocessorGrammarParseCTX_PDECL){
+  yyParser *yypParser = (yyParser*)yypRawParser;
+  PreprocessorGrammarParseCTX_STORE
+#ifdef YYTRACKMAXSTACKDEPTH
+  yypParser->yyhwm = 0;
+#endif
+#if YYSTACKDEPTH<=0
+  yypParser->yytos = NULL;
+  yypParser->yystack = NULL;
+  yypParser->yystksz = 0;
+  if( yyGrowStack(yypParser) ){
+    yypParser->yystack = &yypParser->yystk0;
+    yypParser->yystksz = 1;
+  }
+#endif
+#ifndef YYNOERRORRECOVERY
+  yypParser->yyerrcnt = -1;
+#endif
+  yypParser->yytos = yypParser->yystack;
+  yypParser->yystack[0].stateno = 0;
+  yypParser->yystack[0].major = 0;
+#if YYSTACKDEPTH>0
+  yypParser->yystackEnd = &yypParser->yystack[YYSTACKDEPTH-1];
+#endif
+}
+
+#ifndef PreprocessorGrammarParse_ENGINEALWAYSONSTACK
+/* 
+** This function allocates a new parser.
+** The only argument is a pointer to a function which works like
+** malloc.
+**
+** Inputs:
+** A pointer to the function used to allocate memory.
+**
+** Outputs:
+** A pointer to a parser.  This pointer is used in subsequent calls
+** to PreprocessorGrammarParse and PreprocessorGrammarParseFree.
+*/
+void *PreprocessorGrammarParseAlloc(void *(*mallocProc)(YYMALLOCARGTYPE) PreprocessorGrammarParseCTX_PDECL){
+  yyParser *yypParser;
+  yypParser = (yyParser*)(*mallocProc)( (YYMALLOCARGTYPE)sizeof(yyParser) );
+  if( yypParser ){
+    PreprocessorGrammarParseCTX_STORE
+    PreprocessorGrammarParseInit(yypParser PreprocessorGrammarParseCTX_PARAM);
+  }
+  return (void*)yypParser;
+}
+#endif /* PreprocessorGrammarParse_ENGINEALWAYSONSTACK */
+
+
+/* The following function deletes the "minor type" or semantic value
+** associated with a symbol.  The symbol can be either a terminal
+** or nonterminal. "yymajor" is the symbol code, and "yypminor" is
+** a pointer to the value to be deleted.  The code used to do the 
+** deletions is derived from the %destructor and/or %token_destructor
+** directives of the input grammar.
+*/
+static void yy_destructor(
+  yyParser *yypParser,    /* The parser */
+  YYCODETYPE yymajor,     /* Type code for object to destroy */
+  YYMINORTYPE *yypminor   /* The object to be destroyed */
+){
+  PreprocessorGrammarParseARG_FETCH
+  PreprocessorGrammarParseCTX_FETCH
+  switch( yymajor ){
+    /* Here is inserted the actions which take place when a
+    ** terminal or non-terminal is destroyed.  This can happen
+    ** when the symbol is popped from the stack during a
+    ** reduce or during error processing or when a parser is 
+    ** being destroyed before it is finished parsing.
+    **
+    ** Note: during a reduce, the only symbols destroyed are those
+    ** which appear on the RHS of the rule, but which are *not* used
+    ** inside the C code.
+    */
+/********* Begin destructor definitions ***************************************/
+    case 17: /* applylist */
+{
+ OnPreprocessorFreeApplyList(state, (yypminor->yy2)); 
+}
+      break;
+/********* End destructor definitions *****************************************/
+    default:  break;   /* If no destructor action specified: do nothing */
+  }
+}
+
+/*
+** Pop the parser's stack once.
+**
+** If there is a destructor routine associated with the token which
+** is popped from the stack, then call it.
+*/
+static void yy_pop_parser_stack(yyParser *pParser){
+  yyStackEntry *yytos;
+  assert( pParser->yytos!=0 );
+  assert( pParser->yytos > pParser->yystack );
+  yytos = pParser->yytos--;
+#ifndef NDEBUG
+  if( yyTraceFILE ){
+    fprintf(yyTraceFILE,"%sPopping %s\n",
+      yyTracePrompt,
+      yyTokenName[yytos->major]);
+  }
+#endif
+  yy_destructor(pParser, yytos->major, &yytos->minor);
+}
+
+/*
+** Clear all secondary memory allocations from the parser
+*/
+void PreprocessorGrammarParseFinalize(void *p){
+  yyParser *pParser = (yyParser*)p;
+  while( pParser->yytos>pParser->yystack ) yy_pop_parser_stack(pParser);
+#if YYSTACKDEPTH<=0
+  if( pParser->yystack!=&pParser->yystk0 ) free(pParser->yystack);
+#endif
+}
+
+#ifndef PreprocessorGrammarParse_ENGINEALWAYSONSTACK
+/* 
+** Deallocate and destroy a parser.  Destructors are called for
+** all stack elements before shutting the parser down.
+**
+** If the YYPARSEFREENEVERNULL macro exists (for example because it
+** is defined in a %include section of the input grammar) then it is
+** assumed that the input pointer is never NULL.
+*/
+void PreprocessorGrammarParseFree(
+  void *p,                    /* The parser to be deleted */
+  void (*freeProc)(void*)     /* Function used to reclaim memory */
+){
+#ifndef YYPARSEFREENEVERNULL
+  if( p==0 ) return;
+#endif
+  PreprocessorGrammarParseFinalize(p);
+  (*freeProc)(p);
+}
+#endif /* PreprocessorGrammarParse_ENGINEALWAYSONSTACK */
+
+/*
+** Return the peak depth of the stack for a parser.
+*/
+#ifdef YYTRACKMAXSTACKDEPTH
+int PreprocessorGrammarParseStackPeak(void *p){
+  yyParser *pParser = (yyParser*)p;
+  return pParser->yyhwm;
+}
+#endif
+
+/* This array of booleans keeps track of the parser statement
+** coverage.  The element yycoverage[X][Y] is set when the parser
+** is in state X and has a lookahead token Y.  In a well-tested
+** systems, every element of this matrix should end up being set.
+*/
+#if defined(YYCOVERAGE)
+static unsigned char yycoverage[YYNSTATE][YYNTOKEN];
+#endif
+
+/*
+** Write into out a description of every state/lookahead combination that
+**
+**   (1)  has not been used by the parser, and
+**   (2)  is not a syntax error.
+**
+** Return the number of missed state/lookahead combinations.
+*/
+#if defined(YYCOVERAGE)
+int PreprocessorGrammarParseCoverage(FILE *out){
+  int stateno, iLookAhead, i;
+  int nMissed = 0;
+  for(stateno=0; stateno<YYNSTATE; stateno++){
+    i = yy_shift_ofst[stateno];
+    for(iLookAhead=0; iLookAhead<YYNTOKEN; iLookAhead++){
+      if( yy_lookahead[i+iLookAhead]!=iLookAhead ) continue;
+      if( yycoverage[stateno][iLookAhead]==0 ) nMissed++;
+      if( out ){
+        fprintf(out,"State %d lookahead %s %s\n", stateno,
+                yyTokenName[iLookAhead],
+                yycoverage[stateno][iLookAhead] ? "ok" : "missed");
+      }
+    }
+  }
+  return nMissed;
+}
+#endif
+
+/*
+** Find the appropriate action for a parser given the terminal
+** look-ahead token iLookAhead.
+*/
+static YYACTIONTYPE yy_find_shift_action(
+  YYCODETYPE iLookAhead,    /* The look-ahead token */
+  YYACTIONTYPE stateno      /* Current state number */
+){
+  int i;
+
+  if( stateno>YY_MAX_SHIFT ) return stateno;
+  assert( stateno <= YY_SHIFT_COUNT );
+#if defined(YYCOVERAGE)
+  yycoverage[stateno][iLookAhead] = 1;
+#endif
+  do{
+    i = yy_shift_ofst[stateno];
+    assert( i>=0 );
+    assert( i<=YY_ACTTAB_COUNT );
+    assert( i+YYNTOKEN<=(int)YY_NLOOKAHEAD );
+    assert( iLookAhead!=YYNOCODE );
+    assert( iLookAhead < YYNTOKEN );
+    i += iLookAhead;
+    assert( i<(int)YY_NLOOKAHEAD );
+    if( yy_lookahead[i]!=iLookAhead ){
+#ifdef YYFALLBACK
+      YYCODETYPE iFallback;            /* Fallback token */
+      assert( iLookAhead<sizeof(yyFallback)/sizeof(yyFallback[0]) );
+      iFallback = yyFallback[iLookAhead];
+      if( iFallback!=0 ){
+#ifndef NDEBUG
+        if( yyTraceFILE ){
+          fprintf(yyTraceFILE, "%sFALLBACK %s => %s\n",
+             yyTracePrompt, yyTokenName[iLookAhead], yyTokenName[iFallback]);
+        }
+#endif
+        assert( yyFallback[iFallback]==0 ); /* Fallback loop must terminate */
+        iLookAhead = iFallback;
+        continue;
+      }
+#endif
+#ifdef YYWILDCARD
+      {
+        int j = i - iLookAhead + YYWILDCARD;
+        assert( j<(int)(sizeof(yy_lookahead)/sizeof(yy_lookahead[0])) );
+        if( yy_lookahead[j]==YYWILDCARD && iLookAhead>0 ){
+#ifndef NDEBUG
+          if( yyTraceFILE ){
+            fprintf(yyTraceFILE, "%sWILDCARD %s => %s\n",
+               yyTracePrompt, yyTokenName[iLookAhead],
+               yyTokenName[YYWILDCARD]);
+          }
+#endif /* NDEBUG */
+          return yy_action[j];
+        }
+      }
+#endif /* YYWILDCARD */
+      return yy_default[stateno];
+    }else{
+      assert( i>=0 && i<(int)(sizeof(yy_action)/sizeof(yy_action[0])) );
+      return yy_action[i];
+    }
+  }while(1);
+}
+
+/*
+** Find the appropriate action for a parser given the non-terminal
+** look-ahead token iLookAhead.
+*/
+static YYACTIONTYPE yy_find_reduce_action(
+  YYACTIONTYPE stateno,     /* Current state number */
+  YYCODETYPE iLookAhead     /* The look-ahead token */
+){
+  int i;
+#ifdef YYERRORSYMBOL
+  if( stateno>YY_REDUCE_COUNT ){
+    return yy_default[stateno];
+  }
+#else
+  assert( stateno<=YY_REDUCE_COUNT );
+#endif
+  i = yy_reduce_ofst[stateno];
+  assert( iLookAhead!=YYNOCODE );
+  i += iLookAhead;
+#ifdef YYERRORSYMBOL
+  if( i<0 || i>=YY_ACTTAB_COUNT || yy_lookahead[i]!=iLookAhead ){
+    return yy_default[stateno];
+  }
+#else
+  assert( i>=0 && i<YY_ACTTAB_COUNT );
+  assert( yy_lookahead[i]==iLookAhead );
+#endif
+  return yy_action[i];
+}
+
+/*
+** The following routine is called if the stack overflows.
+*/
+static void yyStackOverflow(yyParser *yypParser){
+   PreprocessorGrammarParseARG_FETCH
+   PreprocessorGrammarParseCTX_FETCH
+#ifndef NDEBUG
+   if( yyTraceFILE ){
+     fprintf(yyTraceFILE,"%sStack Overflow!\n",yyTracePrompt);
+   }
+#endif
+   while( yypParser->yytos>yypParser->yystack ) yy_pop_parser_stack(yypParser);
+   /* Here code is inserted which will execute if the parser
+   ** stack every overflows */
+/******** Begin %stack_overflow code ******************************************/
+/******** End %stack_overflow code ********************************************/
+   PreprocessorGrammarParseARG_STORE /* Suppress warning about unused %extra_argument var */
+   PreprocessorGrammarParseCTX_STORE
+}
+
+/*
+** Print tracing information for a SHIFT action
+*/
+#ifndef NDEBUG
+static void yyTraceShift(yyParser *yypParser, int yyNewState, const char *zTag){
+  if( yyTraceFILE ){
+    if( yyNewState<YYNSTATE ){
+      fprintf(yyTraceFILE,"%s%s '%s', go to state %d\n",
+         yyTracePrompt, zTag, yyTokenName[yypParser->yytos->major],
+         yyNewState);
+    }else{
+      fprintf(yyTraceFILE,"%s%s '%s', pending reduce %d\n",
+         yyTracePrompt, zTag, yyTokenName[yypParser->yytos->major],
+         yyNewState - YY_MIN_REDUCE);
+    }
+  }
+}
+#else
+# define yyTraceShift(X,Y,Z)
+#endif
+
+/*
+** Perform a shift action.
+*/
+static void yy_shift(
+  yyParser *yypParser,          /* The parser to be shifted */
+  YYACTIONTYPE yyNewState,      /* The new state to shift in */
+  YYCODETYPE yyMajor,           /* The major token to shift in */
+  PreprocessorGrammarParseTOKENTYPE yyMinor        /* The minor token to shift in */
+){
+  yyStackEntry *yytos;
+  yypParser->yytos++;
+#ifdef YYTRACKMAXSTACKDEPTH
+  if( (int)(yypParser->yytos - yypParser->yystack)>yypParser->yyhwm ){
+    yypParser->yyhwm++;
+    assert( yypParser->yyhwm == (int)(yypParser->yytos - yypParser->yystack) );
+  }
+#endif
+#if YYSTACKDEPTH>0 
+  if( yypParser->yytos>yypParser->yystackEnd ){
+    yypParser->yytos--;
+    yyStackOverflow(yypParser);
+    return;
+  }
+#else
+  if( yypParser->yytos>=&yypParser->yystack[yypParser->yystksz] ){
+    if( yyGrowStack(yypParser) ){
+      yypParser->yytos--;
+      yyStackOverflow(yypParser);
+      return;
+    }
+  }
+#endif
+  if( yyNewState > YY_MAX_SHIFT ){
+    yyNewState += YY_MIN_REDUCE - YY_MIN_SHIFTREDUCE;
+  }
+  yytos = yypParser->yytos;
+  yytos->stateno = yyNewState;
+  yytos->major = yyMajor;
+  yytos->minor.yy0 = yyMinor;
+  yyTraceShift(yypParser, yyNewState, "Shift");
+}
+
+/* For rule J, yyRuleInfoLhs[J] contains the symbol on the left-hand side
+** of that rule */
+static const YYCODETYPE yyRuleInfoLhs[] = {
+    13,  /* (0) input ::= cmd */
+    16,  /* (1) apply ::= APPLY COMMA|AND TRUE|FALSE ID LP applylist RP */
+    16,  /* (2) apply ::= APPLY COMMA|AND TRUE|FALSE ID LP applylist RP LP applylist RP */
+    17,  /* (3) applylist ::= applylist COMMA tokenlist */
+    17,  /* (4) applylist ::= tokenlist */
+    17,  /* (5) applylist ::= */
+    20,  /* (6) commalesssqltoken ::= OPAQUE */
+    20,  /* (7) commalesssqltoken ::= minvocationid EXCLAIM LP marglist RP */
+    20,  /* (8) commalesssqltoken ::= LP sql RP */
+    20,  /* (9) commalesssqltoken ::= LP RP */
+    20,  /* (10) commalesssqltoken ::= ID */
+    20,  /* (11) commalesssqltoken ::= VARIABLE */
+    21,  /* (12) minvocationid ::= ID */
+    24,  /* (13) marg ::= tokenlist */
+    18,  /* (14) tokenlist ::= tokenlist commalesssqltoken */
+    18,  /* (15) tokenlist ::= commalesssqltoken */
+    14,  /* (16) cmd ::= sql SEMI */
+    14,  /* (17) cmd ::= apply SEMI */
+    15,  /* (18) sql ::= sql sqltoken */
+    15,  /* (19) sql ::= sqltoken */
+    19,  /* (20) sqltoken ::= COMMA */
+    19,  /* (21) sqltoken ::= commalesssqltoken */
+    22,  /* (22) marglist ::= marglistinner */
+    22,  /* (23) marglist ::= */
+    23,  /* (24) marglistinner ::= marglistinner COMMA marg */
+    23,  /* (25) marglistinner ::= marg */
+};
+
+/* For rule J, yyRuleInfoNRhs[J] contains the negative of the number
+** of symbols on the right-hand side of that rule. */
+static const signed char yyRuleInfoNRhs[] = {
+   -1,  /* (0) input ::= cmd */
+   -7,  /* (1) apply ::= APPLY COMMA|AND TRUE|FALSE ID LP applylist RP */
+  -10,  /* (2) apply ::= APPLY COMMA|AND TRUE|FALSE ID LP applylist RP LP applylist RP */
+   -3,  /* (3) applylist ::= applylist COMMA tokenlist */
+   -1,  /* (4) applylist ::= tokenlist */
+    0,  /* (5) applylist ::= */
+   -1,  /* (6) commalesssqltoken ::= OPAQUE */
+   -5,  /* (7) commalesssqltoken ::= minvocationid EXCLAIM LP marglist RP */
+   -3,  /* (8) commalesssqltoken ::= LP sql RP */
+   -2,  /* (9) commalesssqltoken ::= LP RP */
+   -1,  /* (10) commalesssqltoken ::= ID */
+   -1,  /* (11) commalesssqltoken ::= VARIABLE */
+   -1,  /* (12) minvocationid ::= ID */
+   -1,  /* (13) marg ::= tokenlist */
+   -2,  /* (14) tokenlist ::= tokenlist commalesssqltoken */
+   -1,  /* (15) tokenlist ::= commalesssqltoken */
+   -2,  /* (16) cmd ::= sql SEMI */
+   -2,  /* (17) cmd ::= apply SEMI */
+   -2,  /* (18) sql ::= sql sqltoken */
+   -1,  /* (19) sql ::= sqltoken */
+   -1,  /* (20) sqltoken ::= COMMA */
+   -1,  /* (21) sqltoken ::= commalesssqltoken */
+   -1,  /* (22) marglist ::= marglistinner */
+    0,  /* (23) marglist ::= */
+   -3,  /* (24) marglistinner ::= marglistinner COMMA marg */
+   -1,  /* (25) marglistinner ::= marg */
+};
+
+static void yy_accept(yyParser*);  /* Forward Declaration */
+
+/*
+** Perform a reduce action and the shift that must immediately
+** follow the reduce.
+**
+** The yyLookahead and yyLookaheadToken parameters provide reduce actions
+** access to the lookahead token (if any).  The yyLookahead will be YYNOCODE
+** if the lookahead token has already been consumed.  As this procedure is
+** only called from one place, optimizing compilers will in-line it, which
+** means that the extra parameters have no performance impact.
+*/
+static YYACTIONTYPE yy_reduce(
+  yyParser *yypParser,         /* The parser */
+  unsigned int yyruleno,       /* Number of the rule by which to reduce */
+  int yyLookahead,             /* Lookahead token, or YYNOCODE if none */
+  PreprocessorGrammarParseTOKENTYPE yyLookaheadToken  /* Value of the lookahead token */
+  PreprocessorGrammarParseCTX_PDECL                   /* %extra_context */
+){
+  int yygoto;                     /* The next state */
+  YYACTIONTYPE yyact;             /* The next action */
+  yyStackEntry *yymsp;            /* The top of the parser's stack */
+  int yysize;                     /* Amount to pop the stack */
+  PreprocessorGrammarParseARG_FETCH
+  (void)yyLookahead;
+  (void)yyLookaheadToken;
+  yymsp = yypParser->yytos;
+
+  switch( yyruleno ){
+  /* Beginning here are the reduction cases.  A typical example
+  ** follows:
+  **   case 0:
+  **  #line <lineno> <grammarfile>
+  **     { ... }           // User supplied code
+  **  #line <lineno> <thisfile>
+  **     break;
+  */
+/********** Begin reduce actions **********************************************/
+        YYMINORTYPE yylhsminor;
+      case 0: /* input ::= cmd */
+{
+  OnPreprocessorEnd(state);
+}
+        break;
+      case 1: /* apply ::= APPLY COMMA|AND TRUE|FALSE ID LP applylist RP */
+{
+  OnPreprocessorApply(state, &yymsp[-3].minor.yy0, &yymsp[-5].minor.yy0, &yymsp[-4].minor.yy0, yymsp[-1].minor.yy2, 0);
+}
+        break;
+      case 2: /* apply ::= APPLY COMMA|AND TRUE|FALSE ID LP applylist RP LP applylist RP */
+{
+  OnPreprocessorApply(state, &yymsp[-6].minor.yy0, &yymsp[-8].minor.yy0, &yymsp[-7].minor.yy0, yymsp[-4].minor.yy2, yymsp[-1].minor.yy2);
+}
+        break;
+      case 3: /* applylist ::= applylist COMMA tokenlist */
+{
+  yylhsminor.yy2 = OnPreprocessorAppendApplyList(yymsp[-2].minor.yy2, &yymsp[0].minor.yy32);
+}
+  yymsp[-2].minor.yy2 = yylhsminor.yy2;
+        break;
+      case 4: /* applylist ::= tokenlist */
+{
+  yylhsminor.yy2 = OnPreprocessorAppendApplyList(OnPreprocessorCreateApplyList(), &yymsp[0].minor.yy32);
+}
+  yymsp[0].minor.yy2 = yylhsminor.yy2;
+        break;
+      case 5: /* applylist ::= */
+{
+  yymsp[1].minor.yy2 = OnPreprocessorCreateApplyList();
+}
+        break;
+      case 6: /* commalesssqltoken ::= OPAQUE */
+      case 10: /* commalesssqltoken ::= ID */ yytestcase(yyruleno==10);
+{
+  yylhsminor.yy32 = (struct PreprocessorGrammarTokenBounds) {yymsp[0].minor.yy0, yymsp[0].minor.yy0};
+}
+  yymsp[0].minor.yy32 = yylhsminor.yy32;
+        break;
+      case 7: /* commalesssqltoken ::= minvocationid EXCLAIM LP marglist RP */
+{
+  yylhsminor.yy32 = (struct PreprocessorGrammarTokenBounds) {yymsp[-4].minor.yy0, yymsp[0].minor.yy0};
+  OnPreprocessorMacroEnd(state, &yymsp[-4].minor.yy0, &yymsp[0].minor.yy0);
+}
+  yymsp[-4].minor.yy32 = yylhsminor.yy32;
+        break;
+      case 8: /* commalesssqltoken ::= LP sql RP */
+{
+  yylhsminor.yy32 = (struct PreprocessorGrammarTokenBounds) {yymsp[-2].minor.yy0, yymsp[0].minor.yy0};
+}
+  yymsp[-2].minor.yy32 = yylhsminor.yy32;
+        break;
+      case 9: /* commalesssqltoken ::= LP RP */
+{
+  yylhsminor.yy32 = (struct PreprocessorGrammarTokenBounds) {yymsp[-1].minor.yy0, yymsp[0].minor.yy0};
+}
+  yymsp[-1].minor.yy32 = yylhsminor.yy32;
+        break;
+      case 11: /* commalesssqltoken ::= VARIABLE */
+{
+  yylhsminor.yy32 = (struct PreprocessorGrammarTokenBounds) {yymsp[0].minor.yy0, yymsp[0].minor.yy0};
+  OnPreprocessorVariable(state, &yymsp[0].minor.yy0);
+}
+  yymsp[0].minor.yy32 = yylhsminor.yy32;
+        break;
+      case 12: /* minvocationid ::= ID */
+{
+  yylhsminor.yy0 = yymsp[0].minor.yy0;
+  OnPreprocessorMacroId(state, &yymsp[0].minor.yy0);
+}
+  yymsp[0].minor.yy0 = yylhsminor.yy0;
+        break;
+      case 13: /* marg ::= tokenlist */
+{
+  OnPreprocessorMacroArg(state, &yymsp[0].minor.yy32);
+}
+        break;
+      case 14: /* tokenlist ::= tokenlist commalesssqltoken */
+{
+  yylhsminor.yy32 = (struct PreprocessorGrammarTokenBounds) {yymsp[-1].minor.yy32.start, yymsp[0].minor.yy32.end};
+}
+  yymsp[-1].minor.yy32 = yylhsminor.yy32;
+        break;
+      case 15: /* tokenlist ::= commalesssqltoken */
+{
+  yylhsminor.yy32 = (struct PreprocessorGrammarTokenBounds) {yymsp[0].minor.yy32.start, yymsp[0].minor.yy32.end};
+}
+  yymsp[0].minor.yy32 = yylhsminor.yy32;
+        break;
+      default:
+      /* (16) cmd ::= sql SEMI */ yytestcase(yyruleno==16);
+      /* (17) cmd ::= apply SEMI */ yytestcase(yyruleno==17);
+      /* (18) sql ::= sql sqltoken */ yytestcase(yyruleno==18);
+      /* (19) sql ::= sqltoken (OPTIMIZED OUT) */ assert(yyruleno!=19);
+      /* (20) sqltoken ::= COMMA */ yytestcase(yyruleno==20);
+      /* (21) sqltoken ::= commalesssqltoken (OPTIMIZED OUT) */ assert(yyruleno!=21);
+      /* (22) marglist ::= marglistinner */ yytestcase(yyruleno==22);
+      /* (23) marglist ::= */ yytestcase(yyruleno==23);
+      /* (24) marglistinner ::= marglistinner COMMA marg */ yytestcase(yyruleno==24);
+      /* (25) marglistinner ::= marg (OPTIMIZED OUT) */ assert(yyruleno!=25);
+        break;
+/********** End reduce actions ************************************************/
+  };
+  assert( yyruleno<sizeof(yyRuleInfoLhs)/sizeof(yyRuleInfoLhs[0]) );
+  yygoto = yyRuleInfoLhs[yyruleno];
+  yysize = yyRuleInfoNRhs[yyruleno];
+  yyact = yy_find_reduce_action(yymsp[yysize].stateno,(YYCODETYPE)yygoto);
+
+  /* There are no SHIFTREDUCE actions on nonterminals because the table
+  ** generator has simplified them to pure REDUCE actions. */
+  assert( !(yyact>YY_MAX_SHIFT && yyact<=YY_MAX_SHIFTREDUCE) );
+
+  /* It is not possible for a REDUCE to be followed by an error */
+  assert( yyact!=YY_ERROR_ACTION );
+
+  yymsp += yysize+1;
+  yypParser->yytos = yymsp;
+  yymsp->stateno = (YYACTIONTYPE)yyact;
+  yymsp->major = (YYCODETYPE)yygoto;
+  yyTraceShift(yypParser, yyact, "... then shift");
+  return yyact;
+}
+
+/*
+** The following code executes when the parse fails
+*/
+#ifndef YYNOERRORRECOVERY
+static void yy_parse_failed(
+  yyParser *yypParser           /* The parser */
+){
+  PreprocessorGrammarParseARG_FETCH
+  PreprocessorGrammarParseCTX_FETCH
+#ifndef NDEBUG
+  if( yyTraceFILE ){
+    fprintf(yyTraceFILE,"%sFail!\n",yyTracePrompt);
+  }
+#endif
+  while( yypParser->yytos>yypParser->yystack ) yy_pop_parser_stack(yypParser);
+  /* Here code is inserted which will be executed whenever the
+  ** parser fails */
+/************ Begin %parse_failure code ***************************************/
+/************ End %parse_failure code *****************************************/
+  PreprocessorGrammarParseARG_STORE /* Suppress warning about unused %extra_argument variable */
+  PreprocessorGrammarParseCTX_STORE
+}
+#endif /* YYNOERRORRECOVERY */
+
+/*
+** The following code executes when a syntax error first occurs.
+*/
+static void yy_syntax_error(
+  yyParser *yypParser,           /* The parser */
+  int yymajor,                   /* The major type of the error token */
+  PreprocessorGrammarParseTOKENTYPE yyminor         /* The minor type of the error token */
+){
+  PreprocessorGrammarParseARG_FETCH
+  PreprocessorGrammarParseCTX_FETCH
+#define TOKEN yyminor
+/************ Begin %syntax_error code ****************************************/
+
+  OnPreprocessorSyntaxError(state, &yyminor);
+/************ End %syntax_error code ******************************************/
+  PreprocessorGrammarParseARG_STORE /* Suppress warning about unused %extra_argument variable */
+  PreprocessorGrammarParseCTX_STORE
+}
+
+/*
+** The following is executed when the parser accepts
+*/
+static void yy_accept(
+  yyParser *yypParser           /* The parser */
+){
+  PreprocessorGrammarParseARG_FETCH
+  PreprocessorGrammarParseCTX_FETCH
+#ifndef NDEBUG
+  if( yyTraceFILE ){
+    fprintf(yyTraceFILE,"%sAccept!\n",yyTracePrompt);
+  }
+#endif
+#ifndef YYNOERRORRECOVERY
+  yypParser->yyerrcnt = -1;
+#endif
+  assert( yypParser->yytos==yypParser->yystack );
+  /* Here code is inserted which will be executed whenever the
+  ** parser accepts */
+/*********** Begin %parse_accept code *****************************************/
+/*********** End %parse_accept code *******************************************/
+  PreprocessorGrammarParseARG_STORE /* Suppress warning about unused %extra_argument variable */
+  PreprocessorGrammarParseCTX_STORE
+}
+
+/* The main parser program.
+** The first argument is a pointer to a structure obtained from
+** "PreprocessorGrammarParseAlloc" which describes the current state of the parser.
+** The second argument is the major token number.  The third is
+** the minor token.  The fourth optional argument is whatever the
+** user wants (and specified in the grammar) and is available for
+** use by the action routines.
+**
+** Inputs:
+** <ul>
+** <li> A pointer to the parser (an opaque structure.)
+** <li> The major token number.
+** <li> The minor token number.
+** <li> An option argument of a grammar-specified type.
+** </ul>
+**
+** Outputs:
+** None.
+*/
+void PreprocessorGrammarParse(
+  void *yyp,                   /* The parser */
+  int yymajor,                 /* The major token code number */
+  PreprocessorGrammarParseTOKENTYPE yyminor       /* The value for the token */
+  PreprocessorGrammarParseARG_PDECL               /* Optional %extra_argument parameter */
+){
+  YYMINORTYPE yyminorunion;
+  YYACTIONTYPE yyact;   /* The parser action. */
+#if !defined(YYERRORSYMBOL) && !defined(YYNOERRORRECOVERY)
+  int yyendofinput;     /* True if we are at the end of input */
+#endif
+#ifdef YYERRORSYMBOL
+  int yyerrorhit = 0;   /* True if yymajor has invoked an error */
+#endif
+  yyParser *yypParser = (yyParser*)yyp;  /* The parser */
+  PreprocessorGrammarParseCTX_FETCH
+  PreprocessorGrammarParseARG_STORE
+
+  assert( yypParser->yytos!=0 );
+#if !defined(YYERRORSYMBOL) && !defined(YYNOERRORRECOVERY)
+  yyendofinput = (yymajor==0);
+#endif
+
+  yyact = yypParser->yytos->stateno;
+#ifndef NDEBUG
+  if( yyTraceFILE ){
+    if( yyact < YY_MIN_REDUCE ){
+      fprintf(yyTraceFILE,"%sInput '%s' in state %d\n",
+              yyTracePrompt,yyTokenName[yymajor],yyact);
+    }else{
+      fprintf(yyTraceFILE,"%sInput '%s' with pending reduce %d\n",
+              yyTracePrompt,yyTokenName[yymajor],yyact-YY_MIN_REDUCE);
+    }
+  }
+#endif
+
+  while(1){ /* Exit by "break" */
+    assert( yypParser->yytos>=yypParser->yystack );
+    assert( yyact==yypParser->yytos->stateno );
+    yyact = yy_find_shift_action((YYCODETYPE)yymajor,yyact);
+    if( yyact >= YY_MIN_REDUCE ){
+      unsigned int yyruleno = yyact - YY_MIN_REDUCE; /* Reduce by this rule */
+#ifndef NDEBUG
+      assert( yyruleno<(int)(sizeof(yyRuleName)/sizeof(yyRuleName[0])) );
+      if( yyTraceFILE ){
+        int yysize = yyRuleInfoNRhs[yyruleno];
+        if( yysize ){
+          fprintf(yyTraceFILE, "%sReduce %d [%s]%s, pop back to state %d.\n",
+            yyTracePrompt,
+            yyruleno, yyRuleName[yyruleno],
+            yyruleno<YYNRULE_WITH_ACTION ? "" : " without external action",
+            yypParser->yytos[yysize].stateno);
+        }else{
+          fprintf(yyTraceFILE, "%sReduce %d [%s]%s.\n",
+            yyTracePrompt, yyruleno, yyRuleName[yyruleno],
+            yyruleno<YYNRULE_WITH_ACTION ? "" : " without external action");
+        }
+      }
+#endif /* NDEBUG */
+
+      /* Check that the stack is large enough to grow by a single entry
+      ** if the RHS of the rule is empty.  This ensures that there is room
+      ** enough on the stack to push the LHS value */
+      if( yyRuleInfoNRhs[yyruleno]==0 ){
+#ifdef YYTRACKMAXSTACKDEPTH
+        if( (int)(yypParser->yytos - yypParser->yystack)>yypParser->yyhwm ){
+          yypParser->yyhwm++;
+          assert( yypParser->yyhwm ==
+                  (int)(yypParser->yytos - yypParser->yystack));
+        }
+#endif
+#if YYSTACKDEPTH>0 
+        if( yypParser->yytos>=yypParser->yystackEnd ){
+          yyStackOverflow(yypParser);
+          break;
+        }
+#else
+        if( yypParser->yytos>=&yypParser->yystack[yypParser->yystksz-1] ){
+          if( yyGrowStack(yypParser) ){
+            yyStackOverflow(yypParser);
+            break;
+          }
+        }
+#endif
+      }
+      yyact = yy_reduce(yypParser,yyruleno,yymajor,yyminor PreprocessorGrammarParseCTX_PARAM);
+    }else if( yyact <= YY_MAX_SHIFTREDUCE ){
+      yy_shift(yypParser,yyact,(YYCODETYPE)yymajor,yyminor);
+#ifndef YYNOERRORRECOVERY
+      yypParser->yyerrcnt--;
+#endif
+      break;
+    }else if( yyact==YY_ACCEPT_ACTION ){
+      yypParser->yytos--;
+      yy_accept(yypParser);
+      return;
+    }else{
+      assert( yyact == YY_ERROR_ACTION );
+      yyminorunion.yy0 = yyminor;
+#ifdef YYERRORSYMBOL
+      int yymx;
+#endif
+#ifndef NDEBUG
+      if( yyTraceFILE ){
+        fprintf(yyTraceFILE,"%sSyntax Error!\n",yyTracePrompt);
+      }
+#endif
+#ifdef YYERRORSYMBOL
+      /* A syntax error has occurred.
+      ** The response to an error depends upon whether or not the
+      ** grammar defines an error token "ERROR".  
+      **
+      ** This is what we do if the grammar does define ERROR:
+      **
+      **  * Call the %syntax_error function.
+      **
+      **  * Begin popping the stack until we enter a state where
+      **    it is legal to shift the error symbol, then shift
+      **    the error symbol.
+      **
+      **  * Set the error count to three.
+      **
+      **  * Begin accepting and shifting new tokens.  No new error
+      **    processing will occur until three tokens have been
+      **    shifted successfully.
+      **
+      */
+      if( yypParser->yyerrcnt<0 ){
+        yy_syntax_error(yypParser,yymajor,yyminor);
+      }
+      yymx = yypParser->yytos->major;
+      if( yymx==YYERRORSYMBOL || yyerrorhit ){
+#ifndef NDEBUG
+        if( yyTraceFILE ){
+          fprintf(yyTraceFILE,"%sDiscard input token %s\n",
+             yyTracePrompt,yyTokenName[yymajor]);
+        }
+#endif
+        yy_destructor(yypParser, (YYCODETYPE)yymajor, &yyminorunion);
+        yymajor = YYNOCODE;
+      }else{
+        while( yypParser->yytos > yypParser->yystack ){
+          yyact = yy_find_reduce_action(yypParser->yytos->stateno,
+                                        YYERRORSYMBOL);
+          if( yyact<=YY_MAX_SHIFTREDUCE ) break;
+          yy_pop_parser_stack(yypParser);
+        }
+        if( yypParser->yytos <= yypParser->yystack || yymajor==0 ){
+          yy_destructor(yypParser,(YYCODETYPE)yymajor,&yyminorunion);
+          yy_parse_failed(yypParser);
+#ifndef YYNOERRORRECOVERY
+          yypParser->yyerrcnt = -1;
+#endif
+          yymajor = YYNOCODE;
+        }else if( yymx!=YYERRORSYMBOL ){
+          yy_shift(yypParser,yyact,YYERRORSYMBOL,yyminor);
+        }
+      }
+      yypParser->yyerrcnt = 3;
+      yyerrorhit = 1;
+      if( yymajor==YYNOCODE ) break;
+      yyact = yypParser->yytos->stateno;
+#elif defined(YYNOERRORRECOVERY)
+      /* If the YYNOERRORRECOVERY macro is defined, then do not attempt to
+      ** do any kind of error recovery.  Instead, simply invoke the syntax
+      ** error routine and continue going as if nothing had happened.
+      **
+      ** Applications can set this macro (for example inside %include) if
+      ** they intend to abandon the parse upon the first syntax error seen.
+      */
+      yy_syntax_error(yypParser,yymajor, yyminor);
+      yy_destructor(yypParser,(YYCODETYPE)yymajor,&yyminorunion);
+      break;
+#else  /* YYERRORSYMBOL is not defined */
+      /* This is what we do if the grammar does not define ERROR:
+      **
+      **  * Report an error message, and throw away the input token.
+      **
+      **  * If the input token is $, then fail the parse.
+      **
+      ** As before, subsequent error messages are suppressed until
+      ** three input tokens have been successfully shifted.
+      */
+      if( yypParser->yyerrcnt<=0 ){
+        yy_syntax_error(yypParser,yymajor, yyminor);
+      }
+      yypParser->yyerrcnt = 3;
+      yy_destructor(yypParser,(YYCODETYPE)yymajor,&yyminorunion);
+      if( yyendofinput ){
+        yy_parse_failed(yypParser);
+#ifndef YYNOERRORRECOVERY
+        yypParser->yyerrcnt = -1;
+#endif
+      }
+      break;
+#endif
+    }
+  }
+#ifndef NDEBUG
+  if( yyTraceFILE ){
+    yyStackEntry *i;
+    char cDiv = '[';
+    fprintf(yyTraceFILE,"%sReturn. Stack=",yyTracePrompt);
+    for(i=&yypParser->yystack[1]; i<=yypParser->yytos; i++){
+      fprintf(yyTraceFILE,"%c%s", cDiv, yyTokenName[i->major]);
+      cDiv = ' ';
+    }
+    fprintf(yyTraceFILE,"]\n");
+  }
+#endif
+  return;
+}
+
+/*
+** Return the fallback token corresponding to canonical token iToken, or
+** 0 if iToken has no fallback.
+*/
+int PreprocessorGrammarParseFallback(int iToken){
+#ifdef YYFALLBACK
+  assert( iToken<(int)(sizeof(yyFallback)/sizeof(yyFallback[0])) );
+  return yyFallback[iToken];
+#else
+  (void)iToken;
+  return 0;
+#endif
+}
diff --git a/src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.h b/src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.h
new file mode 100644
index 0000000..6cb021e
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.h
@@ -0,0 +1,12 @@
+#define PPTK_SEMI                             1
+#define PPTK_APPLY                            2
+#define PPTK_COMMA                            3
+#define PPTK_AND                              4
+#define PPTK_TRUE                             5
+#define PPTK_FALSE                            6
+#define PPTK_ID                               7
+#define PPTK_LP                               8
+#define PPTK_RP                               9
+#define PPTK_OPAQUE                          10
+#define PPTK_EXCLAIM                         11
+#define PPTK_VARIABLE                        12
diff --git a/src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.y b/src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.y
new file mode 100644
index 0000000..cdfa821
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.y
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+
+%name PreprocessorGrammarParse
+%token_prefix PPTK_
+%start_symbol input
+
+%include {
+#include "src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar_interface.h"
+
+#define YYNOERRORRECOVERY 1
+#define YYPARSEFREENEVERNULL 1
+}
+
+%token_type {struct PreprocessorGrammarToken}
+%default_type {struct PreprocessorGrammarTokenBounds}
+
+%extra_context {struct PreprocessorGrammarState* state}
+%syntax_error {
+  OnPreprocessorSyntaxError(state, &yyminor);
+}
+
+input ::= cmd.  {
+  OnPreprocessorEnd(state);
+}
+
+cmd ::= sql SEMI.
+cmd ::= apply SEMI.
+
+apply ::= APPLY COMMA|AND(J) TRUE|FALSE(P) ID(X) LP applylist(Y) RP. {
+  OnPreprocessorApply(state, &X, &J, &P, Y, 0);
+}
+apply ::= APPLY COMMA|AND(J) TRUE|FALSE(P) ID(X) LP applylist(Y) RP LP applylist(Z) RP. {
+  OnPreprocessorApply(state, &X, &J, &P, Y, Z);
+}
+
+%type applylist {struct PreprocessorGrammarApplyList*}
+%destructor applylist { OnPreprocessorFreeApplyList(state, $$); }
+applylist(A) ::= applylist(F) COMMA tokenlist(X). {
+  A = OnPreprocessorAppendApplyList(F, &X);
+}
+applylist(A) ::= tokenlist(F). {
+  A = OnPreprocessorAppendApplyList(OnPreprocessorCreateApplyList(), &F);
+}
+applylist(A) ::=. {
+  A = OnPreprocessorCreateApplyList();
+}
+
+sql ::= sql sqltoken.
+sql ::= sqltoken.
+
+sqltoken ::= COMMA.
+sqltoken ::= commalesssqltoken.
+
+commalesssqltoken(A) ::= OPAQUE(X). {
+  A = (struct PreprocessorGrammarTokenBounds) {X, X};
+}
+commalesssqltoken(A) ::= minvocationid(S) EXCLAIM LP marglist RP(F). {
+  A = (struct PreprocessorGrammarTokenBounds) {S, F};
+  OnPreprocessorMacroEnd(state, &S, &F);
+}
+commalesssqltoken(A) ::= LP(S) sql RP(F).  {
+  A = (struct PreprocessorGrammarTokenBounds) {S, F};
+}
+commalesssqltoken(A) ::= LP(S) RP(F). {
+  A = (struct PreprocessorGrammarTokenBounds) {S, F};
+}
+commalesssqltoken(A) ::= ID(X). {
+  A = (struct PreprocessorGrammarTokenBounds) {X, X};
+}
+commalesssqltoken(A) ::= VARIABLE(X). {
+  A = (struct PreprocessorGrammarTokenBounds) {X, X};
+  OnPreprocessorVariable(state, &X);
+}
+
+%type minvocationid {struct PreprocessorGrammarToken}
+minvocationid(A) ::= ID(X). {
+  A = X;
+  OnPreprocessorMacroId(state, &X);
+}
+
+marglist ::= marglistinner.
+marglist ::=.
+
+marglistinner ::= marglistinner COMMA marg.
+marglistinner ::= marg.
+
+marg ::= tokenlist(X). {
+  OnPreprocessorMacroArg(state, &X);
+}
+
+tokenlist(A) ::= tokenlist(S) commalesssqltoken(F). {
+  A = (struct PreprocessorGrammarTokenBounds) {S.start, F.end};
+}
+tokenlist(A) ::= commalesssqltoken(X). {
+  A = (struct PreprocessorGrammarTokenBounds) {X.start, X.end};
+}
diff --git a/src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar_interface.h b/src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar_interface.h
new file mode 100644
index 0000000..c69f095
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar_interface.h
@@ -0,0 +1,91 @@
+/*
+ * 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_PREPROCESSOR_PREPROCESSOR_GRAMMAR_INTERFACE_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_PREPROCESSOR_PREPROCESSOR_GRAMMAR_INTERFACE_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stddef.h>
+#include <stdio.h>
+
+#include "src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.h"
+
+#undef NDEBUG
+
+#ifdef __cplusplus
+namespace perfetto::trace_processor {
+namespace {
+#endif
+
+struct PreprocessorGrammarState;
+
+struct PreprocessorGrammarToken {
+  const char* ptr;
+  size_t n;
+  int major;
+};
+
+struct PreprocessorGrammarTokenBounds {
+  struct PreprocessorGrammarToken start;
+  struct PreprocessorGrammarToken end;
+};
+
+struct PreprocessorGrammarApplyList;
+
+void* PreprocessorGrammarParseAlloc(void* (*)(size_t),
+                                    struct PreprocessorGrammarState*);
+void PreprocessorGrammarParse(void* parser,
+                              int,
+                              struct PreprocessorGrammarToken);
+void PreprocessorGrammarParseFree(void* parser, void (*)(void*));
+void PreprocessorGrammarParseTrace(FILE*, char*);
+
+void OnPreprocessorSyntaxError(struct PreprocessorGrammarState*,
+                               struct PreprocessorGrammarToken*);
+void OnPreprocessorApply(struct PreprocessorGrammarState*,
+                         struct PreprocessorGrammarToken* name,
+                         struct PreprocessorGrammarToken* join,
+                         struct PreprocessorGrammarToken* prefix,
+                         struct PreprocessorGrammarApplyList*,
+                         struct PreprocessorGrammarApplyList*);
+void OnPreprocessorVariable(struct PreprocessorGrammarState*,
+                            struct PreprocessorGrammarToken* var);
+void OnPreprocessorMacroId(struct PreprocessorGrammarState*,
+                           struct PreprocessorGrammarToken* name);
+void OnPreprocessorMacroArg(struct PreprocessorGrammarState*,
+                            struct PreprocessorGrammarTokenBounds*);
+void OnPreprocessorMacroEnd(struct PreprocessorGrammarState*,
+                            struct PreprocessorGrammarToken* name,
+                            struct PreprocessorGrammarToken* rp);
+void OnPreprocessorEnd(struct PreprocessorGrammarState*);
+
+struct PreprocessorGrammarApplyList* OnPreprocessorCreateApplyList();
+struct PreprocessorGrammarApplyList* OnPreprocessorAppendApplyList(
+    struct PreprocessorGrammarApplyList*,
+    struct PreprocessorGrammarTokenBounds*);
+void OnPreprocessorFreeApplyList(struct PreprocessorGrammarState*,
+                                 struct PreprocessorGrammarApplyList*);
+
+#ifdef __cplusplus
+}
+}
+}
+#endif
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_PREPROCESSOR_PREPROCESSOR_GRAMMAR_INTERFACE_H_
diff --git a/src/trace_processor/perfetto_sql/stdlib/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
index d351a31..adaeb73 100644
--- a/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
@@ -22,19 +22,17 @@
     "android",
     "callstacks",
     "chrome:chrome_sql",
-    "common",
     "counters",
-    "deprecated/v42/common",
     "export",
     "graphs",
     "intervals",
     "linux",
-    "metasql",
     "pkvm",
     "prelude",
     "sched",
     "slices",
     "stack_trace",
+    "stacks",
     "time",
     "v8",
     "viz",
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn
index 40320fa..4f0208c 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright (C) 2022 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.
@@ -17,6 +17,7 @@
 perfetto_sql_source_set("android") {
   deps = [
     "auto",
+    "battery",
     "cpu",
     "frames",
     "gpu",
@@ -33,6 +34,7 @@
     "binder_breakdown.sql",
     "broadcasts.sql",
     "critical_blocking_calls.sql",
+    "desktop_mode.sql",
     "device.sql",
     "dvfs.sql",
     "freezer.sql",
@@ -40,6 +42,7 @@
     "input.sql",
     "io.sql",
     "job_scheduler.sql",
+    "job_scheduler_states.sql",
     "monitor_contention.sql",
     "network_packets.sql",
     "oom_adjuster.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/battery.sql b/src/trace_processor/perfetto_sql/stdlib/android/battery.sql
index 8d21767..c3723d8 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/battery.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/battery.sql
@@ -24,14 +24,20 @@
   -- Current charge in micro ampers.
   charge_uah DOUBLE,
   -- Current micro ampers.
-  current_ua DOUBLE
+  current_ua DOUBLE,
+  -- Current voltage in micro volts.
+  voltage_uv DOUBLE,
+  -- Current energy counter in microwatt-hours(µWh).
+  energy_counter_uwh DOUBLE
 )  AS
 SELECT
   all_ts.ts,
   current_avg_ua,
   capacity_percent,
   charge_uah,
-  current_ua
+  current_ua,
+  voltage_uv,
+  energy_counter_uwh
 FROM (
   SELECT DISTINCT(ts) AS ts
   FROM counter c
@@ -62,4 +68,16 @@
   JOIN counter_track t ON c.track_id = t.id
   WHERE name = 'batt.current_ua'
 ) USING(ts)
+LEFT JOIN (
+  SELECT ts, value AS voltage_uv
+  FROM counter c
+  JOIN counter_track t ON c.track_id = t.id
+  WHERE name = 'batt.voltage_uv'
+) USING(ts)
+LEFT JOIN (
+  SELECT ts, value AS energy_counter_uwh
+  FROM counter c
+  JOIN counter_track t ON c.track_id = t.id
+  WHERE name = 'batt.energy_counter_uwh'
+) USING(ts)
 ORDER BY ts;
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/battery/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/android/battery/BUILD.gn
new file mode 100644
index 0000000..828c10b
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/android/battery/BUILD.gn
@@ -0,0 +1,19 @@
+# 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("../../../../../../gn/perfetto_sql.gni")
+
+perfetto_sql_source_set("battery") {
+  sources = [ "charging_states.sql" ]
+}
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/battery/charging_states.sql b/src/trace_processor/perfetto_sql/stdlib/android/battery/charging_states.sql
new file mode 100644
index 0000000..6629fe4
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/android/battery/charging_states.sql
@@ -0,0 +1,71 @@
+--
+-- 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 counters.intervals;
+
+-- Device charging states.
+CREATE PERFETTO TABLE android_charging_states(
+  -- Alias of counter.id if a slice with charging state exists otherwise
+  -- there will be a single row where id = 1.
+  id INT,
+  -- Timestamp at which the device charging state began.
+  ts INT,
+  -- Duration of the device charging state.
+  dur INT,
+  -- Device charging state, one of: Charging, Discharging, Not charging
+  -- (when the charger is present but battery is not charging),
+  -- Full, Unknown
+  charging_state STRING
+) AS SELECT
+    id,
+    ts,
+    dur,
+    charging_state
+  FROM (
+    WITH
+      _counter AS (
+        SELECT counter.id, ts, 0 AS track_id, value
+        FROM counter
+        JOIN counter_track
+          ON counter_track.id = counter.track_id
+        WHERE counter_track.name = 'BatteryStatus'
+      )
+    SELECT
+      id,
+      ts,
+      dur,
+      CASE
+        value
+        -- 0 and 1 are both 'Unknown'
+        WHEN 2 THEN 'Charging'
+        WHEN 3 THEN 'Discharging'
+        -- special case when charger is present but battery isn't charging
+        WHEN 4 THEN 'Not charging'
+        WHEN 5 THEN 'Full'
+        ELSE 'Unknown'
+        END AS charging_state
+    FROM counter_leading_intervals !(_counter)
+    WHERE dur > 0
+    -- Either the above select statement is populated or the
+    -- select statement after the union is populated but not both.
+    UNION
+    -- When the trace does not have a slice in the charging state track then
+    -- we will assume that the charging state for the entire trace is Unknown.
+    -- This ensures that we still have job data even if the charging state is
+    -- not known. The following statement will only ever return a single row.
+    SELECT 1, TRACE_START(), TRACE_DUR(), 'Unknown'
+    WHERE NOT EXISTS (
+      SELECT * FROM _counter
+    ) AND TRACE_DUR() > 0
+  );
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/binder.sql b/src/trace_processor/perfetto_sql/stdlib/android/binder.sql
index ec15d90..6994122 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/binder.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/binder.sql
@@ -122,8 +122,9 @@
     JOIN thread reply_thread ON reply_thread.utid = reply_thread_track.utid
     JOIN process reply_process ON reply_process.upid = reply_thread.upid
     LEFT JOIN slice aidl ON aidl.parent_id = binder_reply.id
-        AND (aidl.name GLOB 'AIDL::cpp*Server'
-             OR aidl.name GLOB 'AIDL::java*server'
+        -- Filter for only server side AIDL slices as there are some client side ones for cpp
+        AND (aidl.name GLOB 'AIDL::*Server'
+             OR aidl.name GLOB 'AIDL::*server'
              OR aidl.name GLOB 'HIDL::*server')
   )
 SELECT
@@ -323,8 +324,9 @@
   SELECT id, ts, dur, track_id, name
   FROM slice
   WHERE
-    name GLOB 'AIDL::cpp*Server'
-    OR name GLOB 'AIDL::java*server'
+    -- Filter for only server side AIDL slices as there are some client side ones for cpp
+    name GLOB 'AIDL::*Server'
+    OR name GLOB 'AIDL::*server'
     OR name GLOB 'HIDL::*server'
     OR name = 'binder async rcv'
 ) SELECT *, LEAD(name) OVER (PARTITION BY track_id ORDER BY ts) AS next_name,
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/binder_breakdown.sql b/src/trace_processor/perfetto_sql/stdlib/android/binder_breakdown.sql
index 1ea12eb..e3a07ce 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/binder_breakdown.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/binder_breakdown.sql
@@ -39,7 +39,8 @@
 CREATE PERFETTO VIEW _thread_state_view
 AS
 SELECT id, ts, dur, utid, cpu, state, io_wait
-FROM thread_state WHERE dur > 0;
+FROM thread_state WHERE dur > 0
+ORDER BY ts;
 
 -- Partitions and flattens slices underneath the server or client side of binder txns.
 -- The |name| column in the output is the lowest depth slice name in a given partition.
@@ -82,12 +83,14 @@
 -- Server side flattened descendant slices.
 CREATE PERFETTO TABLE _binder_server_flat_descendants
 AS
-SELECT * FROM _binder_flatten_descendants!(binder_reply_id, server_ts, server_dur, 'binder reply');
+SELECT * FROM _binder_flatten_descendants!(binder_reply_id, server_ts, server_dur, 'binder reply')
+ORDER BY ts;
 
 -- Client side flattened descendant slices.
 CREATE PERFETTO TABLE _binder_client_flat_descendants
 AS
-SELECT * FROM _binder_flatten_descendants!(binder_txn_id, client_ts, client_dur, 'binder transaction');
+SELECT * FROM _binder_flatten_descendants!(binder_txn_id, client_ts, client_dur, 'binder transaction')
+ORDER BY ts;
 
 -- Server side flattened descendants intersected with their thread_states.
 CREATE PERFETTO TABLE _binder_server_flat_descendants_with_thread_state
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/desktop_mode.sql b/src/trace_processor/perfetto_sql/stdlib/android/desktop_mode.sql
new file mode 100644
index 0000000..496e4ee
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/android/desktop_mode.sql
@@ -0,0 +1,73 @@
+--
+-- 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.statsd;
+
+-- Desktop Windows with durations they were open.
+CREATE PERFETTO TABLE android_desktop_mode_windows (
+-- Window add timestamp; NULL if no add event in the trace.
+raw_add_ts INT,
+-- Window remove timestamp; NULL if no remove event in the trace.
+raw_remove_ts INT,
+-- timestamp that the window was added; or trace_start() if no add event in the trace.
+ts INT,
+-- duration the window was open; or until trace_end() if no remove event in the trace.
+dur INT,
+-- Desktop Window instance ID - unique per window.
+instance_id INT,
+-- UID of the app running in the window.
+uid INT
+) AS
+WITH
+  atoms AS (
+    SELECT
+      ts,
+      extract_arg(arg_set_id, 'desktop_mode_session_task_update.task_event') AS type,
+      extract_arg(arg_set_id, 'desktop_mode_session_task_update.instance_id') AS instance_id,
+      extract_arg(arg_set_id, 'desktop_mode_session_task_update.uid') AS uid,
+      extract_arg(arg_set_id, 'desktop_mode_session_task_update.session_id') AS session_id
+    FROM android_statsd_atoms
+    WHERE name = 'desktop_mode_session_task_update'),
+  dw_statsd_events_add AS (
+    SELECT *
+    FROM atoms
+    WHERE type = 'TASK_ADDED'),
+  dw_statsd_events_remove AS (
+    SELECT * FROM atoms
+    WHERE type = 'TASK_REMOVED'),
+  dw_statsd_events_update_by_instance AS (
+    SELECT instance_id, session_id, min(uid) AS uid FROM atoms
+    WHERE type = 'TASK_INFO_CHANGED' GROUP BY instance_id, session_id),
+  dw_windows AS (
+    SELECT
+      a.ts AS raw_add_ts,
+      r.ts AS raw_remove_ts,
+      ifnull(a.ts, trace_start()) AS ts,  -- Assume trace_start() if no add event found.
+      ifnull(r.ts, trace_end()) - ifnull(a.ts, trace_start()) AS dur,  -- Assume trace_end() if no remove event found.
+      ifnull(a.instance_id, r.instance_id) AS instance_id,
+      ifnull(a.uid, r.uid) AS uid
+    FROM dw_statsd_events_add a
+    FULL JOIN dw_statsd_events_remove r USING(instance_id, session_id)),
+  -- Assume window was open for the entire trace if we only see change events for the instance ID.
+  dw_windows_with_update_events AS (
+    SELECT * FROM dw_windows
+    UNION
+    SELECT NULL, NULL, trace_start(), trace_end() - trace_start(), instance_id, uid
+    FROM dw_statsd_events_update_by_instance
+    WHERE
+    instance_id NOT IN (SELECT instance_id FROM dw_windows))
+SELECT * FROM dw_windows_with_update_events;
+
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/frames/per_frame_metrics.sql b/src/trace_processor/perfetto_sql/stdlib/android/frames/per_frame_metrics.sql
index 6e9f000..bdd4a62 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/frames/per_frame_metrics.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/frames/per_frame_metrics.sql
@@ -16,9 +16,9 @@
 INCLUDE PERFETTO MODULE time.conversion;
 INCLUDE PERFETTO MODULE android.frames.timeline;
 
--- The amount by which each frame missed of hit its deadline. Positive if the
+-- The amount by which each frame missed of hit its deadline. Negative if the
 -- deadline was not missed. Frames are considered janky if `overrun` is
--- negative.
+-- positive.
 -- Calculated as the difference between the end of the
 -- `expected_frame_timeline_slice` and `actual_frame_timeline_slice` for the
 -- frame.
@@ -27,13 +27,13 @@
 CREATE PERFETTO TABLE android_frames_overrun(
     -- Frame id.
     frame_id INT,
-    -- Difference between `expected` and `actual` frame ends. Positive if frame
+    -- Difference between `expected` and `actual` frame ends. Negative if frame
     -- didn't miss deadline.
     overrun INT
 ) AS
 SELECT
     frame_id,
-    (exp_slice.ts + exp_slice.dur) - (act_slice.ts + act_slice.dur) AS overrun
+    (act_slice.ts + act_slice.dur) - (exp_slice.ts + exp_slice.dur) AS overrun
 FROM _distinct_from_actual_timeline_slice act
 JOIN _distinct_from_expected_timeline_slice exp USING (frame_id)
 JOIN slice act_slice ON (act.id = act_slice.id)
@@ -57,16 +57,12 @@
 -- Calculated as time difference between the actual frame start (from
 -- `actual_frame_timeline_slice`) and start of the `Choreographer#doFrame`
 -- slice.
--- NOTE: Sometimes because of data losses `app_vsync_delay` can be negative.
--- The frames where it happens are filtered out.
 -- For Googlers: more details in go/android-performance-metrics-glossary.
 CREATE PERFETTO TABLE android_app_vsync_delay_per_frame(
     -- Frame id
     frame_id INT,
     -- App VSYNC delay.
-    app_vsync_delay INT,
-    -- The latency between VSYNC-app and the start of actual_frame_timeline.
-    start_latency INT
+    app_vsync_delay INT
 ) AS
 -- As there can be multiple `DrawFrame` slices, the `frames_surface_slices`
 -- table contains multiple rows for the same `frame_id` which only differ on
@@ -83,13 +79,10 @@
 )
 SELECT
     frame_id,
-    act.ts - do_frame.ts AS app_vsync_delay,
-    act.ts - exp.ts AS start_latency
+    act.ts - exp.ts AS app_vsync_delay
 FROM distinct_frames f
 JOIN slice exp ON (f.expected_frame_timeline_id = exp.id)
-JOIN slice act ON (f.actual_frame_timeline_id = act.id)
-JOIN slice do_frame ON (f.do_frame_id = do_frame.id)
-WHERE act.ts >= do_frame.ts;
+JOIN slice act ON (f.actual_frame_timeline_id = act.id);
 
 -- How much time did the frame take across the UI Thread + RenderThread.
 -- Calculated as sum of `app VSYNC delay` `Choreographer#doFrame` slice
@@ -189,7 +182,7 @@
     overrun,
     cpu_time,
     ui_time,
-    IIF(overrun < 0, 1, NULL) AS was_jank,
+    IIF(overrun > 0, 1, NULL) AS was_jank,
     IIF(cpu_time > time_from_ms(20), 1, NULL) AS was_slow_frame,
     IIF(cpu_time > time_from_ms(50), 1, NULL) AS was_big_jank,
     IIF(cpu_time > time_from_ms(200), 1, NULL) AS was_huge_jank
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 e8683f2..5eb7c52 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline.sql
@@ -30,14 +30,17 @@
     -- Utid.
     utid INT,
     -- Upid.
-    upid INT
+    upid INT,
+    -- Timestamp of the frame slice.
+    ts INT
 ) AS
 WITH all_found AS (
     SELECT
         id,
         cast_int!(STR_SPLIT(name, ' ', 1)) AS frame_id,
         utid,
-        upid
+        upid,
+        ts
     FROM thread_slice
     -- Mostly the frame slice is at depth 0. Though it could be pushed to depth 1 while users
     -- enable low layer trace e.g. atrace_app.
@@ -57,13 +60,16 @@
     -- Utid of the UI thread
     ui_thread_utid INT,
     -- Upid of application process
-    upid INT
+    upid INT,
+    -- Timestamp of the slice.
+    ts INT
 ) AS
 SELECT
     id,
     frame_id,
     utid AS ui_thread_utid,
-    upid
+    upid,
+    ts
 -- Some OEMs have customized `doFrame` to add more information, but we've only
 -- observed it added after the frame ID (b/303823815).
 FROM _get_frame_table_with_id('Choreographer#doFrame*');
@@ -93,21 +99,24 @@
 -- We are getting the first slice with one frame id.
 CREATE PERFETTO TABLE _distinct_from_actual_timeline_slice AS
 SELECT
-    id,
     cast_int!(name) AS frame_id,
-    ts,
-    dur
+    MIN(id) AS id,
+    MIN(ts) AS ts,
+    MAX(dur) AS dur,
+    MAX(ts + dur) AS ts_end,
+    count() AS count
 FROM actual_frame_timeline_slice
-GROUP BY 2;
+GROUP BY 1;
 
 -- `expected_frame_timeline_slice` returns the same slice on different tracks.
 -- We are getting the first slice with one frame id.
 CREATE PERFETTO TABLE _distinct_from_expected_timeline_slice AS
 SELECT
+    cast_int!(name) AS frame_id,
     id,
-    cast_int!(name) AS frame_id
+    count() AS count
 FROM expected_frame_timeline_slice
-GROUP BY 2;
+GROUP BY 1;
 
 -- All slices related to one frame. Aggregates `Choreographer#doFrame`,
 -- `DrawFrame`, `actual_frame_timeline_slice` and
@@ -135,7 +144,11 @@
     -- `utid` of the render thread.
     render_thread_utid INT,
     -- `utid` of the UI thread.
-    ui_thread_utid INT
+    ui_thread_utid INT,
+    -- Count of slices in `actual_frame_timeline_slice` related to this frame.
+    actual_frame_timeline_count INT,
+    -- Count of slices in `expected_frame_timeline_slice` related to this frame.
+    expected_frame_timeline_count INT
 ) AS
 WITH fallback AS MATERIALIZED (
     SELECT
@@ -159,7 +172,9 @@
     do_frame.ui_thread_utid,
     "after_28" AS sdk,
     act.id AS actual_frame_timeline_id,
-    exp.id AS expected_frame_timeline_id
+    exp.id AS expected_frame_timeline_id,
+    act.count AS actual_frame_timeline_count,
+    exp.count AS expected_frame_timeline_count
 FROM android_frames_choreographer_do_frame do_frame
 JOIN android_frames_draw_frame draw_frame USING (frame_id, upid)
 JOIN fallback USING (frame_id)
@@ -173,7 +188,9 @@
     SELECT
         *,
         NULL AS actual_frame_timeline_id,
-        NULL AS expected_frame_timeline_id
+        NULL AS expected_frame_timeline_id,
+        NULL AS actual_frame_timeline_count,
+        NULL AS expected_frame_timeline_count
     FROM _frames_maxsdk_28
 )
 SELECT
@@ -185,7 +202,9 @@
     actual_frame_timeline_id,
     expected_frame_timeline_id,
     render_thread_utid,
-    ui_thread_utid
+    ui_thread_utid,
+    actual_frame_timeline_count,
+    expected_frame_timeline_count
 FROM all_frames
 WHERE sdk = IIF(
     (SELECT COUNT(1) FROM actual_frame_timeline_slice) > 0,
@@ -216,7 +235,17 @@
     -- `utid` of the UI thread.
     ui_thread_utid INT
 ) AS
-SELECT * FROM android_frames
+SELECT
+    frame_id,
+    ts,
+    dur,
+    do_frame_id,
+    draw_frame_id,
+    actual_frame_timeline_id,
+    expected_frame_timeline_id,
+    render_thread_utid,
+    ui_thread_utid
+FROM android_frames
 WHERE ts > $ts
 ORDER BY ts
 LIMIT 1;
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/garbage_collection.sql b/src/trace_processor/perfetto_sql/stdlib/android/garbage_collection.sql
index 06e4ae9..38c287c 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/garbage_collection.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/garbage_collection.sql
@@ -124,7 +124,7 @@
 -- Span join GC events with thread states to breakdown the time spent.
 CREATE VIRTUAL TABLE _gc_slice_heap_thread_state_sp
 USING
-  SPAN_JOIN(thread_state PARTITIONED utid, _gc_slice_heap PARTITIONED utid);
+  SPAN_LEFT_JOIN(_gc_slice_heap PARTITIONED utid, thread_state PARTITIONED utid);
 
 -- All Garbage collection events with a breakdown of the time spent and heap reclaimed.
 CREATE PERFETTO TABLE android_garbage_collection_events (
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/gpu/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/android/gpu/BUILD.gn
index 09a5e85..e481ac5 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/gpu/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/android/gpu/BUILD.gn
@@ -18,5 +18,6 @@
   sources = [
     "frequency.sql",
     "memory.sql",
+    "work_period.sql",
   ]
 }
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/gpu/work_period.sql b/src/trace_processor/perfetto_sql/stdlib/android/gpu/work_period.sql
new file mode 100644
index 0000000..4f498d0
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/android/gpu/work_period.sql
@@ -0,0 +1,36 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+-- Tracks for GPU work period events originating from the
+-- `power/gpu_work_period` Linux ftrace tracepoint.
+--
+-- This tracepoint is usually only available on selected Android devices.
+CREATE PERFETTO TABLE android_gpu_work_period_track (
+  -- Unique identifier for this track. Joinable with track.id.
+  id UINT,
+  -- Machine identifier, non-null for tracks on a remote machine.
+  machine_id UINT,
+  -- The UID of the package for which the GPU work period events were emitted.
+  uid INT,
+  -- The GPU identifier for which the GPU work period events were emitted.
+  gpu_id INT
+) AS
+SELECT
+  id,
+  machine_id,
+  extract_arg(dimension_arg_set_id, 'uid') as uid,
+  extract_arg(dimension_arg_set_id, 'gpu') as gpu_id
+FROM track
+WHERE classification = 'android_gpu_work_period';
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/input.sql b/src/trace_processor/perfetto_sql/stdlib/android/input.sql
index 379c56a..217132d 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/input.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/input.sql
@@ -13,6 +13,9 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 
+INCLUDE PERFETTO MODULE android.frames.timeline;
+INCLUDE PERFETTO MODULE slices.with_context;
+
 CREATE PERFETTO TABLE _input_message_sent
 AS
 SELECT
@@ -33,7 +36,8 @@
   USING (utid)
 JOIN process
   USING (upid)
-WHERE slice.name GLOB 'sendMessage(*';
+WHERE slice.name GLOB 'sendMessage(*'
+order by event_seq;
 
 CREATE PERFETTO TABLE _input_message_received
 AS
@@ -55,7 +59,117 @@
   USING (utid)
 JOIN process
   USING (upid)
-WHERE slice.name GLOB 'receiveMessage(*';
+WHERE slice.name GLOB 'receiveMessage(*'
+ORDER BY event_seq;
+
+CREATE PERFETTO TABLE _input_read_time
+AS
+SELECT
+  name,
+  STR_SPLIT(STR_SPLIT(name, '=', 1), ')', 0) AS input_event_id,
+  ts as read_time
+FROM slice
+WHERE name GLOB 'UnwantedInteractionBlocker::notifyMotion*';
+
+CREATE PERFETTO TABLE _event_seq_to_input_event_id
+AS
+SELECT
+  STR_SPLIT(STR_SPLIT(send_message_slice.name, '=', 2), ',', 0) AS event_seq,
+  STR_SPLIT(STR_SPLIT(send_message_slice.name, '=', 1), ',', 0) AS event_channel,
+  STR_SPLIT(STR_SPLIT(enqeue_slice.name, '=', 2), ')', 0) AS input_event_id,
+  thread_slice.thread_name
+FROM slice send_message_slice
+JOIN slice publish_slice
+  ON send_message_slice.parent_id = publish_slice.id
+JOIN slice start_dispatch_slice
+  ON publish_slice.parent_id = start_dispatch_slice.id
+JOIN slice enqeue_slice
+  ON start_dispatch_slice.parent_id = enqeue_slice.id
+JOIN thread_slice
+  ON send_message_slice.id = thread_slice.id
+WHERE send_message_slice.name GLOB 'sendMessage(*' AND thread_slice.thread_name = 'InputDispatcher';
+
+CREATE PERFETTO TABLE _input_event_id_to_android_frame
+AS
+SELECT
+  STR_SPLIT(deliver_input_slice.name, '=', 3) AS input_event_id,
+  STR_SPLIT(STR_SPLIT(dispatch_input_slice.name, '_', 1), ' ', 0) AS event_action,
+  dispatch_input_slice.ts AS consume_time,
+  dispatch_input_slice.ts + dispatch_input_slice.dur AS finish_time,
+  thread_slice.utid,
+  thread_slice.process_name AS process_name,
+  (
+    SELECT
+      android_frames.frame_id
+    FROM android_frames
+    WHERE android_frames.ts > dispatch_input_slice.ts
+    LIMIT 1
+  ) as frame_id,
+  (
+    SELECT
+      android_frames.ts
+    FROM android_frames
+    WHERE android_frames.ts > dispatch_input_slice.ts
+    LIMIT 1
+  ) as ts,
+  (
+    SELECT
+      _input_message_received.event_channel
+    FROM _input_message_received
+    WHERE _input_message_received.ts < deliver_input_slice.ts
+      AND _input_message_received.track_id = deliver_input_slice.track_id
+    ORDER BY _input_message_received.ts DESC
+    LIMIT 1
+  ) as event_channel
+FROM slice deliver_input_slice
+JOIN slice dispatch_input_slice
+  ON deliver_input_slice.parent_id = dispatch_input_slice.id
+JOIN thread_slice
+  ON deliver_input_slice.id = thread_slice.id
+WHERE deliver_input_slice.name GLOB 'deliverInputEvent src=*';
+
+CREATE PERFETTO TABLE _app_frame_to_surface_flinger_frame
+AS
+SELECT
+  app.surface_frame_token as app_surface_frame_token,
+  surface_flinger.ts as surface_flinger_ts,
+  surface_flinger.dur as surface_flinger_dur,
+  app.ts as app_ts,
+  app.present_type,
+  app.upid
+FROM actual_frame_timeline_slice surface_flinger
+JOIN actual_frame_timeline_slice app
+  ON surface_flinger.display_frame_token = app.display_frame_token
+  AND surface_flinger.id != app.id
+WHERE surface_flinger.surface_frame_token = 0 AND app.present_type != 'Dropped Frame';
+
+CREATE PERFETTO TABLE _first_non_dropped_frame_after_input
+AS
+SELECT
+  _input_read_time.input_event_id,
+  _input_read_time.read_time,
+  (
+    SELECT
+      surface_flinger_ts + surface_flinger_dur
+    FROM _app_frame_to_surface_flinger_frame sf_frames
+    WHERE sf_frames.app_ts >= _input_event_id_to_android_frame.ts
+    LIMIT 1
+  ) AS present_time,
+  (
+    SELECT
+      app_surface_frame_token
+    FROM _app_frame_to_surface_flinger_frame sf_frames
+    WHERE sf_frames.app_ts >= _input_event_id_to_android_frame.ts
+    LIMIT 1
+  ) as frame_id,
+  event_seq,
+  event_action
+FROM _input_event_id_to_android_frame
+RIGHT JOIN _event_seq_to_input_event_id
+  ON _input_event_id_to_android_frame.input_event_id = _event_seq_to_input_event_id.input_event_id
+  AND _input_event_id_to_android_frame.event_channel = _event_seq_to_input_event_id.event_channel
+JOIN _input_read_time
+  ON _input_read_time.input_event_id = _event_seq_to_input_event_id.input_event_id;
 
 -- All input events with round trip latency breakdown. Input delivery is socket based and every
 -- input event sent from the OS needs to be ACK'ed by the app. This gives us 4 subevents to measure
@@ -73,6 +187,8 @@
   ack_latency_dur INT,
   -- Duration from input dispatch to input event ACK received.
   total_latency_dur INT,
+  -- Duration from input read to frame present time. Null if an input event has no associated frame event.
+  end_to_end_latency_dur INT,
   -- Tid of thread receiving the input event.
   tid INT,
   -- Name of thread receiving the input event.
@@ -83,10 +199,16 @@
   process_name STRING,
   -- Input event type. See InputTransport.h: InputMessage#Type
   event_type STRING,
+  -- Input event action.
+  event_action STRING,
   -- Input event sequence number, monotonically increasing for an event channel and pid.
   event_seq STRING,
   -- Input event channel name.
   event_channel STRING,
+  -- Unique identifier for the input event.
+  input_event_id STRING,
+  -- Timestamp input event was read by InputReader.
+  read_time INT,
   -- Thread track id of input event dispatching thread.
   dispatch_track_id INT,
   -- Timestamp input event was dispatched.
@@ -98,38 +220,75 @@
   -- Timestamp input event was received.
   receive_ts INT,
   -- Duration of input event receipt.
-  receive_dur INT
+  receive_dur INT,
+  -- Vsync Id associated with the input. Null if an input event has no associated frame event.
+  frame_id INT
   )
 AS
+WITH dispatch AS MATERIALIZED (
+  SELECT * FROM _input_message_sent
+  WHERE thread_name = 'InputDispatcher'
+  ORDER BY event_seq, event_channel
+),
+receive AS MATERIALIZED (
+  SELECT
+    *,
+    REPLACE(event_channel, '(client)', '(server)') AS dispatch_event_channel
+  FROM _input_message_received
+  WHERE event_type NOT IN ('0x2', 'FINISHED')
+  ORDER BY event_seq, dispatch_event_channel
+),
+finish AS MATERIALIZED (
+  SELECT
+    *,
+    REPLACE(event_channel, '(client)', '(server)') AS dispatch_event_channel
+  FROM _input_message_sent
+  WHERE thread_name != 'InputDispatcher'
+  ORDER BY event_seq, dispatch_event_channel
+),
+finish_ack AS MATERIALIZED(
+  SELECT * FROM _input_message_received
+  WHERE event_type IN ('0x2', 'FINISHED')
+  ORDER BY event_seq, event_channel
+)
 SELECT
   receive.ts - dispatch.ts AS dispatch_latency_dur,
   finish.ts - receive.ts AS handling_latency_dur,
   finish_ack.ts - finish.ts AS ack_latency_dur,
   finish_ack.ts - dispatch.ts AS total_latency_dur,
+  frame.present_time - frame.read_time AS end_to_end_latency_dur,
   finish.tid AS tid,
   finish.thread_name AS thread_name,
   finish.pid AS pid,
   finish.process_name AS process_name,
   dispatch.event_type,
+  frame.event_action,
   dispatch.event_seq,
   dispatch.event_channel,
+  frame.input_event_id,
+  frame.read_time,
   dispatch.track_id AS dispatch_track_id,
   dispatch.ts AS dispatch_ts,
   dispatch.dur AS dispatch_dur,
   receive.ts AS receive_ts,
   receive.dur AS receive_dur,
-  receive.track_id AS receive_track_id
-FROM (SELECT * FROM _input_message_sent WHERE thread_name = 'InputDispatcher') dispatch
-JOIN (SELECT * FROM _input_message_received WHERE event_type NOT IN ('0x2', 'FINISHED')) receive
+  receive.track_id AS receive_track_id,
+  frame.frame_id
+FROM dispatch
+JOIN receive
   ON
-    REPLACE(receive.event_channel, '(client)', '(server)') = dispatch.event_channel
+    receive.dispatch_event_channel = dispatch.event_channel
     AND dispatch.event_seq = receive.event_seq
-JOIN (SELECT * FROM _input_message_sent WHERE thread_name != 'InputDispatcher') finish
+JOIN finish
   ON
-    REPLACE(finish.event_channel, '(client)', '(server)') = dispatch.event_channel
+    finish.dispatch_event_channel = dispatch.event_channel
     AND dispatch.event_seq = finish.event_seq
-JOIN (SELECT * FROM _input_message_received WHERE event_type IN ('0x2', 'FINISHED')) finish_ack
-  ON finish_ack.event_channel = dispatch.event_channel AND dispatch.event_seq = finish_ack.event_seq;
+JOIN finish_ack
+  ON
+    finish_ack.event_channel = dispatch.event_channel
+    AND dispatch.event_seq = finish_ack.event_seq
+LEFT JOIN _first_non_dropped_frame_after_input frame
+  ON frame.event_seq = dispatch.event_seq;
 
 -- Key events processed by the Android framework (from android.input.inputevent data source).
 CREATE PERFETTO VIEW android_key_events(
@@ -141,13 +300,19 @@
   -- The timestamp of when the input event was processed by the system
   ts INT,
   -- Details of the input event parsed from the proto message
-  arg_set_id INT
+  arg_set_id INT,
+  -- Raw proto message encoded in base64
+  base64_proto STRING,
+  -- String id for raw proto message
+  base64_proto_id INT
 ) AS
 SELECT
   id,
   event_id,
   ts,
-  arg_set_id
+  arg_set_id,
+  base64_proto,
+  base64_proto_id
 FROM __intrinsic_android_key_events;
 
 -- Motion events processed by the Android framework (from android.input.inputevent data source).
@@ -160,13 +325,19 @@
   -- The timestamp of when the input event was processed by the system
   ts INT,
   -- Details of the input event parsed from the proto message
-  arg_set_id INT
+  arg_set_id INT,
+  -- Raw proto message encoded in base64
+  base64_proto STRING,
+  -- String id for raw proto message
+  base64_proto_id INT
 ) AS
 SELECT
   id,
   event_id,
   ts,
-  arg_set_id
+  arg_set_id,
+  base64_proto,
+  base64_proto_id
 FROM __intrinsic_android_motion_events;
 
 -- Input event dispatching information in Android (from android.input.inputevent data source).
@@ -175,8 +346,12 @@
   id INT,
   -- Event ID of the input event that was dispatched
   event_id INT,
-  -- Extra args parsed from the proto message
+  -- Details of the input event parsed from the proto message
   arg_set_id INT,
+  -- Raw proto message encoded in base64
+  base64_proto STRING,
+  -- String id for raw proto message
+  base64_proto_id INT,
   -- Vsync ID that identifies the state of the windows during which the dispatch decision was made
   vsync_id INT,
   -- Window ID of the window receiving the event
@@ -186,6 +361,8 @@
   id,
   event_id,
   arg_set_id,
+  base64_proto,
+  base64_proto_id,
   vsync_id,
   window_id
 FROM __intrinsic_android_input_event_dispatch;
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/job_scheduler.sql b/src/trace_processor/perfetto_sql/stdlib/android/job_scheduler.sql
index dc222bf..98f65e0 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/job_scheduler.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/job_scheduler.sql
@@ -15,6 +15,18 @@
 --
 
 -- All scheduled jobs and their latencies.
+--
+-- The table is populated by ATrace using the system server ATrace category
+-- (`atrace_categories: "ss"`). You can also set the `atrace_apps` of interest.
+--
+-- This differs from the `android_job_scheduler_states` table
+-- in the `android.job_scheduler_states` module which is populated
+-- by the `ScheduledJobStateChanged` atom.
+--
+-- Using `android_job_scheduler_states` is preferred when the
+-- `ATOM_SCHEDULED_JOB_STATE_CHANGED` is available in the trace since
+-- it includes the constraint, screen, or charging state changes for
+-- each job in a trace.
 CREATE PERFETTO TABLE android_job_scheduler_events (
   -- Id of the scheduled job assigned by the app developer.
   job_id INT,
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/job_scheduler_states.sql b/src/trace_processor/perfetto_sql/stdlib/android/job_scheduler_states.sql
new file mode 100644
index 0000000..4407660
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/android/job_scheduler_states.sql
@@ -0,0 +1,458 @@
+--
+-- 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 counters.intervals;
+INCLUDE PERFETTO MODULE android.battery.charging_states;
+INCLUDE PERFETTO MODULE intervals.intersect;
+
+CREATE PERFETTO TABLE _screen_states AS
+SELECT
+  id,
+  ts,
+  dur,
+  screen_state
+FROM (
+  WITH _screen_state_span AS (
+  SELECT *
+  FROM counter_leading_intervals!((
+    SELECT counter.id, ts, 0 AS track_id, value
+    FROM counter
+    JOIN counter_track ON counter_track.id = counter.track_id
+    WHERE name = 'ScreenState'
+  ))) SELECT
+    id,
+    ts,
+    dur,
+    CASE value
+      WHEN 1 THEN 'Screen off'
+      WHEN 2 THEN 'Screen on'
+      WHEN 3 THEN 'Always-on display (doze)'
+      ELSE 'Unknown'
+      END AS screen_state
+    FROM _screen_state_span
+    WHERE dur > 0
+    -- Either the above select statement is populated or the
+    -- select statement after the union is populated but not both.
+    UNION
+     -- When the trace does not have a slice in the screen state track then
+    -- we will assume that the screen state for the entire trace is Unknown.
+    -- This ensures that we still have job data even if the screen state is
+    -- not known. The following statement will only ever return a single row.
+    SELECT 1, TRACE_START() as ts, TRACE_DUR() as dur, 'Unknown'
+    WHERE NOT EXISTS (
+      SELECT * FROM _screen_state_span
+    ) AND TRACE_DUR() > 0
+);
+
+CREATE PERFETTO TABLE _job_states AS
+SELECT
+  t.id as track_id,
+  s.ts,
+  s.id AS slice_id,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.job_name') AS job_name,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.attribution_node[0].uid') AS uid,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.state') AS state,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.internal_stop_reason')
+    AS internal_stop_reason,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.public_stop_reason')
+    AS public_stop_reason,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.effective_priority')
+    AS effective_priority,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.has_battery_not_low_constraint')
+    AS has_battery_not_low_constraint,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.has_charging_constraint')
+    AS has_charging_constraint,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.has_connectivity_constraint')
+    AS has_connectivity_constraint,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.has_content_trigger_constraint')
+    AS has_content_trigger_constraint,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.has_deadline_constraint')
+    AS has_deadline_constraint,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.has_idle_constraint')
+    AS has_idle_constraint,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.has_storage_not_low_constraint')
+    AS has_storage_not_low_constraint,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.has_timing_delay_constraint')
+    AS has_timing_delay_constraint,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.is_prefetch') == 1
+    AS is_prefetch,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.is_requested_expedited_job')
+    AS is_requested_expedited_job,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.is_running_as_expedited_job')
+    AS is_running_as_expedited_job,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.job_id') AS job_id,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.num_previous_attempts')
+    AS num_previous_attempts,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.requested_priority')
+    AS requested_priority,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.standby_bucket')
+    AS standby_bucket,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.is_periodic')
+    AS is_periodic,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.is_periodic')
+    AS has_flex_constraint,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.is_requested_as_user_initiated_job')
+    AS is_requested_as_user_initiated_job,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.is_running_as_user_initiated_job')
+    AS is_running_as_user_initiated_job,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.deadline_ms')
+    AS deadline_ms,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.job_start_latency_ms')
+    AS job_start_latency_ms,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.num_uncompleted_work_items')
+    AS num_uncompleted_work_items,
+  extract_arg(arg_set_id, 'scheduled_job_state_changed.proc_state')
+    AS proc_state
+FROM
+  track t
+JOIN slice s
+  ON (s.track_id = t.id)
+WHERE
+  t.name = 'Statsd Atoms' AND s.name = 'scheduled_job_state_changed';
+
+CREATE PERFETTO TABLE _job_started AS
+WITH cte AS (
+  SELECT
+    *,
+    LEAD(state, 1)
+      OVER (PARTITION BY uid, job_name, job_id ORDER BY uid, job_name, job_id, ts) AS lead_state,
+    LEAD(ts, 1, TRACE_END())
+      OVER (PARTITION BY uid, job_name, job_id ORDER BY uid, job_name, job_id, ts) AS ts_lead,
+    --- Filter out statsd lossy issue.
+    LEAD(ts, 1)
+      OVER (PARTITION BY uid, job_name, job_id ORDER BY uid, job_name, job_id, ts) IS NULL AS is_end_slice,
+    LEAD(internal_stop_reason, 1, 'INTERNAL_STOP_REASON_UNKNOWN')
+      OVER (
+        PARTITION BY uid, job_name, job_id
+        ORDER BY uid, job_name, job_id, ts
+      ) AS lead_internal_stop_reason,
+    LEAD(public_stop_reason, 1, 'PUBLIC_STOP_REASON_UNKNOWN')
+      OVER (
+        PARTITION BY uid, job_name, job_id
+        ORDER BY uid, job_name, job_id, ts
+      ) AS lead_public_stop_reason
+  FROM _job_states
+  WHERE state != 'CANCELLED'
+)
+SELECT
+  -- Job name is based on whether the tag and/or namespace are present:
+  -- 1. Both tag and namespace are present: @<namespace>@<tag>:<package name>
+  -- 2. Only tag is present:  <tag>:<package name>
+  -- 3. Only namespace is present: @<namespace>@<package name>/<class name>
+  CASE
+    WHEN substr(job_name, 1, 1) = '@'
+      THEN
+        CASE
+          WHEN substr(STR_SPLIT(job_name, '/', 1), 1, 3) = 'com' THEN STR_SPLIT(job_name, '/', 1)
+          ELSE STR_SPLIT(STR_SPLIT(job_name, '/', 0), '@', 2)
+          END
+    ELSE STR_SPLIT(job_name, '/', 0)
+    END AS package_name,
+  CASE
+    WHEN substr(job_name, 1, 1) = '@' THEN STR_SPLIT(job_name, '@', 1)
+    ELSE STR_SPLIT(job_name, '/', 1)
+    END AS job_namespace,
+  ts_lead - ts AS dur,
+  IIF(lead_state = 'SCHEDULED', TRUE, FALSE) AS is_rescheduled,
+  *
+FROM cte
+WHERE
+  is_end_slice = FALSE
+  AND (ts_lead - ts) > 0
+  AND state = 'STARTED'
+  AND lead_state IN ('FINISHED', 'SCHEDULED');
+
+CREATE PERFETTO TABLE _charging_screen_states AS
+SELECT
+  ROW_NUMBER() OVER () AS id,
+  ii.ts,
+  ii.dur,
+  c.charging_state,
+  s.screen_state
+FROM _interval_intersect!(
+  (android_charging_states, _screen_states),
+  ()
+) ii
+JOIN android_charging_states c ON c.id = ii.id_0
+JOIN _screen_states s ON s.id = ii.id_1;
+
+-- This table returns constraint changes that a
+-- job will go through in a single trace.
+--
+-- Values in this table are derived from the the `ScheduledJobStateChanged`
+-- atom. This table differs from the
+-- `android_job_scheduler_with_screen_charging_states` in this module
+-- (`android.job_scheduler_states`) by only having job constraint information.
+--
+-- See documentation for the `android_job_scheduler_with_screen_charging_states`
+-- for how tables in this module differ from `android_job_scheduler_events`
+-- table in the `android.job_scheduler` module and how to populate this table.
+CREATE PERFETTO TABLE android_job_scheduler_states(
+  -- Unique identifier for row.
+  id INT,
+  -- Timestamp of job state slice.
+  ts INT,
+  -- Duration of job state slice.
+  dur INT,
+  -- Id of the slice.
+  slice_id INT,
+  -- Name of the job (as named by the app).
+  job_name STRING,
+  -- Uid associated with job.
+  uid INT,
+  -- Id of job (assigned by app for T- builds and system generated in U+
+  -- builds).
+  job_id INT,
+  -- Package that the job belongs (ex: associated app).
+  package_name STRING,
+  -- Namespace of job.
+  job_namespace STRING,
+  -- Priority at which JobScheduler ran the job.
+  effective_priority INT,
+  -- True if app requested job should run when the device battery is not low.
+  has_battery_not_low_constraint BOOL,
+  -- True if app requested job should run when the device is charging.
+  has_charging_constraint BOOL,
+  -- True if app requested job should run when device has connectivity.
+  has_connectivity_constraint BOOL,
+  -- True if app requested job should run when there is a content trigger.
+  has_content_trigger_constraint BOOL,
+  -- True if app requested there is a deadline by which the job should run.
+  has_deadline_constraint BOOL,
+  -- True if app requested job should run when device is idle.
+  has_idle_constraint BOOL,
+  -- True if app requested job should run when device storage is not low.
+  has_storage_not_low_constraint BOOL,
+  -- True if app requested job has a timing delay.
+  has_timing_delay_constraint BOOL,
+  -- True if app requested job should run within hours of app launch.
+  is_prefetch BOOL,
+  -- True if app requested that the job is run as an expedited job.
+  is_requested_expedited_job BOOL,
+  -- The job is run as an expedited job.
+  is_running_as_expedited_job BOOL,
+  -- Number of previous attempts at running job.
+  num_previous_attempts INT,
+  -- The requested priority at which the job should run.
+  requested_priority INT,
+  -- The job's standby bucket (one of: Active, Working Set, Frequent, Rare,
+  -- Never, Restricted, Exempt).
+  standby_bucket STRING,
+  -- Job should run in intervals.
+  is_periodic BOOL,
+  -- True if the job should run as a flex job.
+  has_flex_constraint BOOL,
+  -- True is app has requested that a job be run as a user initiated job.
+  is_requested_as_user_initiated_job BOOL,
+  -- True if job is running as a user initiated job.
+  is_running_as_user_initiated_job BOOL,
+  -- Deadline that job has requested and valid if has_deadline_constraint is
+  -- true.
+  deadline_ms INT,
+  -- The latency in ms between when a job is scheduled and when it actually
+  -- starts.
+  job_start_latency_ms INT,
+  -- Number of uncompleted job work items.
+  num_uncompleted_work_items INT,
+  -- Process state of the process responsible for running the job.
+  proc_state STRING,
+  -- Internal stop reason for a job.
+  internal_stop_reason STRING,
+  -- Public stop reason for a job.
+  public_stop_reason STRING
+
+) AS
+SELECT
+  ROW_NUMBER() OVER (ORDER BY ts) AS id,
+  ts,
+  dur,
+  slice_id,
+  job_name,
+  uid,
+  job_id,
+  package_name,
+  job_namespace,
+  effective_priority,
+  has_battery_not_low_constraint,
+  has_charging_constraint,
+  has_connectivity_constraint,
+  has_content_trigger_constraint,
+  has_deadline_constraint,
+  has_idle_constraint,
+  has_storage_not_low_constraint,
+  has_timing_delay_constraint,
+  is_prefetch,
+  is_requested_expedited_job,
+  is_running_as_expedited_job,
+  num_previous_attempts,
+  requested_priority,
+  standby_bucket,
+  is_periodic,
+  has_flex_constraint,
+  is_requested_as_user_initiated_job,
+  is_running_as_user_initiated_job,
+  deadline_ms,
+  job_start_latency_ms,
+  num_uncompleted_work_items,
+  proc_state,
+  lead_internal_stop_reason AS internal_stop_reason,
+  lead_public_stop_reason AS public_stop_reason
+FROM _job_started;
+
+-- This table returns the constraint, charging,
+-- and screen state changes that a job will go through
+-- in a single trace.
+--
+-- Values from this table are derived from
+-- the `ScheduledJobStateChanged` atom. This differs from the
+-- `android_job_scheduler_events` table in the `android.job_scheduler` module
+-- which is derived from ATrace the system server category
+-- (`atrace_categories: "ss"`).
+--
+-- This also differs from the `android_job_scheduler_states` in this module
+-- (`android.job_scheduler_states`) by providing charging and screen state
+-- changes.
+--
+-- To populate this table, enable the Statsd Tracing Config with the
+-- ATOM_SCHEDULED_JOB_STATE_CHANGED push atom id.
+-- https://perfetto.dev/docs/reference/trace-config-proto#StatsdTracingConfig
+--
+-- This table is preferred over `android_job_scheduler_events`
+-- since it contains more information and should be used whenever
+-- `ATOM_SCHEDULED_JOB_STATE_CHANGED` is available in a trace.
+CREATE PERFETTO TABLE android_job_scheduler_with_screen_charging_states(
+  -- Timestamp of job.
+  ts INT,
+  -- Duration of slice in ns.
+  dur INT,
+  -- Id of the slice.
+  slice_id INT,
+  -- Name of the job (as named by the app).
+  job_name STRING,
+  -- Id of job (assigned by app for T- builds and system generated in U+
+  -- builds).
+  job_id INT,
+  -- Uid associated with job.
+  uid INT,
+  -- Duration of entire job in ns.
+  job_dur INT,
+  -- Package that the job belongs (ex: associated app).
+  package_name STRING,
+  -- Namespace of job.
+  job_namespace STRING,
+  -- Device charging state during job (one of: Charging, Discharging, Not charging,
+  -- Full, Unknown).
+  charging_state STRING,
+  -- Device screen state during job (one of: Screen off, Screen on, Always-on display
+  -- (doze), Unknown).
+  screen_state STRING,
+  -- Priority at which JobScheduler ran the job.
+  effective_priority INT,
+  -- True if app requested job should run when the device battery is not low.
+  has_battery_not_low_constraint BOOL,
+  -- True if app requested job should run when the device is charging.
+  has_charging_constraint BOOL,
+  -- True if app requested job should run when device has connectivity.
+  has_connectivity_constraint BOOL,
+  -- True if app requested job should run when there is a content trigger.
+  has_content_trigger_constraint BOOL,
+  -- True if app requested there is a deadline by which the job should run.
+  has_deadline_constraint BOOL,
+  -- True if app requested job should run when device is idle.
+  has_idle_constraint BOOL,
+  -- True if app requested job should run when device storage is not low.
+  has_storage_not_low_constraint BOOL,
+  -- True if app requested job has a timing delay.
+  has_timing_delay_constraint BOOL,
+  -- True if app requested job should run within hours of app launch.
+  is_prefetch BOOL,
+  -- True if app requested that the job is run as an expedited job.
+  is_requested_expedited_job BOOL,
+  -- The job is run as an expedited job.
+  is_running_as_expedited_job BOOL,
+  -- Number of previous attempts at running job.
+  num_previous_attempts INT,
+  -- The requested priority at which the job should run.
+  requested_priority INT,
+  -- The job's standby bucket (one of: Active, Working Set, Frequent, Rare,
+  -- Never, Restricted, Exempt).
+  standby_bucket STRING,
+  -- Job should run in intervals.
+  is_periodic BOOL,
+  -- True if the job should run as a flex job.
+  has_flex_constraint BOOL,
+  -- True is app has requested that a job be run as a user initiated job.
+  is_requested_as_user_initiated_job BOOL,
+  -- True if job is running as a user initiated job.
+  is_running_as_user_initiated_job BOOL,
+  -- Deadline that job has requested and valid if has_deadline_constraint is
+  -- true.
+  deadline_ms INT,
+  -- The latency in ms between when a job is scheduled and when it actually
+  -- starts.
+  job_start_latency_ms INT,
+  -- Number of uncompleted job work items.
+  num_uncompleted_work_items INT,
+  -- Process state of the process responsible for running the job.
+  proc_state STRING,
+  -- Internal stop reason for a job.
+  internal_stop_reason STRING,
+  -- Public stop reason for a job.
+  public_stop_reason STRING
+) AS
+SELECT
+  ii.ts,
+  ii.dur,
+  js.slice_id,
+  js.job_name || '_' || js.job_id AS job_name,
+  js.uid,
+  js.job_id,
+  js.dur AS job_dur,
+  js.package_name,
+  js.job_namespace,
+  c.charging_state,
+  c.screen_state,
+  js.effective_priority,
+  js.has_battery_not_low_constraint,
+  js.has_charging_constraint,
+  js.has_connectivity_constraint,
+  js.has_content_trigger_constraint,
+  js.has_deadline_constraint,
+  js.has_idle_constraint,
+  js.has_storage_not_low_constraint,
+  js.has_timing_delay_constraint,
+  js.is_prefetch,
+  js.is_requested_expedited_job,
+  js.is_running_as_expedited_job,
+  js.num_previous_attempts,
+  js.requested_priority,
+  js.standby_bucket,
+  js.is_periodic,
+  js.has_flex_constraint,
+  js.is_requested_as_user_initiated_job,
+  js.is_running_as_user_initiated_job,
+  js.deadline_ms,
+  js.job_start_latency_ms,
+  js.num_uncompleted_work_items,
+  js.proc_state,
+  js.internal_stop_reason,
+  js.public_stop_reason
+  FROM _interval_intersect!(
+        (_charging_screen_states,
+        android_job_scheduler_states),
+        ()
+      ) ii
+  JOIN _charging_screen_states c ON c.id = ii.id_0
+  JOIN android_job_scheduler_states js ON js.id = ii.id_1;
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/memory/dmabuf.sql b/src/trace_processor/perfetto_sql/stdlib/android/memory/dmabuf.sql
index 1576e9c..afc19b4 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/memory/dmabuf.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/memory/dmabuf.sql
@@ -13,9 +13,6 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 
-INCLUDE PERFETTO MODULE android.binder;
-INCLUDE PERFETTO MODULE slices.with_context;
-
 -- Raw ftrace events
 CREATE PERFETTO TABLE _raw_dmabuf_events AS
 SELECT
@@ -29,14 +26,22 @@
 
 -- gralloc binder reply slices
 CREATE PERFETTO TABLE _gralloc_binders AS
+WITH gralloc_threads AS (
+  SELECT utid
+  FROM process JOIN thread USING (upid)
+  WHERE process.name GLOB '/vendor/bin/hw/android.hardware.graphics.allocator*'
+)
 SELECT
-  id AS gralloc_binder_reply_id,
-  utid,
-  ts,
-  dur
-FROM thread_slice
-WHERE process_name GLOB '/vendor/bin/hw/android.hardware.graphics.allocator*'
-AND name = 'binder reply';
+  flow.slice_out AS client_slice_id,
+  gralloc_slice.ts,
+  gralloc_slice.dur,
+  thread_track.utid
+FROM slice gralloc_slice
+JOIN thread_track ON gralloc_slice.track_id = thread_track.id
+JOIN gralloc_threads USING (utid)
+JOIN flow ON gralloc_slice.id = flow.slice_in
+WHERE gralloc_slice.name = 'binder reply'
+;
 
 -- Match gralloc thread allocations to inbound binders
 CREATE PERFETTO TABLE _attributed_dmabufs AS
@@ -44,10 +49,10 @@
   r.inode,
   r.ts,
   r.buf_size,
-  IFNULL(b.client_utid, r.utid) AS attr_utid
+  IFNULL(client_thread.utid, r.utid) AS attr_utid
 FROM _raw_dmabuf_events r
 LEFT JOIN _gralloc_binders gb ON r.utid = gb.utid AND r.ts BETWEEN gb.ts AND gb.ts + gb.dur
-LEFT JOIN android_binder_txns b ON gb.gralloc_binder_reply_id = b.binder_reply_id
+LEFT JOIN thread_track client_thread ON gb.client_slice_id = client_thread.id
 ORDER BY r.inode, r.ts;
 
 CREATE PERFETTO FUNCTION _alloc_source(is_alloc BOOL, inode INT, ts INT)
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/BUILD.gn
index 2c86bfe..4ecaeb0 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/BUILD.gn
@@ -16,6 +16,7 @@
 
 perfetto_sql_source_set("heap_graph") {
   sources = [
+    "class_summary_tree.sql",
     "class_tree.sql",
     "dominator_class_tree.sql",
     "dominator_tree.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/class_summary_tree.sql b/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/class_summary_tree.sql
new file mode 100644
index 0000000..9c9d5bf
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/class_summary_tree.sql
@@ -0,0 +1,98 @@
+--
+-- 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.memory.heap_graph.class_tree;
+INCLUDE PERFETTO MODULE graphs.scan;
+
+CREATE PERFETTO TABLE _heap_graph_class_tree_cumulatives AS
+SELECT *
+FROM _graph_aggregating_scan!(
+  (
+    SELECT id AS source_node_id, parent_id AS dest_node_id
+    FROM _heap_graph_class_tree
+    WHERE parent_id IS NOT NULL
+  ),
+  (
+    SELECT
+      p.id,
+      p.self_count AS cumulative_count,
+      p.self_size AS cumulative_size
+    FROM _heap_graph_class_tree p
+    LEFT JOIN _heap_graph_class_tree c ON c.parent_id = p.id
+    WHERE c.id IS NULL
+  ),
+  (cumulative_count, cumulative_size),
+  (
+    WITH agg AS (
+      SELECT
+        t.id,
+        SUM(t.cumulative_count) AS child_count,
+        SUM(t.cumulative_size) AS child_size
+      FROM $table t
+      GROUP BY t.id
+    )
+    SELECT
+      a.id,
+      a.child_count + r.self_count as cumulative_count,
+      a.child_size + r.self_size as cumulative_size
+    FROM agg a
+    JOIN _heap_graph_class_tree r USING (id)
+  )
+) a
+ORDER BY id;
+
+-- Table containing all the Android heap graphs in the trace converted to a
+-- shortest-path tree and then aggregated by class name.
+--
+-- This table contains a "flamegraph-like" representation of the contents of the
+-- heap graph.
+CREATE PERFETTO TABLE android_heap_graph_class_summary_tree(
+  -- The timestamp the heap graph was dumped at.
+  graph_sample_ts INT,
+  -- The upid of the process.
+  upid INT,
+  -- The id of the node in the class tree.
+  id INT,
+  -- The parent id of the node in the class tree or NULL if this is the root.
+  parent_id INT,
+  -- The name of the class.
+  name STRING,
+  -- A string describing the type of Java root if this node is a root or NULL
+  -- if this node is not a root.
+  root_type STRING,
+  -- The count of objects with the same class name and the same path to the
+  -- root.
+  self_count INT,
+  -- The size of objects with the same class name and the same path to the
+  -- root.
+  self_size INT,
+  -- The sum of `self_count` of this node and all descendants of this node.
+  cumulative_count INT,
+  -- The sum of `self_size` of this node and all descendants of this node.
+  cumulative_size INT
+) AS
+SELECT
+  t.graph_sample_ts,
+  t.upid,
+  t.id,
+  t.parent_id,
+  t.name,
+  t.root_type,
+  t.self_count,
+  t.self_size,
+  c.cumulative_count,
+  c.cumulative_size
+FROM _heap_graph_class_tree t
+JOIN _heap_graph_class_tree_cumulatives c USING (id);
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/heap_graph_class_aggregation.sql b/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/heap_graph_class_aggregation.sql
index fd0494e..8b7b7c6 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/heap_graph_class_aggregation.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/heap_graph_class_aggregation.sql
@@ -28,14 +28,14 @@
 RETURNS BOOL AS
 SELECT ($obj_name GLOB 'java.*' AND NOT $obj_name GLOB 'java.lang.Class<*>')
   OR $obj_name GLOB 'j$.*'
-  OR $obj_name GLOB 'int[*'
-  OR $obj_name GLOB 'long[*'
-  OR $obj_name GLOB 'byte[*'
-  OR $obj_name GLOB 'char[*'
-  OR $obj_name GLOB 'short[*'
-  OR $obj_name GLOB 'float[*'
-  OR $obj_name GLOB 'double[*'
-  OR $obj_name GLOB 'boolean[*'
+  OR $obj_name GLOB 'int[[]*'
+  OR $obj_name GLOB 'long[[]*'
+  OR $obj_name GLOB 'byte[[]*'
+  OR $obj_name GLOB 'char[[]*'
+  OR $obj_name GLOB 'short[[]*'
+  OR $obj_name GLOB 'float[[]*'
+  OR $obj_name GLOB 'double[[]*'
+  OR $obj_name GLOB 'boolean[[]*'
   OR $obj_name GLOB 'android.util.*Array*';
 
 CREATE PERFETTO TABLE _heap_graph_dominator_tree_for_partition AS
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_profile/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_profile/BUILD.gn
index daaf9e0..aa317e4 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_profile/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_profile/BUILD.gn
@@ -15,5 +15,8 @@
 import("../../../../../../../gn/perfetto_sql.gni")
 
 perfetto_sql_source_set("heap_profile") {
-  sources = [ "callstacks.sql" ]
+  sources = [
+    "callstacks.sql",
+    "summary_tree.sql",
+  ]
 }
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_profile/summary_tree.sql b/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_profile/summary_tree.sql
new file mode 100644
index 0000000..c9e89c3
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/android/memory/heap_profile/summary_tree.sql
@@ -0,0 +1,123 @@
+
+--
+-- 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 callstacks.stack_profile;
+
+CREATE PERFETTO TABLE _android_heap_profile_raw_callstacks AS
+WITH metrics AS MATERIALIZED (
+  SELECT
+    callsite_id,
+    SUM(size) AS self_size,
+    SUM(MAX(size, 0)) AS self_alloc_size
+  FROM heap_profile_allocation
+  GROUP BY callsite_id
+)
+SELECT
+  c.id,
+  c.parent_id,
+  c.name,
+  c.mapping_name,
+  c.source_file,
+  c.line_number,
+  IFNULL(m.self_size, 0) AS self_size,
+  IFNULL(m.self_alloc_size, 0) AS self_alloc_size
+FROM _callstacks_for_stack_profile_samples!(metrics) c
+LEFT JOIN metrics m USING (callsite_id);
+
+CREATE PERFETTO TABLE _android_heap_profile_cumulatives AS
+SELECT a.*
+FROM _graph_aggregating_scan!(
+  (
+    SELECT id AS source_node_id, parent_id AS dest_node_id
+    FROM _android_heap_profile_raw_callstacks
+    WHERE parent_id IS NOT NULL
+  ),
+  (
+    SELECT
+      p.id,
+      p.self_size AS cumulative_size,
+      p.self_alloc_size AS cumulative_alloc_size
+    FROM _android_heap_profile_raw_callstacks p
+    LEFT JOIN _android_heap_profile_raw_callstacks c ON c.parent_id = p.id
+    WHERE c.id IS NULL
+  ),
+  (cumulative_size, cumulative_alloc_size),
+  (
+    WITH agg AS (
+      SELECT
+        t.id,
+        SUM(t.cumulative_size) AS child_size,
+        SUM(t.cumulative_alloc_size) AS child_alloc_size
+      FROM $table t
+      GROUP BY t.id
+    )
+    SELECT
+      a.id,
+      a.child_size + r.self_size as cumulative_size,
+      a.child_alloc_size + r.self_alloc_size AS cumulative_alloc_size
+    FROM agg a
+    JOIN _android_heap_profile_raw_callstacks r USING (id)
+  )
+) a;
+
+-- Table summarising the amount of memory allocated by each
+-- callstack as seen by Android native heap profiling (i.e.
+-- profiling information collected by heapprofd).
+--
+-- Note: this table collapses data from all processes together
+-- into a single table.
+CREATE PERFETTO TABLE android_heap_profile_summary_tree(
+  -- The id of the callstack. A callstack in this context
+  -- is a unique set of frames up to the root.
+  id INT,
+  -- The id of the parent callstack for this callstack.
+  parent_id INT,
+  -- 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, JAR or APK.
+  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 INT,
+  -- The amount of memory allocated and *not freed* with this
+  -- function as the leaf frame.
+  self_size INT,
+  -- The amount of memory allocated and *not freed* with this
+  -- function appearing anywhere on the callstack.
+  cumulative_size INT,
+  -- The amount of memory allocated with this function as the leaf
+  -- frame. This may include memory which was later freed.
+  self_alloc_size INT,
+  -- The amount of memory allocated with this function appearing
+  -- anywhere on the callstack. This may include memory which was
+  -- later freed.
+  cumulative_alloc_size INT
+) AS
+SELECT
+  id,
+  parent_id,
+  name,
+  mapping_name,
+  source_file,
+  line_number,
+  self_size,
+  cumulative_size,
+  self_alloc_size,
+  cumulative_alloc_size
+FROM _android_heap_profile_raw_callstacks r
+JOIN _android_heap_profile_cumulatives a USING (id);
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/monitor_contention.sql b/src/trace_processor/perfetto_sql/stdlib/android/monitor_contention.sql
index c7ad3bf..d1d5b02 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/monitor_contention.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/monitor_contention.sql
@@ -353,44 +353,40 @@
 SELECT utid AS blocking_utid, ts, dur, state, blocked_function
 FROM thread_state;
 
--- Contains the span join of the first waiters in the |android_monitor_contention_chain| with their
--- blocking_thread thread state.
-
--- Note that we only span join the duration where the lock was actually held and contended.
--- This can be less than the duration the lock was 'waited on' when a different waiter acquired the
--- lock earlier than the first waiter.
---
--- @column parent_id Id of slice blocking the blocking_thread.
--- @column blocking_method Name of the method holding the lock.
--- @column blocked_methhod Name of the method trying to acquire the lock.
--- @column short_blocking_method Blocking_method without arguments and return types.
--- @column short_blocked_method Blocked_method without arguments and return types.
--- @column blocking_src File location of blocking_method in form <filename:linenumber>.
--- @column blocked_src File location of blocked_method in form <filename:linenumber>.
--- @column waiter_count Zero indexed number of threads trying to acquire the lock.
--- @column blocking_utid Utid of thread holding the lock.
--- @column blocking_thread_name Thread name of thread holding the lock.
--- @column upid Upid of process experiencing lock contention.
--- @column process_name Process name of process experiencing lock contention.
--- @column id Slice id of lock contention.
--- @column ts Timestamp of lock contention start.
--- @column dur Wall clock duration of lock contention.
--- @column monotonic_dur Monotonic clock duration of lock contention.
--- @column track_id Thread track id of blocked thread.
--- @column is_blocked_main_thread Whether the blocked thread is the main thread.
--- @column is_blocking_main_thread Whether the blocking thread is the main thread.
--- @column binder_reply_id Slice id of binder reply slice if lock contention was part of a binder txn.
--- @column binder_reply_ts Timestamp of binder reply slice if lock contention was part of a binder txn.
--- @column binder_reply_tid Tid of binder reply slice if lock contention was part of a binder txn.
--- @column blocking_utid Utid of the blocking |thread_state|.
--- @column ts Timestamp of the blocking |thread_state|.
--- @column state Thread state of the blocking thread.
--- @column blocked_function Blocked kernel function of the blocking thread.
-CREATE VIRTUAL TABLE android_monitor_contention_chain_thread_state
+CREATE VIRTUAL TABLE _android_monitor_contention_chain_thread_state
 USING
   SPAN_JOIN(_first_blocked_contention PARTITIONED blocking_utid,
             _blocking_thread_state PARTITIONED blocking_utid);
 
+-- Contains the span join of the first waiters in the |android_monitor_contention_chain| with their
+-- blocking_thread thread state.
+--
+-- Note that we only span join the duration where the lock was actually held and contended.
+-- This can be less than the duration the lock was 'waited on' when a different waiter acquired the
+-- lock earlier than the first waiter.
+CREATE PERFETTO TABLE android_monitor_contention_chain_thread_state(
+-- Slice id of lock contention.
+id INT,
+-- Timestamp of lock contention start.
+ts INT,
+-- Wall clock duration of lock contention.
+dur INT,
+-- Utid of the blocking |thread_state|.
+blocking_utid INT,
+-- Blocked kernel function of the blocking thread.
+blocked_function STRING,
+-- Thread state of the blocking thread.
+state STRING
+) AS
+SELECT
+  id,
+  ts,
+  dur,
+  blocking_utid,
+  blocked_function,
+  state
+FROM _android_monitor_contention_chain_thread_state;
+
 -- Aggregated thread_states on the 'blocking thread', the thread holding the lock.
 -- This builds on the data from |android_monitor_contention_chain| and
 -- for each contention slice, it returns the aggregated sum of all the thread states on the
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/startup/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/android/startup/BUILD.gn
index c495f08..bf7a41a 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/startup/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/android/startup/BUILD.gn
@@ -16,6 +16,7 @@
 
 perfetto_sql_source_set("startup") {
   sources = [
+    "startup_breakdowns.sql",
     "startup_events.sql",
     "startups.sql",
     "startups_maxsdk28.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/startup/startup_breakdowns.sql b/src/trace_processor/perfetto_sql/stdlib/android/startup/startup_breakdowns.sql
new file mode 100644
index 0000000..c7af07b
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/android/startup/startup_breakdowns.sql
@@ -0,0 +1,188 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+INCLUDE PERFETTO MODULE android.startup.startups;
+INCLUDE PERFETTO MODULE intervals.overlap;
+INCLUDE PERFETTO MODULE slices.hierarchy;
+INCLUDE PERFETTO MODULE slices.with_context;
+
+-- Maps slice names with common prefixes to a static string key.
+-- Returns NULL if there's no mapping.
+CREATE PERFETTO FUNCTION _normalize_android_string(name STRING)
+RETURNS STRING
+AS
+SELECT
+  CASE
+    WHEN $name = 'mm_vmscan_direct_reclaim' THEN 'kernel_memory_reclaim'
+    WHEN $name GLOB 'GC: Wait For*' THEN 'userspace_memory_reclaim'
+    WHEN ($name GLOB 'monitor contention*' OR $name GLOB 'Lock contention on a monitor lock*')
+      THEN 'monitor_contention'
+    WHEN $name GLOB 'Lock contention*' THEN 'art_lock_contention'
+    WHEN ($name = 'binder transaction' OR $name = 'binder reply') THEN 'binder'
+    WHEN $name = 'Contending for pthread mutex' THEN 'mutex_contention'
+    WHEN $name GLOB 'dlopen*' THEN 'dlopen'
+    WHEN $name GLOB 'VerifyClass*' THEN 'verify_class'
+    WHEN $name = 'inflate' THEN 'inflate'
+    WHEN $name GLOB 'Choreographer#doFrame*' THEN 'choreographer_do_frame'
+    WHEN $name GLOB 'OpenDexFilesFromOat*' THEN 'open_dex_files_from_oat'
+    WHEN $name = 'ResourcesManager#getResources' THEN 'resources_manager_get_resources'
+    WHEN $name = 'bindApplication' THEN 'bind_application'
+    WHEN $name = 'activityStart' THEN 'activity_start'
+    WHEN $name = 'activityResume' THEN 'activity_resume'
+    WHEN $name = 'activityRestart' THEN 'activity_restart'
+    WHEN $name = 'clientTransactionExecuted' THEN 'client_transaction_executed'
+    ELSE NULL
+    END name;
+
+-- Derives a startup reason from a slice name and some thread_state columns.
+CREATE PERFETTO FUNCTION _startup_breakdown_reason(
+  name STRING,
+  state STRING,
+  io_wait INT,
+  irq_context INT)
+RETURNS STRING
+AS
+SELECT
+  CASE
+    WHEN $io_wait = 1 THEN 'io'
+    WHEN $name IS NOT NULL THEN $name
+    WHEN $irq_context = 1 THEN 'irq'
+    ELSE $state
+    END name;
+
+-- List of startups with unique ids for each possible upid. The existing
+-- startup_ids are not necessarily unique (because of multiuser).
+CREATE PERFETTO TABLE _startup_root_slices
+AS
+SELECT
+  (SELECT MAX(id) FROM slice) + row_number() OVER () AS id,
+  android_startups.dur AS dur,
+  android_startups.ts AS ts,
+  android_startups.startup_id,
+  android_startups.startup_type,
+  process.name AS process_name,
+  thread.utid AS utid
+FROM android_startup_processes startup
+JOIN android_startups
+  USING (startup_id)
+JOIN thread
+  ON thread.upid = process.upid AND thread.is_main_thread
+JOIN process
+  ON process.upid = startup.upid
+WHERE android_startups.dur > 0
+ORDER BY ts;
+
+-- All relevant startup slices normalized with _normalize_android_string.
+CREATE PERFETTO TABLE _startup_normalized_slices
+AS
+WITH
+  relevant_startup_slices AS (
+    SELECT slice.*
+    FROM thread_slice slice
+    JOIN _startup_root_slices startup
+      ON
+        slice.utid = startup.utid
+        -- Inline the logic to check whether startup intervals overlap
+        -- with main thread slices. This is to improve performance until
+        -- interval_intersect doesn't require a JOIN.
+        -- TODO(zezeozue): Replace with interval intersect when JOINs are
+        -- not required.
+        AND MAX(slice.ts, startup.ts) < MIN(slice.ts + slice.dur, startup.ts + startup.dur)
+  )
+SELECT p.id, p.parent_id, p.depth, p.name, thread_slice.ts, thread_slice.dur, thread_slice.utid
+FROM
+  _slice_remove_nulls_and_reparent
+    !(
+      (
+        SELECT id, parent_id, depth, _normalize_android_string(name) AS name
+        FROM relevant_startup_slices
+        WHERE dur > 0
+      ),
+      name)
+      p
+JOIN thread_slice
+  USING (id);
+
+-- Subset of _startup_normalized_slices that occurred during any app startups on the main thread.
+-- Their timestamps and durations are chopped to fit within the respective app startup duration.
+CREATE PERFETTO TABLE _startup_slices_breakdown
+AS
+SELECT *
+FROM _intervals_merge_root_and_children_by_intersection !(_startup_root_slices, _startup_normalized_slices, utid);
+
+-- Flattened slice version of _startup_slices_breakdown. This selects the leaf slice at every region
+-- of the slice stack.
+CREATE PERFETTO TABLE _startup_flat_slices_breakdown
+AS
+SELECT i.ts, i.dur, i.root_id, s.id AS slice_id, s.name FROM _intervals_flatten !(_startup_slices_breakdown) i
+JOIN _startup_normalized_slices s USING (id);
+
+-- Subset of thread_states that occurred during any app startups on the main thread.
+CREATE PERFETTO TABLE _startup_thread_states_breakdown
+AS
+SELECT i.ts, i.dur, i.root_id, t.id AS thread_state_id, t.state, t.io_wait, t.irq_context
+  FROM _intervals_merge_root_and_children_by_intersection!(_startup_root_slices,
+                                                           (SELECT *, NULL AS parent_id FROM thread_state),
+                                                           utid) i
+JOIN thread_state t USING(id);
+
+-- Intersection of _startup_flat_slices_breakdown and _startup_thread_states_breakdown.
+-- A left intersection is used since some parts of the slice stack may not have any slices
+-- but will have thread states.
+CREATE VIRTUAL TABLE _startup_thread_states_and_slices_breakdown_sp
+USING
+  SPAN_LEFT_JOIN(
+    _startup_thread_states_breakdown PARTITIONED root_id,
+    _startup_flat_slices_breakdown PARTITIONED root_id);
+
+-- Blended thread state and slice breakdown blocking app startups.
+--
+-- Each row blames a unique period during an app startup with a reason
+-- derived from the slices and thread states on the main thread.
+--
+-- Some helpful events to enables are binder transactions, ART, am and view.
+CREATE PERFETTO TABLE android_startup_opinionated_breakdown(
+  -- Startup id. Alias of `slice.id`
+  startup_id INT,
+  -- Id of relevant slice blocking startup. Alias of `slice.id`.
+  slice_id INT,
+  -- Id of thread_state blocking startup. Alias of `thread_state.id`.
+  thread_state_id INT,
+  -- Timestamp of an exclusive interval during the app startup with a single latency reason.
+  ts INT,
+  -- Duration of an exclusive interval during the app startup with a single latency reason.
+  dur INT,
+  -- Cause of delay during an exclusive interval of the app startup.
+  reason STRING
+)
+AS
+SELECT b.ts, b.dur, startup.startup_id, b.slice_id, b.thread_state_id, _startup_breakdown_reason(name, state, io_wait, irq_context) AS reason
+FROM _startup_thread_states_and_slices_breakdown_sp b
+JOIN _startup_root_slices startup ON startup.id = b.root_id
+UNION ALL
+-- Augment the existing startup breakdown with an artificial slice accounting for
+-- any launch delays before the app starts handling startup on its main thread
+SELECT
+  _startup_root_slices.ts,
+  MIN(_startup_thread_states_breakdown.ts) - _startup_root_slices.ts AS dur,
+  startup_id,
+  NULL AS slice_id,
+  NULL AS thread_state_id,
+  'launch_delay' AS reason
+FROM _startup_thread_states_breakdown
+JOIN _startup_root_slices
+  ON _startup_root_slices.id = root_id
+GROUP BY root_id
+HAVING MIN(_startup_thread_states_breakdown.ts) - _startup_root_slices.ts > 0;
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/startup/startups.sql b/src/trace_processor/perfetto_sql/stdlib/android/startup/startups.sql
index a715f52..2bba728 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/startup/startups.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/startup/startups.sql
@@ -96,6 +96,8 @@
   startup_id INT,
   -- Upid of process on which activity started.
   upid INT,
+  -- Pid of process on which activity started.
+  pid INT,
   -- Type of the startup.
   startup_type STRING
 ) AS
@@ -105,6 +107,7 @@
   SELECT
     startup_id,
     upid,
+    pid,
     CASE
       -- type parsed from platform event takes precedence if available
       WHEN startup_type IS NOT NULL THEN startup_type
@@ -118,6 +121,7 @@
       l.startup_id,
       l.startup_type,
       p.upid,
+      p.pid,
       _startup_indicator_slice_count(l.ts, l.ts_end, t.utid, 'bindApplication') AS bind_app,
       _startup_indicator_slice_count(l.ts, l.ts_end, t.utid, 'activityStart') AS a_start,
       _startup_indicator_slice_count(l.ts, l.ts_end, t.utid, 'activityResume') AS a_resume
@@ -153,8 +157,12 @@
   dur INT,
   -- Upid of process involved in startup.
   upid INT,
+  -- Pid if process involved in startup.
+  pid INT,
   -- Utid of the thread.
   utid INT,
+  -- Tid of the thread.
+  tid INT,
   -- Name of the thread.
   thread_name STRING,
   -- Thread is a main thread.
@@ -165,7 +173,9 @@
   startups.ts,
   startups.dur,
   android_startup_processes.upid,
+  android_startup_processes.pid,
   thread.utid,
+  thread.tid,
   thread.name AS thread_name,
   thread.is_main_thread AS is_main_thread
 FROM android_startups startups
@@ -189,6 +199,8 @@
   startup_id INT,
   -- UTID of thread with slice.
   utid INT,
+  --Tid of thread.
+  tid INT,
   -- Name of thread.
   thread_name STRING,
   -- Whether it is main thread.
@@ -209,6 +221,7 @@
   st.ts + st.dur AS startup_ts_end,
   st.startup_id,
   st.utid,
+  st.tid,
   st.thread_name,
   st.is_main_thread,
   slice.arg_set_id,
@@ -238,10 +251,12 @@
   slice_dur INT,
   -- Name of the thread with the slice.
   thread_name STRING,
+  -- Tid of the thread with the slice.
+  tid  INT,
   -- Arg set id.
   arg_set_id INT
 ) AS
-SELECT slice_id, slice_name, slice_ts, slice_dur, thread_name, arg_set_id
+SELECT slice_id, slice_name, slice_ts, slice_dur, thread_name, tid, arg_set_id
 FROM android_thread_slices_for_all_startups
 WHERE startup_id = $startup_id AND slice_name GLOB $slice_name;
 
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/startup/startups_minsdk33.sql b/src/trace_processor/perfetto_sql/stdlib/android/startup/startups_minsdk33.sql
index 2378473..b96b53a 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/startup/startups_minsdk33.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/startup/startups_minsdk33.sql
@@ -45,6 +45,7 @@
     -- Originally completed was unqualified, but at some point we introduced
     -- the startup type as well
     AND name GLOB 'launchingActivity#*:completed*:*'
+    AND NOT name GLOB '*:completed-same-process:*'
 )
 GROUP BY 1, 2, 3;
 
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql b/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql
index 1551ea6..8d2c88c 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql
@@ -49,11 +49,11 @@
     AND NOT EXISTS(SELECT * FROM suspend_slice_from_minimal)
 ),
 awake_slice AS (
-  -- If we don't have any rows, use the trace bounds.
+  -- If we don't have any rows, use the trace bounds if bounds are defined.
   SELECT
     trace_start() AS ts,
     trace_dur() AS dur
-  WHERE (SELECT COUNT(*) FROM suspend_slice) = 0
+  WHERE (SELECT COUNT(*) FROM suspend_slice) = 0 AND dur > 0
   UNION ALL
   -- If we do have rows, create one slice from the trace start to the first suspend.
   SELECT
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/winscope/inputmethod.sql b/src/trace_processor/perfetto_sql/stdlib/android/winscope/inputmethod.sql
index 15a4a8e..afd234b 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/winscope/inputmethod.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/winscope/inputmethod.sql
@@ -20,12 +20,18 @@
   -- Timestamp when the dump was triggered
   ts INT,
   -- Extra args parsed from the proto message
-  arg_set_id INT
+  arg_set_id INT,
+  -- Raw proto message encoded in base64
+  base64_proto STRING,
+  -- String id for raw proto message
+  base64_proto_id INT
 ) AS
 SELECT
   id,
   ts,
-  arg_set_id
+  arg_set_id,
+  base64_proto,
+  base64_proto_id
 FROM __intrinsic_inputmethod_clients;
 
 -- Android inputmethod manager service state dumps (from android.inputmethod data source).
@@ -35,12 +41,18 @@
   -- Timestamp when the dump was triggered
   ts INT,
   -- Extra args parsed from the proto message
-  arg_set_id INT
+  arg_set_id INT,
+  -- Raw proto message encoded in base64
+  base64_proto STRING,
+  -- String id for raw proto message
+  base64_proto_id INT
 ) AS
 SELECT
   id,
   ts,
-  arg_set_id
+  arg_set_id,
+  base64_proto,
+  base64_proto_id
 FROM __intrinsic_inputmethod_manager_service;
 
 -- Android inputmethod service state dumps (from android.inputmethod data source).
@@ -50,10 +62,16 @@
   -- Timestamp when the dump was triggered
   ts INT,
   -- Extra args parsed from the proto message
-  arg_set_id INT
+  arg_set_id INT,
+  -- Raw proto message encoded in base64
+  base64_proto STRING,
+  -- String id for raw proto message
+  base64_proto_id INT
 ) AS
 SELECT
   id,
   ts,
-  arg_set_id
+  arg_set_id,
+  base64_proto,
+  base64_proto_id
 FROM __intrinsic_inputmethod_service;
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/winscope/viewcapture.sql b/src/trace_processor/perfetto_sql/stdlib/android/winscope/viewcapture.sql
index 77e68b5..11fb341 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/winscope/viewcapture.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/winscope/viewcapture.sql
@@ -20,10 +20,16 @@
   -- Timestamp when the snapshot was triggered
   ts INT,
   -- Extra args parsed from the proto message
-  arg_set_id INT
+  arg_set_id INT,
+  -- Raw proto message encoded in base64
+  base64_proto STRING,
+  -- String id for raw proto message
+  base64_proto_id INT
 ) AS
 SELECT
   id,
   ts,
-  arg_set_id
+  arg_set_id,
+  base64_proto,
+  base64_proto_id
 FROM __intrinsic_viewcapture;
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/winscope/windowmanager.sql b/src/trace_processor/perfetto_sql/stdlib/android/winscope/windowmanager.sql
index e602125..f8d42da 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/winscope/windowmanager.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/winscope/windowmanager.sql
@@ -20,10 +20,16 @@
   -- Timestamp when the snapshot was triggered
   ts INT,
   -- Extra args parsed from the proto message
-  arg_set_id INT
+  arg_set_id INT,
+  -- Raw proto message encoded in base64
+  base64_proto STRING,
+  -- String id for raw proto message
+  base64_proto_id INT
 ) AS
 SELECT
   id,
   ts,
-  arg_set_id
+  arg_set_id,
+  base64_proto,
+  base64_proto_id
 FROM __intrinsic_windowmanager;
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 702151b..d8bc406 100644
--- a/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
@@ -14,6 +14,7 @@
 -- limitations under the License.
 
 INCLUDE PERFETTO MODULE graphs.hierarchy;
+INCLUDE PERFETTO MODULE graphs.scan;
 
 CREATE PERFETTO TABLE _callstack_spf_summary AS
 SELECT
@@ -68,7 +69,7 @@
   -- significant fraction of the runtime on big traces.
   IFNULL(
     DEMANGLE(COALESCE(s.name, f.deobfuscated_name, f.name)),
-    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,
@@ -112,3 +113,63 @@
   JOIN _callstack_spc_forest f USING (id)
   JOIN stack_profile_mapping m ON f.mapping_id = m.id
 );
+
+CREATE PERFETTO MACRO _callstacks_for_callsites(
+  samples TableOrSubquery
+)
+RETURNS TableOrSubquery
+AS
+(
+  WITH metrics AS MATERIALIZED (
+    SELECT
+      callsite_id,
+      COUNT() AS self_count
+    FROM $samples
+    GROUP BY callsite_id
+  )
+  SELECT
+    c.id,
+    c.parent_id,
+    c.name,
+    c.mapping_name,
+    c.source_file,
+    c.line_number,
+    IFNULL(m.self_count, 0) AS self_count
+  FROM _callstacks_for_stack_profile_samples!(metrics) c
+  LEFT JOIN metrics m USING (callsite_id)
+);
+
+CREATE PERFETTO MACRO _callstacks_self_to_cumulative(
+  callstacks TableOrSubquery
+)
+RETURNS TableOrSubquery
+AS
+(
+  SELECT a.*
+  FROM _graph_aggregating_scan!(
+    (
+      SELECT id AS source_node_id, parent_id AS dest_node_id
+      FROM $callstacks
+      WHERE parent_id IS NOT NULL
+    ),
+    (
+      SELECT p.id, p.self_count AS cumulative_count
+      FROM $callstacks p
+      LEFT JOIN $callstacks c ON c.parent_id = p.id
+      WHERE c.id IS NULL
+    ),
+    (cumulative_count),
+    (
+      WITH agg AS (
+        SELECT t.id, SUM(t.cumulative_count) AS child_count
+        FROM $table t
+        GROUP BY t.id
+      )
+      SELECT
+        a.id,
+        a.child_count + r.self_count as cumulative_count
+      FROM agg a
+      JOIN $callstacks r USING (id)
+    )
+  ) a
+)
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/cpu_powerups.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/cpu_powerups.sql
index 8657ca5..a653903 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/cpu_powerups.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/cpu_powerups.sql
@@ -149,23 +149,33 @@
   WHERE s.depth = 0   -- Top-level slices only.
   ORDER BY ts ASC;
 
--- A table holding the slices that executed within the scheduler
--- slice that ran on a CPU immediately after power-up.
---
--- @column  ts        Timestamp of the resulting slice
--- @column dur        Duration of the slice.
--- @column cpu        The CPU the sched slice ran on.
--- @column utid       Unique thread id for the slice.
--- @column sched_id   'id' field from the sched_slice table.
--- @column type       From the sched_slice table, always 'sched_slice'.
--- @column end_state  The ending state for the sched_slice
--- @column priority   The kernel thread priority
--- @column slice_id   Id of the top-level slice for this (sched) slice.
-CREATE VIRTUAL TABLE chrome_cpu_power_post_powerup_slice
+CREATE VIRTUAL TABLE _chrome_cpu_power_post_powerup_slice_sj
 USING
   SPAN_JOIN(chrome_cpu_power_first_sched_slice_after_powerup PARTITIONED utid,
             _cpu_power_thread_and_toplevel_slice PARTITIONED utid);
 
+-- A table holding the slices that executed within the scheduler
+-- slice that ran on a CPU immediately after power-up.
+CREATE PERFETTO TABLE chrome_cpu_power_post_powerup_slice(
+-- Timestamp of the resulting slice
+ts INT,
+-- Duration of the slice.
+dur INT,
+-- The CPU the sched slice ran on.
+cpu INT,
+-- Unique thread id for the slice.
+utid INT,
+-- 'id' field from the sched_slice table.
+sched_id INT,
+-- Id of the top-level slice for this (sched) slice.
+slice_id INT,
+-- Previous power state.
+previous_power_state LONG,
+-- Id of the powerup.
+powerup_id INT
+) AS
+SELECT * FROM _chrome_cpu_power_post_powerup_slice_sj;
+
 -- The first top-level slice that ran after a CPU power-up.
 CREATE PERFETTO VIEW chrome_cpu_power_first_toplevel_slice_after_powerup(
   -- ID of the slice in the slice table.
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 8e58912..0d704db 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/event_latency.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/event_latency.sql
@@ -2,7 +2,22 @@
 -- Use of this source code is governed by a BSD-style license that can be
 -- found in the LICENSE file.
 
-INCLUDE PERFETTO MODULE deprecated.v42.common.slices;
+-- Finds the start timestamp for a given slice's descendant with a given name.
+-- If there are multiple descendants with a given name, the function will return
+-- the first one, so it's most useful when working with a timeline broken down
+-- into phases, where each subphase can happen only once.
+CREATE PERFETTO FUNCTION _descendant_slice_begin(
+  -- Id of the parent slice.
+  parent_id INT,
+  -- Name of the child with the desired start TS.
+  child_name STRING
+)
+-- Start timestamp of the child or NULL if it doesn't exist.
+RETURNS INT AS
+SELECT s.ts
+FROM descendant_slice($parent_id) s
+WHERE s.name GLOB $child_name
+LIMIT 1;
 
 -- Finds the end timestamp for a given slice's descendant with a given name.
 -- If there are multiple descendants with a given name, the function will return
@@ -25,6 +40,22 @@
 WHERE s.name GLOB $child_name
 LIMIT 1;
 
+-- Checks if slice has a descendant with provided name.
+CREATE PERFETTO FUNCTION _has_descendant_slice_with_name(
+  -- Id of the slice to check descendants of.
+  id INT,
+  -- Name of potential descendant slice.
+  descendant_name STRING
+)
+-- Whether `descendant_name` is a name of an descendant slice.
+RETURNS BOOL AS
+SELECT EXISTS(
+  SELECT 1
+  FROM descendant_slice($id)
+  WHERE name = $descendant_name
+  LIMIT 1
+);
+
 -- Returns the presentation timestamp for a given EventLatency slice.
 -- This is either the end of
 -- SwapEndToPresentationCompositorFrame (if it exists),
@@ -59,7 +90,24 @@
   -- EventLatency event type.
   event_type STRING,
   -- Perfetto track this slice is found on.
-  track_id INT
+  track_id INT,
+  -- Vsync interval (in milliseconds).
+  vsync_interval_ms DOUBLE,
+  -- Whether the corresponding frame is janky.
+  is_janky_scrolled_frame BOOL,
+  -- Timestamp of the BufferAvailableToBufferReady substage.
+  buffer_available_timestamp INT,
+  -- Timestamp of the BufferReadyToLatch substage.
+  buffer_ready_timestamp INT,
+  -- Timestamp of the LatchToSwapEnd substage.
+  latch_timestamp INT,
+  -- Timestamp of the SwapEndToPresentationCompositorFrame substage.
+  swap_end_timestamp INT,
+  -- Frame presentation timestamp aka the timestamp of the
+  -- SwapEndToPresentationCompositorFrame substage.
+  -- TODO(b/341047059): temporarily use LatchToSwapEnd as a workaround if
+  -- SwapEndToPresentationCompositorFrame is missing due to b/247542163.
+  presentation_timestamp INT
 ) AS
 SELECT
   slice.id,
@@ -67,12 +115,24 @@
   slice.ts,
   slice.dur,
   EXTRACT_arg(arg_set_id, 'event_latency.event_latency_id') AS scroll_update_id,
-  has_descendant_slice_with_name(
+  _has_descendant_slice_with_name(
     slice.id,
     'SubmitCompositorFrameToPresentationCompositorFrame')
-  AS is_presented,
+    AS is_presented,
   EXTRACT_ARG(arg_set_id, 'event_latency.event_type') AS event_type,
-  slice.track_id
+  slice.track_id,
+  EXTRACT_ARG(arg_set_id, 'event_latency.vsync_interval_ms')
+    AS vsync_interval_ms,
+  COALESCE(EXTRACT_ARG(arg_set_id, 'event_latency.is_janky_scrolled_frame'), 0)
+    AS is_janky_scrolled_frame,
+  _descendant_slice_begin(slice.id, 'BufferAvailableToBufferReady')
+    AS buffer_available_timestamp,
+  _descendant_slice_begin(slice.id, 'BufferReadyToLatch')
+    AS buffer_ready_timestamp,
+  _descendant_slice_begin(slice.id, 'LatchToSwapEnd') AS latch_timestamp,
+  _descendant_slice_begin(slice.id, 'SwapEndToPresentationCompositorFrame')
+    AS swap_end_timestamp,
+  _get_presentation_timestamp(slice.id) AS presentation_timestamp
 FROM slice
 WHERE name = 'EventLatency';
 
@@ -96,7 +156,7 @@
   event_type GLOB '*GESTURE_SCROLL*'
   -- Pinches are only relevant if the frame was presented.
   OR (event_type GLOB '*GESTURE_PINCH_UPDATE'
-    AND has_descendant_slice_with_name(
+    AND _has_descendant_slice_with_name(
       id,
       'SubmitCompositorFrameToPresentationCompositorFrame')
   )
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/graphics_pipeline.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/graphics_pipeline.sql
new file mode 100644
index 0000000..eae5bd5
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/graphics_pipeline.sql
@@ -0,0 +1,119 @@
+-- Copyright 2024 The Chromium Authors
+-- Use of this source code is governed by a BSD-style license that can be
+-- found in the LICENSE file.
+
+INCLUDE PERFETTO MODULE slices.with_context;
+
+-- `Graphics.Pipeline` steps corresponding to work done by a Viz client to
+-- produce a frame (i.e. before surface aggregation). Covers steps:
+--   * STEP_ISSUE_BEGIN_FRAME
+--   * STEP_RECEIVE_BEGIN_FRAME
+--   * STEP_GENERATE_RENDER_PASS
+--   * STEP_GENERATE_COMPOSITOR_FRAME
+--   * STEP_SUBMIT_COMPOSITOR_FRAME
+--   * STEP_RECEIVE_COMPOSITOR_FRAME
+--   * STEP_RECEIVE_BEGIN_FRAME_DISCARD
+--   * STEP_DID_NOT_PRODUCE_FRAME
+--   * STEP_DID_NOT_PRODUCE_COMPOSITOR_FRAME
+CREATE PERFETTO TABLE chrome_graphics_pipeline_surface_frame_steps(
+  -- Slice Id of the `Graphics.Pipeline` slice.
+  id INT,
+  -- The start timestamp of the slice/step.
+  ts INT,
+  -- The duration of the slice/step.
+  dur INT,
+  -- Step name of the `Graphics.Pipeline` slice.
+  step STRING,
+  -- Id of the graphics pipeline, pre-surface aggregation.
+  surface_frame_trace_id INT,
+  -- Utid of the thread where this slice exists.
+  utid INT)
+AS
+SELECT
+  id,
+  ts,
+  dur,
+  extract_arg(arg_set_id, 'chrome_graphics_pipeline.step') AS step,
+  extract_arg(arg_set_id, 'chrome_graphics_pipeline.surface_frame_trace_id')
+    AS surface_frame_trace_id,
+  utid
+FROM thread_slice
+WHERE name = 'Graphics.Pipeline' AND surface_frame_trace_id IS NOT NULL;
+
+-- `Graphics.Pipeline` steps corresponding to work done on creating and
+-- presenting one frame during/after surface aggregation. Covers steps:
+--   * STEP_DRAW_AND_SWAP
+--   * STEP_SURFACE_AGGREGATION
+--   * STEP_SEND_BUFFER_SWAP
+--   * STEP_BUFFER_SWAP_POST_SUBMIT
+--   * STEP_FINISH_BUFFER_SWAP
+--   * STEP_SWAP_BUFFERS_ACK
+CREATE PERFETTO TABLE chrome_graphics_pipeline_display_frame_steps(
+  -- Slice Id of the `Graphics.Pipeline` slice.
+  id INT,
+  -- The start timestamp of the slice/step.
+  ts INT,
+  -- The duration of the slice/step.
+  dur INT,
+  -- Step name of the `Graphics.Pipeline` slice.
+  step STRING,
+  -- Id of the graphics pipeline, post-surface aggregation.
+  display_trace_id INT,
+  -- Utid of the thread where this slice exists.
+  utid INT)
+AS
+SELECT
+  id,
+  ts,
+  dur,
+  extract_arg(arg_set_id, 'chrome_graphics_pipeline.step') AS step,
+  extract_arg(arg_set_id, 'chrome_graphics_pipeline.display_trace_id')
+    AS display_trace_id,
+  utid
+FROM thread_slice
+WHERE name = 'Graphics.Pipeline' AND display_trace_id IS NOT NULL;
+
+-- Links surface frames (`chrome_graphics_pipeline_surface_frame_steps`) to the
+-- display frame (`chrome_graphics_pipeline_display_frame_steps`) into which
+-- they are merged. In other words, in general, multiple
+-- `surface_frame_trace_id`s will correspond to one `display_trace_id`.
+CREATE PERFETTO TABLE chrome_graphics_pipeline_aggregated_frames(
+  -- Id of the graphics pipeline, pre-surface aggregation.
+  surface_frame_trace_id INT,
+  -- Id of the graphics pipeline, post-surface aggregation.
+  display_trace_id INT)
+AS
+SELECT
+  args.int_value AS surface_frame_trace_id,
+  display_trace_id
+FROM chrome_graphics_pipeline_display_frame_steps step
+JOIN slice
+  USING (id)
+JOIN args
+  USING (arg_set_id)
+WHERE
+  step.step = 'STEP_SURFACE_AGGREGATION'
+  AND args.flat_key
+    = 'chrome_graphics_pipeline.aggregated_surface_frame_trace_ids';
+
+-- Links inputs (`chrome_input_pipeline_steps.latency_id`) to the surface frame
+-- (`chrome_graphics_pipeline_surface_frame_steps`) to which they correspond.
+-- In other words, in general, multiple `latency_id`s will correspond to one
+-- `surface_frame_trace_id`.
+CREATE PERFETTO TABLE chrome_graphics_pipeline_inputs_to_surface_frames(
+  -- Id corresponding to the input pipeline.
+  latency_id INT,
+  -- Id of the graphics pipeline, post-surface aggregation.
+  surface_frame_trace_id INT)
+AS
+SELECT
+  args.int_value AS latency_id,
+  surface_frame_trace_id
+FROM chrome_graphics_pipeline_surface_frame_steps step
+JOIN slice
+  USING (id)
+JOIN args
+  USING (arg_set_id)
+WHERE
+  step.step = 'STEP_SUBMIT_COMPOSITOR_FRAME'
+  AND args.flat_key = 'chrome_graphics_pipeline.latency_ids';
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/histograms.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/histograms.sql
index b7bb525..6aded77 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/histograms.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/histograms.sql
@@ -2,8 +2,6 @@
 -- Use of this source code is governed by a BSD-style license that can be
 -- found in the LICENSE file.
 
-DROP VIEW IF EXISTS chrome_histograms;
-
 -- A helper view on top of the histogram events emitted by Chrome.
 -- Requires "disabled-by-default-histogram_samples" Chrome category.
 CREATE PERFETTO TABLE chrome_histograms(
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/input.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/input.sql
new file mode 100644
index 0000000..ebc8f8b
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/input.sql
@@ -0,0 +1,128 @@
+-- Copyright 2024 The Chromium Authors
+-- Use of this source code is governed by a BSD-style license that can be
+-- found in the LICENSE file.
+
+INCLUDE PERFETTO MODULE slices.with_context;
+
+-- Processing steps of the Chrome input pipeline.
+CREATE PERFETTO TABLE _chrome_input_pipeline_steps_no_input_type(
+  -- Id of this Chrome input pipeline (LatencyInfo).
+  latency_id INT,
+  -- Slice id
+  slice_id INT,
+  -- The step timestamp.
+  ts INT,
+  -- Step duration.
+  dur INT,
+  -- Utid of the thread.
+  utid INT,
+  -- Step name (ChromeLatencyInfo.step).
+  step STRING,
+  -- Input type.
+  input_type STRING,
+  -- Start time of the parent Chrome scheduler task (if any) of this step.
+  task_start_time_ts INT
+) AS
+SELECT
+  EXTRACT_ARG(thread_slice.arg_set_id, 'chrome_latency_info.trace_id') AS latency_id,
+  id AS slice_id,
+  ts,
+  dur,
+  utid,
+  EXTRACT_ARG(thread_slice.arg_set_id, 'chrome_latency_info.step') AS step,
+  EXTRACT_ARG(thread_slice.arg_set_id, 'chrome_latency_info.input_type') AS input_type,
+  ts - (EXTRACT_ARG(thread_slice.arg_set_id, 'current_task.event_offset_from_task_start_time_us') * 1000) AS task_start_time_ts
+FROM
+  thread_slice
+WHERE
+  step IS NOT NULL
+  AND latency_id != -1
+ORDER BY slice_id, ts;
+
+-- Each row represents one input pipeline.
+CREATE PERFETTO TABLE chrome_inputs(
+  -- Id of this Chrome input pipeline (LatencyInfo).
+  latency_id INT,
+   -- Input type.
+  input_type STRING
+) AS
+SELECT
+  -- Id of this Chrome input pipeline (LatencyInfo).
+  latency_id,
+  -- MIN selects the first non-null value.
+  MIN(input_type) as input_type
+FROM _chrome_input_pipeline_steps_no_input_type
+WHERE latency_id != -1
+GROUP BY latency_id;
+
+-- Since not all steps have associated input type (but all steps
+-- for a given latency id should have the same input type),
+-- populate input type for steps where it would be NULL.
+CREATE PERFETTO TABLE chrome_input_pipeline_steps(
+  -- Id of this Chrome input pipeline (LatencyInfo).
+  latency_id INT,
+  -- Slice id
+  slice_id INT,
+  -- The step timestamp.
+  ts INT,
+  -- Step duration.
+  dur INT,
+  -- Utid of the thread.
+  utid INT,
+  -- Step name (ChromeLatencyInfo.step).
+  step STRING,
+  -- Input type.
+  input_type STRING,
+  -- Start time of the parent Chrome scheduler task (if any) of this step.
+  task_start_time_ts INT
+) AS
+SELECT
+  latency_id,
+  slice_id,
+  ts,
+  dur,
+  utid,
+  step,
+  chrome_inputs.input_type AS input_type,
+  task_start_time_ts
+FROM
+  chrome_inputs
+LEFT JOIN
+  _chrome_input_pipeline_steps_no_input_type
+  USING (latency_id)
+WHERE chrome_inputs.input_type IS NOT NULL;
+
+-- For each input, get the latency id of the input that it was coalesced into.
+CREATE PERFETTO TABLE chrome_coalesced_inputs(
+  -- The `latency_id` of the coalesced input.
+  coalesced_latency_id INT,
+  -- The `latency_id` of the input that the current input was coalesced into.
+  presented_latency_id INT
+) AS
+SELECT
+  args.int_value AS coalesced_latency_id,
+  latency_id AS presented_latency_id
+FROM chrome_input_pipeline_steps step
+JOIN slice USING (slice_id)
+JOIN args USING (arg_set_id)
+WHERE step.step = 'STEP_RESAMPLE_SCROLL_EVENTS'
+  AND args.flat_key = 'chrome_latency_info.coalesced_trace_ids';
+
+-- Slices with information about non-blocking touch move inputs
+-- that were converted into gesture scroll updates.
+CREATE PERFETTO TABLE chrome_touch_move_to_scroll_update(
+  -- Latency id of the touch move input (LatencyInfo).
+  touch_move_latency_id INT,
+  -- Latency id of the corresponding scroll update input (LatencyInfo).
+  scroll_update_latency_id INT
+) AS
+SELECT
+  scroll_update_step.latency_id AS scroll_update_latency_id,
+  touch_move_step.latency_id AS touch_move_latency_id
+FROM chrome_input_pipeline_steps scroll_update_step
+JOIN ancestor_slice(scroll_update_step.slice_id) AS ancestor
+JOIN chrome_input_pipeline_steps touch_move_step
+  ON ancestor.id = touch_move_step.slice_id
+WHERE scroll_update_step.step = 'STEP_SEND_INPUT_EVENT_UI'
+AND scroll_update_step.input_type = 'GESTURE_SCROLL_UPDATE_EVENT'
+AND touch_move_step.step = 'STEP_TOUCH_EVENT_HANDLED';
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/perfetto_sql_files.gni b/src/trace_processor/perfetto_sql/stdlib/chrome/perfetto_sql_files.gni
index 77eeb14..cd0f974 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/perfetto_sql_files.gni
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/perfetto_sql_files.gni
@@ -8,9 +8,11 @@
   "cpu_powerups.sql",
   "event_latency.sql",
   "event_latency_description.sql",
+  "graphics_pipeline.sql",
   "histograms.sql",
   "interactions.sql",
   "metadata.sql",
+  "input.sql",
   "page_loads.sql",
   "scroll_interactions.sql",
   "speedometer.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/speedometer.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/speedometer.sql
index 9cfcaa8..40529e0 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/speedometer.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/speedometer.sql
@@ -91,3 +91,14 @@
     _chrome_speedometer_version() = '3',
     chrome_speedometer_3_score(),
     chrome_speedometer_2_1_score());
+
+-- Returns the utid for the main thread that ran Speedometer 3
+CREATE PERFETTO FUNCTION chrome_speedometer_renderer_main_utid()
+-- Renderer main utid
+RETURNS INT
+AS
+SELECT
+  IIF(
+    _chrome_speedometer_version() = '3',
+    chrome_speedometer_3_renderer_main_utid(),
+    chrome_speedometer_2_1_renderer_main_utid());
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/speedometer_2_1.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/speedometer_2_1.sql
index bf89628..4d87fc6 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/speedometer_2_1.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/speedometer_2_1.sql
@@ -2,15 +2,20 @@
 -- Use of this source code is governed by a BSD-style license that can be
 -- found in the LICENSE file.
 
--- List Speedometer 2.1 tests.
-CREATE PERFETTO VIEW _chrome_speedometer_2_1_test_name(
+-- List Speedometer 2.1 test marks. Used to find relevant slices.
+CREATE PERFETTO VIEW _chrome_speedometer_2_1_mark_name(
+  -- Expected slice name
+  name STRING,
   -- Suite name
   suite_name STRING,
   -- Test name
-  test_name STRING)
+  test_name STRING,
+  -- Mark type
+  mark_type STRING)
 AS
 WITH
-  data(suite_name, test_name) AS (
+  data(suite_name, test_name)
+  AS (
     VALUES('Angular2-TypeScript-TodoMVC', 'Adding100Items'),
     ('Angular2-TypeScript-TodoMVC', 'CompletingAllItems'),
     ('Angular2-TypeScript-TodoMVC', 'DeletingItems'),
@@ -59,8 +64,14 @@
     ('jQuery-TodoMVC', 'Adding100Items'),
     ('jQuery-TodoMVC', 'CompletingAllItems'),
     ('jQuery-TodoMVC', 'DeletingAllItems')
-  )
-SELECT suite_name, test_name FROM data;
+  ),
+  mark_type(mark_type) AS (VALUES('start'), ('sync-end'), ('async-end'))
+SELECT
+  suite_name || '.' || test_name || '-' || mark_type AS name,
+  suite_name,
+  test_name,
+  mark_type
+FROM data, mark_type;
 
 -- Augmented slices for Speedometer measurements.
 -- These are the intervals of time Speedometer uses to compute the final score.
@@ -84,22 +95,6 @@
   measure_type STRING)
 AS
 WITH
-  mark_type(mark_type) AS (
-    VALUES('start'),
-    ('sync-end'),
-    ('async-end')
-  ),
-  -- Make sure we only look at slices with names we expect.
-  mark_name AS (
-    SELECT
-      suite_name || '.' || test_name || '-' || mark_type AS name,
-      suite_name,
-      test_name,
-      mark_type
-    FROM
-      _chrome_speedometer_2_1_test_name,
-      mark_type
-  ),
   mark AS (
     SELECT
       s.id AS slice_id,
@@ -108,7 +103,8 @@
       m.test_name,
       m.mark_type
     FROM slice AS s
-    JOIN mark_name AS m
+    -- Make sure we only look at slices with names we expect.
+    JOIN _chrome_speedometer_2_1_mark_name AS m
       USING (name)
     WHERE category = 'blink.user_timing'
   ),
@@ -218,3 +214,18 @@
 RETURNS DOUBLE
 AS
 SELECT AVG(score) FROM chrome_speedometer_2_1_iteration;
+
+-- Returns the utid for the main thread that ran Speedometer 2.1
+CREATE PERFETTO FUNCTION chrome_speedometer_2_1_renderer_main_utid()
+-- Renderer main utid
+RETURNS INT
+AS
+SELECT utid
+FROM thread_track
+WHERE
+  id IN (
+    SELECT track_id
+    FROM slice, _chrome_speedometer_2_1_mark_name
+    USING (name)
+    WHERE category = 'blink.user_timing'
+  );
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/speedometer_3.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/speedometer_3.sql
index cc855d8..2283692 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/speedometer_3.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/speedometer_3.sql
@@ -2,15 +2,20 @@
 -- Use of this source code is governed by a BSD-style license that can be
 -- found in the LICENSE file.
 
--- List Speedometer 3 tests.
-CREATE PERFETTO VIEW _chrome_speedometer_3_test_name(
+-- List Speedometer 3 measures. Used to find relevant slices.
+CREATE PERFETTO VIEW _chrome_speedometer_3_measure_name(
+  -- Expected slice name
+  name STRING,
   -- Suite name
   suite_name STRING,
   -- Test name
-  test_name STRING)
+  test_name STRING,
+  -- Measure type
+  measure_type STRING)
 AS
 WITH
-  data(suite_name, test_name) AS (
+  data(suite_name, test_name)
+  AS (
     VALUES('TodoMVC-JavaScript-ES5', 'Adding100Items'),
     ('TodoMVC-JavaScript-ES5', 'CompletingAllItems'),
     ('TodoMVC-JavaScript-ES5', 'DeletingAllItems'),
@@ -69,24 +74,24 @@
     ('Perf-Dashboard', 'Render'),
     ('Perf-Dashboard', 'SelectingPoints'),
     ('Perf-Dashboard', 'SelectingRange')
-  )
-SELECT suite_name, test_name FROM data;
-
-CREATE PERFETTO VIEW _chrome_speedometer_iteration_slice
-AS
-WITH data AS (
-  SELECT
-    *,
-    substr(name, 1 + length('iteration-')) AS iteration_str
-  FROM
-    slice
-  WHERE
-    category = 'blink.user_timing'
-    AND name GLOB 'iteration-*'
-)
+  ),
+  measure_type(measure_type) AS (VALUES('sync'), ('async'))
 SELECT
-  *,
-  CAST(iteration_str AS INT) as iteration
+  suite_name || '.' || test_name || '-' || measure_type AS name,
+  suite_name,
+  test_name,
+  measure_type
+FROM data, measure_type;
+
+CREATE PERFETTO VIEW _chrome_speedometer_3_iteration_slice
+AS
+WITH
+  data AS (
+    SELECT *, substr(name, 1 + length('iteration-')) AS iteration_str
+    FROM slice
+    WHERE category = 'blink.user_timing' AND name GLOB 'iteration-*'
+  )
+SELECT *, CAST(iteration_str AS INT) AS iteration
 FROM data
 WHERE iteration_str = iteration;
 
@@ -110,46 +115,15 @@
   measure_type STRING)
 AS
 WITH
-  measure_type(measure_type) AS (
-    VALUES('sync'),
-    ('async')
-  ),
-  measure_name AS (
-    SELECT
-      suite_name || '.' || test_name || '-' || measure_type AS name,
-      suite_name,
-      test_name,
-      measure_type
-    FROM
-      _chrome_speedometer_3_test_name,
-      measure_type
-  ),
   measure_slice AS (
-    SELECT
-      s.ts,
-      s.dur,
-      s.name,
-      m.suite_name,
-      m.test_name,
-      m.measure_type
-    FROM
-      slice s,
-      measure_name AS m
+    SELECT s.ts, s.dur, s.name, m.suite_name, m.test_name, m.measure_type
+    FROM slice s, _chrome_speedometer_3_measure_name AS m
     USING (name)
-    WHERE
-      s.category = 'blink.user_timing'
+    WHERE s.category = 'blink.user_timing'
   )
 SELECT
-  s.ts,
-  s.dur,
-  s.name,
-  i.iteration,
-  s.suite_name,
-  s.test_name,
-  s.measure_type
-FROM
-  measure_slice AS s,
-  _chrome_speedometer_iteration_slice i
+  s.ts, s.dur, s.name, i.iteration, s.suite_name, s.test_name, s.measure_type
+FROM measure_slice AS s, _chrome_speedometer_3_iteration_slice i
 ON (s.ts >= i.ts AND s.ts < i.ts + i.dur)
 ORDER BY s.ts ASC;
 
@@ -174,8 +148,7 @@
 AS
 WITH
   suite AS (
-    SELECT
-      iteration, suite_name, SUM(dur / (1000.0 * 1000.0)) AS suite_total
+    SELECT iteration, suite_name, SUM(dur / (1000.0 * 1000.0)) AS suite_total
     FROM chrome_speedometer_3_measure
     GROUP BY iteration, suite_name
   ),
@@ -188,14 +161,8 @@
     FROM suite
     GROUP BY iteration
   )
-SELECT
-  s.ts,
-  s.dur,
-  s.name,
-  i.iteration,
-  i.geomean,
-  1000.0 / i.geomean AS score
-FROM iteration AS i, _chrome_speedometer_iteration_slice AS s
+SELECT s.ts, s.dur, s.name, i.iteration, i.geomean, 1000.0 / i.geomean AS score
+FROM iteration AS i, _chrome_speedometer_3_iteration_slice AS s
 USING (iteration);
 
 -- Returns the Speedometer 3 score for all iterations in the trace
@@ -204,3 +171,22 @@
 RETURNS DOUBLE
 AS
 SELECT AVG(score) FROM chrome_speedometer_3_iteration;
+
+-- Returns the utid for the main thread that ran Speedometer 3
+CREATE PERFETTO FUNCTION chrome_speedometer_3_renderer_main_utid()
+-- Renderer main utid
+RETURNS INT
+AS
+WITH
+  start_event AS (
+    SELECT name || '-start' AS name FROM _chrome_speedometer_3_measure_name
+  )
+SELECT utid
+FROM thread_track
+WHERE
+  id IN (
+    SELECT track_id
+    FROM slice, start_event
+    USING (name)
+    WHERE category = 'blink.user_timing'
+  )
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/common/BUILD.gn
deleted file mode 100644
index 73db0a7..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/BUILD.gn
+++ /dev/null
@@ -1,26 +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("../../../../../gn/perfetto_sql.gni")
-
-perfetto_sql_source_set("common") {
-  sources = [
-    "args.sql",
-    "counters.sql",
-    "metadata.sql",
-    "percentiles.sql",
-    "slices.sql",
-    "timestamps.sql",
-  ]
-}
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/OWNERS b/src/trace_processor/perfetto_sql/stdlib/common/OWNERS
deleted file mode 100644
index 0a16b3f..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/OWNERS
+++ /dev/null
@@ -1,8 +0,0 @@
-set noparent
-
-# Please prefer sending to one of the following people
-mayzner@google.com
-lalitm@google.com
-
-# For emergency reviews
-primiano@google.com
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/args.sql b/src/trace_processor/perfetto_sql/stdlib/common/args.sql
deleted file mode 100644
index 3d1e793..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/args.sql
+++ /dev/null
@@ -1,20 +0,0 @@
---
--- Copyright 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
---
---     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.
-
--- No new changes allowed. Will be removed after v45 of Perfetto.
---
--- We decided to move away from the generalised `common` module and migrate the
--- most useful functionality into specialised modules.
-INCLUDE PERFETTO MODULE deprecated.v42.common.args;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/counters.sql b/src/trace_processor/perfetto_sql/stdlib/common/counters.sql
deleted file mode 100644
index f0c1ce6..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/counters.sql
+++ /dev/null
@@ -1,21 +0,0 @@
---
--- Copyright 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
---
---     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.
-
--- No new changes allowed. Will be removed after v45 of Perfetto.
---
--- We decided to move away from the generalised `common` module and migrate the
--- most useful functionality into specialised modules.
-INCLUDE PERFETTO MODULE deprecated.v42.common.args;
-INCLUDE PERFETTO MODULE deprecated.v42.common.counters;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/metadata.sql b/src/trace_processor/perfetto_sql/stdlib/common/metadata.sql
deleted file mode 100644
index bd1a0fd..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/metadata.sql
+++ /dev/null
@@ -1,21 +0,0 @@
---
--- Copyright 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
---
---     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.
-
--- No new changes allowed. Will be removed after v45 of Perfetto.
---
--- We decided to move away from the generalised `common` module and migrate the
--- most useful functionality into specialised modules.
-INCLUDE PERFETTO MODULE deprecated.v42.common.args;
-INCLUDE PERFETTO MODULE deprecated.v42.common.metadata;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/percentiles.sql b/src/trace_processor/perfetto_sql/stdlib/common/percentiles.sql
deleted file mode 100644
index 525c95c..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/percentiles.sql
+++ /dev/null
@@ -1,21 +0,0 @@
---
--- Copyright 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
---
---     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.
-
--- No new changes allowed. Will be removed after v45 of Perfetto.
---
--- We decided to move away from the generalised `common` module and migrate the
--- most useful functionality into specialised modules.
-INCLUDE PERFETTO MODULE deprecated.v42.common.args;
-INCLUDE PERFETTO MODULE deprecated.v42.common.percentiles;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/slices.sql b/src/trace_processor/perfetto_sql/stdlib/common/slices.sql
deleted file mode 100644
index d5d70c9..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/slices.sql
+++ /dev/null
@@ -1,21 +0,0 @@
---
--- Copyright 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
---
---     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.
-
--- No new changes allowed. Will be removed after v45 of Perfetto.
---
--- We decided to move away from the generalised `common` module and migrate the
--- most useful functionality into specialised modules.
-INCLUDE PERFETTO MODULE deprecated.v42.common.args;
-INCLUDE PERFETTO MODULE deprecated.v42.common.slices;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/timestamps.sql b/src/trace_processor/perfetto_sql/stdlib/common/timestamps.sql
deleted file mode 100644
index 8f91d3b..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/timestamps.sql
+++ /dev/null
@@ -1,21 +0,0 @@
---
--- Copyright 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
---
---     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.
-
--- No new changes allowed. Will be removed after v45 of Perfetto.
---
--- We decided to move away from the generalised `common` module and migrate the
--- most useful functionality into specialised modules.
-INCLUDE PERFETTO MODULE deprecated.v42.common.args;
-INCLUDE PERFETTO MODULE deprecated.v42.common.timestamps;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/counters/intervals.sql b/src/trace_processor/perfetto_sql/stdlib/counters/intervals.sql
index cf9e5a3..7eac359 100644
--- a/src/trace_processor/perfetto_sql/stdlib/counters/intervals.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/counters/intervals.sql
@@ -49,23 +49,32 @@
 -- value DOUBLE, next_value DOUBLE, delta_value DOUBLE).
 RETURNS TableOrSubquery AS
 (
-  WITH base AS (
-    SELECT
-      id,
-      ts,
-      track_id,
-      value,
-      LAG(value) OVER (PARTITION BY track_id ORDER BY ts) AS lag_value
-    FROM $counter_table
-  )
   SELECT
-    id,
-    ts,
-    LEAD(ts, 1, trace_end()) OVER(PARTITION BY track_id ORDER BY ts) - ts AS dur,
-    track_id,
-    value,
-    LEAD(value) OVER(PARTITION BY track_id ORDER BY ts) AS next_value,
-    value - lag_value AS delta_value
-  FROM base
-  WHERE value != lag_value OR lag_value IS NULL
+    c0 AS id,
+    c1 AS ts,
+    c2 AS dur,
+    c3 AS track_id,
+    c4 AS value,
+    c5 AS next_value,
+    c6 AS delta_value
+  FROM __intrinsic_table_ptr(
+    __intrinsic_counter_intervals(
+      "leading", TRACE_END(),
+      (SELECT __intrinsic_counter_per_track_agg(
+        input.id,
+        input.ts,
+        input.track_id,
+        input.value
+      )
+      FROM (SELECT * FROM $counter_table ORDER BY ts) input)
+    )
+  )
+
+  WHERE __intrinsic_table_ptr_bind(c0, 'id')
+    AND __intrinsic_table_ptr_bind(c1, 'ts')
+    AND __intrinsic_table_ptr_bind(c2, 'dur')
+    AND __intrinsic_table_ptr_bind(c3, 'track_id')
+    AND __intrinsic_table_ptr_bind(c4, 'value')
+    AND __intrinsic_table_ptr_bind(c5, 'next_value')
+    AND __intrinsic_table_ptr_bind(c6, 'delta_value')
 );
diff --git a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/BUILD.gn
deleted file mode 100644
index a99b51b..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/BUILD.gn
+++ /dev/null
@@ -1,26 +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("../../../../../../../gn/perfetto_sql.gni")
-
-perfetto_sql_source_set("common") {
-  sources = [
-    "args.sql",
-    "counters.sql",
-    "metadata.sql",
-    "percentiles.sql",
-    "slices.sql",
-    "timestamps.sql",
-  ]
-}
diff --git a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/args.sql b/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/args.sql
deleted file mode 100644
index df0615a..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/args.sql
+++ /dev/null
@@ -1,31 +0,0 @@
---
--- Copyright 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
---
---     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.
-
--- Returns the formatted value of a given argument.
--- Similar to EXTRACT_ARG, but instead of returning the raw value, it returns
--- the value formatted according to the 'value_type' column (e.g. for booleans,
--- EXTRACT_ARG will return 0 or 1, while FORMATTED_ARG will return 'true' or
--- 'false').
-CREATE PERFETTO FUNCTION formatted_arg(
-  -- Id of the arg set.
-  arg_set_id INT,
-  -- Key of the argument.
-  arg_key STRING
-)
--- Formatted value of the argument.
-RETURNS STRING AS
-SELECT display_value
-FROM args
-WHERE arg_set_id = $arg_set_id AND key = $arg_key;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/counters.sql b/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/counters.sql
deleted file mode 100644
index 7923c52..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/counters.sql
+++ /dev/null
@@ -1,101 +0,0 @@
---
--- Copyright 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
---
---     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 deprecated.v42.common.timestamps;
-
--- Timestamp of first counter value in a counter.
-CREATE PERFETTO FUNCTION earliest_timestamp_for_counter_track(
-  -- Id of a counter track with a counter.
-  counter_track_id INT)
--- Timestamp of first counter value. Null if doesn't exist.
-RETURNS LONG AS
-SELECT MIN(ts) FROM counter WHERE counter.track_id = $counter_track_id;
-
--- Counter values with details of counter track with calculated duration of each counter value.
--- Duration is calculated as time from counter to the next counter.
-CREATE PERFETTO FUNCTION counter_with_dur_for_track(
-  -- Id of track counter track.
-  counter_track_id INT)
-RETURNS TABLE(
-    -- Timestamp of the counter value.
-    ts LONG,
-    -- Duration of the counter value.
-    dur LONG,
-    -- Counter value.
-    value DOUBLE,
-    -- Id of the counter track.
-    track_id INT,
-    -- Name of the counter track.
-    track_name STRING,
-    -- Counter track set id.
-    track_arg_set_id INT,
-    -- Counter arg set id.
-    arg_set_id INT
-) AS
-SELECT
-  ts,
-  LEAD(ts, 1, trace_end()) OVER(ORDER BY ts) - ts AS dur,
-  value,
-  track.id AS track_id,
-  track.name AS track_name,
-  track.source_arg_set_id AS track_arg_set_id,
-  counter.arg_set_id AS arg_set_id
-FROM counter
-JOIN counter_track track ON track.id = counter.track_id
-WHERE track.id = $counter_track_id;
-
--- COUNTER_WITH_DUR_FOR_TRACK but in a specified time.
--- Does calculation over the table ends - creates an artificial counter value at
--- the start if needed and chops the duration of the last timestamps in range.
-CREATE PERFETTO FUNCTION counter_for_time_range(
-  -- Id of track counter track.
-  counter_track_id INT,
-  -- Timestamp of the timerange start.
-  -- Can be earlier than the first counter value.
-  start_ts LONG,
-  -- Timestamp of the timerange end.
-  end_ts LONG)
-RETURNS TABLE(
-  -- Timestamp of the counter value.
-  ts LONG,
-  -- Duration of the counter value.
-  dur LONG,
-  -- Counter value.
-  value DOUBLE,
-  -- If of the counter track.
-  track_id INT,
-  -- Name of the counter track.
-  track_name STRING,
-  -- Counter track set id.
-  track_arg_set_id INT,
-  -- Counter arg set id.
-  arg_set_id INT
-) AS
-SELECT
-  IIF(ts < $start_ts, $start_ts, ts) AS ts,
-  IIF(
-    ts < $start_ts,
-    dur - ($start_ts - ts),
-    IIF(ts + dur > $end_ts, $end_ts - ts, dur)) AS dur,
-  value,
-  track_id,
-  track_name,
-  track_arg_set_id,
-  arg_set_id
-FROM counter_with_dur_for_track($counter_track_id)
-WHERE TRUE
-  AND ts + dur >= $start_ts
-  AND ts < $end_ts
-ORDER BY ts ASC;
diff --git a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/metadata.sql b/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/metadata.sql
deleted file mode 100644
index e667477..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/metadata.sql
+++ /dev/null
@@ -1,22 +0,0 @@
---
--- Copyright 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
---
---     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.
-
--- Extracts an int value with the given name from the metadata table.
-CREATE PERFETTO FUNCTION extract_int_metadata(
-  -- The name of the metadata entry.
-  name STRING)
--- int_value for the given name. NULL if there's no such entry.
-RETURNS LONG AS
-SELECT int_value FROM metadata WHERE name = ($name);
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/percentiles.sql b/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/percentiles.sql
deleted file mode 100644
index e807a78..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/percentiles.sql
+++ /dev/null
@@ -1,169 +0,0 @@
---
--- Copyright 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
---
---     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 deprecated.v42.common.counters;
-INCLUDE PERFETTO MODULE deprecated.v42.common.timestamps;
-
-CREATE PERFETTO FUNCTION _number_generator(upper_limit INT)
-RETURNS TABLE(num INT) AS
-WITH nums AS
-    (SELECT 1 num UNION SELECT num + 1
-    from NUMS
-    WHERE num < $upper_limit)
-SELECT num FROM nums;
-
-CREATE PERFETTO FUNCTION _earliest_timestamp_for_counter_track(
-  -- Id of a counter track with a counter.
-  counter_track_id INT)
--- Timestamp of first counter value. Null if doesn't exist.
-RETURNS LONG AS
-SELECT MIN(ts) FROM counter WHERE counter.track_id = $counter_track_id;
-
--- COUNTER_WITH_DUR_FOR_TRACK but in a specified time.
--- Does calculation over the table ends - creates an artificial counter value at
--- the start if needed and chops the duration of the last timestamps in range.
-CREATE PERFETTO FUNCTION _counter_for_time_range(
-  -- Id of track counter track.
-  counter_track_id INT,
-  -- Timestamp of the timerange start.
-  -- Can be earlier than the first counter value.
-  start_ts LONG,
-  -- Timestamp of the timerange end.
-  end_ts LONG)
-RETURNS TABLE(
-  -- Timestamp of the counter value.
-  ts LONG,
-  -- Duration of the counter value.
-  dur LONG,
-  -- Counter value.
-  value DOUBLE,
-  -- If of the counter track.
-  track_id INT,
-  -- Name of the counter track.
-  track_name STRING,
-  -- Counter track set id.
-  track_arg_set_id INT,
-  -- Counter arg set id.
-  arg_set_id INT
-) AS
-SELECT
-  IIF(ts < $start_ts, $start_ts, ts) AS ts,
-  IIF(
-    ts < $start_ts,
-    dur - ($start_ts - ts),
-    IIF(ts + dur > $end_ts, $end_ts - ts, dur)) AS dur,
-  value,
-  track_id,
-  track_name,
-  track_arg_set_id,
-  arg_set_id
-FROM counter_with_dur_for_track($counter_track_id)
-WHERE TRUE
-  AND ts + dur >= $start_ts
-  AND ts < $end_ts
-ORDER BY ts ASC;
-
---
--- Get durations for percentile
---
-
--- All percentiles (range 1-100) for counter track ID in a given time range.
---
--- Percentiles are calculated by:
--- 1. Dividing the sum of duration in time range for each value in the counter
--- by duration of the counter in range. This gives us `percentile_for)value` (DOUBLE).
--- 2. Fetching each percentile by taking floor of each `percentile_for_value`, grouping by
--- resulting `percentile` and MIN from value for each grouping. As we are rounding down,
--- taking MIN assures most reliable data.
--- 3. Filling the possible gaps in percentiles by getting the minimal value from higher
--- percentiles for each gap.
-CREATE PERFETTO FUNCTION counter_percentiles_for_time_range(
-  -- Id of the counter track.
-  counter_track_id INT,
-  -- Timestamp of start of time range.
-  start_ts LONG,
-  -- Timestamp of end of time range.
-  end_ts LONG)
-RETURNS TABLE(
-  -- All of the numbers from 1 to 100.
-  percentile INT,
-  -- Value for the percentile.
-  value DOUBLE
-) AS
-WITH percentiles_for_value AS (
-    SELECT
-        value,
-        (CAST(SUM(dur) OVER(ORDER BY value ASC) AS DOUBLE) /
-            ($end_ts - MAX($start_ts, _earliest_timestamp_for_counter_track($counter_track_id)))) * 100
-        AS percentile_for_value
-    FROM _COUNTER_FOR_TIME_RANGE($counter_track_id, $start_ts, $end_ts)
-    ORDER BY value ASC
-),
-with_gaps AS (
-    SELECT
-        CAST(percentile_for_value AS INT) AS percentile,
-        MIN(value) AS value
-    FROM percentiles_for_value
-    GROUP BY percentile
-    ORDER BY percentile ASC)
-SELECT
-    num AS percentile,
-    IFNULL(value, MIN(value) OVER (ORDER BY percentile DESC)) AS value
-FROM _NUMBER_GENERATOR(100) AS nums
-LEFT JOIN with_gaps ON with_gaps.percentile = nums.num
-ORDER BY percentile DESC;
-
--- All percentiles (range 1-100) for counter track ID.
-CREATE PERFETTO FUNCTION counter_percentiles_for_track(
-  -- Id of the counter track.
-  counter_track_id INT)
-RETURNS TABLE(
-  -- All of the numbers from 1 to 100.
-  percentile INT,
-  -- Value for the percentile.
-  value DOUBLE
-) AS
-SELECT *
-FROM counter_percentiles_for_time_range(
-  $counter_track_id, trace_start(), trace_end());
-
--- Value for specific percentile (range 1-100) for counter track ID in time range.
-CREATE PERFETTO FUNCTION counter_track_percentile_for_time(
-  -- Id of the counter track.
-  counter_track_id INT,
-  -- Any of the numbers from 1 to 100.
-  percentile INT,
-  -- Timestamp of start of time range.
-  start_ts LONG,
-  -- Timestamp of end of time range.
-  end_ts LONG)
--- Value for the percentile.
-RETURNS DOUBLE AS
-SELECT value
-FROM counter_percentiles_for_time_range($counter_track_id, $start_ts, $end_ts)
-WHERE percentile = $percentile;
-
--- Value for specific percentile (range 1-100) for counter track ID.
-CREATE PERFETTO FUNCTION counter_track_percentile(
-  -- Id of the counter track.
-  counter_track_id INT,
-  -- Any of the numbers from 1 to 100.
-  percentile INT)
--- Value for the percentile.
-RETURNS DOUBLE AS
-SELECT counter_track_percentile_for_time($counter_track_id,
-                                         $percentile,
-                                         trace_start(),
-                                         trace_end());
diff --git a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/slices.sql b/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/slices.sql
deleted file mode 100644
index 05b6b21..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/slices.sql
+++ /dev/null
@@ -1,133 +0,0 @@
---
--- Copyright 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
---
---     https://www.apache.org/licenses/LICENSE-2.0
---
--- Unless required by applicable law or agreed to in writing, software
--- distributed under the License is distributed on an "AS IS" BASIS,
--- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
--- See the License for the specific language governing permissions and
--- limitations under the License.
-
-INCLUDE PERFETTO MODULE slices.with_context;
-
--- Checks if slice has an ancestor with provided name.
-CREATE PERFETTO FUNCTION has_parent_slice_with_name(
-  -- Id of the slice to check parents of.
-  id INT,
-  -- Name of potential ancestor slice.
-  parent_name STRING)
--- Whether `parent_name` is a name of an ancestor slice.
-RETURNS BOOL AS
-SELECT EXISTS(
-  SELECT 1
-  FROM ancestor_slice($id)
-  WHERE name = $parent_name
-  LIMIT 1
-);
-
--- Checks if slice has a descendant with provided name.
-CREATE PERFETTO FUNCTION has_descendant_slice_with_name(
-  -- Id of the slice to check descendants of.
-  id INT,
-  -- Name of potential descendant slice.
-  descendant_name STRING
-)
--- Whether `descendant_name` is a name of an descendant slice.
-RETURNS BOOL AS
-SELECT EXISTS(
-  SELECT 1
-  FROM descendant_slice($id)
-  WHERE name = $descendant_name
-  LIMIT 1
-);
-
--- Finds the end timestamp for a given slice's descendant with a given name.
--- If there are multiple descendants with a given name, the function will return the
--- first one, so it's most useful when working with a timeline broken down into phases,
--- where each subphase can happen only once.
-CREATE PERFETTO FUNCTION descendant_slice_end(
-  -- Id of the parent slice.
-  parent_id INT,
-  -- Name of the child with the desired end TS.
-  child_name STRING
-)
--- End timestamp of the child or NULL if it doesn't exist.
-RETURNS INT AS
-SELECT
-  CASE WHEN s.dur
-    IS NOT -1 THEN s.ts + s.dur
-    ELSE NULL
-  END
-FROM descendant_slice($parent_id) s
-WHERE s.name = $child_name
-LIMIT 1;
-
--- Finds all slices with a direct parent with the given parent_id.
-CREATE PERFETTO FUNCTION direct_children_slice(
-  -- Id of the parent slice.
-  parent_id LONG)
-RETURNS TABLE(
-  -- Alias for `slice.id`.
-  id LONG,
-  -- Alias for `slice.type`.
-  type STRING,
-  -- Alias for `slice.ts`.
-  ts LONG,
-  -- Alias for `slice.dur`.
-  dur LONG,
-  -- Alias for `slice.category`.
-  category LONG,
-  -- Alias for `slice.name`.
-  name STRING,
-  -- Alias for `slice.track_id`.
-  track_id LONG,
-  -- Alias for `slice.depth`.
-  depth LONG,
-  -- Alias for `slice.parent_id`.
-  parent_id LONG,
-  -- Alias for `slice.arg_set_id`.
-  arg_set_id LONG,
-  -- Alias for `slice.thread_ts`.
-  thread_ts LONG,
-  -- Alias for `slice.thread_dur`.
-  thread_dur LONG
-) AS
-SELECT
-  slice.id,
-  slice.type,
-  slice.ts,
-  slice.dur,
-  slice.category,
-  slice.name,
-  slice.track_id,
-  slice.depth,
-  slice.parent_id,
-  slice.arg_set_id,
-  slice.thread_ts,
-  slice.thread_dur
-FROM slice
-WHERE parent_id = $parent_id;
-
--- Given a slice id, returns the name of the slice.
-CREATE PERFETTO FUNCTION slice_name_from_id(
-  -- The slice id which we need the name for.
-  id LONG
-)
--- The name of slice with the given id.
-RETURNS STRING AS
-SELECT
-  name
-FROM slice
-WHERE $id = id;
-
-CREATE PERFETTO FUNCTION slice_count(
-  -- Name of the slices to counted.
-  slice_glob STRING)
--- Number of slices with the name.
-RETURNS INT AS
-SELECT COUNT(1) FROM slice WHERE name GLOB $slice_glob;
diff --git a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/timestamps.sql b/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/timestamps.sql
deleted file mode 100644
index bff333f..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/timestamps.sql
+++ /dev/null
@@ -1,72 +0,0 @@
---
--- Copyright 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
---
---     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 time.conversion;
-
-CREATE PERFETTO FUNCTION is_spans_overlapping(
-  ts1 LONG,
-  ts_end1 LONG,
-  ts2 LONG,
-  ts_end2 LONG)
-RETURNS BOOL AS
-SELECT (IIF($ts1 < $ts2, $ts2, $ts1)
-      < IIF($ts_end1 < $ts_end2, $ts_end1, $ts_end2));
-
-CREATE PERFETTO FUNCTION spans_overlapping_dur(
-  ts1 LONG,
-  dur1 LONG,
-  ts2 LONG,
-  dur2 LONG
-)
-RETURNS INT AS
-SELECT
-  CASE
-    WHEN $dur1 = -1 OR $dur2 = -1 THEN 0
-    WHEN $ts1 + $dur1 < $ts2 OR $ts2 + $dur2 < $ts1 THEN 0
-    WHEN ($ts1 >= $ts2) AND ($ts1 + $dur1 <= $ts2 + $dur2) THEN $dur1
-    WHEN ($ts1 < $ts2) AND ($ts1 + $dur1 < $ts2 + $dur2) THEN $ts1 + $dur1 - $ts2
-    WHEN ($ts1 > $ts2) AND ($ts1 + $dur1 > $ts2 + $dur2) THEN $ts2 + $dur2 - $ts1
-    ELSE $dur2
-  END;
-
--- Renames
-
-CREATE PERFETTO FUNCTION ns(nanos INT)
-RETURNS INT AS
-SELECT time_from_ns($nanos);
-
-CREATE PERFETTO FUNCTION us(micros INT)
-RETURNS INT AS
-SELECT time_from_us($micros);
-
-CREATE PERFETTO FUNCTION ms(millis INT)
-RETURNS INT AS
-SELECT time_from_ms($millis);
-
-CREATE PERFETTO FUNCTION seconds(seconds INT)
-RETURNS INT AS
-SELECT time_from_s($seconds);
-
-CREATE PERFETTO FUNCTION minutes(minutes INT)
-RETURNS INT AS
-SELECT time_from_min($minutes);
-
-CREATE PERFETTO FUNCTION hours(hours INT)
-RETURNS INT AS
-SELECT time_from_hours($hours);
-
-CREATE PERFETTO FUNCTION days(days INT)
-RETURNS INT AS
-SELECT time_from_days($days);
diff --git a/src/trace_processor/perfetto_sql/stdlib/graphs/scan.sql b/src/trace_processor/perfetto_sql/stdlib/graphs/scan.sql
index 320a73b..b272af2 100644
--- a/src/trace_processor/perfetto_sql/stdlib/graphs/scan.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/graphs/scan.sql
@@ -51,11 +51,10 @@
 (
   select
     c0 as id,
-    __intrinsic_token_zip_join!(
-      (c1, c2, c3, c4, c5, c6, c7),
-      $scan_columns,
+    __intrinsic_token_apply!(
       _graph_scan_select,
-      __intrinsic_token_comma!()
+      (c1, c2, c3, c4, c5, c6, c7),
+      $scan_columns
     )
   from  __intrinsic_table_ptr(__intrinsic_graph_scan(
     (
@@ -65,24 +64,22 @@
     (
       select __intrinsic_row_dataframe_agg(
         'id', init_table.id,
-        __intrinsic_token_zip_join!(
-          $scan_columns,
-          $scan_columns,
+        __intrinsic_token_apply!(
           _graph_scan_df_agg,
-          __intrinsic_token_comma!()
+          $scan_columns,
+          $scan_columns
         )
       )
       from $init_table AS init_table
     ),
-    __intrinsic_stringify!($step_query, table),
+    __intrinsic_stringify_ignore_table!($step_query),
     __intrinsic_stringify!($scan_columns)
   )) result
   where __intrinsic_table_ptr_bind(result.c0, 'id')
-    and __intrinsic_token_zip_join!(
-          (c1, c2, c3, c4, c5, c6, c7),
-          $scan_columns,
+    and __intrinsic_token_apply_and!(
           _graph_scan_bind,
-          AND
+          (c1, c2, c3, c4, c5, c6, c7),
+          $scan_columns
         )
 );
 
@@ -113,11 +110,10 @@
 (
   select
     c0 as id,
-    __intrinsic_token_zip_join!(
-      (c1, c2, c3, c4, c5, c6, c7),
-      $agg_columns,
+    __intrinsic_token_apply!(
       _graph_scan_select,
-      __intrinsic_token_comma!()
+      (c1, c2, c3, c4, c5, c6, c7),
+      $agg_columns
     )
   from  __intrinsic_table_ptr(__intrinsic_graph_aggregating_scan(
     (
@@ -127,23 +123,21 @@
     (
       select __intrinsic_row_dataframe_agg(
         'id', init_table.id,
-        __intrinsic_token_zip_join!(
-          $agg_columns,
-          $agg_columns,
+        __intrinsic_token_apply!(
           _graph_scan_df_agg,
-          __intrinsic_token_comma!()
+          $agg_columns,
+          $agg_columns
         )
       )
       from $init_table AS init_table
     ),
-    __intrinsic_stringify!($agg_query, table),
+    __intrinsic_stringify_ignore_table!($agg_query),
     __intrinsic_stringify!($agg_columns)
   )) result
   where __intrinsic_table_ptr_bind(result.c0, 'id')
-    and __intrinsic_token_zip_join!(
-          (c1, c2, c3, c4, c5, c6, c7),
-          $agg_columns,
+    and __intrinsic_token_apply_and!(
           _graph_scan_bind,
-          AND
+          (c1, c2, c3, c4, c5, c6, c7),
+          $agg_columns
         )
 );
diff --git a/src/trace_processor/perfetto_sql/stdlib/intervals/intersect.sql b/src/trace_processor/perfetto_sql/stdlib/intervals/intersect.sql
index 08d9808..c62a1a5 100644
--- a/src/trace_processor/perfetto_sql/stdlib/intervals/intersect.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/intervals/intersect.sql
@@ -13,8 +13,6 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 
-INCLUDE PERFETTO MODULE metasql.table_list;
-
 CREATE PERFETTO MACRO _ii_df_agg(x Expr, y Expr)
 RETURNS Expr AS __intrinsic_stringify!($x), input.$y;
 
@@ -37,11 +35,10 @@
     input.id,
     input.ts,
     input.dur
-    __intrinsic_prefixed_token_zip_join!(
-      $agg_columns,
-      $agg_columns,
+    __intrinsic_token_apply_prefix!(
       _ii_df_agg,
-      __intrinsic_token_comma!()
+      $agg_columns,
+      $agg_columns
     )
   )
   FROM (SELECT * FROM $tab ORDER BY ts) input
@@ -57,24 +54,25 @@
     c0 AS ts,
     c1 AS dur,
     -- Columns for tables ids, in the order of provided tables.
-    __intrinsic_token_zip_join!(
-      (c2 AS id_0, c3 AS id_1, c4 AS id_2, c5 AS id_3, c6 AS id_4),
-      $tabs,
+    __intrinsic_token_apply!(
       __first_arg,
-      __intrinsic_token_comma!()
+      (c2 AS id_0, c3 AS id_1, c4 AS id_2, c5 AS id_3, c6 AS id_4),
+      $tabs
     )
-    -- Columns for partitions, one for each column with partition. Prefixed to
-    -- handle case of no partitions.
-    __intrinsic_prefixed_token_zip_join!(
-      (c7, c8, c9, c10),
-      $agg_columns,
+    -- Columns for partitions, one for each column with partition.
+    __intrinsic_token_apply_prefix!(
       _ii_df_select,
-      __intrinsic_token_comma!()
+      (c7, c8, c9, c10),
+      $agg_columns
     )
   -- Interval intersect result table.
   FROM __intrinsic_table_ptr(
     __intrinsic_interval_intersect(
-      _metasql_map_join_table_list_with_capture!($tabs, _interval_agg, ($agg_columns)),
+      __intrinsic_token_apply!(
+        _interval_agg,
+        $tabs,
+        ($agg_columns, $agg_columns, $agg_columns, $agg_columns, $agg_columns)
+      ),
       __intrinsic_stringify!($agg_columns)
     )
   )
@@ -89,12 +87,11 @@
     AND __intrinsic_table_ptr_bind(c5, 'id_3')
     AND __intrinsic_table_ptr_bind(c6, 'id_4')
 
-    -- Partition columns. Prefixed to handle case of no partitions.
-    __intrinsic_prefixed_token_zip_join!(
-      (c7, c8, c9, c10),
-      $agg_columns,
+    -- Partition columns.
+    __intrinsic_token_apply_and_prefix!(
       _ii_df_bind,
-      AND
+      (c7, c8, c9, c10),
+      $agg_columns
     )
 );
 
diff --git a/src/trace_processor/perfetto_sql/stdlib/intervals/overlap.sql b/src/trace_processor/perfetto_sql/stdlib/intervals/overlap.sql
index cd84164..356ec31 100644
--- a/src/trace_processor/perfetto_sql/stdlib/intervals/overlap.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/intervals/overlap.sql
@@ -13,6 +13,8 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 
+INCLUDE PERFETTO MODULE intervals.intersect;
+
 -- Compute the distribution of the overlap of the given intervals over time.
 --
 -- Each interval is a (ts, dur) pair and the overlap represented as a (ts, value)
@@ -94,6 +96,8 @@
 
 -- Merges a |roots_table| and |children_table| into one table. See _intervals_flatten
 -- that accepts the output of this macro to flatten intervals.
+
+-- See: _intervals_merge_root_and_children_by_intersection.
 CREATE PERFETTO MACRO _intervals_merge_root_and_children(
   -- Table or subquery containing all the root intervals: (id, ts, dur).
   -- Note that parent_id is not necessary in this table as it will be NULL anyways.
@@ -141,6 +145,46 @@
     JOIN _roots USING(root_id)
 );
 
+-- Merges a |roots_table| and |children_table| into one table. See _intervals_flatten
+-- that accepts the output of this macro to flatten intervals.
+
+-- This is very similar to _intervals_merge_root_and_children but there is no explicit
+-- root_id shared between the root and the children. Instead an _interval_intersect is
+-- used to derive the root and child relationships.
+CREATE PERFETTO MACRO _intervals_merge_root_and_children_by_intersection(
+  -- Table or subquery containing all the root intervals: (id, ts, dur).
+  -- Note that parent_id is not necessary in this table as it will be NULL anyways.
+  roots_table TableOrSubquery,
+  -- Table or subquery containing all the child intervals:
+  -- (root_id, id, parent_id, ts, dur)
+  children_table TableOrSubquery,
+  -- intersection key used in deriving the root child relationships.
+  key ColumnName)
+RETURNS TableOrSubQuery
+AS (
+  WITH
+    _roots AS (
+      SELECT * FROM $roots_table WHERE dur > 0 ORDER BY ts
+    ),
+    _children AS (
+      SELECT * FROM $children_table WHERE dur > 0 ORDER BY ts
+    )
+    SELECT
+      ii.ts,
+      ii.dur,
+      _children.id,
+      IIF(_children.parent_id IS NULL, id_1, _children.parent_id) AS parent_id,
+      _roots.id AS root_id,
+      _roots.ts AS root_ts,
+      _roots.dur AS root_dur,
+      ii.$key
+    FROM _interval_intersect!((_children, _roots), ($key)) ii
+    JOIN _children
+      ON _children.id = id_0
+    JOIN _roots
+      ON _roots.id = id_1
+);
+
 -- Partition and flatten a hierarchy of intervals into non-overlapping intervals where
 -- each resulting interval is the leaf in the hierarchy at any given time. The result also
 -- denotes the 'self-time' of each interval.
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/linux/BUILD.gn
index 23371df..95f49d8 100644
--- a/src/trace_processor/perfetto_sql/stdlib/linux/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/BUILD.gn
@@ -15,7 +15,10 @@
 import("../../../../../gn/perfetto_sql.gni")
 
 perfetto_sql_source_set("linux") {
-  sources = [ "threads.sql" ]
+  sources = [
+    "devfreq.sql",
+    "threads.sql",
+  ]
   deps = [
     "cpu",
     "memory",
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/BUILD.gn
index 42654eb..60d462d 100644
--- a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/BUILD.gn
@@ -19,6 +19,7 @@
     "frequency.sql",
     "idle.sql",
     "idle_stats.sql",
+    "idle_time_in_state.sql",
   ]
   deps = [ "utilization" ]
 }
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/frequency.sql b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/frequency.sql
index 4d1c566..8821502 100644
--- a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/frequency.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/frequency.sql
@@ -29,6 +29,8 @@
   -- Frequency in kHz of the CPU that corresponds to this counter. NULL if not
   -- found or undefined.
   freq INT,
+  -- Unique CPU id.
+  ucpu INT,
   -- CPU that corresponds to this counter.
   cpu INT
 ) AS
@@ -38,6 +40,7 @@
   count_w_dur.ts,
   count_w_dur.dur,
   cast_int!(count_w_dur.value) as freq,
+  cct.ucpu,
   cct.cpu
 FROM
 counter_leading_intervals!((
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle_time_in_state.sql b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle_time_in_state.sql
new file mode 100644
index 0000000..8bff4b3
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle_time_in_state.sql
@@ -0,0 +1,74 @@
+--
+-- 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 time.conversion;
+
+-- Counter information for sysfs cpuidle states.
+-- Tracks the percentage of time spent in each state between two timestamps, by
+-- dividing the incremental time spent in one state, by time all CPUS spent in
+-- any state.
+CREATE PERFETTO TABLE cpu_idle_time_in_state_counters(
+  -- Timestamp.
+  ts LONG,
+  -- State name.
+  state_name STRING,
+  -- Percentage of time all CPUS spent in this state.
+  idle_percentage DOUBLE,
+  -- Incremental time spent in this state (residency), in microseconds.
+  total_residency DOUBLE,
+  -- Time all CPUS spent in any state, in microseconds.
+  time_slice INT
+) AS
+WITH residency_deltas AS (
+  SELECT
+  ts,
+  c.name as state_name,
+  value - (LAG(value) OVER (PARTITION BY c.name, cct.cpu ORDER BY ts)) as delta
+  FROM counters c
+  JOIN cpu_counter_track cct on c.track_id=cct.id
+  WHERE c.name GLOB 'cpuidle.*'
+),
+total_residency_calc AS (
+SELECT
+  ts,
+  state_name,
+  sum(delta) as total_residency,
+  -- Perfetto timestamp is in nanoseconds whereas sysfs cpuidle time
+  -- is in microseconds.
+  (
+    (SELECT count(distinct cpu) from cpu_counter_track) *
+    (time_to_us(ts - LAG(ts,1) over (partition by state_name order by ts)))
+  )  as time_slice
+  FROM residency_deltas
+GROUP BY ts, state_name
+)
+SELECT
+  ts,
+  state_name,
+  MIN(100, (total_residency / time_slice) * 100) as idle_percentage,
+  total_residency,
+  time_slice
+FROM total_residency_calc
+WHERE time_slice IS NOT NULL
+UNION ALL
+-- Calculate c0 state by subtracting all other states from total time.
+SELECT
+  ts,
+  'cpuidle.C0' as state_name,
+  (MAX(0,time_slice - SUM(total_residency)) / time_slice) * 100 AS idle_percentage,
+  time_slice - SUM(total_residency),
+  time_slice
+FROM total_residency_calc
+WHERE time_slice IS NOT NULL
+GROUP BY ts;
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/BUILD.gn
index 55294e7..2d51ae8 100644
--- a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/BUILD.gn
@@ -18,6 +18,7 @@
   sources = [
     "general.sql",
     "process.sql",
+    "slice.sql",
     "system.sql",
     "thread.sql",
   ]
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/general.sql b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/general.sql
index e8d05e2..d47bf20 100644
--- a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/general.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/general.sql
@@ -82,19 +82,35 @@
     ts,
     dur,
     cpu,
+    ucpu,
     freq
-FROM cpu_frequency_counters;
+FROM cpu_frequency_counters
+WHERE freq IS NOT NULL;
 
 CREATE PERFETTO VIEW _sched_without_id AS
-SELECT ts, dur, utid, cpu
+SELECT ts, dur, utid, ucpu
 FROM sched
 WHERE utid != 0 AND dur != -1;
 
 CREATE VIRTUAL TABLE _cpu_freq_per_thread_span_join
 USING SPAN_LEFT_JOIN(
-    _sched_without_id PARTITIONED cpu,
-    _cpu_freq_for_metrics PARTITIONED cpu);
+    _sched_without_id PARTITIONED ucpu,
+    _cpu_freq_for_metrics PARTITIONED ucpu);
 
-CREATE PERFETTO TABLE _cpu_freq_per_thread
-AS SELECT * FROM _cpu_freq_per_thread_span_join;
+CREATE PERFETTO TABLE _cpu_freq_per_thread_no_id
+AS
+SELECT *
+FROM _cpu_freq_per_thread_span_join;
+
+CREATE PERFETTO VIEW _cpu_freq_per_thread AS
+SELECT
+  _auto_id AS id,
+  ts,
+  dur,
+  ucpu,
+  cpu,
+  utid,
+  freq,
+  id AS counter_id
+FROM _cpu_freq_per_thread_no_id;
 
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/process.sql b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/process.sql
index 3b998d4..48d2efc 100644
--- a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/process.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/process.sql
@@ -14,7 +14,9 @@
 -- limitations under the License.
 
 INCLUDE PERFETTO MODULE linux.cpu.utilization.general;
+
 INCLUDE PERFETTO MODULE time.conversion;
+INCLUDE PERFETTO MODULE intervals.intersect;
 
 -- Returns a table of process utilization per given period.
 -- Utilization is calculated as sum of average utilization of each CPU in each
@@ -88,17 +90,55 @@
   -- Average CPU frequency in kHz
   avg_freq INT
 ) AS
-WITH threads AS (
-  SELECT upid, utid FROM thread
-)
 SELECT
   upid,
-  cast_int!(SUM(dur * freq) / 1000) AS millicycles,
-  cast_int!(SUM(dur * freq) / 1000 / 1e9) AS megacycles,
+  cast_int!(SUM(dur * freq / 1000)) AS millicycles,
+  cast_int!(SUM(dur * freq / 1000) / 1e9) AS megacycles,
   SUM(dur) AS runtime,
   MIN(freq) AS min_freq,
   MAX(freq) AS max_freq,
-  cast_int!(SUM((dur * freq) / 1000) / SUM(dur / 1000)) AS avg_freq
+  cast_int!(SUM((dur * freq / 1000)) / SUM(dur / 1000)) AS avg_freq
 FROM _cpu_freq_per_thread
-JOIN threads USING (utid)
-GROUP BY upid;
\ No newline at end of file
+JOIN thread USING (utid)
+WHERE upid IS NOT NULL
+GROUP BY upid;
+
+-- Aggregated CPU statistics for each process in a provided interval.
+CREATE PERFETTO FUNCTION cpu_cycles_per_process_in_interval(
+    -- Start of the interval.
+    ts INT,
+    -- Duration of the interval.
+    dur INT
+)
+RETURNS TABLE(
+  -- Unique process id. Joinable with `process.id`.
+  upid INT,
+  -- Sum of CPU millicycles
+  millicycles INT,
+  -- Sum of CPU megacycles
+  megacycles INT,
+  -- Total runtime duration
+  runtime INT,
+  -- Minimum CPU frequency in kHz
+  min_freq INT,
+  -- Maximum CPU frequency in kHz
+  max_freq INT,
+  -- Average CPU frequency in kHz
+  avg_freq INT
+) AS
+WITH threads_counters AS (
+  SELECT c.id, c.ts, c.dur, c.freq, upid
+  FROM _cpu_freq_per_thread c
+  JOIN thread USING (utid)
+)
+SELECT
+  upid,
+  cast_int!(SUM(ii.dur * freq / 1000)) AS millicycles,
+  cast_int!(SUM(ii.dur * freq / 1000) / 1e9) AS megacycles,
+  SUM(ii.dur) AS runtime,
+  MIN(freq) AS min_freq,
+  MAX(freq) AS max_freq,
+  cast_int!(SUM((ii.dur * freq / 1000)) / SUM(ii.dur / 1000)) AS avg_freq
+FROM _interval_intersect_single!($ts, $dur, threads_counters) ii
+JOIN threads_counters USING (id)
+GROUP BY upid;
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/slice.sql b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/slice.sql
new file mode 100644
index 0000000..9cbfcd8
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/slice.sql
@@ -0,0 +1,126 @@
+--
+-- 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 linux.cpu.utilization.general;
+
+INCLUDE PERFETTO MODULE time.conversion;
+INCLUDE PERFETTO MODULE intervals.intersect;
+INCLUDE PERFETTO MODULE slices.with_context;
+
+-- CPU cycles per each slice.
+CREATE PERFETTO TABLE cpu_cycles_per_thread_slice(
+  -- Id of a slice. Alias of `slice.id`.
+  id INT,
+  -- Name of the slice.
+  name STRING,
+  -- Id of the thread the slice is running on. Alias of `thread.id`.
+  utid INT,
+  -- Name of the thread.
+  thread_name STRING,
+  -- Id of the process the slice is running on. Alias of `process.id`.
+  upid INT,
+  -- Name of the process.
+  process_name STRING,
+  -- Sum of CPU millicycles. Null if frequency couldn't be fetched for any
+  -- period during the runtime of the slice.
+  millicycles INT,
+  -- Sum of CPU megacycles. Null if frequency couldn't be fetched for any
+  -- period during the runtime of the slice.
+  megacycles INT
+) AS
+WITH intersected AS (
+  SELECT
+    id_0 AS slice_id,
+    ii.utid,
+    sum(ii.dur) AS dur,
+    cast_int!(SUM(ii.dur * freq / 1000)) AS millicycles,
+    cast_int!(SUM(ii.dur * freq / 1000) / 1e9) AS megacycles
+  FROM _interval_intersect!(
+    ((SELECT * FROM thread_slice WHERE dur > 0 AND utid > 0),
+    _cpu_freq_per_thread), (utid)) ii
+  JOIN _cpu_freq_per_thread f ON f.id = ii.id_1
+  WHERE freq IS NOT NULL
+  GROUP BY slice_id
+)
+SELECT
+  id,
+  ts.name,
+  ts.utid,
+  ts.thread_name,
+  ts.upid,
+  ts.process_name,
+  millicycles,
+  megacycles
+FROM thread_slice ts
+LEFT JOIN intersected ON slice_id = ts.id AND ts.dur = intersected.dur;
+
+-- CPU cycles per each slice in interval.
+CREATE PERFETTO FUNCTION cpu_cycles_per_thread_slice_in_interval(
+    -- Start of the interval.
+    ts INT,
+    -- Duration of the interval.
+    dur INT
+)
+RETURNS TABLE(
+  -- Id of a slice. Alias of `slice.id`.
+  id INT,
+  -- Name of the slice.
+  name STRING,
+  -- Id of the thread the slice is running on. Alias of `thread.id`.
+  utid INT,
+  -- Name of the thread.
+  thread_name STRING,
+  -- Id of the process the slice is running on. Alias of `process.id`.
+  upid INT,
+  -- Name of the process.
+  process_name STRING,
+  -- Sum of CPU millicycles. Null if frequency couldn't be fetched for any
+  -- period during the runtime of the slice.
+  millicycles INT,
+  -- Sum of CPU megacycles. Null if frequency couldn't be fetched for any
+  -- period during the runtime of the slice.
+  megacycles INT
+) AS
+WITH cut_thread_slice AS (
+  SELECT id, ii.ts, ii.dur, thread_slice.*
+  FROM _interval_intersect_single!(
+    $ts, $dur, 
+    (SELECT * FROM thread_slice WHERE dur > 0 AND utid > 0)) ii
+  JOIN thread_slice USING (id)
+),
+intersected AS (
+  SELECT
+    id_0 AS slice_id,
+    ii.utid,
+    sum(ii.dur) AS dur,
+    cast_int!(SUM(ii.dur * freq / 1000)) AS millicycles,
+    cast_int!(SUM(ii.dur * freq / 1000) / 1e9) AS megacycles
+  FROM _interval_intersect!(
+    (cut_thread_slice, _cpu_freq_per_thread), (utid)) ii
+  JOIN _cpu_freq_per_thread f ON f.id = ii.id_1
+  WHERE freq IS NOT NULL
+  GROUP BY slice_id
+)
+SELECT
+  id,
+  ts.name,
+  ts.utid,
+  ts.thread_name,
+  ts.upid,
+  ts.process_name,
+  millicycles,
+  megacycles
+FROM cut_thread_slice ts
+LEFT JOIN intersected ON slice_id = ts.id AND ts.dur = intersected.dur;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/system.sql b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/system.sql
index 97e96b9..413f0f3 100644
--- a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/system.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/system.sql
@@ -15,6 +15,7 @@
 
 INCLUDE PERFETTO MODULE linux.cpu.utilization.general;
 INCLUDE PERFETTO MODULE time.conversion;
+INCLUDE PERFETTO MODULE intervals.intersect;
 
 -- The purpose of this module is to provide high level aggregates of system
 -- utilization, akin to /proc/stat results.
@@ -66,70 +67,126 @@
   unnormalized_utilization
 FROM cpu_utilization_per_period(time_from_s(1));
 
--- Aggregated CPU statistics for runtime of each thread on a CPU.
-CREATE PERFETTO TABLE _cpu_cycles_raw(
-  -- The id of CPU
-  cpu INT,
-  -- Unique thread id
-  utid INT,
-  -- Sum of CPU millicycles
+-- Aggregated CPU statistics for whole trace. Results in only one row.
+CREATE PERFETTO TABLE cpu_cycles(
+  -- Sum of CPU millicycles.
   millicycles INT,
-  -- Sum of CPU megacycles
+  -- Sum of CPU megacycles.
   megacycles INT,
-  -- Total runtime duration
+  -- Total runtime of all threads running on all CPUs.
   runtime INT,
-  -- Minimum CPU frequency in kHz
+  -- Minimum CPU frequency in kHz.
   min_freq INT,
-  -- Maximum CPU frequency in kHz
+  -- Maximum CPU frequency in kHz.
   max_freq INT,
-  -- Average CPU frequency in kHz
+  -- Average CPU frequency in kHz.
   avg_freq INT
 ) AS
 SELECT
-  cpu,
-  utid,
-  -- We divide by 1e3 here as dur is in ns and freq in khz. In total
-  -- this means we need to divide the duration by 1e9 and multiply the
-  -- frequency by 1e3 then multiply again by 1e3 to get millicycles
-  -- i.e. divide by 1e3 in total.
-  -- We use millicycles as we want to preserve this level of precision
-  -- for future calculations.
-  cast_int!(SUM(dur * freq) / 1000) AS millicycles,
-  cast_int!(SUM(dur * freq) / 1000 / 1e9) AS megacycles,
+  cast_int!(SUM(dur * freq / 1000)) AS millicycles,
+  cast_int!(SUM(dur * freq / 1000) / 1e9) AS megacycles,
   SUM(dur) AS runtime,
   MIN(freq) AS min_freq,
   MAX(freq) AS max_freq,
-  -- We choose to work in micros space in both the numerator and
-  -- denominator as this gives us good enough precision without risking
-  -- overflows.
-  cast_int!(SUM((dur * freq) / 1000) / SUM(dur / 1000)) AS avg_freq
-FROM _cpu_freq_per_thread
-GROUP BY utid, cpu;
+  cast_int!(SUM((dur * freq / 1000)) / SUM(dur / 1000)) AS avg_freq
+FROM _cpu_freq_per_thread;
+
+-- Aggregated CPU statistics in a provided interval. Results in one row.
+CREATE PERFETTO FUNCTION cpu_cycles_in_interval(
+    -- Start of the interval.
+    ts INT,
+    -- Duration of the interval.
+    dur INT
+)
+RETURNS TABLE(
+  -- Sum of CPU millicycles.
+  millicycles INT,
+  -- Sum of CPU megacycles.
+  megacycles INT,
+  -- Total runtime of all threads running on all CPUs.
+  runtime INT,
+  -- Minimum CPU frequency in kHz.
+  min_freq INT,
+  -- Maximum CPU frequency in kHz.
+  max_freq INT,
+  -- Average CPU frequency in kHz.
+  avg_freq INT
+) AS
+SELECT
+  cast_int!(SUM(ii.dur * freq / 1000)) AS millicycles,
+  cast_int!(SUM(ii.dur * freq / 1000) / 1e9) AS megacycles,
+  SUM(ii.dur) AS runtime,
+  MIN(freq) AS min_freq,
+  MAX(freq) AS max_freq,
+  cast_int!(SUM((ii.dur * freq / 1000)) / SUM(ii.dur / 1000)) AS avg_freq
+FROM _interval_intersect_single!($ts, $dur, _cpu_freq_per_thread) ii
+JOIN _cpu_freq_per_thread USING (id);
 
 -- Aggregated CPU statistics for each CPU.
 CREATE PERFETTO TABLE cpu_cycles_per_cpu(
-  -- The id of CPU
+  -- Unique CPU id. Joinable with `cpu.id`.
+  ucpu INT,
+  -- The number of the CPU. Might not be the same as ucpu in multi machine cases.
   cpu INT,
-  -- Sum of CPU millicycles
+  -- Sum of CPU millicycles.
   millicycles INT,
-  -- Sum of CPU megacycles
+  -- Sum of CPU megacycles.
   megacycles INT,
-  -- Total runtime of all threads running on CPU
+  -- Total runtime of all threads running on CPU.
   runtime INT,
-  -- Minimum CPU frequency in kHz
+  -- Minimum CPU frequency in kHz.
   min_freq INT,
-  -- Maximum CPU frequency in kHz
+  -- Maximum CPU frequency in kHz.
   max_freq INT,
-  -- Average CPU frequency in kHz
+  -- Average CPU frequency in kHz.
   avg_freq INT
 ) AS
 SELECT
+  ucpu,
   cpu,
-  cast_int!(SUM(dur * freq) / 1000) AS millicycles,
-  cast_int!(SUM(dur * freq) / 1000 / 1e9) AS megacycles,
+  cast_int!(SUM(dur * freq / 1000)) AS millicycles,
+  cast_int!(SUM(dur * freq / 1000) / 1e9) AS megacycles,
   SUM(dur) AS runtime,
   MIN(freq) AS min_freq,
   MAX(freq) AS max_freq,
-  cast_int!(SUM((dur * freq) / 1000) / SUM(dur / 1000)) AS avg_freq
+  cast_int!(SUM((dur * freq / 1000)) / SUM(dur / 1000)) AS avg_freq
 FROM _cpu_freq_per_thread
-GROUP BY cpu;
\ No newline at end of file
+GROUP BY ucpu;
+
+-- Aggregated CPU statistics for each CPU in a provided interval.
+CREATE PERFETTO FUNCTION cpu_cycles_per_cpu_in_interval(
+    -- Start of the interval.
+    ts INT,
+    -- Duration of the interval.
+    dur INT
+)
+RETURNS TABLE(
+  -- Unique CPU id. Joinable with `cpu.id`.
+  ucpu INT,
+  -- CPU number.
+  cpu INT,
+  -- Sum of CPU millicycles.
+  millicycles INT,
+  -- Sum of CPU megacycles.
+  megacycles INT,
+  -- Total runtime of all threads running on CPU.
+  runtime INT,
+  -- Minimum CPU frequency in kHz.
+  min_freq INT,
+  -- Maximum CPU frequency in kHz.
+  max_freq INT,
+  -- Average CPU frequency in kHz.
+  avg_freq INT
+) AS
+SELECT
+  ucpu,
+  cpu,
+  cast_int!(SUM(ii.dur * freq / 1000)) AS millicycles,
+  cast_int!(SUM(ii.dur * freq / 1000) / 1e9) AS megacycles,
+  SUM(ii.dur) AS runtime,
+  MIN(freq) AS min_freq,
+  MAX(freq) AS max_freq,
+  cast_int!(SUM((ii.dur * freq / 1000)) / SUM(ii.dur / 1000)) AS avg_freq
+FROM _interval_intersect_single!($ts, $dur, _cpu_freq_per_thread) ii
+JOIN _cpu_freq_per_thread USING (id)
+GROUP BY ucpu;
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/thread.sql b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/thread.sql
index 376c06d..defa9e8 100644
--- a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/thread.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/thread.sql
@@ -15,6 +15,7 @@
 
 INCLUDE PERFETTO MODULE linux.cpu.utilization.general;
 INCLUDE PERFETTO MODULE time.conversion;
+INCLUDE PERFETTO MODULE intervals.intersect;
 
 -- Returns a table of thread utilization per given period.
 -- Utilization is calculated as sum of average utilization of each CPU in each
@@ -88,11 +89,46 @@
 ) AS
 SELECT
   utid,
-  cast_int!(SUM(dur * freq) / 1000) AS millicycles,
-  cast_int!(SUM(dur * freq) / 1000 / 1e9) AS megacycles,
+  cast_int!(SUM(dur * freq / 1000)) AS millicycles,
+  cast_int!(SUM(dur * freq / 1000) / 1e9) AS megacycles,
   SUM(dur) AS runtime,
   MIN(freq) AS min_freq,
   MAX(freq) AS max_freq,
-  cast_int!(SUM((dur * freq) / 1000) / SUM(dur / 1000)) AS avg_freq
+  cast_int!(SUM((dur * freq / 1000)) / SUM(dur / 1000)) AS avg_freq
 FROM _cpu_freq_per_thread
-GROUP BY utid;
\ No newline at end of file
+GROUP BY utid;
+
+-- Aggregated CPU statistics for each thread in a provided interval.
+CREATE PERFETTO FUNCTION cpu_cycles_per_thread_in_interval(
+    -- Start of the interval.
+    ts INT,
+    -- Duration of the interval.
+    dur INT
+)
+RETURNS TABLE(
+  -- Unique thread id. Joinable with `thread.id`.
+  utid INT,
+  -- Sum of CPU millicycles
+  millicycles INT,
+  -- Sum of CPU megacycles
+  megacycles INT,
+  -- Total runtime duration
+  runtime INT,
+  -- Minimum CPU frequency in kHz
+  min_freq INT,
+  -- Maximum CPU frequency in kHz
+  max_freq INT,
+  -- Average CPU frequency in kHz
+  avg_freq INT
+) AS
+SELECT
+  utid,
+  cast_int!(SUM(ii.dur * freq / 1000)) AS millicycles,
+  cast_int!(SUM(ii.dur * freq / 1000 )/ 1e9) AS megacycles,
+  SUM(ii.dur) AS runtime,
+  MIN(freq) AS min_freq,
+  MAX(freq) AS max_freq,
+  cast_int!(SUM((ii.dur * freq / 1000)) / SUM(ii.dur / 1000)) AS avg_freq
+FROM _interval_intersect_single!($ts, $dur, _cpu_freq_per_thread) ii
+JOIN _cpu_freq_per_thread c USING (id)
+GROUP BY utid;
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/devfreq.sql b/src/trace_processor/perfetto_sql/stdlib/linux/devfreq.sql
new file mode 100644
index 0000000..7fa09bc
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/devfreq.sql
@@ -0,0 +1,62 @@
+--
+-- 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 counters.intervals;
+
+-- Gets devfreq frequency counter based on device queried. These counters will
+-- only be available if the "devfreq/devfreq_frequency" ftrace event is enabled.
+CREATE PERFETTO FUNCTION _get_devfreq_counters(
+  -- Devfreq name to query for.
+  device_name STRING
+)
+RETURNS TABLE(
+  -- Unique identifier for this counter.
+  id INT,
+  -- Starting timestamp of the counter.
+  ts LONG,
+  -- Duration in which counter is constant and frequency doesn't chamge.
+  dur INT,
+  -- Frequency in kHz of the device that corresponds to the counter.
+  freq INT
+) AS
+SELECT
+  count_w_dur.id,
+  count_w_dur.ts,
+  count_w_dur.dur,
+  cast_int!(count_w_dur.value) as freq
+FROM counter_leading_intervals!((
+  SELECT c.*
+  FROM counter c
+  JOIN track t ON t.id = c.track_id
+  WHERE t.classification = 'linux_device_frequency'
+    AND EXTRACT_ARG(t.dimension_arg_set_id, 'device_name') = $device_name
+)) AS count_w_dur;
+
+-- ARM DSU device frequency counters. This table will only be populated on
+-- traces collected with "devfreq/devfreq_frequency" ftrace event enabled,
+-- and from ARM devices with the DSU (DynamIQ Shared Unit) hardware.
+CREATE PERFETTO TABLE linux_devfreq_dsu_counter(
+  -- Unique identifier for this counter.
+  id INT,
+  -- Starting timestamp of the counter.
+  ts LONG,
+  -- Duration in which counter is constant and frequency doesn't chamge.
+  dur INT,
+  -- Frequency in kHz of the device that corresponds to the counter.
+  dsu_freq INT
+) AS
+SELECT
+  id, ts, dur, freq as dsu_freq
+FROM _get_devfreq_counters("dsufreq");
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/perf/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/linux/perf/BUILD.gn
index ca58438..73df9a8 100644
--- a/src/trace_processor/perfetto_sql/stdlib/linux/perf/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/perf/BUILD.gn
@@ -15,5 +15,8 @@
 import("../../../../../../gn/perfetto_sql.gni")
 
 perfetto_sql_source_set("perf") {
-  sources = [ "samples.sql" ]
+  sources = [
+    "samples.sql",
+    "spe.sql",
+  ]
 }
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql b/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql
index 1a22bc1..2584316 100644
--- a/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/perf/samples.sql
@@ -15,27 +15,48 @@
 
 INCLUDE PERFETTO MODULE callstacks.stack_profile;
 
-CREATE PERFETTO MACRO _linux_perf_callstacks_for_samples(
-  samples TableOrSubquery
-)
-RETURNS TableOrSubquery
-AS
-(
-  WITH metrics AS MATERIALIZED (
-    SELECT
-      callsite_id,
-      COUNT() AS self_count
-    FROM $samples
-    GROUP BY callsite_id
-  )
-  SELECT
-    c.id,
-    c.parent_id,
-    c.name,
-    c.mapping_name,
-    c.source_file,
-    c.line_number,
-    IFNULL(m.self_count, 0) AS self_count
-  FROM _callstacks_for_stack_profile_samples!(metrics) c
-  LEFT JOIN metrics m USING (callsite_id)
-);
+CREATE PERFETTO TABLE _linux_perf_raw_callstacks AS
+SELECT *
+FROM _callstacks_for_callsites!((
+  SELECT p.callsite_id
+  FROM perf_sample p
+)) c
+ORDER BY c.id;
+
+-- Table summarising the callstacks captured during all
+-- perf 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 linux_perf_samples_summary_tree(
+  -- The id of the callstack. A callstack in this context
+  -- is a unique set of frames up to the root.
+  id INT,
+  -- The id of the parent callstack for this callstack.
+  parent_id INT,
+  -- 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, JAR or APK.
+  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 INT,
+  -- The number of samples with this function as the leaf
+  -- frame.
+  self_count INT,
+  -- The number of samples with this function appearing
+  -- anywhere on the callstack.
+  cumulative_count INT
+) AS
+SELECT r.*, a.cumulative_count
+FROM _callstacks_self_to_cumulative!((
+  SELECT id, parent_id, self_count
+  FROM _linux_perf_raw_callstacks
+)) a
+JOIN _linux_perf_raw_callstacks r USING (id)
+ORDER BY r.id;
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/perf/spe.sql b/src/trace_processor/perfetto_sql/stdlib/linux/perf/spe.sql
new file mode 100644
index 0000000..d16b0ca
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/perf/spe.sql
@@ -0,0 +1,125 @@
+--
+-- 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.
+
+-- Contains ARM Statistical Profiling Extension records
+CREATE PERFETTO VIEW linux_perf_spe_record(
+  -- Timestap when the operation was sampled
+  ts LONG,
+  -- Thread the operation executed in
+  utid INT,
+  -- Exception level the instruction was executed in
+  exception_level STRING,
+  -- Instruction virtual address
+  instruction_frame_id INT,
+  -- Type of operation sampled
+  operation STRING,
+  -- The virtual address accessed by the operation (0 if no memory access was
+  -- performed)
+  data_virtual_address LONG,
+  -- The physical address accessed by the operation (0 if no memory access was
+  -- performed)
+  data_physical_address LONG,
+  -- Cycle count from the operation being dispatched for issue to the operation
+  -- being complete.
+  total_latency INT,
+  -- Cycle count from the operation being dispatched for issue to the operation
+  -- being issued for execution.
+  issue_latency INT,
+  -- Cycle count from a virtual address being passed to the MMU for translation
+  -- to the result of the translation being available.
+  translation_latency INT,
+  -- Where the data returned for a load operation was sourced
+  data_source STRING,
+  -- Operation generated an exception
+  exception_gen BOOL,
+  -- Operation architecturally retired
+  retired BOOL,
+  -- Operation caused a level 1 data cache access
+  l1d_access BOOL,
+  -- Operation caused a level 1 data cache refill
+  l1d_refill BOOL,
+  -- Operation caused a TLB access
+  tlb_access BOOL,
+  -- Operation caused a TLB refill involving at least one translation table walk
+  tlb_refill BOOL,
+  -- Conditional instruction failed its condition code check
+  not_taken BOOL,
+  -- Whether a branch caused a correction to the predicted program flow
+  mispred BOOL,
+  -- Operation caused a last level data or unified cache access
+  llc_access BOOL,
+  -- Whether the operation could not be completed by the last level data cache
+  -- (or any above)
+  llc_refill BOOL,
+  -- Operation caused an access to another socket in a multi-socket system
+  remote_access BOOL,
+  -- Operation that incurred additional latency due to the alignment of the
+  -- address and the size of the data being accessed
+  alignment BOOL,
+  -- Whether the operation executed in transactional state
+  tme_transaction BOOL,
+  -- SVE or SME operation with at least one false element in the governing
+  -- predicate(s)
+  sve_partial_pred BOOL,
+  -- SVE or SME operation with no true element in the governing predicate(s)
+  sve_empty_pred BOOL,
+  -- Whether a load operation caused a cache access to at least the level 2 data
+  -- or unified cache
+  l2d_access BOOL,
+  -- Whether a load operation accessed and missed the level 2 data or unified
+  -- cache. Not set for accesses that are satisfied from refilling data of a
+  -- previous miss
+  l2d_hit BOOL,
+  -- Whether a load operation accessed modified data in a cache
+  cache_data_modified BOOL,
+  -- Wheter a load operation hit a recently fetched line in a cache
+  recenty_fetched BOOL,
+  -- Whether a load operation snooped data from a cache outside the cache
+  -- hierarchy of this core
+  data_snooped BOOL
+) AS
+SELECT
+  ts,
+  utid,
+  exception_level,
+  instruction_frame_id,
+  operation,
+  data_virtual_address,
+  data_physical_address,
+  total_latency,
+  issue_latency,
+  translation_latency,
+  data_source,
+  (events_bitmask & (1 << 0)) <> 0 AS exception_gen,
+  (events_bitmask & (1 << 1)) <> 0 AS retired,
+  (events_bitmask & (1 << 2)) <> 0 AS l1d_access,
+  (events_bitmask & (1 << 3)) <> 0 AS l1d_refill,
+  (events_bitmask & (1 << 4)) <> 0 AS tlb_access,
+  (events_bitmask & (1 << 5)) <> 0 AS tlb_refill,
+  (events_bitmask & (1 << 6)) <> 0 AS not_taken,
+  (events_bitmask & (1 << 7)) <> 0 AS mispred,
+  (events_bitmask & (1 << 8)) <> 0 AS llc_access,
+  (events_bitmask & (1 << 9)) <> 0 AS llc_refill,
+  (events_bitmask & (1 << 10)) <> 0 AS remote_access,
+  (events_bitmask & (1 << 11)) <> 0 AS alignment,
+  (events_bitmask & (1 << 17)) <> 0 AS tme_transaction,
+  (events_bitmask & (1 << 17)) <> 0 AS sve_partial_pred,
+  (events_bitmask & (1 << 18)) <> 0 AS sve_empty_pred,
+  (events_bitmask & (1 << 19)) <> 0 AS l2d_access,
+  (events_bitmask & (1 << 20)) <> 0 AS l2d_hit,
+  (events_bitmask & (1 << 21)) <> 0 AS cache_data_modified,
+  (events_bitmask & (1 << 22)) <> 0 AS recenty_fetched,
+  (events_bitmask & (1 << 23)) <> 0 AS data_snooped
+FROM __intrinsic_spe_record;
diff --git a/src/trace_processor/perfetto_sql/stdlib/metasql/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/metasql/BUILD.gn
deleted file mode 100644
index a82b2d2..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/metasql/BUILD.gn
+++ /dev/null
@@ -1,22 +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("../../../../../gn/perfetto_sql.gni")
-
-perfetto_sql_source_set("metasql") {
-  sources = [
-    "column_list.sql",
-    "table_list.sql",
-  ]
-}
diff --git a/src/trace_processor/perfetto_sql/stdlib/metasql/column_list.sql b/src/trace_processor/perfetto_sql/stdlib/metasql/column_list.sql
deleted file mode 100644
index c78aab5..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/metasql/column_list.sql
+++ /dev/null
@@ -1,34 +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.
-
-CREATE PERFETTO MACRO _col_list_id(a ColumnName)
-RETURNS _SqlFragment AS $a;
-
--- Given a list of column names, applies an arbitrary macro to each column
--- and joins the result with a comma.
-CREATE PERFETTO MACRO _metasql_map_join_column_list(
-  columns _ColumnNameList,
-  map_macro _Macro
-)
-RETURNS _SqlFragment
-AS __intrinsic_token_map_join!($columns, $map_macro, __intrinsic_token_comma!());
-
--- Given a list of column names, removes the parentheses allowing the usage
--- of these in a select statement, window function etc.
-CREATE PERFETTO MACRO _metasql_unparenthesize_column_list(
-  columns _ColumnNameList
-)
-RETURNS _SqlFragment
-AS _metasql_map_join_column_list!($columns, _col_list_id);
diff --git a/src/trace_processor/perfetto_sql/stdlib/metasql/table_list.sql b/src/trace_processor/perfetto_sql/stdlib/metasql/table_list.sql
deleted file mode 100644
index 32d327b..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/metasql/table_list.sql
+++ /dev/null
@@ -1,33 +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.
-
--- Given a list of table names, applies an arbitrary macro to each table
--- and joins the result with a comma.
-CREATE PERFETTO MACRO _metasql_map_join_table_list(
-  tables _TableNameList,
-  map_macro _Macro
-)
-RETURNS _SqlFragment
-AS __intrinsic_token_map_join!($tables, $map_macro, __intrinsic_token_comma!());
-
--- Given a list of table names, applies an arbitrary macro to each table
--- and joins the result with a comma.
-CREATE PERFETTO MACRO _metasql_map_join_table_list_with_capture(
-  tables _TableNameList,
-  map_macro _Macro,
-  args _ArgumentList
-)
-RETURNS _SqlFragment
-AS __intrinsic_token_map_join_with_capture!($tables, $map_macro, $args, __intrinsic_token_comma!());
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/prelude/BUILD.gn
index 446a002..32aba6f 100644
--- a/src/trace_processor/perfetto_sql/stdlib/prelude/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/prelude/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright (C) 2023 The Android Open Source Project
+# Copyright (C) 2024 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -15,12 +15,9 @@
 import("../../../../../gn/perfetto_sql.gni")
 
 perfetto_sql_source_set("prelude") {
-  sources = [
-    "casts.sql",
-    "slices.sql",
-    "tables.sql",
-    "tables_views.sql",
-    "trace_bounds.sql",
-    "views.sql",
+  sources = []
+  deps = [
+    "after_eof",
+    "before_eof",
   ]
 }
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/BUILD.gn
new file mode 100644
index 0000000..c564a57
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/BUILD.gn
@@ -0,0 +1,24 @@
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("../../../../../../gn/perfetto_sql.gni")
+
+perfetto_sql_source_set("after_eof") {
+  sources = [
+    "casts.sql",
+    "slices.sql",
+    "tables_views.sql",
+    "views.sql",
+  ]
+}
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/casts.sql b/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/casts.sql
similarity index 100%
rename from src/trace_processor/perfetto_sql/stdlib/prelude/casts.sql
rename to src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/casts.sql
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/slices.sql b/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/slices.sql
new file mode 100644
index 0000000..f68f872
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/slices.sql
@@ -0,0 +1,34 @@
+--
+-- Copyright 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
+--
+--     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 prelude.after_eof.views;
+
+-- Given two slice ids, returns whether the first is an ancestor of the second.
+CREATE PERFETTO FUNCTION slice_is_ancestor(
+  -- Id of the potential ancestor slice.
+  ancestor_id LONG,
+  -- Id of the potential descendant slice.
+  descendant_id LONG
+)
+-- Whether `ancestor_id` slice is an ancestor of `descendant_id`.
+RETURNS BOOL AS
+SELECT
+  ancestor.track_id = descendant.track_id AND
+  ancestor.ts <= descendant.ts AND
+  (ancestor.dur == -1 OR ancestor.ts + ancestor.dur >= descendant.ts + descendant.dur)
+FROM slice ancestor
+JOIN slice descendant
+WHERE ancestor.id = $ancestor_id
+  AND descendant.id = $descendant_id;
\ No newline at end of file
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
new file mode 100644
index 0000000..e074220
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/tables_views.sql
@@ -0,0 +1,479 @@
+--
+-- 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 prelude.after_eof.views;
+
+-- Lists all metrics built-into trace processor.
+CREATE PERFETTO VIEW trace_metrics(
+  -- The name of the metric.
+  name STRING
+) AS
+SELECT name FROM _trace_metrics;
+
+-- Definition of `trace_bounds` table. The values are being filled by Trace
+-- Processor when parsing the trace.
+-- It is recommended to depend on the `trace_start()` and `trace_end()`
+-- functions rather than directly on `trace_bounds`.
+CREATE PERFETTO VIEW trace_bounds(
+  -- First ts in the trace.
+  start_ts INT,
+  -- End of the trace.
+  end_ts INT
+) AS
+SELECT start_ts, end_ts FROM _trace_bounds;
+
+-- Tracks are a fundamental concept in trace processor and represent a
+-- "timeline" for events of the same type and with the same context. See
+-- https://perfetto.dev/docs/analysis/trace-processor#tracks for a more
+-- detailed explanation, with examples.
+CREATE PERFETTO VIEW track (
+  -- Unique identifier for this track. Identical to |track_id|, prefer using
+  -- |track_id| instead.
+  id UINT,
+  -- The name of the "most-specific" child table containing this row.
+  type STRING,
+  -- Name of the track; can be null for some types of tracks (e.g. thread
+  -- tracks).
+  name STRING,
+  -- The classification of a track indicates the "type of data" the track
+  -- contains.
+  --
+  -- Every track is uniquely identified by the the combination of the
+  -- classification and a set of dimensions: classifications allow identifying
+  -- a set of tracks with the same type of data within the whole universe of
+  -- tracks while dimensions allow distinguishing between different tracks in
+  -- that set.
+  classification STRING,
+  -- The dimensions of the track which uniquely identify the track within a
+  -- given classification.
+  --
+  -- Join with the `args` table or use the `EXTRACT_ARG` helper function to
+  -- expand the args.
+  dimension_arg_set_id UINT,
+  -- The track which is the "parent" of this track. Only non-null for tracks
+  -- created using Perfetto's track_event API.
+  parent_id UINT,
+  -- Generic key-value pairs containing extra information about the track.
+  --
+  -- Join with the `args` table or use the `EXTRACT_ARG` helper function to
+  -- expand the args.
+  source_arg_set_id UINT,
+  -- Machine identifier, non-null for tracks on a remote machine.
+  machine_id UINT
+) AS
+SELECT
+  id,
+  type,
+  name,
+  classification,
+  dimension_arg_set_id,
+  parent_id,
+  source_arg_set_id,
+  machine_id
+FROM __intrinsic_track;
+
+-- Contains information about the CPUs on the device this trace was taken on.
+CREATE PERFETTO VIEW cpu (
+  -- Unique identifier for this CPU. Identical to |ucpu|, prefer using |ucpu|
+  -- instead.
+  id UINT,
+  -- Unique identifier for this CPU. Isn't equal to |cpu| for remote machines
+  -- and is equal to |cpu| for the host machine.
+  ucpu UINT,
+  -- The 0-based CPU core identifier.
+  cpu UINT,
+  -- The name of the "most-specific" child table containing this row.
+  type STRING,
+  -- The cluster id is shared by CPUs in the same cluster.
+  cluster_id UINT,
+  -- A string describing this core.
+  processor STRING,
+  -- Machine identifier, non-null for CPUs on a remote machine.
+  machine_id UINT,
+  -- Capacity of a CPU of a device, a metric which indicates the
+  -- relative performance of a CPU on a device
+  -- For details see:
+  -- https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/cpu-capacity.txt
+  capacity UINT,
+  -- Extra key/value pairs associated with this cpu.
+  arg_set_id UINT
+) AS
+SELECT
+  id,
+  id AS ucpu,
+  cpu,
+  type AS type,
+  cluster_id,
+  processor,
+  machine_id,
+  capacity,
+  arg_set_id
+FROM
+  __intrinsic_cpu
+WHERE
+  cpu IS NOT NULL;
+
+-- Contains the frequency values that the CPUs on the device are capable of
+-- running at.
+CREATE PERFETTO VIEW cpu_available_frequencies (
+  -- Unique identifier for this cpu frequency.
+  id UINT,
+  -- The CPU for this frequency, 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 UINT,
+  -- CPU frequency in KHz.
+  freq UINT,
+  -- The CPU that the slice executed 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.
+  ucpu UINT
+) AS
+SELECT
+  id,
+  ucpu AS cpu,
+  freq,
+  ucpu
+FROM
+  __intrinsic_cpu_freq;
+
+-- This table holds slices with kernel thread scheduling information. These
+-- slices are collected when the Linux "ftrace" data source is used with the
+-- "sched/switch" and "sched/wakeup*" events enabled.
+--
+-- The rows in this table will always have a matching row in the |thread_state|
+-- table with |thread_state.state| = 'Running'
+CREATE PERFETTO VIEW sched_slice (
+  --  Unique identifier for this scheduling slice.
+  id UINT,
+  -- The name of the "most-specific" child table containing this row.
+  type STRING,
+  -- The timestamp at the start of the slice (in nanoseconds).
+  ts LONG,
+  -- The duration of the slice (in nanoseconds).
+  dur LONG,
+  -- The CPU that the slice executed 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 UINT,
+  -- The thread's unique id in the trace.
+  utid UINT,
+  -- 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 INT,
+  -- The unique CPU identifier that the slice executed on.
+  ucpu UINT
+) AS
+SELECT
+  id,
+  type,
+  ts,
+  dur,
+  ucpu AS cpu,
+  utid,
+  end_state,
+  priority,
+  ucpu
+FROM
+  __intrinsic_sched_slice;
+
+-- Shorter alias for table `sched_slice`.
+CREATE PERFETTO VIEW sched(
+  -- Alias for `sched_slice.id`.
+  id UINT,
+  -- Alias for `sched_slice.type`.
+  type STRING,
+  -- Alias for `sched_slice.ts`.
+  ts LONG,
+  -- Alias for `sched_slice.dur`.
+  dur LONG,
+  -- Alias for `sched_slice.cpu`.
+  cpu UINT,
+  -- Alias for `sched_slice.utid`.
+  utid UINT,
+  -- Alias for `sched_slice.end_state`.
+  end_state STRING,
+  -- Alias for `sched_slice.priority`.
+  priority INT,
+  -- Alias for `sched_slice.ucpu`.
+  ucpu UINT,
+  -- Legacy column, should no longer be used.
+  ts_end UINT
+) AS
+SELECT *, ts + dur as ts_end
+FROM sched_slice;
+
+-- This table contains the scheduling state of every thread on the system during
+-- the trace.
+--
+-- The rows in this table which have |state| = 'Running', will have a
+-- corresponding row in the |sched_slice| table.
+CREATE PERFETTO VIEW thread_state (
+  -- Unique identifier for this thread state.
+  id UINT,
+  -- The name of the "most-specific" child table containing this row.
+  type STRING,
+  -- The timestamp at the start of the slice (in nanoseconds).
+  ts LONG,
+  -- The duration of the slice (in nanoseconds).
+  dur LONG,
+  -- The CPU that the thread executed 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 UINT,
+  -- The thread's unique id in the trace.
+  utid UINT,
+  -- The scheduling state of the thread. Can be "Running" or any of the states
+  -- described in |sched_slice.end_state|.
+  state STRING,
+  -- Indicates whether this thread was blocked on IO.
+  io_wait UINT,
+  -- The function in the kernel this thread was blocked on.
+  blocked_function STRING,
+  -- The unique thread id of the thread which caused a wakeup of this thread.
+  waker_utid UINT,
+  -- The unique thread state id which caused a wakeup of this thread.
+  waker_id UINT,
+  -- Whether the wakeup was from interrupt context or process context.
+  irq_context UINT,
+  -- The unique CPU identifier that the thread executed on.
+  ucpu UINT
+) AS
+SELECT
+  id,
+  type,
+  ts,
+  dur,
+  ucpu AS cpu,
+  utid,
+  state,
+  io_wait,
+  blocked_function,
+  waker_utid,
+  waker_id,
+  irq_context,
+  ucpu
+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 UINT,
+  -- The name of the "most-specific" child table containing this row.
+  type STRING,
+  -- The timestamp of this event.
+  ts LONG,
+  -- 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 UINT,
+  -- The thread this event was emitted on.
+  utid UINT,
+  -- The set of key/value pairs associated with this event.
+  arg_set_id UINT,
+  -- Ftrace event flags for this event. Currently only emitted for sched_waking
+  -- events.
+  common_flags UINT,
+  -- The unique CPU identifier that this event was emitted on.
+  ucpu UINT
+) AS
+SELECT
+  id,
+  type,
+  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.
+-- metrics, standard library etc). Note also that this table might be empty if
+-- raw ftrace parsing has been disabled.
+CREATE PERFETTO VIEW ftrace_event (
+  -- Unique identifier for this ftrace event.
+  id UINT,
+  -- The name of the "most-specific" child table containing this row.
+  type STRING,
+  -- The timestamp of this event.
+  ts LONG,
+  -- 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 UINT,
+  -- The thread this event was emitted on.
+  utid UINT,
+  -- The set of key/value pairs associated with this event.
+  arg_set_id UINT,
+  -- Ftrace event flags for this event. Currently only emitted for
+  -- sched_waking events.
+  common_flags UINT,
+  -- The unique CPU identifier that this event was emitted on.
+  ucpu UINT
+) AS
+SELECT
+  id,
+  type,
+  ts,
+  name,
+  ucpu AS cpu,
+  utid,
+  arg_set_id,
+  common_flags,
+  ucpu
+FROM
+  __intrinsic_ftrace_event;
+
+-- The sched_slice table with the upid column.
+CREATE PERFETTO VIEW experimental_sched_upid (
+  --  Unique identifier for this scheduling slice.
+  id UINT,
+  -- The name of the "most-specific" child table containing this row.
+  type STRING,
+  -- The timestamp at the start of the slice (in nanoseconds).
+  ts LONG,
+  -- The duration of the slice (in nanoseconds).
+  dur LONG,
+  -- The CPU that the slice executed 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 UINT,
+  -- The thread's unique id in the trace.
+  utid UINT,
+  -- 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 INT,
+  -- The unique CPU identifier that the slice executed on.
+  ucpu UINT,
+  -- The process's unique id in the trace.
+  upid UINT
+) AS
+SELECT
+  id,
+  type,
+  ts,
+  dur,
+  ucpu AS cpu,
+  utid,
+  end_state,
+  priority,
+  ucpu,
+  upid
+FROM
+  __intrinsic_sched_upid;
+
+-- Tracks which are associated to a single CPU.
+CREATE PERFETTO VIEW cpu_track (
+  -- Unique identifier for this cpu track.
+  id UINT,
+  -- The name of the "most-specific" child table containing this row.
+  type STRING,
+  -- Name of the track.
+  name STRING,
+  -- The track which is the "parent" of this track. Only non-null for tracks
+  -- created using Perfetto's track_event API.
+  parent_id UINT,
+  -- 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 UINT,
+  -- Machine identifier, non-null for tracks on a remote machine.
+  machine_id UINT,
+  -- The CPU that the track is associated with (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 UINT,
+  -- The unique CPU identifier that this track is associated with.
+  ucpu UINT
+) AS
+SELECT
+  id,
+  type,
+  name,
+  parent_id,
+  source_arg_set_id,
+  machine_id,
+  ucpu AS cpu,
+  ucpu
+FROM
+  __intrinsic_cpu_track;
+
+-- Tracks containing counter-like events associated to a CPU.
+CREATE PERFETTO VIEW cpu_counter_track (
+  -- Unique identifier for this cpu counter track.
+  id UINT,
+  -- The name of the "most-specific" child table containing this row.
+  type STRING,
+  -- Name of the track.
+  name STRING,
+  -- The track which is the "parent" of this track. Only non-null for tracks
+  -- created using Perfetto's track_event API.
+  parent_id UINT,
+  -- 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 UINT,
+  -- Machine identifier, non-null for tracks on a remote machine.
+  machine_id UINT,
+  -- The units of the counter. This column is rarely filled.
+  unit STRING,
+  -- The description for this track. For debugging purposes only.
+  description STRING,
+  -- The CPU that the track is associated with (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 UINT,
+  -- The unique CPU identifier that this track is associated with.
+  ucpu UINT
+) AS
+SELECT
+  id,
+  type,
+  name,
+  parent_id,
+  source_arg_set_id,
+  machine_id,
+  unit,
+  description,
+  ucpu AS cpu,
+  ucpu
+FROM
+  __intrinsic_cpu_counter_track;
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/views.sql b/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/views.sql
new file mode 100644
index 0000000..8f3a90f
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/views.sql
@@ -0,0 +1,285 @@
+--
+-- 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 prelude.after_eof.casts;
+
+-- Alias of the `counter` table.
+CREATE PERFETTO VIEW counters(
+  -- Alias of `counter.id`.
+  id INT,
+  -- Alias of `counter.type`.
+  type STRING,
+  -- Alias of `counter.ts`.
+  ts LONG,
+  -- Alias of `counter.track_id`.
+  track_id INT,
+  -- Alias of `counter.value`.
+  value DOUBLE,
+  -- Alias of `counter.arg_set_id`.
+  arg_set_id INT,
+  -- Legacy column, should no longer be used.
+  name STRING,
+  -- Legacy column, should no longer be used.
+  unit STRING,
+  -- Legacy column, should no longer be used.
+  description STRING
+) AS
+SELECT v.*, t.name, t.unit, t.description
+FROM counter v
+JOIN counter_track t ON v.track_id = t.id
+ORDER BY ts;
+
+-- Contains slices from userspace which explains what threads were doing
+-- during the trace.
+CREATE PERFETTO VIEW slice(
+  -- The id of the slice.
+  id INT,
+  -- The name of the "most-specific" child table containing this row.
+  type STRING,
+  -- The timestamp at the start of the slice (in nanoseconds).
+  ts LONG,
+  -- The duration of the slice (in nanoseconds).
+  dur LONG,
+  -- The id of the track this slice is located on.
+  track_id INT,
+  -- The "category" of the slice. If this slice originated with track_event,
+  -- this column contains the category emitted.
+  -- Otherwise, it is likely to be null (with limited exceptions).
+  category STRING,
+  -- The name of the slice. The name describes what was happening during the
+  -- slice.
+  name STRING,
+  -- The depth of the slice in the current stack of slices.
+  depth INT,
+  -- A unique identifier obtained from the names of all slices in this stack.
+  -- This is rarely useful and kept around only for legacy reasons.
+  stack_id LONG,
+  -- The stack_id for the parent of this slice. Rarely useful.
+  parent_stack_id LONG,
+  -- The id of the parent (i.e. immediate ancestor) slice for this slice.
+  parent_id INT,
+  -- The id of the argument set associated with this slice.
+  arg_set_id INT,
+  -- The thread timestamp at the start of the slice. This column will only be
+  -- populated if thread timestamp collection is enabled with track_event.
+  thread_ts LONG,
+  -- The thread time used by this slice. This column will only be populated if
+  -- thread timestamp collection is enabled with track_event.
+  thread_dur LONG,
+  -- The value of the CPU instruction counter at the start of the slice. This
+  -- column will only be populated if thread instruction collection is enabled
+  -- with track_event.
+  thread_instruction_count LONG,
+  -- The change in value of the CPU instruction counter between the start and
+  -- end of the slice. This column will only be populated if thread instruction
+  -- collection is enabled with track_event.
+  thread_instruction_delta LONG,
+  -- Alias of `category`.
+  cat STRING,
+  -- Alias of `id`.
+  slice_id LONG
+) AS
+SELECT *, category AS cat, id AS slice_id
+FROM __intrinsic_slice;
+
+-- Contains instant events from userspace which indicates what happened at a
+-- single moment in time.
+CREATE PERFETTO VIEW instant(
+  -- The timestamp of the instant (in nanoseconds).
+  ts LONG,
+  -- The id of the track this instant is located on.
+  track_id INT,
+  -- The name of the instant. The name describes what happened during the
+  -- instant.
+  name STRING,
+  -- The id of the argument set associated with this instant.
+  arg_set_id INT
+) AS
+SELECT ts, track_id, name, arg_set_id
+FROM slice
+WHERE dur = 0;
+
+-- Alternative alias of table `slice`.
+CREATE PERFETTO VIEW slices(
+  -- Alias of `slice.id`.
+  id UINT,
+  -- Alias of `slice.type`.
+  type STRING,
+  -- Alias of `slice.ts`.
+  ts LONG,
+  -- Alias of `slice.dur`.
+  dur LONG,
+  -- Alias of `slice.track_id`.
+  track_id INT,
+  -- Alias of `slice.category`.
+  category STRING,
+  -- Alias of `slice.name`.
+  name STRING,
+  -- Alias of `slice.depth`.
+  depth INT,
+  -- Alias of `slice.stack_id`.
+  stack_id LONG,
+  -- Alias of `slice.parent_stack_id`.
+  parent_stack_id LONG,
+  -- Alias of `slice.parent_id`.
+  parent_id INT,
+  -- Alias of `slice.arg_set_id`.
+  arg_set_id INT,
+  -- Alias of `slice.thread_ts`.
+  thread_ts LONG,
+  -- Alias of `slice.thread_dur`.
+  thread_dur LONG,
+  -- Alias of `slice.thread_instruction_count`.
+  thread_instruction_count LONG,
+  -- Alias of `slice.thread_instruction_delta`.
+  thread_instruction_delta LONG,
+  -- Alias of `slice.cat`.
+  cat STRING,
+  -- Alias of `slice.slice_id`.
+  slice_id LONG
+) AS
+SELECT * FROM slice;
+
+-- Contains information of threads seen during the trace.
+CREATE PERFETTO VIEW thread(
+  -- The id of the thread. Prefer using `utid` instead.
+  id INT,
+  -- The name of the "most-specific" child table containing this row.
+  type STRING,
+  -- Unique thread id. This is != the OS tid. This is a monotonic number
+  -- associated to each thread. The OS thread id (tid) cannot be used as primary
+  -- key because tids and pids are recycled by most kernels.
+  utid INT,
+  -- The OS id for this thread. Note: this is *not* unique over the lifetime of
+  -- the trace so cannot be used as a primary key. Use |utid| instead.
+  tid INT,
+  -- The name of the thread. Can be populated from many sources (e.g. ftrace,
+  -- /proc scraping, track event etc).
+  name STRING,
+  -- The start timestamp of this thread (if known). Is null in most cases unless
+  -- a thread creation event is enabled (e.g. task_newtask ftrace event on
+  -- Linux/Android).
+  start_ts LONG,
+  -- The end timestamp of this thread (if known). Is null in most cases unless
+  -- a thread destruction event is enabled (e.g. sched_process_free ftrace event
+  -- on Linux/Android).
+  end_ts LONG,
+  -- The process hosting this thread.
+  upid LONG,
+  -- Boolean indicating if this thread is the main thread in the process.
+  is_main_thread BOOL,
+  -- Machine identifier, non-null for threads on a remote machine.
+  machine_id INT
+) AS
+SELECT id as utid, *
+FROM __intrinsic_thread;
+
+-- Contains information of processes seen during the trace.
+CREATE PERFETTO VIEW process(
+  -- The id of the process. Prefer using `upid` instead.
+  id INT,
+  -- The name of the "most-specific" child table containing this row.
+  type STRING,
+  -- Unique process id. This is != the OS pid. This is a monotonic number
+  -- associated to each process. The OS process id (pid) cannot be used as
+  -- primary key because tids and pids are recycled by most kernels.
+  upid LONG,
+  -- The OS id for this process. Note: this is *not* unique over the lifetime of
+  -- the trace so cannot be used as a primary key. Use |upid| instead.
+  pid LONG,
+  -- The name of the process. Can be populated from many sources (e.g. ftrace,
+  -- /proc scraping, track event etc).
+  name STRING,
+  -- The start timestamp of this process (if known). Is null in most cases
+  -- unless a process creation event is enabled (e.g. task_newtask ftrace event
+  -- on Linux/Android).
+  start_ts LONG,
+  -- The end timestamp of this process (if known). Is null in most cases unless
+  -- a process destruction event is enabled (e.g. sched_process_free ftrace
+  -- event on Linux/Android).
+  end_ts LONG,
+  -- The upid of the process which caused this process to be spawned.
+  parent_upid INT,
+  -- The Unix user id of the process.
+  uid INT,
+  -- Android appid of this process.
+  android_appid INT,
+  -- /proc/cmdline for this process.
+  cmdline STRING,
+  -- Extra args for this process.
+  arg_set_id INT,
+  -- Machine identifier, non-null for processes on a remote machine.
+  machine_id INT
+) AS
+SELECT id as upid, *
+FROM __intrinsic_process;
+
+-- Arbitrary key-value pairs which allow adding metadata to other, strongly
+-- typed tables.
+-- Note: for a given row, only one of |int_value|, |string_value|, |real_value|
+-- will be non-null.
+CREATE PERFETTO VIEW args(
+  -- The id of the arg.
+  id INT,
+  -- The name of the "most-specific" child table containing this row.
+  type STRING,
+  -- The id for a single set of arguments.
+  arg_set_id INT,
+  -- The "flat key" of the arg: this is the key without any array indexes.
+  flat_key STRING,
+  -- The key for the arg.
+  key STRING,
+  -- The integer value of the arg.
+  int_value LONG,
+  -- The string value of the arg.
+  string_value STRING,
+  -- The double value of the arg.
+  real_value DOUBLE,
+  -- The type of the value of the arg. Will be one of 'int', 'uint', 'string',
+  -- 'real', 'pointer', 'bool' or 'json'.
+  value_type STRING,
+  -- The human-readable formatted value of the arg.
+  display_value STRING
+) AS
+SELECT
+  *,
+  -- This should be kept in sync with GlobalArgsTracker::AddArgSet.
+  CASE value_type
+    WHEN 'int' THEN cast_string!(int_value)
+    WHEN 'uint' THEN cast_string!(int_value)
+    WHEN 'string' THEN string_value
+    WHEN 'real' THEN cast_string!(real_value)
+    WHEN 'pointer' THEN printf('0x%x', int_value)
+    WHEN 'bool' THEN (
+      CASE WHEN int_value <> 0 THEN 'true'
+      ELSE 'false' END)
+    WHEN 'json' THEN string_value
+  ELSE NULL END AS display_value
+FROM __intrinsic_args;
+
+-- Contains the Linux perf sessions in the trace.
+CREATE PERFETTO VIEW perf_session(
+  -- The id of the perf session. Prefer using `perf_session_id` instead.
+  id INT,
+  -- The name of the "most-specific" child table containing this row.
+  type STRING,
+  -- The id of the perf session.
+  perf_session_id INT,
+  -- Command line used to collect the data.
+  cmdline STRING
+)
+AS
+SELECT *, id AS perf_session_id
+FROM __intrinsic_perf_session;
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/before_eof/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/prelude/before_eof/BUILD.gn
new file mode 100644
index 0000000..ebe56f9
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/prelude/before_eof/BUILD.gn
@@ -0,0 +1,22 @@
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("../../../../../../gn/perfetto_sql.gni")
+
+perfetto_sql_source_set("before_eof") {
+  sources = [
+    "tables.sql",
+    "trace_bounds.sql",
+  ]
+}
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/before_eof/tables.sql b/src/trace_processor/perfetto_sql/stdlib/prelude/before_eof/tables.sql
new file mode 100644
index 0000000..2689491
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/prelude/before_eof/tables.sql
@@ -0,0 +1,23 @@
+--
+-- 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.
+
+-- Lists all metrics built-into trace processor.
+CREATE TABLE _trace_metrics(
+  -- The name of the metric.
+  name STRING
+);
+
+-- Helper table to generate a time-interval.
+CREATE VIRTUAL TABLE window USING window();
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/before_eof/trace_bounds.sql b/src/trace_processor/perfetto_sql/stdlib/prelude/before_eof/trace_bounds.sql
new file mode 100644
index 0000000..ece2ecd
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/prelude/before_eof/trace_bounds.sql
@@ -0,0 +1,37 @@
+--
+-- Copyright 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
+--
+--     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.
+
+-- The values are being filled by Trace Processor when parsing the trace.
+-- Exposed with `trace_bounds`.
+CREATE TABLE _trace_bounds AS
+SELECT 0 AS start_ts, 0 AS end_ts;
+
+-- Fetch start of the trace.
+CREATE PERFETTO FUNCTION trace_start()
+-- Start of the trace in nanoseconds.
+RETURNS LONG AS
+SELECT start_ts FROM _trace_bounds;
+
+-- Fetch end of the trace.
+CREATE PERFETTO FUNCTION trace_end()
+-- End of the trace in nanoseconds.
+RETURNS LONG AS
+SELECT end_ts FROM _trace_bounds;
+
+-- Fetch duration of the trace.
+CREATE PERFETTO FUNCTION trace_dur()
+-- Duration of the trace in nanoseconds.
+RETURNS LONG AS
+SELECT trace_end() - trace_start();
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/slices.sql b/src/trace_processor/perfetto_sql/stdlib/prelude/slices.sql
deleted file mode 100644
index 48f7132..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/prelude/slices.sql
+++ /dev/null
@@ -1,34 +0,0 @@
---
--- Copyright 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
---
---     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 prelude.views;
-
--- Given two slice ids, returns whether the first is an ancestor of the second.
-CREATE PERFETTO FUNCTION slice_is_ancestor(
-  -- Id of the potential ancestor slice.
-  ancestor_id LONG,
-  -- Id of the potential descendant slice.
-  descendant_id LONG
-)
--- Whether `ancestor_id` slice is an ancestor of `descendant_id`.
-RETURNS BOOL AS
-SELECT
-  ancestor.track_id = descendant.track_id AND
-  ancestor.ts <= descendant.ts AND
-  (ancestor.dur == -1 OR ancestor.ts + ancestor.dur >= descendant.ts + descendant.dur)
-FROM slice ancestor
-JOIN slice descendant
-WHERE ancestor.id = $ancestor_id
-  AND descendant.id = $descendant_id;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/tables.sql b/src/trace_processor/perfetto_sql/stdlib/prelude/tables.sql
deleted file mode 100644
index e8b5db0..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/prelude/tables.sql
+++ /dev/null
@@ -1,23 +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.
-
--- Lists all metrics built-into trace processor.
-CREATE TABLE trace_metrics(
-  -- The name of the metric.
-  name STRING
-);
-
--- Helper table to generate a time-interval.
-CREATE VIRTUAL TABLE window USING window();
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/tables_views.sql b/src/trace_processor/perfetto_sql/stdlib/prelude/tables_views.sql
deleted file mode 100644
index 6411698..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/prelude/tables_views.sql
+++ /dev/null
@@ -1,407 +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 prelude.views;
-
--- Contains information about the CPUs on the device this trace was taken on.
-CREATE PERFETTO VIEW cpu (
-  -- Unique identifier for this CPU. Identical to |ucpu|, prefer using |ucpu|
-  -- instead.
-  id UINT,
-  -- Unique identifier for this CPU. Isn't equal to |cpu| for remote machines
-  -- and is equal to |cpu| for the host machine.
-  ucpu UINT,
-  -- The 0-based CPU core identifier.
-  cpu UINT,
-  -- The name of the "most-specific" child table containing this row.
-  type STRING,
-  -- The cluster id is shared by CPUs in the same cluster.
-  cluster_id UINT,
-  -- A string describing this core.
-  processor STRING,
-  -- Machine identifier, non-null for CPUs on a remote machine.
-  machine_id UINT,
-  -- Capacity of a CPU of a device, a metric which indicates the
-  -- relative performance of a CPU on a device
-  -- For details see: 
-  -- https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/cpu-capacity.txt
-  capacity UINT
-) AS
-SELECT
-  id,
-  id AS ucpu,
-  cpu,
-  type AS type,
-  cluster_id,
-  processor,
-  machine_id,
-  capacity
-FROM
-  __intrinsic_cpu
-WHERE
-  cpu IS NOT NULL;
-
--- Contains the frequency values that the CPUs on the device are capable of
--- running at.
-CREATE PERFETTO VIEW cpu_available_frequencies (
-  -- Unique identifier for this cpu frequency.
-  id UINT,
-  -- The CPU for this frequency, 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 UINT,
-  -- CPU frequency in KHz.
-  freq UINT,
-  -- The CPU that the slice executed 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.
-  ucpu UINT
-) AS
-SELECT
-  id,
-  ucpu AS cpu,
-  freq,
-  ucpu
-FROM
-  __intrinsic_cpu_freq;
-
--- This table holds slices with kernel thread scheduling information. These
--- slices are collected when the Linux "ftrace" data source is used with the
--- "sched/switch" and "sched/wakeup*" events enabled.
---
--- The rows in this table will always have a matching row in the |thread_state|
--- table with |thread_state.state| = 'Running'
-CREATE PERFETTO VIEW sched_slice (
-  --  Unique identifier for this scheduling slice.
-  id UINT,
-  -- The name of the "most-specific" child table containing this row.
-  type STRING,
-  -- The timestamp at the start of the slice (in nanoseconds).
-  ts LONG,
-  -- The duration of the slice (in nanoseconds).
-  dur LONG,
-  -- The CPU that the slice executed 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 UINT,
-  -- The thread's unique id in the trace.
-  utid UINT,
-  -- 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 INT,
-  -- The unique CPU identifier that the slice executed on.
-  ucpu UINT
-) AS
-SELECT
-  id,
-  type,
-  ts,
-  dur,
-  ucpu AS cpu,
-  utid,
-  end_state,
-  priority,
-  ucpu
-FROM
-  __intrinsic_sched_slice;
-
--- Shorter alias for table `sched_slice`.
-CREATE PERFETTO VIEW sched(
-  -- Alias for `sched_slice.id`.
-  id UINT,
-  -- Alias for `sched_slice.type`.
-  type STRING,
-  -- Alias for `sched_slice.ts`.
-  ts LONG,
-  -- Alias for `sched_slice.dur`.
-  dur LONG,
-  -- Alias for `sched_slice.cpu`.
-  cpu UINT,
-  -- Alias for `sched_slice.utid`.
-  utid UINT,
-  -- Alias for `sched_slice.end_state`.
-  end_state STRING,
-  -- Alias for `sched_slice.priority`.
-  priority INT,
-  -- Alias for `sched_slice.ucpu`.
-  ucpu UINT,
-  -- Legacy column, should no longer be used.
-  ts_end UINT
-) AS
-SELECT *, ts + dur as ts_end
-FROM sched_slice;
-
--- This table contains the scheduling state of every thread on the system during
--- the trace.
---
--- The rows in this table which have |state| = 'Running', will have a
--- corresponding row in the |sched_slice| table.
-CREATE PERFETTO VIEW thread_state (
-  -- Unique identifier for this thread state.
-  id UINT,
-  -- The name of the "most-specific" child table containing this row.
-  type STRING,
-  -- The timestamp at the start of the slice (in nanoseconds).
-  ts LONG,
-  -- The duration of the slice (in nanoseconds).
-  dur LONG,
-  -- The CPU that the thread executed 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 UINT,
-  -- The thread's unique id in the trace.
-  utid UINT,
-  -- The scheduling state of the thread. Can be "Running" or any of the states
-  -- described in |sched_slice.end_state|.
-  state STRING,
-  -- Indicates whether this thread was blocked on IO.
-  io_wait UINT,
-  -- The function in the kernel this thread was blocked on.
-  blocked_function STRING,
-  -- The unique thread id of the thread which caused a wakeup of this thread.
-  waker_utid UINT,
-  -- The unique thread state id which caused a wakeup of this thread.
-  waker_id UINT,
-  -- Whether the wakeup was from interrupt context or process context.
-  irq_context UINT,
-  -- The unique CPU identifier that the thread executed on.
-  ucpu UINT
-) AS
-SELECT
-  id,
-  type,
-  ts,
-  dur,
-  ucpu AS cpu,
-  utid,
-  state,
-  io_wait,
-  blocked_function,
-  waker_utid,
-  waker_id,
-  irq_context,
-  ucpu
-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 UINT,
-  -- The name of the "most-specific" child table containing this row.
-  type STRING,
-  -- The timestamp of this event.
-  ts LONG,
-  -- 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 UINT,
-  -- The thread this event was emitted on.
-  utid UINT,
-  -- The set of key/value pairs associated with this event.
-  arg_set_id UINT,
-  -- Ftrace event flags for this event. Currently only emitted for sched_waking
-  -- events.
-  common_flags UINT,
-  -- The unique CPU identifier that this event was emitted on.
-  ucpu UINT
-) AS
-SELECT
-  id,
-  type,
-  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.
--- metrics, standard library etc). Note also that this table might be empty if
--- raw ftrace parsing has been disabled.
-CREATE PERFETTO VIEW ftrace_event (
-  -- Unique identifier for this ftrace event.
-  id UINT,
-  -- The name of the "most-specific" child table containing this row.
-  type STRING,
-  -- The timestamp of this event.
-  ts LONG,
-  -- 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 UINT,
-  -- The thread this event was emitted on.
-  utid UINT,
-  -- The set of key/value pairs associated with this event.
-  arg_set_id UINT,
-  -- Ftrace event flags for this event. Currently only emitted for
-  -- sched_waking events.
-  common_flags UINT,
-  -- The unique CPU identifier that this event was emitted on.
-  ucpu UINT
-) AS
-SELECT
-  id,
-  type,
-  ts,
-  name,
-  ucpu AS cpu,
-  utid,
-  arg_set_id,
-  common_flags,
-  ucpu
-FROM
-  __intrinsic_ftrace_event;
-
--- The sched_slice table with the upid column.
-CREATE PERFETTO VIEW experimental_sched_upid (
-  --  Unique identifier for this scheduling slice.
-  id UINT,
-  -- The name of the "most-specific" child table containing this row.
-  type STRING,
-  -- The timestamp at the start of the slice (in nanoseconds).
-  ts LONG,
-  -- The duration of the slice (in nanoseconds).
-  dur LONG,
-  -- The CPU that the slice executed 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 UINT,
-  -- The thread's unique id in the trace.
-  utid UINT,
-  -- 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 INT,
-  -- The unique CPU identifier that the slice executed on.
-  ucpu UINT,
-  -- The process's unique id in the trace.
-  upid UINT
-) AS
-SELECT
-  id,
-  type,
-  ts,
-  dur,
-  ucpu AS cpu,
-  utid,
-  end_state,
-  priority,
-  ucpu,
-  upid
-FROM
-  __intrinsic_sched_upid;
-
--- Tracks which are associated to a single CPU.
-CREATE PERFETTO VIEW cpu_track (
-  -- Unique identifier for this cpu track.
-  id UINT,
-  -- The name of the "most-specific" child table containing this row.
-  type STRING,
-  -- Name of the track.
-  name STRING,
-  -- The track which is the "parent" of this track. Only non-null for tracks
-  -- created using Perfetto's track_event API.
-  parent_id UINT,
-  -- 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 UINT,
-  -- Machine identifier, non-null for tracks on a remote machine.
-  machine_id UINT,
-  -- The CPU that the track is associated with (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 UINT,
-  -- The unique CPU identifier that this track is associated with.
-  ucpu UINT
-) AS
-SELECT
-  id,
-  type,
-  name,
-  parent_id,
-  source_arg_set_id,
-  machine_id,
-  ucpu AS cpu,
-  ucpu
-FROM
-  __intrinsic_cpu_track;
-
--- Tracks containing counter-like events associated to a CPU.
-CREATE PERFETTO VIEW cpu_counter_track (
-  -- Unique identifier for this cpu counter track.
-  id UINT,
-  -- The name of the "most-specific" child table containing this row.
-  type STRING,
-  -- Name of the track.
-  name STRING,
-  -- The track which is the "parent" of this track. Only non-null for tracks
-  -- created using Perfetto's track_event API.
-  parent_id UINT,
-  -- 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 UINT,
-  -- Machine identifier, non-null for tracks on a remote machine.
-  machine_id UINT,
-  -- The units of the counter. This column is rarely filled.
-  unit STRING,
-  -- The description for this track. For debugging purposes only.
-  description STRING,
-  -- The CPU that the track is associated with (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 UINT,
-  -- The unique CPU identifier that this track is associated with.
-  ucpu UINT
-) AS
-SELECT
-  id,
-  type,
-  name,
-  parent_id,
-  source_arg_set_id,
-  machine_id,
-  unit,
-  description,
-  ucpu AS cpu,
-  ucpu
-FROM
-  __intrinsic_cpu_counter_track;
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/trace_bounds.sql b/src/trace_processor/perfetto_sql/stdlib/prelude/trace_bounds.sql
deleted file mode 100644
index 51db48d..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/prelude/trace_bounds.sql
+++ /dev/null
@@ -1,40 +0,0 @@
---
--- Copyright 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
---
---     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.
-
--- Definition of `trace_bounds` table. The values are being filled by Trace
--- Processor when parsing the trace. Can't be a Perfetto table because it has
--- to be mutable. 
--- It is recommended to depend on the `trace_start()` and `trace_end()`
--- functions rather than directly on `trace_bounds`.
-CREATE TABLE trace_bounds AS
-SELECT 0 AS start_ts, 0 AS end_ts;
-
--- Fetch start of the trace.
-CREATE PERFETTO FUNCTION trace_start()
--- Start of the trace in nanoseconds.
-RETURNS LONG AS
-SELECT start_ts FROM trace_bounds;
-
--- Fetch end of the trace.
-CREATE PERFETTO FUNCTION trace_end()
--- End of the trace in nanoseconds.
-RETURNS LONG AS
-SELECT end_ts FROM trace_bounds;
-
--- Fetch duration of the trace.
-CREATE PERFETTO FUNCTION trace_dur()
--- Duration of the trace in nanoseconds.
-RETURNS LONG AS
-SELECT trace_end() - trace_start();
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/views.sql b/src/trace_processor/perfetto_sql/stdlib/prelude/views.sql
deleted file mode 100644
index fa1eece..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/prelude/views.sql
+++ /dev/null
@@ -1,285 +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 prelude.casts;
-
--- Alias of the `counter` table.
-CREATE PERFETTO VIEW counters(
-  -- Alias of `counter.id`.
-  id INT,
-  -- Alias of `counter.type`.
-  type STRING,
-  -- Alias of `counter.ts`.
-  ts LONG,
-  -- Alias of `counter.track_id`.
-  track_id INT,
-  -- Alias of `counter.value`.
-  value DOUBLE,
-  -- Alias of `counter.arg_set_id`.
-  arg_set_id INT,
-  -- Legacy column, should no longer be used.
-  name STRING,
-  -- Legacy column, should no longer be used.
-  unit STRING,
-  -- Legacy column, should no longer be used.
-  description STRING
-) AS
-SELECT v.*, t.name, t.unit, t.description
-FROM counter v
-JOIN counter_track t ON v.track_id = t.id
-ORDER BY ts;
-
--- Contains slices from userspace which explains what threads were doing
--- during the trace.
-CREATE PERFETTO VIEW slice(
-  -- The id of the slice.
-  id INT,
-  -- The name of the "most-specific" child table containing this row.
-  type STRING,
-  -- The timestamp at the start of the slice (in nanoseconds).
-  ts LONG,
-  -- The duration of the slice (in nanoseconds).
-  dur LONG,
-  -- The id of the track this slice is located on.
-  track_id INT,
-  -- The "category" of the slice. If this slice originated with track_event,
-  -- this column contains the category emitted.
-  -- Otherwise, it is likely to be null (with limited exceptions).
-  category STRING,
-  -- The name of the slice. The name describes what was happening during the
-  -- slice.
-  name STRING,
-  -- The depth of the slice in the current stack of slices.
-  depth INT,
-  -- A unique identifier obtained from the names of all slices in this stack.
-  -- This is rarely useful and kept around only for legacy reasons.
-  stack_id LONG,
-  -- The stack_id for the parent of this slice. Rarely useful.
-  parent_stack_id LONG,
-  -- The id of the parent (i.e. immediate ancestor) slice for this slice.
-  parent_id INT,
-  -- The id of the argument set associated with this slice.
-  arg_set_id INT,
-  -- The thread timestamp at the start of the slice. This column will only be
-  -- populated if thread timestamp collection is enabled with track_event.
-  thread_ts LONG,
-  -- The thread time used by this slice. This column will only be populated if
-  -- thread timestamp collection is enabled with track_event.
-  thread_dur LONG,
-  -- The value of the CPU instruction counter at the start of the slice. This
-  -- column will only be populated if thread instruction collection is enabled
-  -- with track_event.
-  thread_instruction_count LONG,
-  -- The change in value of the CPU instruction counter between the start and
-  -- end of the slice. This column will only be populated if thread instruction
-  -- collection is enabled with track_event.
-  thread_instruction_delta LONG,
-  -- Alias of `category`.
-  cat STRING,
-  -- Alias of `id`.
-  slice_id LONG
-) AS
-SELECT *, category AS cat, id AS slice_id
-FROM __intrinsic_slice;
-
--- Contains instant events from userspace which indicates what happened at a
--- single moment in time.
-CREATE PERFETTO VIEW instant(
-  -- The timestamp of the instant (in nanoseconds).
-  ts LONG,
-  -- The id of the track this instant is located on.
-  track_id INT,
-  -- The name of the instant. The name describes what happened during the
-  -- instant.
-  name STRING,
-  -- The id of the argument set associated with this instant.
-  arg_set_id INT
-) AS
-SELECT ts, track_id, name, arg_set_id
-FROM slice
-WHERE dur = 0;
-
--- Alternative alias of table `slice`.
-CREATE PERFETTO VIEW slices(
-  -- Alias of `slice.id`.
-  id UINT,
-  -- Alias of `slice.type`.
-  type STRING,
-  -- Alias of `slice.ts`.
-  ts LONG,
-  -- Alias of `slice.dur`.
-  dur LONG,
-  -- Alias of `slice.track_id`.
-  track_id INT,
-  -- Alias of `slice.category`.
-  category STRING,
-  -- Alias of `slice.name`.
-  name STRING,
-  -- Alias of `slice.depth`.
-  depth INT,
-  -- Alias of `slice.stack_id`.
-  stack_id LONG,
-  -- Alias of `slice.parent_stack_id`.
-  parent_stack_id LONG,
-  -- Alias of `slice.parent_id`.
-  parent_id INT,
-  -- Alias of `slice.arg_set_id`.
-  arg_set_id INT,
-  -- Alias of `slice.thread_ts`.
-  thread_ts LONG,
-  -- Alias of `slice.thread_dur`.
-  thread_dur LONG,
-  -- Alias of `slice.thread_instruction_count`.
-  thread_instruction_count LONG,
-  -- Alias of `slice.thread_instruction_delta`.
-  thread_instruction_delta LONG,
-  -- Alias of `slice.cat`.
-  cat LONG,
-  -- Alias of `slice.slice_id`.
-  slice_id LONG
-) AS
-SELECT * FROM slice;
-
--- Contains information of threads seen during the trace.
-CREATE PERFETTO VIEW thread(
-  -- The id of the thread. Prefer using `utid` instead.
-  id INT,
-  -- The name of the "most-specific" child table containing this row.
-  type STRING,
-  -- Unique thread id. This is != the OS tid. This is a monotonic number
-  -- associated to each thread. The OS thread id (tid) cannot be used as primary
-  -- key because tids and pids are recycled by most kernels.
-  utid INT,
-  -- The OS id for this thread. Note: this is *not* unique over the lifetime of
-  -- the trace so cannot be used as a primary key. Use |utid| instead.
-  tid INT,
-  -- The name of the thread. Can be populated from many sources (e.g. ftrace,
-  -- /proc scraping, track event etc).
-  name STRING,
-  -- The start timestamp of this thread (if known). Is null in most cases unless
-  -- a thread creation event is enabled (e.g. task_newtask ftrace event on
-  -- Linux/Android).
-  start_ts LONG,
-  -- The end timestamp of this thread (if known). Is null in most cases unless
-  -- a thread destruction event is enabled (e.g. sched_process_free ftrace event
-  -- on Linux/Android).
-  end_ts LONG,
-  -- The process hosting this thread.
-  upid LONG,
-  -- Boolean indicating if this thread is the main thread in the process.
-  is_main_thread BOOL,
-  -- Machine identifier, non-null for threads on a remote machine.
-  machine_id INT
-) AS
-SELECT id as utid, *
-FROM __intrinsic_thread;
-
--- Contains information of processes seen during the trace.
-CREATE PERFETTO VIEW process(
-  -- The id of the process. Prefer using `upid` instead.
-  id INT,
-  -- The name of the "most-specific" child table containing this row.
-  type STRING,
-  -- Unique process id. This is != the OS pid. This is a monotonic number
-  -- associated to each process. The OS process id (pid) cannot be used as
-  -- primary key because tids and pids are recycled by most kernels.
-  upid LONG,
-  -- The OS id for this process. Note: this is *not* unique over the lifetime of
-  -- the trace so cannot be used as a primary key. Use |upid| instead.
-  pid LONG,
-  -- The name of the process. Can be populated from many sources (e.g. ftrace,
-  -- /proc scraping, track event etc).
-  name STRING,
-  -- The start timestamp of this process (if known). Is null in most cases
-  -- unless a process creation event is enabled (e.g. task_newtask ftrace event
-  -- on Linux/Android).
-  start_ts LONG,
-  -- The end timestamp of this process (if known). Is null in most cases unless
-  -- a process destruction event is enabled (e.g. sched_process_free ftrace
-  -- event on Linux/Android).
-  end_ts LONG,
-  -- The upid of the process which caused this process to be spawned.
-  parent_upid INT,
-  -- The Unix user id of the process.
-  uid INT,
-  -- Android appid of this process.
-  android_appid INT,
-  -- /proc/cmdline for this process.
-  cmdline STRING,
-  -- Extra args for this process.
-  arg_set_id INT,
-  -- Machine identifier, non-null for processes on a remote machine.
-  machine_id INT
-) AS
-SELECT id as upid, *
-FROM __intrinsic_process;
-
--- Arbitrary key-value pairs which allow adding metadata to other, strongly
--- typed tables.
--- Note: for a given row, only one of |int_value|, |string_value|, |real_value|
--- will be non-null.
-CREATE PERFETTO VIEW args(
-  -- The id of the arg.
-  id INT,
-  -- The name of the "most-specific" child table containing this row.
-  type STRING,
-  -- The id for a single set of arguments.
-  arg_set_id INT,
-  -- The "flat key" of the arg: this is the key without any array indexes.
-  flat_key STRING,
-  -- The key for the arg.
-  key STRING,
-  -- The integer value of the arg.
-  int_value LONG,
-  -- The string value of the arg.
-  string_value STRING,
-  -- The double value of the arg.
-  real_value DOUBLE,
-  -- The type of the value of the arg. Will be one of 'int', 'uint', 'string',
-  -- 'real', 'pointer', 'bool' or 'json'.
-  value_type STRING,
-  -- The human-readable formatted value of the arg.
-  display_value STRING
-) AS
-SELECT
-  *,
-  -- This should be kept in sync with GlobalArgsTracker::AddArgSet.
-  CASE value_type
-    WHEN 'int' THEN cast_string!(int_value)
-    WHEN 'uint' THEN cast_string!(int_value)
-    WHEN 'string' THEN string_value
-    WHEN 'real' THEN cast_string!(real_value)
-    WHEN 'pointer' THEN printf('0x%x', int_value)
-    WHEN 'bool' THEN (
-      CASE WHEN int_value <> 0 THEN 'true'
-      ELSE 'false' END)
-    WHEN 'json' THEN string_value
-  ELSE NULL END AS display_value
-FROM __intrinsic_args;
-
--- Contains the Linux perf sessions in the trace.
-CREATE PERFETTO VIEW perf_session(
-  -- The id of the perf session. Prefer using `perf_session_id` instead.
-  id INT,
-  -- The name of the "most-specific" child table containing this row.
-  type STRING,
-  -- The id of the perf session.
-  perf_session_id INT,
-  -- Command line used to collect the data.
-  cmdline STRING
-)
-AS
-SELECT *, id AS perf_session_id
-FROM __intrinsic_perf_session;
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/thread_level_parallelism.sql b/src/trace_processor/perfetto_sql/stdlib/sched/thread_level_parallelism.sql
index e7081ee..50331e6 100644
--- a/src/trace_processor/perfetto_sql/stdlib/sched/thread_level_parallelism.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/thread_level_parallelism.sql
@@ -37,6 +37,24 @@
 FROM intervals_overlap_count!(runnable, ts, dur)
 ORDER BY ts;
 
+-- The count of threads in uninterruptible sleep over time.
+CREATE PERFETTO TABLE sched_uninterruptible_sleep_thread_count(
+  -- Timestamp when the thread count changed to the current value.
+  ts INT,
+  -- Number of threads in uninterrutible sleep, covering the range from this timestamp to the
+  -- next row's timestamp.
+  uninterruptible_sleep_thread_count INT
+) AS
+WITH
+uninterruptible_sleep AS (
+  SELECT ts, dur FROM thread_state
+  where state = 'D'
+)
+SELECT
+  ts, value as uninterruptible_sleep_thread_count
+FROM intervals_overlap_count!(uninterruptible_sleep, ts, dur)
+ORDER BY ts;
+
 -- The count of active CPUs over time.
 CREATE PERFETTO TABLE sched_active_cpu_count(
   -- Timestamp when the number of active CPU changed.
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/time_in_state.sql b/src/trace_processor/perfetto_sql/stdlib/sched/time_in_state.sql
index e627991..80696a1 100644
--- a/src/trace_processor/perfetto_sql/stdlib/sched/time_in_state.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/time_in_state.sql
@@ -93,36 +93,58 @@
 GROUP BY utid;
 
 -- Time the thread spent each state in a given interval.
+--
+-- This function is only designed to run over a small number of intervals
+-- (10-100 at most). It will be *very slow* for large sets of intervals.
+--
+-- Specifically for any non-trivial subset of thread slices, prefer using
+-- `thread_slice_time_in_state` in the `slices.time_in_state` module for this
+-- purpose instead.
 CREATE PERFETTO FUNCTION sched_time_in_state_for_thread_in_interval(
   -- The start of the interval.
   ts INT,
   -- The duration of the interval.
   dur INT,
   -- The utid of the thread.
-  utid INT)
+  utid INT
+)
 RETURNS TABLE(
-  -- Thread state (from the `thread_state` table).
-  -- Use `sched_state_to_human_readable_string` function to get full name.
-  state INT,
+  -- The scheduling state (from the `thread_state` table).
+  --
+  -- Use the `sched_state_to_human_readable_string` function in the `sched`
+  -- package to get full name.
+  state STRING,
   -- A (posssibly NULL) boolean indicating, if the device was in uninterruptible
   -- sleep, if it was an IO sleep.
   io_wait BOOL,
-  -- Some states can specify the blocked function. Usually NULL.
+  -- If the `state` is uninterruptible sleep, `io_wait` indicates if it was
+  -- an IO sleep. Will be null if `state` is *not* uninterruptible sleep or if
+  -- we cannot tell if it was an IO sleep or not.
+  --
+  -- Only available on Android when
+  -- `sched/sched_blocked_reason` ftrace tracepoint is enabled.
   blocked_function INT,
-  -- Total time spent with this state, cpu and blocked function.
-  dur INT) AS
+  -- The duration of time the threads slice spent for each
+  -- (state, io_wait, blocked_function) tuple.
+  dur INT
+) AS
 SELECT
   state,
   io_wait,
   blocked_function,
   sum(ii.dur) as dur
 FROM thread_state
-JOIN
-  (SELECT * FROM _interval_intersect_single!(
+JOIN (
+  SELECT *
+  FROM _interval_intersect_single!(
     $ts, $dur,
-    (SELECT id, ts, dur
-    FROM thread_state
-    WHERE utid = $utid AND dur > 0))) ii USING (id)
+    (
+      SELECT id, ts, dur
+      FROM thread_state
+      WHERE utid = $utid AND dur > 0
+    )
+  )
+) ii USING (id)
 GROUP BY 1, 2, 3
 ORDER BY 4 DESC;
 
@@ -137,7 +159,7 @@
 RETURNS TABLE(
   -- Thread state (from the `thread_state` table).
   -- Use `sched_state_to_human_readable_string` function to get full name.
-  state INT,
+  state STRING,
   -- A (posssibly NULL) boolean indicating, if the device was in uninterruptible
   -- sleep, if it was an IO sleep.
   io_wait BOOL,
diff --git a/src/trace_processor/perfetto_sql/stdlib/slices/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/slices/BUILD.gn
index 2e58615..4d616ea 100644
--- a/src/trace_processor/perfetto_sql/stdlib/slices/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/slices/BUILD.gn
@@ -21,6 +21,7 @@
     "flow.sql",
     "hierarchy.sql",
     "slices.sql",
+    "time_in_state.sql",
     "with_context.sql",
   ]
 }
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 4a36d9c..c395c21 100644
--- a/src/trace_processor/perfetto_sql/stdlib/slices/cpu_time.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/slices/cpu_time.sql
@@ -13,60 +13,70 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 
--- TODO(mayzner): Replace with good implementation of interval intersect.
-CREATE PERFETTO MACRO _interval_intersect_partition_utid(
-  left_table TableOrSubquery,
-  right_table TableOrSubquery
-)
-RETURNS TableOrSubquery AS
-(
-  WITH on_left AS (
-    SELECT
-      B.ts,
-      IIF(
-        A.ts + A.dur <= B.ts + B.dur,
-        A.ts + A.dur - B.ts, B.dur) AS dur,
-      A.id AS left_id,
-      B.id as right_id
-    FROM $left_table A
-    JOIN $right_table B ON (A.ts <= B.ts AND A.ts + A.dur > B.ts AND A.utid = B.utid)
-  ), on_right AS (
-    SELECT
-      B.ts,
-      IIF(
-        A.ts + A.dur <= B.ts + B.dur,
-        A.ts + A.dur - B.ts, B.dur) AS dur,
-      B.id as left_id,
-      A.id AS right_id
-    FROM $right_table A
-    -- The difference between this table and on_left is the lack of equality on
-    -- A.ts <= B.ts. This is to remove the issue of double accounting
-    -- timestamps that start at the same time.
-    JOIN $left_table B ON (A.ts < B.ts AND A.ts + A.dur > B.ts AND A.utid = B.utid)
-  )
-  SELECT * FROM on_left
-  UNION ALL
-  SELECT * FROM on_right
-);
+INCLUDE PERFETTO MODULE intervals.intersect;
+INCLUDE PERFETTO MODULE linux.cpu.utilization.slice;
 
 -- Time each thread slice spent running on CPU.
 -- Requires scheduling data to be available in the trace.
 CREATE PERFETTO TABLE thread_slice_cpu_time(
-    -- Slice id.
-    id INT,
-    -- Duration of the time the slice was running.
-    cpu_time INT) AS
-WITH slice_with_utid AS (
-  SELECT
-      slice.id,
-      slice.ts,
-      slice.dur,
-      utid
-  FROM slice
-  JOIN thread_track ON slice.track_id = thread_track.id
-  JOIN thread USING (utid)
-  WHERE utid != 0)
-SELECT left_id AS id, SUM(dur) AS cpu_time
-FROM _interval_intersect_partition_utid!(slice_with_utid, sched)
-GROUP BY 1
-ORDER BY 1;
\ No newline at end of file
+  -- Id of a slice. Alias of `slice.id`.
+  id INT,
+  -- Name of the slice.
+  name STRING,
+  -- Id of the thread the slice is running on. Alias of `thread.id`.
+  utid INT,
+  -- Name of the thread.
+  thread_name STRING,
+  -- Id of the process the slice is running on. Alias of `process.id`.
+  upid INT,
+  -- Name of the process.
+  process_name STRING,
+  -- Duration of the time the slice was running.
+  cpu_time INT) AS
+SELECT
+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)
+  ), (utid)) ii
+JOIN thread_slice ts ON ts.id = ii.id_0
+GROUP BY id
+ORDER BY id;
+
+-- CPU cycles per each slice.
+CREATE PERFETTO VIEW thread_slice_cpu_cycles(
+  -- Id of a slice. Alias of `slice.id`.
+  id INT,
+  -- Name of the slice.
+  name STRING,
+  -- Id of the thread the slice is running on. Alias of `thread.id`.
+  utid INT,
+  -- Name of the thread.
+  thread_name STRING,
+  -- Id of the process the slice is running on. Alias of `process.id`.
+  upid INT,
+  -- Name of the process.
+  process_name STRING,
+  -- Sum of CPU millicycles. Null if frequency couldn't be fetched for any
+  -- period during the runtime of the slice.
+  millicycles INT,
+  -- Sum of CPU megacycles. Null if frequency couldn't be fetched for any
+  -- period during the runtime of the slice.
+  megacycles INT
+) AS
+SELECT
+  id,
+  name,
+  utid,
+  thread_name,
+  upid,
+  process_name,
+  millicycles,
+  megacycles
+FROM cpu_cycles_per_thread_slice;
diff --git a/src/trace_processor/perfetto_sql/stdlib/slices/flow.sql b/src/trace_processor/perfetto_sql/stdlib/slices/flow.sql
index f939478..4741de1 100644
--- a/src/trace_processor/perfetto_sql/stdlib/slices/flow.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/slices/flow.sql
@@ -15,6 +15,15 @@
 
 INCLUDE PERFETTO MODULE graphs.search;
 
+-- It's very typical to filter the flow table on either incoming or outgoing slice ids.
+--
+-- Ideally, this should be automatic and shouldn't require any additional imports, however we
+-- can't add it to prelude (because it is initialised before the trace is loaded and the indexes
+-- are not rebuilt when the new data is loaded), so the interested parties should remember to import
+-- this module.
+CREATE PERFETTO INDEX flow_in ON flow(slice_in);
+CREATE PERFETTO INDEX flow_out ON flow(slice_out);
+
 -- Computes the "reachable" set of slices from the |flows| table, starting from slice ids
 -- specified in |source_table|. This provides a more efficient result than with the in-built
 -- following_flow operator.
diff --git a/src/trace_processor/perfetto_sql/stdlib/slices/hierarchy.sql b/src/trace_processor/perfetto_sql/stdlib/slices/hierarchy.sql
index dff6475..137a123 100644
--- a/src/trace_processor/perfetto_sql/stdlib/slices/hierarchy.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/slices/hierarchy.sql
@@ -91,4 +91,41 @@
 UNION ALL
 SELECT
   id, type, ts, dur, track_id, category, name, depth, parent_id, arg_set_id, thread_ts, thread_dur
-FROM descendant_slice($slice_id);
\ No newline at end of file
+FROM descendant_slice($slice_id);
+
+-- Delete rows from |slice_table| where the |column_name| value is NULL.
+--
+-- The |parent_id| of the remaining rows are adjusted to point to the closest
+-- ancestor remaining. This keeps the trees as connected as possible,
+-- allowing further graph analysis.
+CREATE PERFETTO MACRO _slice_remove_nulls_and_reparent(
+  -- Table or subquery containing a subset of the slice table. Required columns are
+  -- (id INT64, parent_id INT64, depth UINT32, <column_name>).
+  slice_table TableOrSubQuery,
+  -- Column name for which a NULL value indicates the row will be deleted.
+  column_name ColumnName)
+  -- The returned table has the schema (id INT64, parent_id INT64, depth UINT32, <column_name>).
+RETURNS TableOrSubQuery
+AS (
+  WITH _slice AS (
+    SELECT * FROM $slice_table WHERE $column_name IS NOT NULL
+  )
+  SELECT
+    id,
+    parent_id,
+    depth,
+    $column_name
+  FROM _slice
+  WHERE depth = 0
+  UNION ALL
+  SELECT
+    child.id,
+    anc.id AS parent_id,
+    MAX(IIF(parent.$column_name IS NULL, 0, anc.depth)) AS depth,
+    child.$column_name
+  FROM _slice child
+  JOIN ancestor_slice(child.id) anc
+  LEFT JOIN _slice parent
+    ON parent.id = anc.id
+  GROUP BY child.id
+);
diff --git a/src/trace_processor/perfetto_sql/stdlib/slices/time_in_state.sql b/src/trace_processor/perfetto_sql/stdlib/slices/time_in_state.sql
new file mode 100644
index 0000000..142a664
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/slices/time_in_state.sql
@@ -0,0 +1,77 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+INCLUDE PERFETTO MODULE intervals.intersect;
+INCLUDE PERFETTO MODULE slices.with_context;
+
+-- For each thread slice, returns the sum of the time it spent in various
+-- scheduling states.
+--
+-- Requires scheduling data to be available in the trace.
+CREATE PERFETTO TABLE thread_slice_time_in_state(
+  -- Id of a slice. Alias of `slice.id`.
+  id INT,
+  -- Name of the slice.
+  name STRING,
+  -- Id of the thread the slice is running on. Alias of `thread.id`.
+  utid INT,
+  -- Name of the thread.
+  thread_name STRING,
+  -- Id of the process the slice is running on. Alias of `process.id`.
+  upid INT,
+  -- Name of the process.
+  process_name STRING,
+  -- The scheduling state (from the `thread_state` table).
+  --
+  -- Use the `sched_state_to_human_readable_string` function in the `sched`
+  -- package to get full name.
+  state STRING,
+  -- If the `state` is uninterruptible sleep, `io_wait` indicates if it was
+  -- an IO sleep. Will be null if `state` is *not* uninterruptible sleep or if
+  -- we cannot tell if it was an IO sleep or not.
+  --
+  -- Only available on Android when
+  -- `sched/sched_blocked_reason` ftrace tracepoint is enabled.
+  io_wait BOOL,
+  -- If in uninterruptible sleep (D), the kernel function on which was blocked.
+  -- Only available on userdebug Android builds when
+  -- `sched/sched_blocked_reason` ftrace tracepoint is enabled.
+  blocked_function INT,
+  -- The duration of time the threads slice spent for each
+  -- (state, io_wait, blocked_function) tuple.
+  dur INT
+) AS
+SELECT
+  ii.id_0 AS id,
+  ts.name,
+  ts.utid,
+  ts.thread_name,
+  ts.upid,
+  ts.process_name,
+  tstate.state,
+  tstate.io_wait,
+  tstate.blocked_function,
+  SUM(ii.dur) AS dur
+FROM _interval_intersect!(
+  (
+    (SELECT * FROM thread_slice WHERE utid > 0 AND dur > 0),
+    (SELECT * FROM thread_state WHERE dur > 0)
+  ),
+  (utid)
+) ii
+JOIN thread_slice ts ON ts.id = ii.id_0
+JOIN thread_state tstate ON tstate.id = ii.id_1
+GROUP BY ii.id_0, tstate.state, tstate.io_wait, tstate.blocked_function
+ORDER BY ii.id_0;
diff --git a/src/trace_processor/perfetto_sql/stdlib/stacks/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/stacks/BUILD.gn
new file mode 100644
index 0000000..6f4ad9f
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/stacks/BUILD.gn
@@ -0,0 +1,19 @@
+# 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("../../../../../gn/perfetto_sql.gni")
+
+perfetto_sql_source_set("stacks") {
+  sources = [ "cpu_profiling.sql" ]
+}
diff --git a/src/trace_processor/perfetto_sql/stdlib/stacks/cpu_profiling.sql b/src/trace_processor/perfetto_sql/stdlib/stacks/cpu_profiling.sql
new file mode 100644
index 0000000..d117f05
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/stacks/cpu_profiling.sql
@@ -0,0 +1,125 @@
+--
+-- 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 callstacks.stack_profile;
+
+-- Table containing all the timestamped samples of CPU profiling which occurred
+-- during the trace.
+--
+-- Currently, this table is backed by the following data sources:
+--  * Linux perf
+--  * macOS instruments
+--  * Chrome CPU profiling
+--  * Legacy V8 CPU profiling
+--  * Profiling data in Gecko traces
+CREATE PERFETTO TABLE cpu_profiling_samples(
+  -- The id of the sample.
+  id INT,
+  -- The timestamp of the sample.
+  ts INT,
+  -- The utid of the thread of the sample, if available.
+  utid INT,
+  -- The tid of the sample, if available.
+  tid INT,
+  -- The thread name of thread of the sample, if available.
+  thread_name STRING,
+  -- The ucpu of the sample, if available.
+  ucpu INT,
+  -- The cpu of the sample, if available.
+  cpu INT,
+  -- The callsite id of the sample.
+  callsite_id INT
+)
+AS
+WITH raw_samples AS (
+  -- Linux perf samples.
+  SELECT p.ts, p.utid, p.cpu AS ucpu, p.callsite_id
+  FROM perf_sample p
+  UNION ALL
+  -- Instruments samples.
+  SELECT p.ts, p.utid, p.cpu AS ucpu, p.callsite_id
+  FROM instruments_sample p
+  UNION ALL
+  -- All other CPU profiling.
+  SELECT s.ts, s.utid, NULL AS ucpu, s.callsite_id
+  FROM cpu_profile_stack_sample s
+)
+SELECT
+  ROW_NUMBER() OVER (ORDER BY ts) AS id,
+  r.*,
+  t.tid,
+  t.name AS thread_name,
+  c.cpu
+FROM raw_samples r
+LEFT JOIN thread t USING (utid)
+LEFT JOIN cpu c USING (ucpu)
+ORDER BY ts;
+
+CREATE PERFETTO TABLE _cpu_profiling_self_callsites AS
+SELECT *
+FROM _callstacks_for_callsites!((
+  SELECT callsite_id
+  FROM cpu_profiling_samples
+))
+ORDER BY id;
+
+-- Table summarising the callstacks captured during any CPU profiling which
+-- occurred during 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.
+--
+-- The data sources supported are the same as the `cpu_profiling_samples` table.
+CREATE PERFETTO TABLE cpu_profiling_summary_tree(
+  -- The id of the callstack; by callstack we mean a unique set of frames up to
+  -- the root frame.
+  id INT,
+  -- The id of the parent callstack for this callstack. NULL if this is root.
+  parent_id INT,
+  -- 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, JAR or APK.
+  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 INT,
+  -- The number of samples with this function as the leaf frame.
+  self_count INT,
+  -- The number of samples with this function appearing anywhere on the
+  -- callstack.
+  cumulative_count INT
+) AS
+SELECT
+  id,
+  parent_id,
+  name,
+  mapping_name,
+  source_file,
+  line_number,
+  SUM(self_count) AS self_count,
+  SUM(cumulative_count) AS cumulative_count
+FROM (
+  SELECT r.*, a.cumulative_count
+  FROM _cpu_profiling_self_callsites r
+  JOIN _callstacks_self_to_cumulative!((
+    SELECT id, parent_id, self_count
+    FROM _cpu_profiling_self_callsites
+  )) a USING (id)
+)
+GROUP BY id;
diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/flamegraph.sql b/src/trace_processor/perfetto_sql/stdlib/viz/flamegraph.sql
index 3d6ac3a..77c2b10 100644
--- a/src/trace_processor/perfetto_sql/stdlib/viz/flamegraph.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/viz/flamegraph.sql
@@ -14,7 +14,6 @@
 -- limitations under the License.
 
 INCLUDE PERFETTO MODULE graphs.scan;
-INCLUDE PERFETTO MODULE metasql.column_list;
 
 CREATE PERFETTO MACRO _viz_flamegraph_hash_coalesce(col ColumnName)
 RETURNS _SqlFragment AS IFNULL($col, 0);
@@ -41,7 +40,7 @@
     $pivot AS isPivot,
     HASH(
       name,
-      _metasql_map_join_column_list!($grouping, _viz_flamegraph_hash_coalesce)
+      __intrinsic_token_apply!(_viz_flamegraph_hash_coalesce, $grouping)
     ) AS groupingHash
   FROM $tab
   ORDER BY id
@@ -214,8 +213,8 @@
     g.parentHash,
     g.depth,
     s.name,
-    _metasql_map_join_column_list!($grouping, _viz_flamegraph_s_prefix),
-    _metasql_map_join_column_list!($grouped, _viz_flamegraph_s_prefix),
+    __intrinsic_token_apply!(_viz_flamegraph_s_prefix, $grouping),
+    __intrinsic_token_apply!(_viz_flamegraph_s_prefix, $grouped),
     f.value,
     g.cumulativeValue
   FROM _graph_scan!(
@@ -271,8 +270,8 @@
     g.parentHash,
     g.depth,
     s.name,
-    _metasql_map_join_column_list!($grouping, _viz_flamegraph_s_prefix),
-    _metasql_map_join_column_list!($grouped, _viz_flamegraph_s_prefix),
+    __intrinsic_token_apply!(_viz_flamegraph_s_prefix, $grouping),
+    __intrinsic_token_apply!(_viz_flamegraph_s_prefix, $grouped),
     f.value,
     a.cumulativeValue
   FROM _graph_scan!(
@@ -295,18 +294,15 @@
   ORDER BY hash
 );
 
-CREATE PERFETTO MACRO _viz_flamegraph_merge_grouped(
-  col ColumnName
-)
-RETURNS _SqlFragment
-AS IIF(COUNT() = 1, $col, NULL) AS $col;
+CREATE PERFETTO MACRO _col_list_id(a ColumnName)
+RETURNS _SqlFragment AS $a;
 
 -- Converts a table of hashes and paretn hashes into ids and parent
 -- ids, grouping all hashes together.
 CREATE PERFETTO MACRO _viz_flamegraph_merge_hashes(
   hashed TableOrSubquery,
   grouping _ColumnNameList,
-  grouped _ColumnNameList
+  grouped_agged_exprs _ColumnNameList
 )
 RETURNS TableOrSubquery
 AS (
@@ -323,8 +319,8 @@
     -- The grouping columns should be passed through as-is because the
     -- hash took them into account: we would not merged any nodes where
     -- the grouping columns were different.
-    _metasql_unparenthesize_column_list!($grouping),
-    _metasql_map_join_column_list!($grouped, _viz_flamegraph_merge_grouped),
+    __intrinsic_token_apply!(_col_list_id, $grouping),
+    __intrinsic_token_apply!(_col_list_id, $grouped_agged_exprs),
     SUM(value) AS value,
     SUM(cumulativeValue) AS cumulativeValue
   FROM $hashed c
@@ -381,10 +377,11 @@
     s.id,
     IFNULL(s.parentId, -1) AS parentId,
     IIF(s.name = '', 'unknown', s.name) AS name,
-    _metasql_map_join_column_list!($grouping, _viz_flamegraph_s_prefix),
-    _metasql_map_join_column_list!($grouped, _viz_flamegraph_s_prefix),
+    __intrinsic_token_apply!(_viz_flamegraph_s_prefix, $grouping),
+    __intrinsic_token_apply!(_viz_flamegraph_s_prefix, $grouped),
     s.value AS selfValue,
     s.cumulativeValue,
+    p.cumulativeValue AS parentCumulativeValue,
     s.depth,
     g.xStart,
     g.xEnd
@@ -403,5 +400,6 @@
     )
   ) g
   JOIN $merged s USING (id)
+  LEFT JOIN $merged p ON s.parentId = p.id
   ORDER BY rootDistance, xStart
 );
diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql b/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql
index 7a1d5d0..49fb3f0 100644
--- a/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql
@@ -15,22 +15,114 @@
 
 INCLUDE PERFETTO MODULE viz.summary.slices;
 
-CREATE PERFETTO TABLE _process_track_summary_by_upid_and_name AS
+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)
-GROUP BY upid, name;
-
-CREATE PERFETTO TABLE _uid_track_track_summary_by_uid_and_name AS
-SELECT
-  uid,
-  name,
-  GROUP_CONCAT(id) AS track_ids,
-  COUNT() AS track_count
-FROM uid_track
-JOIN _slice_track_summary USING (id)
-GROUP BY uid, name;
+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 cd7377a..185d6ef 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/BUILD.gn
@@ -17,12 +17,16 @@
 perfetto_sql_source_set("wattson") {
   sources = [
     "arm_dsu.sql",
+    "cpu_freq.sql",
+    "cpu_freq_idle.sql",
     "cpu_idle.sql",
     "cpu_split.sql",
     "curves/device.sql",
-    "curves/grouped.sql",
-    "curves/ungrouped.sql",
+    "curves/estimates.sql",
+    "curves/idle_attribution.sql",
     "curves/utils.sql",
+    "curves/w_cpu_dependence.sql",
+    "curves/w_dsu_dependence.sql",
     "device_infos.sql",
     "system_state.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
new file mode 100644
index 0000000..94f8fd7
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql
@@ -0,0 +1,54 @@
+--
+-- 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 linux.cpu.frequency;
+INCLUDE PERFETTO MODULE wattson.device_infos;
+
+CREATE PERFETTO TABLE _adjusted_cpu_freq AS
+  WITH _cpu_freq AS (
+    SELECT
+      ts,
+      dur,
+      freq,
+      cf.ucpu as cpu,
+      d_map.policy
+    FROM cpu_frequency_counters as cf
+    JOIN _dev_cpu_policy_map as d_map
+    ON cf.ucpu = d_map.cpu
+  ),
+  -- Get first freq transition per CPU
+  first_cpu_freq_slices AS (
+    SELECT 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
+  -- Construct slices from first cpu ts up to first freq event for each cpu
+  trace_start() as ts,
+  first_slices.ts - trace_start() as dur,
+  NULL as freq,
+  first_slices.cpu,
+  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
+UNION ALL
+SELECT
+  ts,
+  dur,
+  freq,
+  cpu,
+  policy
+FROM _cpu_freq;
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
new file mode 100644
index 0000000..756ebed
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq_idle.sql
@@ -0,0 +1,76 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+INCLUDE PERFETTO MODULE intervals.intersect;
+INCLUDE PERFETTO MODULE wattson.cpu_freq;
+INCLUDE PERFETTO MODULE wattson.cpu_idle;
+INCLUDE PERFETTO MODULE wattson.curves.utils;
+INCLUDE PERFETTO MODULE wattson.device_infos;
+
+-- Helper macro for using Perfetto table with interval intersect
+CREATE PERFETTO MACRO _ii_subquery(tab TableOrSubquery)
+RETURNS TableOrSubquery AS (SELECT _auto_id AS id, * FROM $tab);
+
+-- Wattson estimation is valid from when first CPU0 frequency appears
+CREATE PERFETTO TABLE _valid_window
+AS
+WITH window_start AS (
+  SELECT ts as start_ts
+  FROM _adjusted_cpu_freq
+  WHERE cpu = 0 AND freq IS NOT NULL
+  ORDER BY ts ASC
+  LIMIT 1
+),
+window AS (
+  SELECT start_ts as ts, trace_end() - start_ts as dur
+  FROM window_start
+)
+SELECT *, 0 as cpu FROM window
+UNION ALL
+SELECT *, 1 as cpu FROM window
+UNION ALL
+SELECT *, 2 as cpu FROM window
+UNION ALL
+SELECT *, 3 as cpu FROM window
+UNION ALL
+SELECT *, 4 as cpu FROM window
+UNION ALL
+SELECT *, 5 as cpu FROM window
+UNION ALL
+SELECT *, 6 as cpu FROM window
+UNION ALL
+SELECT *, 7 as cpu FROM window;
+
+-- Start matching CPUs with 1D curves based on combination of freq and idle
+CREATE PERFETTO TABLE _idle_freq_materialized
+AS
+SELECT
+  ii.ts, ii.dur, ii.cpu, freq.policy, freq.freq, idle.idle, lut.curve_value
+FROM _interval_intersect!(
+  (
+    _ii_subquery!(_valid_window),
+    _ii_subquery!(_adjusted_cpu_freq),
+    _ii_subquery!(_adjusted_deep_idle)
+  ),
+  (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
+-- Left join since some CPUs may only match the 2D LUT
+LEFT JOIN _filtered_curves_1d lut ON
+  freq.policy = lut.policy AND
+  freq.freq = lut.freq_khz AND
+  idle.idle = lut.idle;
+
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_idle.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_idle.sql
index e59f724..5c8c6b5 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_idle.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_idle.sql
@@ -17,8 +17,7 @@
 INCLUDE PERFETTO MODULE wattson.device_infos;
 
 -- Get the corresponding deep idle time offset based on device and CPU.
-CREATE PERFETTO TABLE _filtered_deep_idle_offsets
-AS
+CREATE PERFETTO VIEW _filtered_deep_idle_offsets AS
 SELECT cpu, offset_ns
 FROM _device_cpu_deep_idle_offsets as offsets
 JOIN _wattson_device as device
@@ -27,13 +26,12 @@
 -- Adjust duration of active portion to be slightly longer to account for
 -- overhead cost of transitioning out of deep idle. This is done because the
 -- device is active and consumes power for longer than the logs actually report.
-CREATE PERFETTO TABLE _adjusted_deep_idle
-AS
+CREATE PERFETTO TABLE _adjusted_deep_idle AS
 WITH
   idle_prev AS (
     SELECT
       ts,
-      dur,
+      LAG(ts, 1, trace_start()) OVER (PARTITION BY cpu ORDER by ts) as prev_ts,
       value AS idle,
       cli.value - cli.delta_value AS idle_prev,
       cct.cpu
@@ -47,28 +45,50 @@
     )) AS cli
     JOIN cpu_counter_track AS cct ON cli.track_id = cct.id
   ),
-  -- Adjusted ts if applicable, which makes the current deep idle state
-  -- slightly shorter.
+  -- Adjusted ts if applicable, which makes the current active state longer if
+  -- it is coming from an idle exit.
   idle_mod AS (
     SELECT
       IIF(
-        idle_prev = 4294967295 AND idle = 1,
-        IIF(dur > offset_ns, ts + offset_ns, ts + dur),
+        idle_prev = 1 AND idle = 4294967295,
+        -- extend ts backwards by offset_ns at most up to prev_ts
+        MAX(ts - offset_ns, prev_ts),
         ts
       ) as ts,
-      -- ts_next is the starting timestamp of the next slice (i.e. end ts of
-      -- current slice)
-      ts + dur as ts_next,
       cpu,
       idle
     FROM idle_prev
     JOIN _filtered_deep_idle_offsets USING (cpu)
+  ),
+  _cpu_idle AS (
+    SELECT
+      ts,
+      LEAD(ts, 1, trace_end()) OVER (PARTITION BY cpu ORDER by ts) - ts as dur,
+      cpu,
+      cast_int!(IIF(idle = 4294967295, -1, idle)) AS idle
+    FROM idle_mod
+  ),
+  -- Get first idle transition per CPU
+  first_cpu_idle_slices AS (
+    SELECT ts, cpu FROM _cpu_idle
+    GROUP BY cpu
+    ORDER by ts ASC
   )
+-- Prepend NULL slices up to first idle events on a per CPU basis
+SELECT
+  -- Construct slices from first cpu ts up to first freq event for each cpu
+  trace_start() as ts,
+  first_slices.ts - trace_start() as dur,
+  first_slices.cpu,
+  NULL as idle
+FROM first_cpu_idle_slices as first_slices
+WHERE dur > 0
+UNION ALL
 SELECT
   ts,
-  lead(ts, 1, trace_end()) OVER (PARTITION BY cpu ORDER by ts) - ts as dur,
+  dur,
   cpu,
-  cast_int!(IIF(idle = 4294967295, -1, idle)) AS idle
-FROM idle_mod
-WHERE ts != ts_next;
-
+  idle
+FROM _cpu_idle
+-- Some durations are 0 post-adjustment and won't work with interval intersect
+WHERE dur > 0;
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_split.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_split.sql
index 8c496d9..3d35a94 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_split.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_split.sql
@@ -13,208 +13,206 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 
-INCLUDE PERFETTO MODULE linux.cpu.frequency;
+INCLUDE PERFETTO MODULE android.suspend;
+INCLUDE PERFETTO MODULE intervals.intersect;
 INCLUDE PERFETTO MODULE time.conversion;
 INCLUDE PERFETTO MODULE wattson.arm_dsu;
-INCLUDE PERFETTO MODULE wattson.cpu_idle;
+INCLUDE PERFETTO MODULE wattson.cpu_freq_idle;
 INCLUDE PERFETTO MODULE wattson.curves.utils;
 INCLUDE PERFETTO MODULE wattson.device_infos;
 
-CREATE PERFETTO TABLE _cpu_freq
-AS
-SELECT
-  ts,
-  dur,
-  freq,
-  cf.cpu,
-  d_map.policy
-FROM cpu_frequency_counters as cf
-JOIN _dev_cpu_policy_map as d_map
-ON cf.cpu = d_map.cpu;
-
--- Combines idle and freq tables of all CPUs to create system state.
-CREATE VIRTUAL TABLE _idle_freq
-USING
-  SPAN_OUTER_JOIN(
-    _cpu_freq partitioned cpu, _adjusted_deep_idle partitioned cpu
-  );
-
--- Add extra column indicating that frequency info are present
-CREATE PERFETTO TABLE _valid_window
-AS
-WITH window_start AS (
-  SELECT ts as start_ts
-  FROM _idle_freq
-  WHERE cpu = 0 and freq GLOB '*[0-9]*'
-  ORDER BY ts ASC
-  LIMIT 1
-),
-window_end AS (
-  SELECT ts + dur as end_ts
-  FROM cpu_frequency_counters
-  ORDER by ts DESC
-  LIMIT 1
+-- Helper macro to do pivot function without policy information
+CREATE PERFETTO MACRO _stats_wo_policy_subquery(
+  cpu Expr, curve_col ColumnName, freq_col ColumnName, idle_col ColumnName
 )
-SELECT
-  start_ts as ts,
-  end_ts - start_ts as dur
-FROM window_start, window_end;
+RETURNS TableOrSubquery AS (
+  SELECT
+    ts,
+    dur,
+    curve_value as $curve_col,
+    freq as $freq_col,
+    idle as $idle_col
+  FROM _idle_freq_materialized
+  WHERE cpu = $cpu
+);
 
-CREATE VIRTUAL TABLE _idle_freq_filtered
+-- Helper macro to do pivot function with policy information
+CREATE PERFETTO MACRO _stats_w_policy_subquery(
+  cpu Expr,
+  policy_col ColumnName,
+  curve_col ColumnName,
+  freq_col ColumnName,
+  idle_col ColumnName
+)
+RETURNS TableOrSubquery AS (
+  SELECT
+    ts,
+    dur,
+    policy AS $policy_col,
+    curve_value as $curve_col,
+    freq as $freq_col,
+    idle as $idle_col
+  FROM _idle_freq_materialized
+  WHERE cpu = $cpu
+);
+
+CREATE PERFETTO TABLE _stats_cpu0 AS
+SELECT * FROM _stats_wo_policy_subquery!(0, cpu0_curve, freq_0, idle_0);
+
+CREATE PERFETTO TABLE _stats_cpu1 AS
+SELECT * FROM _stats_wo_policy_subquery!(1, cpu1_curve, freq_1, idle_1);
+
+CREATE PERFETTO TABLE _stats_cpu2 AS
+SELECT * FROM _stats_wo_policy_subquery!(2, cpu2_curve, freq_2, idle_2);
+
+CREATE PERFETTO TABLE _stats_cpu3 AS
+SELECT * FROM _stats_wo_policy_subquery!(3, cpu3_curve, freq_3, idle_3);
+
+CREATE PERFETTO TABLE _stats_cpu4 AS
+SELECT * FROM _stats_w_policy_subquery!(4, policy_4, cpu4_curve, freq_4, idle_4);
+
+CREATE PERFETTO TABLE _stats_cpu5 AS
+SELECT * FROM _stats_w_policy_subquery!(5, policy_5, cpu5_curve, freq_5, idle_5);
+
+CREATE PERFETTO TABLE _stats_cpu6 AS
+SELECT * FROM _stats_w_policy_subquery!(6, policy_6, cpu6_curve, freq_6, idle_6);
+
+CREATE PERFETTO TABLE _stats_cpu7 AS
+SELECT * FROM _stats_w_policy_subquery!(7, policy_7, cpu7_curve, freq_7, idle_7);
+
+CREATE PERFETTO TABLE _stats_cpu0123_suspend AS
+SELECT
+  ii.ts,
+  ii.dur,
+  id_0 as cpu0_id, id_1 as cpu1_id, id_2 as cpu2_id, id_3 as cpu3_id,
+  ss.power_state = 'suspended' as suspended
+FROM _interval_intersect!(
+  (
+    _ii_subquery!(_stats_cpu0),
+    _ii_subquery!(_stats_cpu1),
+    _ii_subquery!(_stats_cpu2),
+    _ii_subquery!(_stats_cpu3),
+    -- Includes suspend AND awake portions, which will cover entire trace and
+    -- allows us to use _interval_intersect instead of SPAN_OUTER_JOIN()
+    _ii_subquery!(android_suspend_state)
+  ),
+  ()
+) as ii
+JOIN android_suspend_state AS ss ON ss._auto_id = id_4;
+
+CREATE PERFETTO TABLE _stats_cpu4567 AS
+SELECT
+  ii.ts,
+  ii.dur,
+  id_0 as cpu4_id, id_1 as cpu5_id, id_2 as cpu6_id, id_3 as cpu7_id
+FROM _interval_intersect!(
+  (
+    _ii_subquery!(_stats_cpu4),
+    _ii_subquery!(_stats_cpu5),
+    _ii_subquery!(_stats_cpu6),
+    _ii_subquery!(_stats_cpu7)
+  ),
+  ()
+) as ii;
+
+-- SPAN OUTER JOIN because sometimes CPU4/5/6/7 are empty tables
+CREATE VIRTUAL TABLE _stats_cpu01234567_suspend
 USING
-  SPAN_JOIN(_valid_window, _idle_freq);
-
--- Start matching split CPUs with curves
-CREATE PERFETTO TABLE _idle_freq_materialized
-AS
-SELECT
-  iff.ts, iff.dur, iff.cpu, iff.policy, iff.freq, iff.idle, lut.curve_value
-FROM _idle_freq_filtered iff
--- Left join since some CPUs may only match the 2D LUT
-LEFT JOIN _filtered_curves_1d lut ON
-  iff.policy = lut.policy AND
-  iff.freq = lut.freq_khz AND
-  iff.idle = lut.idle;
-
-CREATE PERFETTO TABLE _stats_cpu0
-AS
-SELECT
-  ts,
-  dur,
-  curve_value as cpu0_curve,
-  freq as freq_0,
-  idle as idle_0
-FROM _idle_freq_materialized
-WHERE cpu = 0;
-
-CREATE PERFETTO TABLE _stats_cpu1
-AS
-SELECT
-  ts,
-  dur,
-  curve_value as cpu1_curve,
-  freq as freq_1,
-  idle as idle_1
-FROM _idle_freq_materialized
-WHERE cpu = 1;
-
-CREATE PERFETTO TABLE _stats_cpu2
-AS
-SELECT
-  ts,
-  dur,
-  curve_value as cpu2_curve,
-  freq as freq_2,
-  idle as idle_2
-FROM _idle_freq_materialized
-WHERE cpu = 2;
-
-CREATE PERFETTO TABLE _stats_cpu3
-AS
-SELECT
-  ts,
-  dur,
-  curve_value as cpu3_curve,
-  freq as freq_3,
-  idle as idle_3
-FROM _idle_freq_materialized
-WHERE cpu = 3;
-
-CREATE PERFETTO TABLE _stats_cpu4
-AS
-SELECT
-  ts,
-  dur,
-  policy as policy_4,
-  curve_value as cpu4_curve,
-  freq as freq_4,
-  idle as idle_4
-FROM _idle_freq_materialized
-WHERE cpu = 4;
-
-CREATE PERFETTO TABLE _stats_cpu5
-AS
-SELECT
-  ts,
-  dur,
-  policy as policy_5,
-  curve_value as cpu5_curve,
-  freq as freq_5,
-  idle as idle_5
-FROM _idle_freq_materialized
-WHERE cpu = 5;
-
-CREATE PERFETTO TABLE _stats_cpu6
-AS
-SELECT
-  ts,
-  dur,
-  policy as policy_6,
-  curve_value as cpu6_curve,
-  freq as freq_6,
-  idle as idle_6
-FROM _idle_freq_materialized
-WHERE cpu = 6;
-
-CREATE PERFETTO TABLE _stats_cpu7
-AS
-SELECT
-  ts,
-  dur,
-  policy as policy_7,
-  curve_value as cpu7_curve,
-  freq as freq_7,
-  idle as idle_7
-FROM _idle_freq_materialized
-WHERE cpu = 7;
-
-CREATE VIRTUAL TABLE _stats_cpu01
-USING
-  SPAN_OUTER_JOIN(_stats_cpu1, _stats_cpu0);
-
-CREATE VIRTUAL TABLE _stats_cpu012
-USING
-  SPAN_OUTER_JOIN(_stats_cpu2, _stats_cpu01);
-
-CREATE VIRTUAL TABLE _stats_cpu0123
-USING
-  SPAN_OUTER_JOIN(_stats_cpu3, _stats_cpu012);
-
-CREATE VIRTUAL TABLE _stats_cpu01234
-USING
-  SPAN_OUTER_JOIN(_stats_cpu4, _stats_cpu0123);
-
-CREATE VIRTUAL TABLE _stats_cpu012345
-USING
-  SPAN_OUTER_JOIN(_stats_cpu5, _stats_cpu01234);
-
-CREATE VIRTUAL TABLE _stats_cpu0123456
-USING
-  SPAN_OUTER_JOIN(_stats_cpu6, _stats_cpu012345);
-
-CREATE VIRTUAL TABLE _stats_cpu01234567
-USING
-  SPAN_OUTER_JOIN(_stats_cpu7, _stats_cpu0123456);
-
--- get suspend resume state as logged by ftrace.
-CREATE PERFETTO TABLE _suspend_slice
-AS
-SELECT
-  ts, dur, TRUE AS suspended
-FROM slice
-WHERE name GLOB "timekeeping_freeze(0)";
-
--- Combine suspend information with CPU idle and frequency system states.
-CREATE VIRTUAL TABLE _idle_freq_suspend_slice
-USING
-  SPAN_OUTER_JOIN(_stats_cpu01234567, _suspend_slice);
+  SPAN_OUTER_JOIN(_stats_cpu0123_suspend, _stats_cpu4567);
 
 -- Combine system state so that it has idle, freq, and L3 hit info.
 CREATE VIRTUAL TABLE _idle_freq_l3_hit_slice
 USING
-  SPAN_OUTER_JOIN(_idle_freq_suspend_slice, _arm_l3_hit_rate);
+  SPAN_OUTER_JOIN(_stats_cpu01234567_suspend, _arm_l3_hit_rate);
 
 -- Combine system state so that it has idle, freq, L3 hit, and L3 miss info.
 CREATE VIRTUAL TABLE _idle_freq_l3_hit_l3_miss_slice
 USING
   SPAN_OUTER_JOIN(_idle_freq_l3_hit_slice, _arm_l3_miss_rate);
+
+-- Does calculations for CPUs that are independent of other CPUs or frequencies
+-- This is the last generic table before going to device specific table calcs
+CREATE PERFETTO TABLE _w_independent_cpus_calc
+AS
+SELECT
+  base.ts,
+  base.dur,
+  cast_int!(l3_hit_rate * base.dur) as l3_hit_count,
+  cast_int!(l3_miss_rate * base.dur) as l3_miss_count,
+  freq_0,
+  idle_0,
+  freq_1,
+  idle_1,
+  freq_2,
+  idle_2,
+  freq_3,
+  idle_3,
+  freq_4,
+  idle_4,
+  freq_5,
+  idle_5,
+  freq_6,
+  idle_6,
+  freq_7,
+  idle_7,
+  policy_4,
+  policy_5,
+  policy_6,
+  policy_7,
+  IIF(
+    suspended,
+    1,
+    MIN(
+      IFNULL(idle_0, 1),
+      IFNULL(idle_1, 1),
+      IFNULL(idle_2, 1),
+      IFNULL(idle_3, 1)
+    )
+  ) as no_static,
+  IIF(suspended, 0.0, cpu0_curve) as cpu0_curve,
+  IIF(suspended, 0.0, cpu1_curve) as cpu1_curve,
+  IIF(suspended, 0.0, cpu2_curve) as cpu2_curve,
+  IIF(suspended, 0.0, cpu3_curve) as cpu3_curve,
+  IIF(suspended, 0.0, cpu4_curve) as cpu4_curve,
+  IIF(suspended, 0.0, cpu5_curve) as cpu5_curve,
+  IIF(suspended, 0.0, cpu6_curve) as cpu6_curve,
+  IIF(suspended, 0.0, cpu7_curve) as cpu7_curve,
+  -- If dependency CPUs are active, then that CPU could contribute static power
+  IIF(idle_4 = -1, lut4.curve_value, -1) as static_4,
+  IIF(idle_5 = -1, lut5.curve_value, -1) as static_5,
+  IIF(idle_6 = -1, lut6.curve_value, -1) as static_6,
+  IIF(idle_7 = -1, lut7.curve_value, -1) as static_7
+FROM _idle_freq_l3_hit_l3_miss_slice as base
+-- Get CPU power curves for CPUs guaranteed on device
+JOIN _stats_cpu0 ON _stats_cpu0._auto_id = base.cpu0_id
+JOIN _stats_cpu1 ON _stats_cpu1._auto_id = base.cpu1_id
+JOIN _stats_cpu2 ON _stats_cpu2._auto_id = base.cpu2_id
+JOIN _stats_cpu3 ON _stats_cpu3._auto_id = base.cpu3_id
+-- Get CPU power curves for CPUs that aren't always present
+LEFT JOIN _stats_cpu4 ON _stats_cpu4._auto_id = base.cpu4_id
+LEFT JOIN _stats_cpu5 ON _stats_cpu5._auto_id = base.cpu5_id
+LEFT JOIN _stats_cpu6 ON _stats_cpu6._auto_id = base.cpu6_id
+LEFT JOIN _stats_cpu7 ON _stats_cpu7._auto_id = base.cpu7_id
+-- Match power curves if possible on CPUs that decide 2D dependence
+LEFT JOIN _filtered_curves_2d lut4 ON
+  _stats_cpu0.freq_0 = lut4.freq_khz AND
+  _stats_cpu4.policy_4 = lut4.other_policy AND
+  _stats_cpu4.freq_4 = lut4.other_freq_khz AND
+  lut4.idle = 255
+LEFT JOIN _filtered_curves_2d lut5 ON
+  _stats_cpu0.freq_0 = lut5.freq_khz AND
+  _stats_cpu5.policy_5 = lut5.other_policy AND
+  _stats_cpu5.freq_5 = lut5.other_freq_khz AND
+  lut5.idle = 255
+LEFT JOIN _filtered_curves_2d lut6 ON
+  _stats_cpu0.freq_0 = lut6.freq_khz AND
+  _stats_cpu6.policy_6 = lut6.other_policy AND
+  _stats_cpu6.freq_6 = lut6.other_freq_khz AND
+  lut6.idle = 255
+LEFT JOIN _filtered_curves_2d lut7 ON
+  _stats_cpu0.freq_0 = lut7.freq_khz AND
+  _stats_cpu7.policy_7 = lut7.other_policy AND
+  _stats_cpu7.freq_7 = lut7.other_freq_khz AND
+  lut7.idle = 255
+-- Needs to be at least 1us to reduce inconsequential rows.
+WHERE base.dur > time_from_us(1);
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/curves/device.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/curves/device.sql
index de8e173..e65cf1c 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/curves/device.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/curves/device.sql
@@ -54,7 +54,42 @@
   ("Tensor", 6, 2507000, 0, 1267.19, 66.14, 0),
   ("Tensor", 6, 2630000, 0, 1500.6, 82.36, 0),
   ("Tensor", 6, 2704000, 0, 1660.81, 95.11, 0),
-  ("Tensor", 6, 2802000, 0, 1942.89, 121.43, 0)
+  ("Tensor", 6, 2802000, 0, 1942.89, 121.43, 0),
+  ("Tensor G4", 4, 357000, 0, 39.49, 6.1, 0),
+  ("Tensor G4", 4, 578000, 0, 62.09, 6.5, 0),
+  ("Tensor G4", 4, 648000, 0, 70.05, 6.93, 0),
+  ("Tensor G4", 4, 787000, 0, 83.26, 7.31, 0),
+  ("Tensor G4", 4, 910000, 0, 97.12, 7.55, 0),
+  ("Tensor G4", 4, 1065000, 0, 116.15, 7.9, 0),
+  ("Tensor G4", 4, 1221000, 0, 138.37, 8.47, 0),
+  ("Tensor G4", 4, 1328000, 0, 155.59, 8.94, 0),
+  ("Tensor G4", 4, 1418000, 0, 172.52, 9.37, 0),
+  ("Tensor G4", 4, 1549000, 0, 200.69, 10.21, 0),
+  ("Tensor G4", 4, 1795000, 0, 267.18, 11.89, 0),
+  ("Tensor G4", 4, 1945000, 0, 317.06, 13.58, 0),
+  ("Tensor G4", 4, 2130000, 0, 388.15, 16.02, 0),
+  ("Tensor G4", 4, 2245000, 0, 430.4, 17.54, 0),
+  ("Tensor G4", 4, 2367000, 0, 504.35, 20.92, 0),
+  ("Tensor G4", 4, 2450000, 0, 579.03, 23.39, 0),
+  ("Tensor G4", 4, 2600000, 0, 674.24, 31.07, 0),
+  ("Tensor G4", 7, 700000, 0, 211.41, 17.97, 0),
+  ("Tensor G4", 7, 1164000, 0, 375.49, 20.24, 0),
+  ("Tensor G4", 7, 1396000, 0, 491.17, 22.35, 0),
+  ("Tensor G4", 7, 1557000, 0, 589.06, 24.29, 0),
+  ("Tensor G4", 7, 1745000, 0, 742.95, 26.79, 0),
+  ("Tensor G4", 7, 1885000, 0, 862.73, 28.61, 0),
+  ("Tensor G4", 7, 1999000, 0, 965.94, 30.04, 0),
+  ("Tensor G4", 7, 2147000, 0, 1136.58, 32.65, 0),
+  ("Tensor G4", 7, 2294000, 0, 1309.39, 35.62, 0),
+  ("Tensor G4", 7, 2363000, 0, 1415.82, 37.93, 0),
+  ("Tensor G4", 7, 2499000, 0, 1669.61, 42.96, 0),
+  ("Tensor G4", 7, 2687000, 0, 2052.32, 52.16, 0),
+  ("Tensor G4", 7, 2802000, 0, 2354.18, 60.2, 0),
+  ("Tensor G4", 7, 2914000, 0, 2789.17, 77.16, 0),
+  ("Tensor G4", 7, 2943000, 0, 2840.06, 79.64, 0),
+  ("Tensor G4", 7, 2970000, 0, 2949.03, 84.78, 0),
+  ("Tensor G4", 7, 3015000, 0, 3029.38, 87.22, 0),
+  ("Tensor G4", 7, 3105000, 0, 3327.56, 99.47, 0)
 )
 select * from data;
 
@@ -315,7 +350,157 @@
   ("Tensor", 1803000, 6, 2507000, 34.89, 240.7, 3.58, 0),
   ("Tensor", 1803000, 6, 2630000, 35.21, 150.76, 3.42, 0),
   ("Tensor", 1803000, 6, 2704000, 35.2, 277.28, 3.44, 0),
-  ("Tensor", 1803000, 6, 2802000, 35.12, 269.2, 3.62, 0)
+  ("Tensor", 1803000, 6, 2802000, 35.12, 269.2, 3.62, 0),
+  ("Tensor G4", 820000, 255, 610000, 3.3, 47.6, 1.04, 0),
+  ("Tensor G4", 820000, 255, 820000, 6.77, 65.48, 1.17, 0),
+  ("Tensor G4", 820000, 255, 970000, 8.61, 78.56, 1.28, 0),
+  ("Tensor G4", 820000, 255, 1098000, 12.5, 92.7, 1.28, 0),
+  ("Tensor G4", 820000, 255, 1197000, 15.24, 110.72, 1.46, 0),
+  ("Tensor G4", 820000, 255, 1328000, 21.73, 134.04, 1.58, 0),
+  ("Tensor G4", 820000, 255, 1444000, 26.89, 151.02, 1.75, 0),
+  ("Tensor G4", 820000, 255, 1548000, 31.53, 164.93, 1.8, 0),
+  ("Tensor G4", 820000, 255, 1704000, 43.86, 157.18, 2.24, 0),
+  ("Tensor G4", 820000, 255, 1800000, 52.1, 137.62, 2.64, 0),
+  ("Tensor G4", 820000, 255, 1880000, 59.74, 145.28, 2.44, 0),
+  ("Tensor G4", 820000, 255, 1950000, 71.34, 156.19, 3.12, 0),
+  ("Tensor G4", 820000, 255, 2024000, 86.3, 155.05, 3.53, 0),
+  ("Tensor G4", 820000, 255, 2120000, 112.45, 176.15, 4.74, 0),
+  ("Tensor G4", 820000, 255, 2150000, 112.1, 155.11, 4.69, 0),
+  ("Tensor G4", 955000, 255, 610000, 6.18, 56.03, 0.68, 0),
+  ("Tensor G4", 955000, 255, 820000, 6.39, 74.22, 1.21, 0),
+  ("Tensor G4", 955000, 255, 970000, 9.18, 82.2, 1.26, 0),
+  ("Tensor G4", 955000, 255, 1098000, 12.62, 98.11, 1.34, 0),
+  ("Tensor G4", 955000, 255, 1197000, 15.72, 117.95, 1.41, 0),
+  ("Tensor G4", 955000, 255, 1328000, 21.94, 141.91, 1.57, 0),
+  ("Tensor G4", 955000, 255, 1444000, 27.38, 162.99, 1.85, 0),
+  ("Tensor G4", 955000, 255, 1548000, 31.94, 175.19, 1.72, 0),
+  ("Tensor G4", 955000, 255, 1704000, 43.84, 130.38, 2.44, 0),
+  ("Tensor G4", 955000, 255, 1800000, 52.67, 117.99, 2.5, 0),
+  ("Tensor G4", 955000, 255, 1880000, 59.69, 145.08, 2.91, 0),
+  ("Tensor G4", 955000, 255, 1950000, 73.33, 141.24, 3.23, 0),
+  ("Tensor G4", 955000, 255, 2024000, 86.96, 171.19, 3.7, 0),
+  ("Tensor G4", 955000, 255, 2120000, 112.7, 188.38, 5.0, 0),
+  ("Tensor G4", 955000, 255, 2150000, 111.86, 179.34, 5.53, 0),
+  ("Tensor G4", 1098000, 255, 610000, 7.23, 66.6, 1.26, 0),
+  ("Tensor G4", 1098000, 255, 820000, 8.04, 80.19, 1.21, 0),
+  ("Tensor G4", 1098000, 255, 970000, 9.56, 90.34, 1.22, 0),
+  ("Tensor G4", 1098000, 255, 1098000, 12.86, 109.5, 1.33, 0),
+  ("Tensor G4", 1098000, 255, 1197000, 16.57, 120.41, 1.07, 0),
+  ("Tensor G4", 1098000, 255, 1328000, 22.15, 145.31, 1.54, 0),
+  ("Tensor G4", 1098000, 255, 1444000, 27.91, 163.9, 1.68, 0),
+  ("Tensor G4", 1098000, 255, 1548000, 32.01, 174.89, 1.87, 0),
+  ("Tensor G4", 1098000, 255, 1704000, 44.5, 139.63, 2.24, 0),
+  ("Tensor G4", 1098000, 255, 1800000, 53.21, 140.32, 2.52, 0),
+  ("Tensor G4", 1098000, 255, 1880000, 60.44, 157.97, 2.83, 0),
+  ("Tensor G4", 1098000, 255, 1950000, 73.65, 169.76, 3.28, 0),
+  ("Tensor G4", 1098000, 255, 2024000, 87.15, 182.83, 3.98, 0),
+  ("Tensor G4", 1098000, 255, 2120000, 114.08, 187.49, 4.17, 0),
+  ("Tensor G4", 1098000, 255, 2150000, 113.79, 189.6, 4.65, 0),
+  ("Tensor G4", 1197000, 255, 610000, 8.34, 75.11, 1.27, 0),
+  ("Tensor G4", 1197000, 255, 820000, 9.54, 84.82, 1.14, 0),
+  ("Tensor G4", 1197000, 255, 970000, 10.37, 89.93, 1.18, 0),
+  ("Tensor G4", 1197000, 255, 1098000, 12.81, 104.44, 1.37, 0),
+  ("Tensor G4", 1197000, 255, 1197000, 16.36, 129.81, 1.39, 0),
+  ("Tensor G4", 1197000, 255, 1328000, 22.4, 145.01, 1.64, 0),
+  ("Tensor G4", 1197000, 255, 1444000, 28.1, 170.53, 1.61, 0),
+  ("Tensor G4", 1197000, 255, 1548000, 32.23, 186.28, 1.91, 0),
+  ("Tensor G4", 1197000, 255, 1704000, 44.93, 156.69, 2.32, 0),
+  ("Tensor G4", 1197000, 255, 1800000, 53.17, 151.91, 2.43, 0),
+  ("Tensor G4", 1197000, 255, 1880000, 60.94, 141.69, 2.72, 0),
+  ("Tensor G4", 1197000, 255, 1950000, 73.72, 189.86, 3.42, 0),
+  ("Tensor G4", 1197000, 255, 2024000, 87.87, 158.58, 3.7, 0),
+  ("Tensor G4", 1197000, 255, 2120000, 114.16, 193.12, 4.81, 0),
+  ("Tensor G4", 1197000, 255, 2150000, 113.59, 191.22, 4.8, 0),
+  ("Tensor G4", 1328000, 255, 610000, 10.73, 90.03, 1.33, 0),
+  ("Tensor G4", 1328000, 255, 820000, 11.88, 99.06, 1.31, 0),
+  ("Tensor G4", 1328000, 255, 970000, 12.77, 106.72, 1.33, 0),
+  ("Tensor G4", 1328000, 255, 1098000, 13.12, 110.06, 1.39, 0),
+  ("Tensor G4", 1328000, 255, 1197000, 16.68, 127.98, 1.33, 0),
+  ("Tensor G4", 1328000, 255, 1328000, 22.66, 154.27, 1.68, 0),
+  ("Tensor G4", 1328000, 255, 1444000, 28.49, 174.25, 1.72, 0),
+  ("Tensor G4", 1328000, 255, 1548000, 32.16, 191.25, 1.73, 0),
+  ("Tensor G4", 1328000, 255, 1704000, 44.27, 129.41, 2.25, 0),
+  ("Tensor G4", 1328000, 255, 1800000, 53.79, 154.61, 2.51, 0),
+  ("Tensor G4", 1328000, 255, 1880000, 61.04, 163.47, 2.68, 0),
+  ("Tensor G4", 1328000, 255, 1950000, 75.05, 189.16, 3.08, 0),
+  ("Tensor G4", 1328000, 255, 2024000, 89.05, 204.54, 3.43, 0),
+  ("Tensor G4", 1328000, 255, 2120000, 115.33, 210.24, 4.36, 0),
+  ("Tensor G4", 1328000, 255, 2150000, 114.98, 206.93, 4.34, 0),
+  ("Tensor G4", 1425000, 255, 610000, 13.32, 101.33, 1.43, 0),
+  ("Tensor G4", 1425000, 255, 820000, 14.56, 111.02, 1.46, 0),
+  ("Tensor G4", 1425000, 255, 970000, 15.11, 121.09, 1.47, 0),
+  ("Tensor G4", 1425000, 255, 1098000, 16.25, 128.03, 1.41, 0),
+  ("Tensor G4", 1425000, 255, 1197000, 16.68, 127.43, 1.45, 0),
+  ("Tensor G4", 1425000, 255, 1328000, 22.57, 156.98, 1.68, 0),
+  ("Tensor G4", 1425000, 255, 1444000, 28.81, 182.29, 1.72, 0),
+  ("Tensor G4", 1425000, 255, 1548000, 33.08, 198.0, 1.83, 0),
+  ("Tensor G4", 1425000, 255, 1704000, 45.21, 162.21, 2.12, 0),
+  ("Tensor G4", 1425000, 255, 1800000, 54.37, 167.27, 2.5, 0),
+  ("Tensor G4", 1425000, 255, 1880000, 61.48, 116.14, 2.89, 0),
+  ("Tensor G4", 1425000, 255, 1950000, 74.85, 180.6, 3.49, 0),
+  ("Tensor G4", 1425000, 255, 2024000, 89.32, 187.51, 3.6, 0),
+  ("Tensor G4", 1425000, 255, 2120000, 115.2, 203.97, 4.57, 0),
+  ("Tensor G4", 1425000, 255, 2150000, 115.53, 210.01, 4.25, 0),
+  ("Tensor G4", 1548000, 255, 610000, 16.36, 123.83, 1.45, 0),
+  ("Tensor G4", 1548000, 255, 820000, 17.5, 128.9, 1.62, 0),
+  ("Tensor G4", 1548000, 255, 970000, 18.34, 139.52, 1.58, 0),
+  ("Tensor G4", 1548000, 255, 1098000, 19.32, 149.77, 1.53, 0),
+  ("Tensor G4", 1548000, 255, 1197000, 19.8, 152.01, 1.43, 0),
+  ("Tensor G4", 1548000, 255, 1328000, 22.59, 159.55, 1.61, 0),
+  ("Tensor G4", 1548000, 255, 1444000, 28.75, 198.79, 1.86, 0),
+  ("Tensor G4", 1548000, 255, 1548000, 33.46, 211.95, 1.77, 0),
+  ("Tensor G4", 1548000, 255, 1704000, 46.36, 169.26, 2.11, 0),
+  ("Tensor G4", 1548000, 255, 1800000, 54.71, 177.24, 2.42, 0),
+  ("Tensor G4", 1548000, 255, 1880000, 62.25, 145.44, 2.76, 0),
+  ("Tensor G4", 1548000, 255, 1950000, 75.84, 191.27, 3.09, 0),
+  ("Tensor G4", 1548000, 255, 2024000, 88.97, 198.32, 3.86, 0),
+  ("Tensor G4", 1548000, 255, 2120000, 115.79, 232.48, 4.72, 0),
+  ("Tensor G4", 1548000, 255, 2150000, 115.31, 222.76, 4.71, 0),
+  ("Tensor G4", 1696000, 255, 610000, 19.61, 132.84, 1.68, 0),
+  ("Tensor G4", 1696000, 255, 820000, 21.09, 151.29, 1.59, 0),
+  ("Tensor G4", 1696000, 255, 970000, 21.92, 157.59, 1.75, 0),
+  ("Tensor G4", 1696000, 255, 1098000, 22.76, 163.33, 1.59, 0),
+  ("Tensor G4", 1696000, 255, 1197000, 23.53, 173.96, 1.67, 0),
+  ("Tensor G4", 1696000, 255, 1328000, 24.28, 184.05, 1.58, 0),
+  ("Tensor G4", 1696000, 255, 1444000, 29.47, 203.99, 1.77, 0),
+  ("Tensor G4", 1696000, 255, 1548000, 33.94, 225.78, 1.7, 0),
+  ("Tensor G4", 1696000, 255, 1704000, 46.92, 171.8, 2.16, 0),
+  ("Tensor G4", 1696000, 255, 1800000, 55.32, 217.17, 2.38, 0),
+  ("Tensor G4", 1696000, 255, 1880000, 62.55, 224.61, 2.77, 0),
+  ("Tensor G4", 1696000, 255, 1950000, 76.98, 204.48, 2.82, 0),
+  ("Tensor G4", 1696000, 255, 2024000, 90.13, 226.98, 3.76, 0),
+  ("Tensor G4", 1696000, 255, 2120000, 116.77, 245.48, 4.52, 0),
+  ("Tensor G4", 1696000, 255, 2150000, 112.69, 222.79, 6.43, 0),
+  ("Tensor G4", 1849000, 255, 610000, 29.35, 176.28, 1.8, 0),
+  ("Tensor G4", 1849000, 255, 820000, 30.31, 187.61, 1.94, 0),
+  ("Tensor G4", 1849000, 255, 970000, 31.7, 202.99, 2.05, 0),
+  ("Tensor G4", 1849000, 255, 1098000, 32.48, 207.22, 2.01, 0),
+  ("Tensor G4", 1849000, 255, 1197000, 33.7, 222.81, 1.9, 0),
+  ("Tensor G4", 1849000, 255, 1328000, 34.79, 229.5, 1.9, 0),
+  ("Tensor G4", 1849000, 255, 1444000, 35.97, 228.13, 1.91, 0),
+  ("Tensor G4", 1849000, 255, 1548000, 36.59, 235.62, 2.01, 0),
+  ("Tensor G4", 1849000, 255, 1704000, 47.47, 233.89, 2.16, 0),
+  ("Tensor G4", 1849000, 255, 1800000, 55.69, 211.69, 2.53, 0),
+  ("Tensor G4", 1849000, 255, 1880000, 63.47, 225.85, 2.39, 0),
+  ("Tensor G4", 1849000, 255, 1950000, 77.22, 209.34, 3.0, 0),
+  ("Tensor G4", 1849000, 255, 2024000, 90.92, 230.3, 3.48, 0),
+  ("Tensor G4", 1849000, 255, 2120000, 117.19, 247.78, 4.49, 0),
+  ("Tensor G4", 1849000, 255, 2150000, 117.53, 239.55, 4.32, 0),
+  ("Tensor G4", 1950000, 255, 610000, 40.27, 197.26, 2.54, 0),
+  ("Tensor G4", 1950000, 255, 820000, 41.93, 221.2, 2.67, 0),
+  ("Tensor G4", 1950000, 255, 970000, 43.45, 239.45, 2.56, 0),
+  ("Tensor G4", 1950000, 255, 1098000, 44.27, 240.43, 2.64, 0),
+  ("Tensor G4", 1950000, 255, 1197000, 45.84, 259.94, 2.42, 0),
+  ("Tensor G4", 1950000, 255, 1328000, 47.03, 273.66, 2.55, 0),
+  ("Tensor G4", 1950000, 255, 1444000, 48.53, 267.32, 2.32, 0),
+  ("Tensor G4", 1950000, 255, 1548000, 49.59, 232.85, 2.35, 0),
+  ("Tensor G4", 1950000, 255, 1704000, 51.2, 234.87, 2.23, 0),
+  ("Tensor G4", 1950000, 255, 1800000, 55.47, 205.6, 2.67, 0),
+  ("Tensor G4", 1950000, 255, 1880000, 63.68, 201.13, 2.59, 0),
+  ("Tensor G4", 1950000, 255, 1950000, 77.22, 201.28, 3.13, 0),
+  ("Tensor G4", 1950000, 255, 2024000, 90.93, 230.61, 3.81, 0),
+  ("Tensor G4", 1950000, 255, 2120000, 118.19, 233.8, 4.28, 0),
+  ("Tensor G4", 1950000, 255, 2150000, 118.61, 240.57, 4.6, 0)
 )
 select * from data;
 
@@ -572,7 +757,157 @@
   ("Tensor", 1803000, 6, 2507000, 2.6630, 1.2641),
   ("Tensor", 1803000, 6, 2630000, 2.7385, 2.3263),
   ("Tensor", 1803000, 6, 2704000, 2.6901, 1.0629),
-  ("Tensor", 1803000, 6, 2802000, 2.7476, 1.0673)
+  ("Tensor", 1803000, 6, 2802000, 2.7476, 1.0673),
+  ("Tensor G4", 820000, 255, 610000, 0.4824, 0.8357),
+  ("Tensor G4", 955000, 255, 610000, 0.4801, 0.852),
+  ("Tensor G4", 1098000, 255, 610000, 0.4988, 0.8219),
+  ("Tensor G4", 1197000, 255, 610000, 0.5025, 0.8369),
+  ("Tensor G4", 1328000, 255, 610000, 0.516, 0.8928),
+  ("Tensor G4", 1425000, 255, 610000, 0.5266, 0.8895),
+  ("Tensor G4", 1548000, 255, 610000, 0.5286, 0.915),
+  ("Tensor G4", 1696000, 255, 610000, 0.5288, 1.0169),
+  ("Tensor G4", 1849000, 255, 610000, 0.5326, 1.1313),
+  ("Tensor G4", 1950000, 255, 610000, 0.5495, 1.1839),
+  ("Tensor G4", 820000, 255, 820000, 0.4815, 0.8114),
+  ("Tensor G4", 955000, 255, 820000, 0.5055, 0.9356),
+  ("Tensor G4", 1098000, 255, 820000, 0.5168, 0.9567),
+  ("Tensor G4", 1197000, 255, 820000, 0.5228, 0.8653),
+  ("Tensor G4", 1328000, 255, 820000, 0.5228, 0.8895),
+  ("Tensor G4", 1425000, 255, 820000, 0.5309, 0.9692),
+  ("Tensor G4", 1548000, 255, 820000, 0.5461, 0.9987),
+  ("Tensor G4", 1696000, 255, 820000, 0.564, 1.053),
+  ("Tensor G4", 1849000, 255, 820000, 0.5649, 0.9087),
+  ("Tensor G4", 1950000, 255, 820000, 0.5737, 1.0893),
+  ("Tensor G4", 820000, 255, 970000, 0.4923, 0.852),
+  ("Tensor G4", 955000, 255, 970000, 0.5014, 0.8357),
+  ("Tensor G4", 1098000, 255, 970000, 0.4952, 1.0169),
+  ("Tensor G4", 1197000, 255, 970000, 0.5284, 0.8594),
+  ("Tensor G4", 1328000, 255, 970000, 0.5332, 0.8521),
+  ("Tensor G4", 1425000, 255, 970000, 0.5424, 0.8612),
+  ("Tensor G4", 1548000, 255, 970000, 0.5641, 0.883),
+  ("Tensor G4", 1696000, 255, 970000, 0.5524, 0.915),
+  ("Tensor G4", 1849000, 255, 970000, 0.5685, 0.8765),
+  ("Tensor G4", 1950000, 255, 970000, 0.5527, 0.8164),
+  ("Tensor G4", 820000, 255, 1098000, 0.5084, 0.947),
+  ("Tensor G4", 955000, 255, 1098000, 0.5177, 0.8058),
+  ("Tensor G4", 1098000, 255, 1098000, 0.5284, 0.763),
+  ("Tensor G4", 1197000, 255, 1098000, 0.596, 0.8633),
+  ("Tensor G4", 1328000, 255, 1098000, 0.596, 0.8633),
+  ("Tensor G4", 1425000, 255, 1098000, 0.5373, 0.7504),
+  ("Tensor G4", 1548000, 255, 1098000, 0.5186, 0.8604),
+  ("Tensor G4", 1696000, 255, 1098000, 0.5772, 89.73),
+  ("Tensor G4", 1849000, 255, 1098000, 0.5758, 0.9588),
+  ("Tensor G4", 1950000, 255, 1098000, 0.5904, 1.1074),
+  ("Tensor G4", 820000, 255, 1197000, 0.5105, 0.866),
+  ("Tensor G4", 955000, 255, 1197000, 0.5241, 0.8533),
+  ("Tensor G4", 1098000, 255, 1197000, 0.5347, 0.8569),
+  ("Tensor G4", 1197000, 255, 1197000, 0.524, 0.8318),
+  ("Tensor G4", 1328000, 255, 1197000, 0.5298, 0.8374),
+  ("Tensor G4", 1425000, 255, 1197000, 0.5201, 0.9479),
+  ("Tensor G4", 1548000, 255, 1197000, 0.497, 0.9093),
+  ("Tensor G4", 1696000, 255, 1197000, 0.5007, 0.8765),
+  ("Tensor G4", 1849000, 255, 1197000, 0.6107, 0.8682),
+  ("Tensor G4", 1950000, 255, 1197000, 0.5888, 0.9738),
+  ("Tensor G4", 820000, 255, 1328000, 0.534, 0.9162),
+  ("Tensor G4", 955000, 255, 1328000, 0.5551, 0.9499),
+  ("Tensor G4", 1098000, 255, 1328000, 0.556, 0.954),
+  ("Tensor G4", 1197000, 255, 1328000, 0.5711, 0.9117),
+  ("Tensor G4", 1328000, 255, 1328000, 0.5372, 0.956),
+  ("Tensor G4", 1425000, 255, 1328000, 0.5482, 0.934),
+  ("Tensor G4", 1548000, 255, 1328000, 0.5615, 0.7495),
+  ("Tensor G4", 1696000, 255, 1328000, 0.53, 0.9145),
+  ("Tensor G4", 1849000, 255, 1328000, 0.5504, 0.9478),
+  ("Tensor G4", 1950000, 255, 1328000, 0.6243, 0.9335),
+  ("Tensor G4", 820000, 255, 1444000, 0.5437, 0.9403),
+  ("Tensor G4", 955000, 255, 1444000, 0.5635, 0.9997),
+  ("Tensor G4", 1098000, 255, 1444000, 0.5711, 0.919),
+  ("Tensor G4", 1197000, 255, 1444000, 0.5595, 0.9455),
+  ("Tensor G4", 1328000, 255, 1444000, 0.5772, 0.9415),
+  ("Tensor G4", 1425000, 255, 1444000, 0.5507, 0.9582),
+  ("Tensor G4", 1548000, 255, 1444000, 0.5535, 0.8986),
+  ("Tensor G4", 1696000, 255, 1444000, 0.507, 0.9369),
+  ("Tensor G4", 1849000, 255, 1444000, 0.628, 0.8878),
+  ("Tensor G4", 1950000, 255, 1444000, 0.658, 1.0974),
+  ("Tensor G4", 820000, 255, 1548000, 0.578, 0.8322),
+  ("Tensor G4", 955000, 255, 1548000, 0.603, 0.9371),
+  ("Tensor G4", 1098000, 255, 1548000, 0.6163, 0.962),
+  ("Tensor G4", 1197000, 255, 1548000, 0.6105, 0.9323),
+  ("Tensor G4", 1328000, 255, 1548000, 0.589, 0.8355),
+  ("Tensor G4", 1425000, 255, 1548000, 0.5924, 0.9416),
+  ("Tensor G4", 1548000, 255, 1548000, 0.5611, 0.8911),
+  ("Tensor G4", 1696000, 255, 1548000, 0.5792, 0.905),
+  ("Tensor G4", 1849000, 255, 1548000, 0.6021, 0.9761),
+  ("Tensor G4", 1950000, 255, 1548000, 0.6579, 1.0438),
+  ("Tensor G4", 820000, 255, 1704000, 1.5584, 1.2388),
+  ("Tensor G4", 955000, 255, 1704000, 1.5287, 1.104),
+  ("Tensor G4", 1098000, 255, 1704000, 1.6412, 1.2419),
+  ("Tensor G4", 1197000, 255, 1704000, 1.9947, 1.318),
+  ("Tensor G4", 1328000, 255, 1704000, 1.6701, 1.178),
+  ("Tensor G4", 1425000, 255, 1704000, 1.5944, 1.3017),
+  ("Tensor G4", 1548000, 255, 1704000, 1.6769, 1.6058),
+  ("Tensor G4", 1696000, 255, 1704000, 1.7215, 1.8),
+  ("Tensor G4", 1849000, 255, 1704000, 1.9143, 2.0842),
+  ("Tensor G4", 1950000, 255, 1704000, 1.932, 2.8572),
+  ("Tensor G4", 820000, 255, 1800000, 1.575, 1.5401),
+  ("Tensor G4", 955000, 255, 1800000, 1.6833, 1.5087),
+  ("Tensor G4", 1098000, 255, 1800000, 1.5571, 2.0737),
+  ("Tensor G4", 1197000, 255, 1800000, 1.6815, 1.8473),
+  ("Tensor G4", 1328000, 255, 1800000, 1.7828, 1.666),
+  ("Tensor G4", 1425000, 255, 1800000, 1.8061, 1.8018),
+  ("Tensor G4", 1548000, 255, 1800000, 1.9956, 1.64),
+  ("Tensor G4", 1696000, 255, 1800000, 1.8592, 1.701),
+  ("Tensor G4", 1849000, 255, 1800000, 1.5758, 1.5364),
+  ("Tensor G4", 1950000, 255, 1800000, 1.9823, 1.7952),
+  ("Tensor G4", 820000, 255, 1880000, 1.7249, 1.7787),
+  ("Tensor G4", 955000, 255, 1880000, 1.7635, 2.0677),
+  ("Tensor G4", 1098000, 255, 1880000, 1.805, 1.5907),
+  ("Tensor G4", 1197000, 255, 1880000, 1.7293, 2.0645),
+  ("Tensor G4", 1328000, 255, 1880000, 1.918, 1.9844),
+  ("Tensor G4", 1425000, 255, 1880000, 2.0013, 1.5541),
+  ("Tensor G4", 1548000, 255, 1880000, 2.0504, 1.9877),
+  ("Tensor G4", 1696000, 255, 1880000, 1.9603, 1.8955),
+  ("Tensor G4", 1849000, 255, 1880000, 2.1168, 1.9674),
+  ("Tensor G4", 1950000, 255, 1880000, 2.2047, 2.4697),
+  ("Tensor G4", 820000, 255, 1950000, 1.9756, 1.4938),
+  ("Tensor G4", 955000, 255, 1950000, 1.90147, 1.8144),
+  ("Tensor G4", 1098000, 255, 1950000, 2.1539, 2.0811),
+  ("Tensor G4", 1197000, 255, 1950000, 2.2192, 1.8968),
+  ("Tensor G4", 1328000, 255, 1950000, 2.2974, 2.1572),
+  ("Tensor G4", 1425000, 255, 1950000, 2.3865, 2.2972),
+  ("Tensor G4", 1548000, 255, 1950000, 2.4361, 2.3942),
+  ("Tensor G4", 1696000, 255, 1950000, 2.8677, 2.2737),
+  ("Tensor G4", 1849000, 255, 1950000, 2.6123, 2.5181),
+  ("Tensor G4", 1950000, 255, 1950000, 2.7293, 2.5798),
+  ("Tensor G4", 820000, 255, 2024000, 2.0136, 1.7557),
+  ("Tensor G4", 955000, 255, 2024000, 1.9834, 1.6844),
+  ("Tensor G4", 1098000, 255, 2024000, 2.1129, 1.8735),
+  ("Tensor G4", 1197000, 255, 2024000, 2.5894, 2.2185),
+  ("Tensor G4", 1328000, 255, 2024000, 2.6949, 2.1905),
+  ("Tensor G4", 1425000, 255, 2024000, 2.8143, 2.7641),
+  ("Tensor G4", 1548000, 255, 2024000, 2.8513, 2.6064),
+  ("Tensor G4", 1696000, 255, 2024000, 2.663, 2.3265),
+  ("Tensor G4", 1849000, 255, 2024000, 2.6618, 2.595),
+  ("Tensor G4", 1950000, 255, 2024000, 2.7227, 2.7533),
+  ("Tensor G4", 820000, 255, 2120000, 2.5414, 2.6693),
+  ("Tensor G4", 955000, 255, 2120000, 2.7682, 2.7252),
+  ("Tensor G4", 1098000, 255, 2120000, 3.1524, 2.8636),
+  ("Tensor G4", 1197000, 255, 2120000, 3.0192, 2.6325),
+  ("Tensor G4", 1328000, 255, 2120000, 2.9797, 3.1899),
+  ("Tensor G4", 1425000, 255, 2120000, 3.176, 3.069),
+  ("Tensor G4", 1548000, 255, 2120000, 3.1105, 2.2982),
+  ("Tensor G4", 1696000, 255, 2120000, 2.9221, 3.2752),
+  ("Tensor G4", 1849000, 255, 2120000, 3.2659, 3.0112),
+  ("Tensor G4", 1950000, 255, 2120000, 3.1351, 3.5576),
+  ("Tensor G4", 820000, 255, 2150000, 2.9947, 2.6827),
+  ("Tensor G4", 955000, 255, 2150000, 2.9357, 2.5455),
+  ("Tensor G4", 1098000, 255, 2150000, 3.1562, 2.8923),
+  ("Tensor G4", 1197000, 255, 2150000, 3.0068, 3.2811),
+  ("Tensor G4", 1328000, 255, 2150000, 3.1187, 3.0526),
+  ("Tensor G4", 1425000, 255, 2150000, 3.2907, 2.9659),
+  ("Tensor G4", 1548000, 255, 2150000, 2.9841, 3.347),
+  ("Tensor G4", 1696000, 255, 2150000, 3.2313, 3.1632),
+  ("Tensor G4", 1849000, 255, 2150000, 3.091, 3.1305),
+  ("Tensor G4", 1950000, 255, 2150000, 3.326, 3.0211)
 )
 select * from data;
 
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/curves/estimates.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/curves/estimates.sql
new file mode 100644
index 0000000..1c81f49
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/curves/estimates.sql
@@ -0,0 +1,183 @@
+--
+-- 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 wattson.cpu_split;
+INCLUDE PERFETTO MODULE wattson.curves.utils;
+INCLUDE PERFETTO MODULE wattson.curves.w_cpu_dependence;
+INCLUDE PERFETTO MODULE wattson.curves.w_dsu_dependence;
+INCLUDE PERFETTO MODULE wattson.device_infos;
+
+-- One of the two tables will be empty, depending on whether the device is
+-- dependent on devfreq or a different CPU's frequency
+CREATE PERFETTO VIEW _curves_w_dependencies(
+  ts LONG,
+  dur LONG,
+  freq_0 INT,
+  idle_0 INT,
+  freq_1 INT,
+  idle_1 INT,
+  freq_2 INT,
+  idle_2 INT,
+  freq_3 INT,
+  idle_3 INT,
+  cpu0_curve FLOAT,
+  cpu1_curve FLOAT,
+  cpu2_curve FLOAT,
+  cpu3_curve FLOAT,
+  cpu4_curve FLOAT,
+  cpu5_curve FLOAT,
+  cpu6_curve FLOAT,
+  cpu7_curve FLOAT,
+  l3_hit_count INT,
+  l3_miss_count INT,
+  no_static INT,
+  all_cpu_deep_idle INT,
+  dependent_freq INT,
+  dependent_policy INT
+) AS
+-- Table that is dependent on differet CPU's frequency
+SELECT * FROM _w_cpu_dependence
+UNION ALL
+-- Table that is dependent of devfreq frequency
+SELECT * FROM _w_dsu_dependence;
+
+-- Final table showing the curves per CPU per slice
+CREATE PERFETTO TABLE _system_state_curves
+AS
+SELECT
+  base.ts,
+  base.dur,
+  -- base.cpu[0-3]_curve will be non-zero if CPU has 1D dependency
+  -- base.cpu[0-3]_curve will be zero if device is suspended or deep idle
+  -- base.cpu[0-3]_curve will be NULL if 2D dependency required
+  COALESCE(base.cpu0_curve, lut0.curve_value) as cpu0_curve,
+  COALESCE(base.cpu1_curve, lut1.curve_value) as cpu1_curve,
+  COALESCE(base.cpu2_curve, lut2.curve_value) as cpu2_curve,
+  COALESCE(base.cpu3_curve, lut3.curve_value) as cpu3_curve,
+  -- base.cpu[4-7]_curve will be non-zero if CPU has 1D dependency
+  -- base.cpu[4-7]_curve will be zero if device is suspended or deep idle
+  -- base.cpu[4-7]_curve will be NULL if CPU doesn't exist on device
+  COALESCE(base.cpu4_curve, 0.0) as cpu4_curve,
+  COALESCE(base.cpu5_curve, 0.0) as cpu5_curve,
+  COALESCE(base.cpu6_curve, 0.0) as cpu6_curve,
+  COALESCE(base.cpu7_curve, 0.0) as cpu7_curve,
+  IIF(
+    no_static = 1,
+    0.0,
+    COALESCE(static_1d.curve_value, static_2d.curve_value)
+  ) as static_curve,
+  IIF(
+    all_cpu_deep_idle = 1,
+    0,
+    base.l3_hit_count * l3_hit_lut.curve_value
+  ) as l3_hit_value,
+  IIF(
+    all_cpu_deep_idle = 1,
+    0,
+    base.l3_miss_count * l3_miss_lut.curve_value
+  ) as l3_miss_value
+FROM _curves_w_dependencies as base
+-- LUT for 2D dependencies
+LEFT JOIN _filtered_curves_2d lut0 ON
+  lut0.freq_khz = base.freq_0 AND
+  lut0.other_policy = base.dependent_policy AND
+  lut0.other_freq_khz = base.dependent_freq AND
+  lut0.idle = base.idle_0
+LEFT JOIN _filtered_curves_2d lut1 ON
+  lut1.freq_khz = base.freq_1 AND
+  lut1.other_policy = base.dependent_policy AND
+  lut1.other_freq_khz = base.dependent_freq AND
+  lut1.idle = base.idle_1
+LEFT JOIN _filtered_curves_2d lut2 ON
+  lut2.freq_khz = base.freq_2 AND
+  lut2.other_policy = base.dependent_policy AND
+  lut2.other_freq_khz = base.dependent_freq AND
+  lut2.idle = base.idle_2
+LEFT JOIN _filtered_curves_2d lut3 ON
+  lut3.freq_khz = base.freq_3 AND
+  lut3.other_policy = base.dependent_policy AND
+  lut3.other_freq_khz = base.dependent_freq AND
+  lut3.idle = base.idle_3
+-- LUT for static curve lookup
+LEFT JOIN _filtered_curves_2d static_2d ON
+  static_2d.freq_khz = base.freq_0 AND
+  static_2d.other_policy = base.dependent_policy AND
+  static_2d.other_freq_khz = base.dependent_freq AND
+  static_2d.idle = 255
+LEFT JOIN _filtered_curves_1d static_1d ON
+  static_1d.policy = 0 AND
+  static_1d.freq_khz = base.freq_0 AND
+  static_1d.idle = 255
+-- LUT joins for L3 cache
+LEFT JOIN _filtered_curves_l3 l3_hit_lut ON
+  l3_hit_lut.freq_khz = base.freq_0 AND
+  l3_hit_lut.other_policy = base.dependent_policy AND
+  l3_hit_lut.other_freq_khz = base.dependent_freq AND
+  l3_hit_lut.action = 'hit'
+LEFT JOIN _filtered_curves_l3 l3_miss_lut ON
+  l3_miss_lut.freq_khz = base.freq_0 AND
+  l3_miss_lut.other_policy = base.dependent_policy AND
+  l3_miss_lut.other_freq_khz = base.dependent_freq AND
+  l3_miss_lut.action = 'miss';
+
+-- The most basic components of Wattson, all normalized to be in mW on a per
+-- system state basis
+CREATE PERFETTO TABLE _system_state_mw
+AS
+SELECT
+  ts,
+  dur,
+  cpu0_curve as cpu0_mw,
+  cpu1_curve as cpu1_mw,
+  cpu2_curve as cpu2_mw,
+  cpu3_curve as cpu3_mw,
+  cpu4_curve as cpu4_mw,
+  cpu5_curve as cpu5_mw,
+  cpu6_curve as cpu6_mw,
+  cpu7_curve as cpu7_mw,
+  -- LUT for l3 is scaled by 10^6 to save resolution and in units of kWs. Scale
+  -- this by 10^3 so when divided by ns, result is in units of mW
+  (
+    (
+      IFNULL(l3_hit_value, 0) + IFNULL(l3_miss_value, 0)
+    ) * 1000 / dur
+  ) + static_curve as dsu_scu_mw
+FROM _system_state_curves;
+
+-- API to get power from each system state in an arbitrary time window
+CREATE PERFETTO FUNCTION _windowed_system_state_mw(ts LONG, dur LONG)
+RETURNS TABLE(
+  cpu0_mw FLOAT,
+  cpu1_mw FLOAT,
+  cpu2_mw FLOAT,
+  cpu3_mw FLOAT,
+  cpu4_mw FLOAT,
+  cpu5_mw FLOAT,
+  cpu6_mw FLOAT,
+  cpu7_mw FLOAT,
+  dsu_scu_mw FLOAT
+) AS
+SELECT
+  SUM(ss.cpu0_mw * ss.dur) / SUM(ss.dur) AS cpu0_mw,
+  SUM(ss.cpu1_mw * ss.dur) / SUM(ss.dur) AS cpu1_mw,
+  SUM(ss.cpu2_mw * ss.dur) / SUM(ss.dur) AS cpu2_mw,
+  SUM(ss.cpu3_mw * ss.dur) / SUM(ss.dur) AS cpu3_mw,
+  SUM(ss.cpu4_mw * ss.dur) / SUM(ss.dur) AS cpu4_mw,
+  SUM(ss.cpu5_mw * ss.dur) / SUM(ss.dur) AS cpu5_mw,
+  SUM(ss.cpu6_mw * ss.dur) / SUM(ss.dur) AS cpu6_mw,
+  SUM(ss.cpu7_mw * ss.dur) / SUM(ss.dur) AS cpu7_mw,
+  SUM(ss.dsu_scu_mw * ss.dur) / SUM(ss.dur) AS dsu_scu_mw
+FROM _interval_intersect_single!($ts, $dur, _ii_subquery!(_system_state_mw)) ii
+JOIN _system_state_mw AS ss ON ss._auto_id = id;
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/curves/grouped.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/curves/grouped.sql
deleted file mode 100644
index 18d1770..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/curves/grouped.sql
+++ /dev/null
@@ -1,66 +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 wattson.curves.ungrouped;
-
--- Wattson's estimated usage of the system, split out into cpu cluster based on
--- the natural grouping of the hardware.
-CREATE PERFETTO TABLE wattson_estimate_per_component(
-  -- Starting timestamp of the slice
-  ts LONG,
-  -- Duration of the slice
-  dur INT,
-  -- Total L3 estimated usage in mW during this slice
-  l3 FLOAT,
-  -- Total little CPU estimated usage in mW during this slice
-  little_cpus FLOAT,
-  -- Total mid CPU cluster estimated usage in mW during this slice
-  mid_cpus FLOAT,
-  -- Total big CPU cluster estimated usage in mW during this slice
-  big_cpus FLOAT
-)
-AS
-SELECT
-  ts,
-  dur,
-  IFNULL(l3_hit_value, 0.0) + IFNULL(l3_miss_value, 0.0) as l3,
-  IFNULL(cpu0_curve, 0.0) + IFNULL(cpu1_curve, 0.0) + IFNULL(cpu2_curve, 0.0) +
-    IFNULL(cpu3_curve, 0.0) + static_curve as little_cpus,
-  cpu4_curve + cpu5_curve as mid_cpus,
-  cpu6_curve + cpu7_curve as big_cpus
-FROM _system_state_curves;
-
--- Gives total contribution of each HW component for the entire trace, bringing
--- the output of the table to parity with the Python version of Wattson
-CREATE PERFETTO TABLE _wattson_entire_trace
-AS
-WITH _individual_totals AS (
-  SELECT
-    -- LUT for l3 is scaled by 10^6 to save resolution, so do the inversion
-    -- scaling by 10^6 after the summation to minimize losing resolution
-    SUM(l3) / 1000000 as total_l3,
-    SUM(dur * little_cpus) / 1000000000 as total_little_cpus,
-    SUM(dur * mid_cpus) / 1000000000 as total_mid_cpus,
-    SUM(dur * big_cpus) / 1000000000 as total_big_cpus
-  FROM wattson_estimate_per_component
-  )
-SELECT
-  ROUND(total_l3, 2) as total_l3,
-  ROUND(total_little_cpus, 2) as total_little_cpus,
-  ROUND(total_mid_cpus, 2) as total_mid_cpus,
-  ROUND(total_big_cpus, 2) as total_big_cpus,
-  ROUND(total_l3 + total_little_cpus + total_mid_cpus + total_big_cpus, 2) as total
-FROM _individual_totals;
-
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/curves/idle_attribution.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/curves/idle_attribution.sql
new file mode 100644
index 0000000..77f045d
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/curves/idle_attribution.sql
@@ -0,0 +1,144 @@
+--
+-- 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 intervals.intersect;
+INCLUDE PERFETTO MODULE wattson.curves.estimates;
+
+-- Get slice info of threads/processes
+CREATE PERFETTO TABLE _thread_process_slices AS
+SELECT
+  sched.ts,
+  sched.dur,
+  sched.cpu,
+  thread.utid,
+  thread.upid
+FROM thread
+JOIN sched USING (utid)
+WHERE dur > 0;
+
+-- Helper macro so Perfetto tables can be used with interval intersect
+CREATE PERFETTO MACRO _ii_table(tab TableOrSubquery)
+RETURNS TableOrSubquery AS (SELECT _auto_id AS id, * FROM $tab);
+
+-- Get slices only where there is transition from deep idle to active
+CREATE PERFETTO TABLE _idle_exits AS
+SELECT
+  ts,
+  dur,
+  cpu,
+  idle
+FROM _adjusted_deep_idle
+WHERE idle = -1 and dur > 0;
+
+-- Gets the slices where the CPU transitions from deep idle to active, and the
+-- associated thread that causes the idle exit
+CREATE PERFETTO TABLE _idle_w_threads AS
+WITH _ii_idle_threads AS (
+  SELECT
+    ii.ts,
+    ii.dur,
+    ii.cpu,
+    threads.utid,
+    threads.upid,
+    id_1 as idle_group
+  FROM _interval_intersect!(
+    (
+      _ii_table!(_thread_process_slices),
+      _ii_table!(_idle_exits)
+    ),
+    (cpu)
+  ) ii
+  JOIN _thread_process_slices AS threads
+    ON threads._auto_id = id_0
+),
+-- Since sorted by time, MIN() is fast aggregate function that will return the
+-- first time slice, which will be the utid = 0 slice immediately succeeding the
+-- idle to active transition, and immediately preceding the active thread
+first_swapper_slice AS (
+  SELECT
+    ts,
+    dur,
+    cpu,
+    idle_group,
+    MIN(ts) as min
+  FROM _ii_idle_threads
+  GROUP BY idle_group
+),
+-- MIN() here will give the first active thread immediately succeeding the idle
+-- to active transition slice, which means this the the thread that causes the
+-- idle exit
+first_non_swapper_slice AS (
+  SELECT
+    idle_group,
+    utid,
+    upid,
+    MIN(ts) as min
+  FROM _ii_idle_threads
+  WHERE utid != 0
+  GROUP BY idle_group
+)
+SELECT
+  ts,
+  dur,
+  cpu,
+  utid,
+  upid
+FROM first_non_swapper_slice
+JOIN first_swapper_slice USING (idle_group);
+
+-- Interval intersect with the estimate power track, so that each slice can be
+-- attributed to the power of the CPU in that time duration
+CREATE PERFETTO TABLE _idle_transition_cost AS
+SELECT
+  ii.ts,
+  ii.dur,
+  threads.cpu,
+  threads.utid,
+  threads.upid,
+  CASE threads.cpu
+    WHEN 0 THEN power.cpu0_mw
+    WHEN 1 THEN power.cpu1_mw
+    WHEN 2 THEN power.cpu2_mw
+    WHEN 3 THEN power.cpu3_mw
+    WHEN 4 THEN power.cpu4_mw
+    WHEN 5 THEN power.cpu5_mw
+    WHEN 6 THEN power.cpu6_mw
+    WHEN 7 THEN power.cpu7_mw
+    ELSE 0
+  END estimated_mw
+FROM _interval_intersect!(
+  (
+    _ii_table!(_idle_w_threads),
+    _ii_table!(_system_state_mw)
+  ),
+  ()
+) ii
+JOIN _idle_w_threads as threads ON threads._auto_id = id_0
+JOIN _system_state_mw as power ON power._auto_id = id_1;
+
+-- Macro for easily filtering idle attribution to a specified time window. This
+-- information can then further be filtered by specific CPU and GROUP BY on
+-- either utid or upid
+CREATE PERFETTO FUNCTION _filter_idle_attribution(ts LONG, dur LONG)
+RETURNS Table(idle_cost_mws LONG, utid INT, upid INT, cpu INT) AS
+SELECT
+  cost.estimated_mw * cost.dur / 1e9 as idle_cost_mws,
+  cost.utid,
+  cost.upid,
+  cost.cpu
+FROM _interval_intersect_single!(
+  $ts, $dur, _ii_table!(_idle_transition_cost)
+) ii
+JOIN _idle_transition_cost as cost ON cost._auto_id = id;
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/curves/ungrouped.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/curves/ungrouped.sql
deleted file mode 100644
index 37da258..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/curves/ungrouped.sql
+++ /dev/null
@@ -1,236 +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 time.conversion;
-INCLUDE PERFETTO MODULE wattson.arm_dsu;
-INCLUDE PERFETTO MODULE wattson.cpu_split;
-INCLUDE PERFETTO MODULE wattson.curves.utils;
-INCLUDE PERFETTO MODULE wattson.device_infos;
-
--- System state table with LUT for CPUs and intermediate values for calculations
-CREATE PERFETTO TABLE _w_independent_cpus_calc
-AS
-SELECT
-  ts,
-  dur,
-  cast_int!(l3_hit_rate * dur) as l3_hit_count,
-  cast_int!(l3_miss_rate * dur) as l3_miss_count,
-  freq_0,
-  idle_0,
-  freq_1,
-  idle_1,
-  freq_2,
-  idle_2,
-  freq_3,
-  idle_3,
-  freq_4,
-  idle_4,
-  freq_5,
-  idle_5,
-  freq_6,
-  idle_6,
-  freq_7,
-  idle_7,
-  policy_4,
-  policy_5,
-  policy_6,
-  policy_7,
-  IIF(
-    suspended = 1,
-    1,
-    MIN(
-      IFNULL(idle_0, 1),
-      IFNULL(idle_1, 1),
-      IFNULL(idle_2, 1),
-      IFNULL(idle_3, 1)
-    )
-  ) as no_static,
-  IIF(suspended = 1, 0, cpu0_curve) as cpu0_curve,
-  IIF(suspended = 1, 0, cpu1_curve) as cpu1_curve,
-  IIF(suspended = 1, 0, cpu2_curve) as cpu2_curve,
-  IIF(suspended = 1, 0, cpu3_curve) as cpu3_curve,
-  IIF(suspended = 1, 0, cpu4_curve) as cpu4_curve,
-  IIF(suspended = 1, 0, cpu5_curve) as cpu5_curve,
-  IIF(suspended = 1, 0, cpu6_curve) as cpu6_curve,
-  IIF(suspended = 1, 0, cpu7_curve) as cpu7_curve,
-  -- If dependency CPUs are active, then that CPU could contribute static power
-  IIF(idle_4 = -1, lut4.curve_value, -1) as static_4,
-  IIF(idle_5 = -1, lut5.curve_value, -1) as static_5,
-  IIF(idle_6 = -1, lut6.curve_value, -1) as static_6,
-  IIF(idle_7 = -1, lut7.curve_value, -1) as static_7
-FROM _idle_freq_l3_hit_l3_miss_slice as base
-LEFT JOIN _filtered_curves_2d lut4 ON
-  base.freq_0 = lut4.freq_khz AND
-  base.policy_4 = lut4.other_policy AND
-  base.freq_4 = lut4.other_freq_khz AND
-  lut4.idle = 255
-LEFT JOIN _filtered_curves_2d lut5 ON
-  base.freq_0 = lut5.freq_khz AND
-  base.policy_5 = lut5.other_policy AND
-  base.freq_5 = lut5.other_freq_khz AND
-  lut5.idle = 255
-LEFT JOIN _filtered_curves_2d lut6 ON
-  base.freq_0 = lut6.freq_khz AND
-  base.policy_6 = lut6.other_policy AND
-  base.freq_6 = lut6.other_freq_khz AND
-  lut6.idle = 255
-LEFT JOIN _filtered_curves_2d lut7 ON
-  base.freq_0 = lut7.freq_khz AND
-  base.policy_7 = lut7.other_policy AND
-  base.freq_7 = lut7.other_freq_khz AND
-  lut7.idle = 255
--- Needs to be at least 1us to reduce inconsequential rows.
-WHERE dur > time_from_us(1);
-
--- Find the CPU states creating the max vote
-CREATE PERFETTO TABLE _get_max_vote
-AS
-WITH max_power_tbl AS (
-  SELECT
-    *,
-    -- Indicates if all CPUs are in deep idle
-    MIN(
-      no_static,
-      IFNULL(idle_4, 1),
-      IFNULL(idle_5, 1),
-      IFNULL(idle_6, 1),
-      IFNULL(idle_7, 1)
-    ) as all_cpu_deep_idle,
-    -- Determines which CPU has highest vote
-    MAX(
-      static_4,
-      static_5,
-      static_6,
-      static_7
-    ) as max_static_vote
-  FROM _w_independent_cpus_calc
-)
-SELECT
-  *,
-  CASE max_static_vote
-    WHEN -1 THEN _get_min_freq_vote()
-    WHEN static_4 THEN freq_4
-    WHEN static_5 THEN freq_5
-    WHEN static_6 THEN freq_6
-    WHEN static_7 THEN freq_7
-    ELSE 400000
-  END max_freq_vote,
-  CASE max_static_vote
-    WHEN -1 THEN _get_min_policy_vote()
-    WHEN static_4 THEN policy_4
-    WHEN static_5 THEN policy_5
-    WHEN static_6 THEN policy_6
-    WHEN static_7 THEN policy_7
-    ELSE 4
-  END max_policy_vote
-FROM max_power_tbl;
-
--- Final table showing the curves per CPU per slice
-CREATE PERFETTO TABLE _system_state_curves
-AS
-SELECT
-  base.ts,
-  base.dur,
-  COALESCE(lut0.curve_value, cpu0_curve) as cpu0_curve,
-  COALESCE(lut1.curve_value, cpu1_curve) as cpu1_curve,
-  COALESCE(lut2.curve_value, cpu2_curve) as cpu2_curve,
-  COALESCE(lut3.curve_value, cpu3_curve) as cpu3_curve,
-  COALESCE(base.cpu4_curve, 0.0) as cpu4_curve,
-  COALESCE(base.cpu5_curve, 0.0) as cpu5_curve,
-  COALESCE(base.cpu6_curve, 0.0) as cpu6_curve,
-  COALESCE(base.cpu7_curve, 0.0) as cpu7_curve,
-  IIF(
-    no_static = 1,
-    0.0,
-    COALESCE(static_1d.curve_value, static_2d.curve_value)
-  ) as static_curve,
-  IIF(
-    all_cpu_deep_idle = 1,
-    0,
-    base.l3_hit_count * l3_hit_lut.curve_value
-  ) as l3_hit_value,
-  IIF(
-    all_cpu_deep_idle = 1,
-    0,
-    base.l3_miss_count * l3_miss_lut.curve_value
-  ) as l3_miss_value
-FROM _get_max_vote as base
--- LUT for 2D dependencies
-LEFT JOIN _filtered_curves_2d lut0 ON
-  lut0.freq_khz = base.freq_0 AND
-  lut0.other_policy = base.max_policy_vote AND
-  lut0.other_freq_khz = base.max_freq_vote AND
-  lut0.idle = base.idle_0
-LEFT JOIN _filtered_curves_2d lut1 ON
-  lut1.freq_khz = base.freq_1 AND
-  lut1.other_policy = base.max_policy_vote AND
-  lut1.other_freq_khz = base.max_freq_vote AND
-  lut1.idle = base.idle_1
-LEFT JOIN _filtered_curves_2d lut2 ON
-  lut2.freq_khz = base.freq_2 AND
-  lut2.other_policy = base.max_policy_vote AND
-  lut2.other_freq_khz = base.max_freq_vote AND
-  lut2.idle = base.idle_2
-LEFT JOIN _filtered_curves_2d lut3 ON
-  lut3.freq_khz = base.freq_3 AND
-  lut3.other_policy = base.max_policy_vote AND
-  lut3.other_freq_khz = base.max_freq_vote AND
-  lut3.idle = base.idle_3
--- LUT for static curve lookup
-LEFT JOIN _filtered_curves_2d static_2d ON
-  static_2d.freq_khz = base.freq_0 AND
-  static_2d.other_policy = base.max_policy_vote AND
-  static_2d.other_freq_khz = base.max_freq_vote AND
-  static_2d.idle = 255
-LEFT JOIN _filtered_curves_1d static_1d ON
-  static_1d.policy = 0 AND
-  static_1d.freq_khz = base.freq_0 AND
-  static_1d.idle = 255
--- LUT joins for L3 cache
-LEFT JOIN _filtered_curves_l3 l3_hit_lut ON
-  l3_hit_lut.freq_khz = base.freq_0 AND
-  l3_hit_lut.other_policy = base.max_policy_vote AND
-  l3_hit_lut.other_freq_khz = base.max_freq_vote AND
-  l3_hit_lut.action = 'hit'
-LEFT JOIN _filtered_curves_l3 l3_miss_lut ON
-  l3_miss_lut.freq_khz = base.freq_0 AND
-  l3_miss_lut.other_policy = base.max_policy_vote AND
-  l3_miss_lut.other_freq_khz = base.max_freq_vote AND
-  l3_miss_lut.action = 'miss';
-
--- The most basic components of Wattson, all normalized to be in mW on a per
--- system state basis
-CREATE PERFETTO TABLE _system_state_mw
-AS
-SELECT
-  ts,
-  dur,
-  cpu0_curve as cpu0_mw,
-  cpu1_curve as cpu1_mw,
-  cpu2_curve as cpu2_mw,
-  cpu3_curve as cpu3_mw,
-  cpu4_curve as cpu4_mw,
-  cpu5_curve as cpu5_mw,
-  cpu6_curve as cpu6_mw,
-  cpu7_curve as cpu7_mw,
-  -- LUT for l3 is scaled by 10^6 to save resolution and in units of kWs. Scale
-  -- this by 10^3 so when divided by ns, result is in units of mW
-  (
-    (
-      IFNULL(l3_hit_value, 0) + IFNULL(l3_miss_value, 0)
-    ) * 1000 / dur
-  ) + static_curve as dsu_scu_mw
-FROM _system_state_curves;
-
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/curves/w_cpu_dependence.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/curves/w_cpu_dependence.sql
new file mode 100644
index 0000000..70b6ea2
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/curves/w_cpu_dependence.sql
@@ -0,0 +1,78 @@
+--
+-- 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 wattson.cpu_split;
+
+-- Find the CPU states creating the max vote
+CREATE PERFETTO TABLE _w_cpu_dependence
+AS
+WITH max_power_tbl AS (
+  SELECT
+    *,
+    -- Indicates if all CPUs are in deep idle
+    MIN(
+      no_static,
+      IFNULL(idle_4, 1),
+      IFNULL(idle_5, 1),
+      IFNULL(idle_6, 1),
+      IFNULL(idle_7, 1)
+    ) as all_cpu_deep_idle,
+    -- Determines which CPU has highest vote
+    MAX(
+      static_4,
+      static_5,
+      static_6,
+      static_7
+    ) as max_static_vote
+  FROM _w_independent_cpus_calc
+  -- _skip_devfreq_for_calc short circuits this table if devfreq is needed
+  JOIN _skip_devfreq_for_calc
+)
+SELECT
+  ts,
+  dur,
+  freq_0, idle_0,
+  freq_1, idle_1,
+  freq_2, idle_2,
+  freq_3, idle_3,
+  cpu0_curve,
+  cpu1_curve,
+  cpu2_curve,
+  cpu3_curve,
+  cpu4_curve,
+  cpu5_curve,
+  cpu6_curve,
+  cpu7_curve,
+  l3_hit_count,
+  l3_miss_count,
+  no_static,
+  all_cpu_deep_idle,
+  CASE max_static_vote
+    WHEN -1 THEN _get_min_freq_vote()
+    WHEN static_4 THEN freq_4
+    WHEN static_5 THEN freq_5
+    WHEN static_6 THEN freq_6
+    WHEN static_7 THEN freq_7
+    ELSE 400000
+  END dependent_freq,
+  CASE max_static_vote
+    WHEN -1 THEN _get_min_policy_vote()
+    WHEN static_4 THEN policy_4
+    WHEN static_5 THEN policy_5
+    WHEN static_6 THEN policy_6
+    WHEN static_7 THEN policy_7
+    ELSE 4
+  END dependent_policy
+FROM max_power_tbl;
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/curves/w_dsu_dependence.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/curves/w_dsu_dependence.sql
new file mode 100644
index 0000000..dbaf4ac
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/curves/w_dsu_dependence.sql
@@ -0,0 +1,89 @@
+--
+-- 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 intervals.intersect;
+INCLUDE PERFETTO MODULE linux.devfreq;
+INCLUDE PERFETTO MODULE wattson.cpu_split;
+INCLUDE PERFETTO MODULE wattson.curves.utils;
+INCLUDE PERFETTO MODULE wattson.device_infos;
+
+CREATE PERFETTO TABLE _cpu_curves AS
+SELECT
+  ts, dur,
+  freq_0, idle_0,
+  freq_1, idle_1,
+  freq_2, idle_2,
+  freq_3, idle_3,
+  lut4.curve_value as cpu4_curve,
+  lut5.curve_value as cpu5_curve,
+  lut6.curve_value as cpu6_curve,
+  lut7.curve_value as cpu7_curve,
+  l3_hit_count, l3_miss_count,
+  no_static,
+  MIN(
+    no_static,
+    IFNULL(idle_4, 1),
+    IFNULL(idle_5, 1),
+    IFNULL(idle_6, 1),
+    IFNULL(idle_7, 1)
+  ) as all_cpu_deep_idle
+FROM _w_independent_cpus_calc as base
+-- _use_devfreq_for_calc short circuits this table if devfreq isn't needed
+JOIN _use_devfreq_for_calc
+LEFT JOIN _filtered_curves_1d lut4 ON
+  base.freq_4 = lut4.freq_khz AND
+  base.idle_4 = lut4.idle
+LEFT JOIN _filtered_curves_1d lut5 ON
+  base.freq_5 = lut5.freq_khz AND
+  base.idle_5 = lut5.idle
+LEFT JOIN _filtered_curves_1d lut6 ON
+  base.freq_6 = lut6.freq_khz AND
+  base.idle_6 = lut6.idle
+LEFT JOIN _filtered_curves_1d lut7 ON
+  base.freq_7 = lut7.freq_khz AND
+  base.idle_7 = lut7.idle;
+
+CREATE PERFETTO TABLE _w_dsu_dependence AS
+SELECT
+  c.ts, c.dur,
+  c.freq_0, c.idle_0,
+  c.freq_1, c.idle_1,
+  c.freq_2, c.idle_2,
+  c.freq_3, c.idle_3,
+  -- NULL columns needed to match columns of _get_max_vote before UNION
+  NULL as cpu0_curve,
+  NULL as cpu1_curve,
+  NULL as cpu2_curve,
+  NULL as cpu3_curve,
+  c.cpu4_curve,
+  c.cpu5_curve,
+  c.cpu6_curve,
+  c.cpu7_curve,
+  c.l3_hit_count,
+  c.l3_miss_count,
+  c.no_static,
+  c.all_cpu_deep_idle,
+  d.dsu_freq as dependent_freq,
+  255 as dependent_policy
+FROM _interval_intersect!(
+  (
+    _ii_subquery!(_cpu_curves),
+    _ii_subquery!(linux_devfreq_dsu_counter)
+  ),
+  ()
+) ii
+JOIN _cpu_curves AS c ON c._auto_id = id_0
+JOIN linux_devfreq_dsu_counter AS d on d._auto_id = id_1;
+
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 89d128b..6b8cda5 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/device_infos.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/device_infos.sql
@@ -31,7 +31,15 @@
   ("monaco", 0, 450000),
   ("monaco", 1, 450000),
   ("monaco", 2, 450000),
-  ("monaco", 3, 450000)
+  ("monaco", 3, 450000),
+  ("Tensor G4", 0, 0),
+  ("Tensor G4", 1, 0),
+  ("Tensor G4", 2, 0),
+  ("Tensor G4", 3, 0),
+  ("Tensor G4", 4, 110000),
+  ("Tensor G4", 5, 110000),
+  ("Tensor G4", 6, 110000),
+  ("Tensor G4", 7, 400000)
 )
 select * from data;
 
@@ -50,6 +58,8 @@
 CREATE PERFETTO TABLE _wattson_device AS
 WITH soc_model AS (
   SELECT COALESCE(
+    -- Get guest model from metadata, which takes precedence if set
+    (SELECT str_value FROM metadata WHERE name = 'android_guest_soc_model'),
     -- Get model from metadata
     (SELECT str_value FROM metadata WHERE name = 'android_soc_model'),
     -- Get device name from metadata and map it to model
@@ -82,7 +92,17 @@
   ("Tensor", 4, 4),
   ("Tensor", 5, 4),
   ("Tensor", 6, 6),
-  ("Tensor", 7, 6)
+  ("Tensor", 7, 6),
+  ("Tensor G4", 0, 0),
+  ("Tensor G4", 1, 0),
+  ("Tensor G4", 2, 0),
+  ("Tensor G4", 3, 0),
+  ("Tensor G4", 4, 4),
+  ("Tensor G4", 5, 4),
+  ("Tensor G4", 6, 4),
+  ("Tensor G4", 7, 7),
+  -- need 255 policy to match devfreq
+  ("Tensor G4", 255, 255)
 )
 select * from data;
 
@@ -102,7 +122,8 @@
 WITH data(device, policy, freq) AS (
   VALUES
   ("monaco", 0, 614400),
-  ("Tensor", 4, 400000)
+  ("Tensor", 4, 400000),
+  ("Tensor G4", 0, 700000)
 )
 select * from data;
 
@@ -123,3 +144,26 @@
 FROM _device_min_volt_vote as vote_tbl
 JOIN _wattson_device as device
 WHERE vote_tbl.device = device.name;
+
+-- Devices that require using devfreq
+CREATE PERFETTO TABLE _use_devfreq
+AS
+WITH data(device) AS (
+  VALUES
+  ("Tensor G4")
+)
+select * from data;
+
+-- Creates non-empty table if device needs devfreq
+CREATE PERFETTO TABLE _use_devfreq_for_calc AS
+SELECT TRUE AS devfreq_necessary
+FROM _use_devfreq as d
+JOIN _wattson_device as device
+ON d.device = device.name;
+
+-- Creates empty table if device needs devfreq; inverse of _use_devfreq_for_calc
+CREATE PERFETTO TABLE _skip_devfreq_for_calc AS
+SELECT FALSE AS devfreq_necessary
+FROM _use_devfreq as d
+JOIN _wattson_device as device
+ON d.device != device.name;
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/system_state.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/system_state.sql
index bfa12ca..61907a5 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/system_state.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/system_state.sql
@@ -63,10 +63,10 @@
 )
 AS
 SELECT
-  ts,
-  dur,
-  cast_int!(round(l3_hit_rate * dur, 0)) as l3_hit_count,
-  cast_int!(round(l3_miss_rate * dur, 0)) as l3_miss_count,
+  s.ts,
+  s.dur,
+  cast_int!(round(l3_hit_rate * s.dur, 0)) as l3_hit_count,
+  cast_int!(round(l3_miss_rate * s.dur, 0)) as l3_miss_count,
   freq_0,
   idle_0,
   freq_1,
@@ -84,7 +84,14 @@
   freq_7,
   idle_7,
   IFNULL(suspended, FALSE) as suspended
-FROM _idle_freq_l3_hit_l3_miss_slice
+FROM _idle_freq_l3_hit_l3_miss_slice s
+JOIN _stats_cpu0 ON _stats_cpu0._auto_id = s.cpu0_id
+JOIN _stats_cpu1 ON _stats_cpu1._auto_id = s.cpu1_id
+JOIN _stats_cpu2 ON _stats_cpu2._auto_id = s.cpu2_id
+JOIN _stats_cpu3 ON _stats_cpu3._auto_id = s.cpu3_id
+LEFT JOIN _stats_cpu4 ON _stats_cpu4._auto_id = s.cpu4_id
+LEFT JOIN _stats_cpu5 ON _stats_cpu5._auto_id = s.cpu5_id
+LEFT JOIN _stats_cpu6 ON _stats_cpu6._auto_id = s.cpu6_id
+LEFT JOIN _stats_cpu7 ON _stats_cpu7._auto_id = s.cpu7_id
 -- Needs to be at least 1us to reduce inconsequential rows.
-WHERE dur > time_from_us(1);
-
+WHERE s.dur > time_from_us(1);
diff --git a/src/trace_processor/perfetto_sql/tokenizer/BUILD.gn b/src/trace_processor/perfetto_sql/tokenizer/BUILD.gn
new file mode 100644
index 0000000..0252d09
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/tokenizer/BUILD.gn
@@ -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("../../../../gn/test.gni")
+
+assert(enable_perfetto_trace_processor_sqlite)
+
+source_set("tokenizer") {
+  sources = [
+    "sqlite_tokenizer.cc",
+    "sqlite_tokenizer.h",
+  ]
+  deps = [
+    ":tokenize_internal",
+    "../../../../gn:default_deps",
+    "../../../../gn:sqlite",
+    "../../../base",
+    "../../sqlite",
+    "../grammar",
+  ]
+}
+
+source_set("tokenize_internal") {
+  sources = [
+    "tokenize_internal.c",
+    "tokenize_internal_helper.h",
+  ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../grammar",
+  ]
+  visibility = [ ":tokenizer" ]
+  if (perfetto_build_standalone) {
+    configs -= [ "//gn/standalone:extra_warnings" ]  # nogncheck
+  } else {
+    cflags_c = [
+      "-Wno-implicit-fallthrough",
+      "-Wno-unused-function",
+      "-Wno-unused-parameter",
+      "-Wno-unreachable-code",
+    ]
+  }
+}
+
+perfetto_unittest_source_set("unittests") {
+  testonly = true
+  sources = [ "sqlite_tokenizer_unittest.cc" ]
+  deps = [
+    ":tokenizer",
+    "../../../../gn:default_deps",
+    "../../../../gn:gtest_and_gmock",
+    "../../../../gn:sqlite",
+    "../../../base",
+    "../../../base:test_support",
+    "../../sqlite",
+    "../../util",
+  ]
+}
diff --git a/src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.cc b/src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.cc
new file mode 100644
index 0000000..4929026
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.cc
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+
+#include "src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.h"
+
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <utility>
+
+#include "perfetto/base/logging.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+
+namespace perfetto::trace_processor {
+extern "C" {
+
+int sqlite3GetToken(const unsigned char* z, int* tokenType);
+}
+
+SqliteTokenizer::SqliteTokenizer(SqlSource sql) : source_(std::move(sql)) {}
+
+SqliteTokenizer::Token SqliteTokenizer::Next() {
+  Token token;
+  const char* start = source_.sql().data() + offset_;
+  int n = sqlite3GetToken(reinterpret_cast<const unsigned char*>(start),
+                          &token.token_type);
+  offset_ += static_cast<uint32_t>(n);
+  token.str = std::string_view(start, static_cast<uint32_t>(n));
+  return token;
+}
+
+SqliteTokenizer::Token SqliteTokenizer::NextNonWhitespace() {
+  Token t;
+  for (t = Next(); t.token_type == TK_SPACE; t = Next()) {
+  }
+  return t;
+}
+
+SqliteTokenizer::Token SqliteTokenizer::NextTerminal() {
+  Token tok = Next();
+  while (!tok.IsTerminal()) {
+    tok = Next();
+  }
+  return tok;
+}
+
+SqlSource SqliteTokenizer::Substr(const Token& start,
+                                  const Token& end,
+                                  EndToken end_token) const {
+  auto offset = static_cast<uint32_t>(start.str.data() - source_.sql().c_str());
+  const char* e =
+      end.str.data() +
+      (end_token == SqliteTokenizer::EndToken::kInclusive ? end.str.size() : 0);
+  auto len = static_cast<uint32_t>(e - start.str.data());
+  return source_.Substr(offset, len);
+}
+
+SqlSource SqliteTokenizer::SubstrToken(const Token& token) const {
+  auto offset = static_cast<uint32_t>(token.str.data() - source_.sql().c_str());
+  auto len = static_cast<uint32_t>(token.str.size());
+  return source_.Substr(offset, len);
+}
+
+std::string SqliteTokenizer::AsTraceback(const Token& token) const {
+  PERFETTO_CHECK(source_.sql().c_str() <= token.str.data());
+  PERFETTO_CHECK(token.str.data() <=
+                 source_.sql().c_str() + source_.sql().size());
+  auto offset = static_cast<uint32_t>(token.str.data() - source_.sql().c_str());
+  return source_.AsTraceback(offset);
+}
+
+void SqliteTokenizer::Rewrite(SqlSource::Rewriter& rewriter,
+                              const Token& start,
+                              const Token& end,
+                              SqlSource rewrite,
+                              EndToken end_token) const {
+  auto s_off = static_cast<uint32_t>(start.str.data() - source_.sql().c_str());
+  auto e_off = static_cast<uint32_t>(end.str.data() - source_.sql().c_str());
+  uint32_t e_diff = end_token == EndToken::kInclusive
+                        ? static_cast<uint32_t>(end.str.size())
+                        : 0;
+  rewriter.Rewrite(s_off, e_off + e_diff, std::move(rewrite));
+}
+
+void SqliteTokenizer::RewriteToken(SqlSource::Rewriter& rewriter,
+                                   const Token& token,
+                                   SqlSource rewrite) const {
+  auto s_off = static_cast<uint32_t>(token.str.data() - source_.sql().c_str());
+  auto e_off = static_cast<uint32_t>(token.str.data() + token.str.size() -
+                                     source_.sql().c_str());
+  rewriter.Rewrite(s_off, e_off, std::move(rewrite));
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.h b/src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.h
new file mode 100644
index 0000000..809092b
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.h
@@ -0,0 +1,128 @@
+/*
+ * 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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_TOKENIZER_SQLITE_TOKENIZER_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_TOKENIZER_SQLITE_TOKENIZER_H_
+
+#include <cstdint>
+#include <string_view>
+#include <utility>
+
+#include "src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+
+namespace perfetto::trace_processor {
+
+// Tokenizes SQL statements according to SQLite SQL language specification:
+// https://www2.sqlite.org/hlr40000.html
+//
+// Usage of this class:
+// SqliteTokenizer tzr(std::move(my_sql_source));
+// for (auto t = tzr.Next(); t.token_type != TK_SEMI; t = tzr.Next()) {
+//   // Handle t here
+// }
+class SqliteTokenizer {
+ public:
+  // A single SQL token according to the SQLite standard.
+  struct Token {
+    // The string contents of the token.
+    std::string_view str;
+
+    // The type of the token.
+    int token_type = TK_ILLEGAL;
+
+    bool operator==(const Token& o) const {
+      return str == o.str && token_type == o.token_type;
+    }
+
+    // Returns if the token is empty or semicolon.
+    bool IsTerminal() const { return token_type == TK_SEMI || str.empty(); }
+  };
+
+  enum class EndToken {
+    kExclusive,
+    kInclusive,
+  };
+
+  // Creates a tokenizer which tokenizes |sql|.
+  explicit SqliteTokenizer(SqlSource sql);
+
+  SqliteTokenizer(const SqliteTokenizer&) = delete;
+  SqliteTokenizer& operator=(const SqliteTokenizer&) = delete;
+
+  SqliteTokenizer(SqliteTokenizer&&) = delete;
+  SqliteTokenizer& operator=(SqliteTokenizer&&) = delete;
+
+  // Returns the next SQL token.
+  Token Next();
+
+  // Returns the next SQL token which is not of type TK_SPACE.
+  Token NextNonWhitespace();
+
+  // Returns the next SQL token which is terminal.
+  Token NextTerminal();
+
+  // Returns an SqlSource containing all the tokens between |start| and |end|.
+  //
+  // Note: |start| and |end| must both have been previously returned by this
+  // tokenizer. If |end_token| == kInclusive, the end token is also included
+  // in the substring.
+  SqlSource Substr(const Token& start,
+                   const Token& end,
+                   EndToken end_token = EndToken::kExclusive) const;
+
+  // Returns an SqlSource containing only the SQL backing |token|.
+  //
+  // Note: |token| must have been previously returned by this tokenizer.
+  SqlSource SubstrToken(const Token& token) const;
+
+  // Returns a traceback error message for the SqlSource backing this tokenizer
+  // pointing to |token|. See SqlSource::AsTraceback for more information about
+  // this method.
+  //
+  // Note: |token| must have been previously returned by this tokenizer.
+  std::string AsTraceback(const Token&) const;
+
+  // Replaces the SQL in |rewriter| between |start| and |end| with the contents
+  // of |rewrite|. If |end_token| == kInclusive, the end token is also included
+  // in the rewrite.
+  void Rewrite(SqlSource::Rewriter& rewriter,
+               const Token& start,
+               const Token& end,
+               SqlSource rewrite,
+               EndToken end_token = EndToken::kExclusive) const;
+
+  // Replaces the SQL in |rewriter| backing |token| with the contents of
+  // |rewrite|.
+  void RewriteToken(SqlSource::Rewriter&,
+                    const Token&,
+                    SqlSource rewrite) const;
+
+  // Resets this tokenizer to tokenize |source|. Any previous returned tokens
+  // are invalidated.
+  void Reset(SqlSource source) {
+    source_ = std::move(source);
+    offset_ = 0;
+  }
+
+ private:
+  SqlSource source_;
+  uint32_t offset_ = 0;
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_TOKENIZER_SQLITE_TOKENIZER_H_
diff --git a/src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer_unittest.cc b/src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer_unittest.cc
new file mode 100644
index 0000000..ba46d03
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer_unittest.cc
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+#include "src/trace_processor/perfetto_sql/tokenizer/sqlite_tokenizer.h"
+
+#include <vector>
+
+#include "src/trace_processor/sqlite/sql_source.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto::trace_processor {
+namespace {
+
+using Token = SqliteTokenizer::Token;
+
+class SqliteTokenizerTest : public ::testing::Test {
+ protected:
+  std::vector<SqliteTokenizer::Token> Tokenize(const char* ptr) {
+    tokenizer_.Reset(SqlSource::FromTraceProcessorImplementation(ptr));
+    std::vector<SqliteTokenizer::Token> tokens;
+    for (auto t = tokenizer_.Next(); !t.str.empty(); t = tokenizer_.Next()) {
+      tokens.push_back(t);
+    }
+    return tokens;
+  }
+
+  SqliteTokenizer tokenizer_{SqlSource::FromTraceProcessorImplementation("")};
+};
+
+TEST_F(SqliteTokenizerTest, EmptyString) {
+  ASSERT_THAT(Tokenize(""), testing::IsEmpty());
+}
+
+TEST_F(SqliteTokenizerTest, OnlySpace) {
+  ASSERT_THAT(Tokenize(" "), testing::ElementsAre(Token{" ", TK_SPACE}));
+}
+
+TEST_F(SqliteTokenizerTest, SpaceColon) {
+  ASSERT_THAT(Tokenize(" ;"),
+              testing::ElementsAre(Token{" ", TK_SPACE}, Token{";", TK_SEMI}));
+}
+
+TEST_F(SqliteTokenizerTest, Select) {
+  ASSERT_THAT(
+      Tokenize("SELECT * FROM slice;"),
+      testing::ElementsAre(Token{"SELECT", TK_SELECT}, Token{" ", TK_SPACE},
+                           Token{"*", TK_STAR}, Token{" ", TK_SPACE},
+                           Token{"FROM", TK_FROM}, Token{" ", TK_SPACE},
+                           Token{"slice", TK_ID}, Token{";", TK_SEMI}));
+}
+
+TEST_F(SqliteTokenizerTest, PastEndErrorToken) {
+  tokenizer_.Reset(SqlSource::FromTraceProcessorImplementation("S"));
+  ASSERT_EQ(tokenizer_.Next(), (Token{"S", TK_ID}));
+
+  auto end_token = tokenizer_.Next();
+  ASSERT_EQ(end_token, (Token{"", TK_ILLEGAL}));
+  ASSERT_EQ(tokenizer_.AsTraceback(end_token),
+            "  Trace Processor Internal line 1 col 2\n"
+            "    S\n"
+            "     ^\n");
+}
+
+}  // namespace
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/tokenizer/tokenize_internal.c b/src/trace_processor/perfetto_sql/tokenizer/tokenize_internal.c
new file mode 100644
index 0000000..be5eabf
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/tokenizer/tokenize_internal.c
@@ -0,0 +1,563 @@
+/*
+** 2001 September 15
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** An tokenizer for SQL
+**
+** This file contains C code that splits an SQL input string up into
+** individual tokens and sends those tokens one-by-one over to the
+** parser for analysis.
+*/
+#include "src/trace_processor/perfetto_sql/tokenizer/tokenize_internal_helper.h"
+#include <stdlib.h>
+
+/* Character classes for tokenizing
+**
+** In the sqlite3GetToken() function, a switch() on aiClass[c] is implemented
+** using a lookup table, whereas a switch() directly on c uses a binary search.
+** The lookup table is much faster.  To maximize speed, and to ensure that
+** a lookup table is used, all of the classes need to be small integers and
+** all of them need to be used within the switch.
+*/
+#define CC_X          0    /* The letter 'x', or start of BLOB literal */
+#define CC_KYWD0      1    /* First letter of a keyword */
+#define CC_KYWD       2    /* Alphabetics or '_'.  Usable in a keyword */
+#define CC_DIGIT      3    /* Digits */
+#define CC_DOLLAR     4    /* '$' */
+#define CC_VARALPHA   5    /* '@', '#', ':'.  Alphabetic SQL variables */
+#define CC_VARNUM     6    /* '?'.  Numeric SQL variables */
+#define CC_SPACE      7    /* Space characters */
+#define CC_QUOTE      8    /* '"', '\'', or '`'.  String literals, quoted ids */
+#define CC_QUOTE2     9    /* '['.   [...] style quoted ids */
+#define CC_PIPE      10    /* '|'.   Bitwise OR or concatenate */
+#define CC_MINUS     11    /* '-'.  Minus or SQL-style comment */
+#define CC_LT        12    /* '<'.  Part of < or <= or <> */
+#define CC_GT        13    /* '>'.  Part of > or >= */
+#define CC_EQ        14    /* '='.  Part of = or == */
+#define CC_BANG      15    /* '!'.  Part of != */
+#define CC_SLASH     16    /* '/'.  / or c-style comment */
+#define CC_LP        17    /* '(' */
+#define CC_RP        18    /* ')' */
+#define CC_SEMI      19    /* ';' */
+#define CC_PLUS      20    /* '+' */
+#define CC_STAR      21    /* '*' */
+#define CC_PERCENT   22    /* '%' */
+#define CC_COMMA     23    /* ',' */
+#define CC_AND       24    /* '&' */
+#define CC_TILDA     25    /* '~' */
+#define CC_DOT       26    /* '.' */
+#define CC_ID        27    /* unicode characters usable in IDs */
+#define CC_ILLEGAL   28    /* Illegal character */
+#define CC_NUL       29    /* 0x00 */
+#define CC_BOM       30    /* First byte of UTF8 BOM:  0xEF 0xBB 0xBF */
+
+static const unsigned char aiClass[] = {
+#ifdef SQLITE_ASCII
+/*         x0  x1  x2  x3  x4  x5  x6  x7  x8  x9  xa  xb  xc  xd  xe  xf */
+/* 0x */   29, 28, 28, 28, 28, 28, 28, 28, 28,  7,  7, 28,  7,  7, 28, 28,
+/* 1x */   28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+/* 2x */    7, 15,  8,  5,  4, 22, 24,  8, 17, 18, 21, 20, 23, 11, 26, 16,
+/* 3x */    3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  5, 19, 12, 14, 13,  6,
+/* 4x */    5,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,
+/* 5x */    1,  1,  1,  1,  1,  1,  1,  1,  0,  2,  2,  9, 28, 28, 28,  2,
+/* 6x */    8,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,
+/* 7x */    1,  1,  1,  1,  1,  1,  1,  1,  0,  2,  2, 28, 10, 28, 25, 28,
+/* 8x */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
+/* 9x */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
+/* Ax */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
+/* Bx */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
+/* Cx */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
+/* Dx */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
+/* Ex */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 30,
+/* Fx */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27
+#endif
+#ifdef SQLITE_EBCDIC
+/*         x0  x1  x2  x3  x4  x5  x6  x7  x8  x9  xa  xb  xc  xd  xe  xf */
+/* 0x */   29, 28, 28, 28, 28,  7, 28, 28, 28, 28, 28, 28,  7,  7, 28, 28,
+/* 1x */   28, 28, 28, 28, 28,  7, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+/* 2x */   28, 28, 28, 28, 28,  7, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+/* 3x */   28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+/* 4x */    7, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 26, 12, 17, 20, 10,
+/* 5x */   24, 28, 28, 28, 28, 28, 28, 28, 28, 28, 15,  4, 21, 18, 19, 28,
+/* 6x */   11, 16, 28, 28, 28, 28, 28, 28, 28, 28, 28, 23, 22,  2, 13,  6,
+/* 7x */   28, 28, 28, 28, 28, 28, 28, 28, 28,  8,  5,  5,  5,  8, 14,  8,
+/* 8x */   28,  1,  1,  1,  1,  1,  1,  1,  1,  1, 28, 28, 28, 28, 28, 28,
+/* 9x */   28,  1,  1,  1,  1,  1,  1,  1,  1,  1, 28, 28, 28, 28, 28, 28,
+/* Ax */   28, 25,  1,  1,  1,  1,  1,  0,  2,  2, 28, 28, 28, 28, 28, 28,
+/* Bx */   28, 28, 28, 28, 28, 28, 28, 28, 28, 28,  9, 28, 28, 28, 28, 28,
+/* Cx */   28,  1,  1,  1,  1,  1,  1,  1,  1,  1, 28, 28, 28, 28, 28, 28,
+/* Dx */   28,  1,  1,  1,  1,  1,  1,  1,  1,  1, 28, 28, 28, 28, 28, 28,
+/* Ex */   28, 28,  1,  1,  1,  1,  1,  0,  2,  2, 28, 28, 28, 28, 28, 28,
+/* Fx */    3,  3,  3,  3,  3,  3,  3,  3,  3,  3, 28, 28, 28, 28, 28, 28,
+#endif
+};
+
+/*
+** The charMap() macro maps alphabetic characters (only) into their
+** lower-case ASCII equivalent.  On ASCII machines, this is just
+** an upper-to-lower case map.  On EBCDIC machines we also need
+** to adjust the encoding.  The mapping is only valid for alphabetics
+** which are the only characters for which this feature is used. 
+**
+** Used by keywordhash.h
+*/
+#ifdef SQLITE_ASCII
+# define charMap(X) sqlite3UpperToLower[(unsigned char)X]
+#endif
+#ifdef SQLITE_EBCDIC
+# define charMap(X) ebcdicToAscii[(unsigned char)X]
+const unsigned char ebcdicToAscii[] = {
+/* 0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F */
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  /* 0x */
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  /* 1x */
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  /* 2x */
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  /* 3x */
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  /* 4x */
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  /* 5x */
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 95,  0,  0,  /* 6x */
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  /* 7x */
+   0, 97, 98, 99,100,101,102,103,104,105,  0,  0,  0,  0,  0,  0,  /* 8x */
+   0,106,107,108,109,110,111,112,113,114,  0,  0,  0,  0,  0,  0,  /* 9x */
+   0,  0,115,116,117,118,119,120,121,122,  0,  0,  0,  0,  0,  0,  /* Ax */
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  /* Bx */
+   0, 97, 98, 99,100,101,102,103,104,105,  0,  0,  0,  0,  0,  0,  /* Cx */
+   0,106,107,108,109,110,111,112,113,114,  0,  0,  0,  0,  0,  0,  /* Dx */
+   0,  0,115,116,117,118,119,120,121,122,  0,  0,  0,  0,  0,  0,  /* Ex */
+   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  /* Fx */
+};
+#endif
+
+/*
+** The sqlite3KeywordCode function looks up an identifier to determine if
+** it is a keyword.  If it is a keyword, the token code of that keyword is 
+** returned.  If the input is not a keyword, TK_ID is returned.
+**
+** The implementation of this routine was generated by a program,
+** mkkeywordhash.c, located in the tool subdirectory of the distribution.
+** The output of the mkkeywordhash.c program is written into a file
+** named keywordhash.h and then included into this source file by
+** the #include below.
+*/
+
+
+/*
+** If X is a character that can be used in an identifier then
+** IdChar(X) will be true.  Otherwise it is false.
+**
+** For ASCII, any character with the high-order bit set is
+** allowed in an identifier.  For 7-bit characters, 
+** sqlite3IsIdChar[X] must be 1.
+**
+** For EBCDIC, the rules are more complex but have the same
+** end result.
+**
+** Ticket #1066.  the SQL standard does not allow '$' in the
+** middle of identifiers.  But many SQL implementations do. 
+** SQLite will allow '$' in identifiers for compatibility.
+** But the feature is undocumented.
+*/
+#ifdef SQLITE_ASCII
+#define IdChar(C)  ((sqlite3CtypeMap[(unsigned char)C]&0x46)!=0)
+#endif
+#ifdef SQLITE_EBCDIC
+const char sqlite3IsEbcdicIdChar[] = {
+/* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF */
+    0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,  /* 4x */
+    0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0,  /* 5x */
+    0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0,  /* 6x */
+    0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,  /* 7x */
+    0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0,  /* 8x */
+    0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0,  /* 9x */
+    1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0,  /* Ax */
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  /* Bx */
+    0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1,  /* Cx */
+    0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1,  /* Dx */
+    0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1,  /* Ex */
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0,  /* Fx */
+};
+#define IdChar(C)  (((c=C)>=0x42 && sqlite3IsEbcdicIdChar[c-0x40]))
+#endif
+
+/* Make the IdChar function accessible from ctime.c and alter.c */
+int sqlite3IsIdChar(u8 c){ return IdChar(c); }
+
+#ifndef SQLITE_OMIT_WINDOWFUNC
+/*
+** Return the id of the next token in string (*pz). Before returning, set
+** (*pz) to point to the byte following the parsed token.
+*/
+static int getToken(const unsigned char **pz){
+  const unsigned char *z = *pz;
+  int t;                          /* Token type to return */
+  do {
+    z += sqlite3GetToken(z, &t);
+  }while( t==TK_SPACE );
+  if( t==TK_ID 
+   || t==TK_STRING 
+   || t==TK_JOIN_KW 
+   || t==TK_WINDOW 
+   || t==TK_OVER 
+   || sqlite3ParserFallback(t)==TK_ID 
+  ){
+    t = TK_ID;
+  }
+  *pz = z;
+  return t;
+}
+
+/*
+** The following three functions are called immediately after the tokenizer
+** reads the keywords WINDOW, OVER and FILTER, respectively, to determine
+** whether the token should be treated as a keyword or an SQL identifier.
+** This cannot be handled by the usual lemon %fallback method, due to
+** the ambiguity in some constructions. e.g.
+**
+**   SELECT sum(x) OVER ...
+**
+** In the above, "OVER" might be a keyword, or it might be an alias for the 
+** sum(x) expression. If a "%fallback ID OVER" directive were added to 
+** grammar, then SQLite would always treat "OVER" as an alias, making it
+** impossible to call a window-function without a FILTER clause.
+**
+** WINDOW is treated as a keyword if:
+**
+**   * the following token is an identifier, or a keyword that can fallback
+**     to being an identifier, and
+**   * the token after than one is TK_AS.
+**
+** OVER is a keyword if:
+**
+**   * the previous token was TK_RP, and
+**   * the next token is either TK_LP or an identifier.
+**
+** FILTER is a keyword if:
+**
+**   * the previous token was TK_RP, and
+**   * the next token is TK_LP.
+*/
+static int analyzeWindowKeyword(const unsigned char *z){
+  int t;
+  t = getToken(&z);
+  if( t!=TK_ID ) return TK_ID;
+  t = getToken(&z);
+  if( t!=TK_AS ) return TK_ID;
+  return TK_WINDOW;
+}
+static int analyzeOverKeyword(const unsigned char *z, int lastToken){
+  if( lastToken==TK_RP ){
+    int t = getToken(&z);
+    if( t==TK_LP || t==TK_ID ) return TK_OVER;
+  }
+  return TK_ID;
+}
+static int analyzeFilterKeyword(const unsigned char *z, int lastToken){
+  if( lastToken==TK_RP && getToken(&z)==TK_LP ){
+    return TK_FILTER;
+  }
+  return TK_ID;
+}
+#endif /* SQLITE_OMIT_WINDOWFUNC */
+
+/*
+** Return the length (in bytes) of the token that begins at z[0]. 
+** Store the token type in *tokenType before returning.
+*/
+int sqlite3GetToken(const unsigned char *z, int *tokenType){
+  int i, c;
+  switch( aiClass[*z] ){  /* Switch on the character-class of the first byte
+                          ** of the token. See the comment on the CC_ defines
+                          ** above. */
+    case CC_SPACE: {
+      testcase( z[0]==' ' );
+      testcase( z[0]=='\t' );
+      testcase( z[0]=='\n' );
+      testcase( z[0]=='\f' );
+      testcase( z[0]=='\r' );
+      for(i=1; sqlite3Isspace(z[i]); i++){}
+      *tokenType = TK_SPACE;
+      return i;
+    }
+    case CC_MINUS: {
+      if( z[1]=='-' ){
+        for(i=2; (c=z[i])!=0 && c!='\n'; i++){}
+        *tokenType = TK_SPACE;   /* IMP: R-22934-25134 */
+        return i;
+      }else if( z[1]=='>' ){
+        *tokenType = TK_PTR;
+        return 2 + (z[2]=='>');
+      }
+      *tokenType = TK_MINUS;
+      return 1;
+    }
+    case CC_LP: {
+      *tokenType = TK_LP;
+      return 1;
+    }
+    case CC_RP: {
+      *tokenType = TK_RP;
+      return 1;
+    }
+    case CC_SEMI: {
+      *tokenType = TK_SEMI;
+      return 1;
+    }
+    case CC_PLUS: {
+      *tokenType = TK_PLUS;
+      return 1;
+    }
+    case CC_STAR: {
+      *tokenType = TK_STAR;
+      return 1;
+    }
+    case CC_SLASH: {
+      if( z[1]!='*' || z[2]==0 ){
+        *tokenType = TK_SLASH;
+        return 1;
+      }
+      for(i=3, c=z[2]; (c!='*' || z[i]!='/') && (c=z[i])!=0; i++){}
+      if( c ) i++;
+      *tokenType = TK_SPACE;   /* IMP: R-22934-25134 */
+      return i;
+    }
+    case CC_PERCENT: {
+      *tokenType = TK_REM;
+      return 1;
+    }
+    case CC_EQ: {
+      *tokenType = TK_EQ;
+      return 1 + (z[1]=='=');
+    }
+    case CC_LT: {
+      if( (c=z[1])=='=' ){
+        *tokenType = TK_LE;
+        return 2;
+      }else if( c=='>' ){
+        *tokenType = TK_NE;
+        return 2;
+      }else if( c=='<' ){
+        *tokenType = TK_LSHIFT;
+        return 2;
+      }else{
+        *tokenType = TK_LT;
+        return 1;
+      }
+    }
+    case CC_GT: {
+      if( (c=z[1])=='=' ){
+        *tokenType = TK_GE;
+        return 2;
+      }else if( c=='>' ){
+        *tokenType = TK_RSHIFT;
+        return 2;
+      }else{
+        *tokenType = TK_GT;
+        return 1;
+      }
+    }
+    case CC_BANG: {
+      if( z[1]!='=' ){
+        *tokenType = TK_ILLEGAL;
+        return 1;
+      }else{
+        *tokenType = TK_NE;
+        return 2;
+      }
+    }
+    case CC_PIPE: {
+      if( z[1]!='|' ){
+        *tokenType = TK_BITOR;
+        return 1;
+      }else{
+        *tokenType = TK_CONCAT;
+        return 2;
+      }
+    }
+    case CC_COMMA: {
+      *tokenType = TK_COMMA;
+      return 1;
+    }
+    case CC_AND: {
+      *tokenType = TK_BITAND;
+      return 1;
+    }
+    case CC_TILDA: {
+      *tokenType = TK_BITNOT;
+      return 1;
+    }
+    case CC_QUOTE: {
+      int delim = z[0];
+      testcase( delim=='`' );
+      testcase( delim=='\'' );
+      testcase( delim=='"' );
+      for(i=1; (c=z[i])!=0; i++){
+        if( c==delim ){
+          if( z[i+1]==delim ){
+            i++;
+          }else{
+            break;
+          }
+        }
+      }
+      if( c=='\'' ){
+        *tokenType = TK_STRING;
+        return i+1;
+      }else if( c!=0 ){
+        *tokenType = TK_ID;
+        return i+1;
+      }else{
+        *tokenType = TK_ILLEGAL;
+        return i;
+      }
+    }
+    case CC_DOT: {
+#ifndef SQLITE_OMIT_FLOATING_POINT
+      if( !sqlite3Isdigit(z[1]) )
+#endif
+      {
+        *tokenType = TK_DOT;
+        return 1;
+      }
+      /* If the next character is a digit, this is a floating point
+      ** number that begins with ".".  Fall thru into the next case */
+      /* no break */ deliberate_fall_through
+    }
+    case CC_DIGIT: {
+      testcase( z[0]=='0' );  testcase( z[0]=='1' );  testcase( z[0]=='2' );
+      testcase( z[0]=='3' );  testcase( z[0]=='4' );  testcase( z[0]=='5' );
+      testcase( z[0]=='6' );  testcase( z[0]=='7' );  testcase( z[0]=='8' );
+      testcase( z[0]=='9' );  testcase( z[0]=='.' );
+      *tokenType = TK_INTEGER;
+#ifndef SQLITE_OMIT_HEX_INTEGER
+      if( z[0]=='0' && (z[1]=='x' || z[1]=='X') && sqlite3Isxdigit(z[2]) ){
+        for(i=3; sqlite3Isxdigit(z[i]); i++){}
+        return i;
+      }
+#endif
+      for(i=0; sqlite3Isdigit(z[i]); i++){}
+#ifndef SQLITE_OMIT_FLOATING_POINT
+      if( z[i]=='.' ){
+        i++;
+        while( sqlite3Isdigit(z[i]) ){ i++; }
+        *tokenType = TK_FLOAT;
+      }
+      if( (z[i]=='e' || z[i]=='E') &&
+           ( sqlite3Isdigit(z[i+1]) 
+            || ((z[i+1]=='+' || z[i+1]=='-') && sqlite3Isdigit(z[i+2]))
+           )
+      ){
+        i += 2;
+        while( sqlite3Isdigit(z[i]) ){ i++; }
+        *tokenType = TK_FLOAT;
+      }
+#endif
+      while( IdChar(z[i]) ){
+        *tokenType = TK_ILLEGAL;
+        i++;
+      }
+      return i;
+    }
+    case CC_QUOTE2: {
+      for(i=1, c=z[0]; c!=']' && (c=z[i])!=0; i++){}
+      *tokenType = c==']' ? TK_ID : TK_ILLEGAL;
+      return i;
+    }
+    case CC_VARNUM: {
+      *tokenType = TK_VARIABLE;
+      for(i=1; sqlite3Isdigit(z[i]); i++){}
+      return i;
+    }
+    case CC_DOLLAR:
+    case CC_VARALPHA: {
+      int n = 0;
+      testcase( z[0]=='$' );  testcase( z[0]=='@' );
+      testcase( z[0]==':' );  testcase( z[0]=='#' );
+      *tokenType = TK_VARIABLE;
+      for(i=1; (c=z[i])!=0; i++){
+        if( IdChar(c) ){
+          n++;
+#ifndef SQLITE_OMIT_TCL_VARIABLE
+        }else if( c=='(' && n>0 ){
+          do{
+            i++;
+          }while( (c=z[i])!=0 && !sqlite3Isspace(c) && c!=')' );
+          if( c==')' ){
+            i++;
+          }else{
+            *tokenType = TK_ILLEGAL;
+          }
+          break;
+        }else if( c==':' && z[i+1]==':' ){
+          i++;
+#endif
+        }else{
+          break;
+        }
+      }
+      if( n==0 ) *tokenType = TK_ILLEGAL;
+      return i;
+    }
+    case CC_KYWD0: {
+      if( aiClass[z[1]]>CC_KYWD ){ i = 1;  break; }
+      for(i=2; aiClass[z[i]]<=CC_KYWD; i++){}
+      if( IdChar(z[i]) ){
+        /* This token started out using characters that can appear in keywords,
+        ** but z[i] is a character not allowed within keywords, so this must
+        ** be an identifier instead */
+        i++;
+        break;
+      }
+      *tokenType = TK_ID;
+      return keywordCode((char*)z, i, tokenType);
+    }
+    case CC_X: {
+#ifndef SQLITE_OMIT_BLOB_LITERAL
+      testcase( z[0]=='x' ); testcase( z[0]=='X' );
+      if( z[1]=='\'' ){
+        *tokenType = TK_BLOB;
+        for(i=2; sqlite3Isxdigit(z[i]); i++){}
+        if( z[i]!='\'' || i%2 ){
+          *tokenType = TK_ILLEGAL;
+          while( z[i] && z[i]!='\'' ){ i++; }
+        }
+        if( z[i] ) i++;
+        return i;
+      }
+#endif
+      /* If it is not a BLOB literal, then it must be an ID, since no
+      ** SQL keywords start with the letter 'x'.  Fall through */
+      /* no break */ deliberate_fall_through
+    }
+    case CC_KYWD:
+    case CC_ID: {
+      i = 1;
+      break;
+    }
+    case CC_BOM: {
+      if( z[1]==0xbb && z[2]==0xbf ){
+        *tokenType = TK_SPACE;
+        return 3;
+      }
+      i = 1;
+      break;
+    }
+    case CC_NUL: {
+      *tokenType = TK_ILLEGAL;
+      return 0;
+    }
+    default: {
+      *tokenType = TK_ILLEGAL;
+      return 1;
+    }
+  }
+  while( IdChar(z[i]) ){ i++; }
+  *tokenType = TK_ID;
+  return i;
+}
+
diff --git a/src/trace_processor/perfetto_sql/tokenizer/tokenize_internal_helper.h b/src/trace_processor/perfetto_sql/tokenizer/tokenize_internal_helper.h
new file mode 100644
index 0000000..08cbc75
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/tokenizer/tokenize_internal_helper.h
@@ -0,0 +1,96 @@
+/*
+ * 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_TOKENIZER_TOKENIZE_INTERNAL_HELPER_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_TOKENIZER_TOKENIZE_INTERNAL_HELPER_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <assert.h>
+#include <ctype.h>
+
+#include "src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.h"
+#include "src/trace_processor/perfetto_sql/grammar/perfettosql_keywordhash.h"
+
+#define SQLITE_ASCII 1
+
+const unsigned char sqlite3CtypeMap[256] = {
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 00..07    ........ */
+    0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, /* 08..0f    ........ */
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 10..17    ........ */
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 18..1f    ........ */
+    0x01, 0x00, 0x80, 0x00, 0x40, 0x00, 0x00, 0x80, /* 20..27     !"#$%&' */
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 28..2f    ()*+,-./ */
+    0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, /* 30..37    01234567 */
+    0x0c, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 38..3f    89:;<=>? */
+
+    0x00, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x02, /* 40..47    @ABCDEFG */
+    0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, /* 48..4f    HIJKLMNO */
+    0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, /* 50..57    PQRSTUVW */
+    0x02, 0x02, 0x02, 0x80, 0x00, 0x00, 0x00, 0x40, /* 58..5f    XYZ[\]^_ */
+    0x80, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x22, /* 60..67    `abcdefg */
+    0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, /* 68..6f    hijklmno */
+    0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, /* 70..77    pqrstuvw */
+    0x22, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, /* 78..7f    xyz{|}~. */
+
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* 80..87    ........ */
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* 88..8f    ........ */
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* 90..97    ........ */
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* 98..9f    ........ */
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* a0..a7    ........ */
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* a8..af    ........ */
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* b0..b7    ........ */
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* b8..bf    ........ */
+
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* c0..c7    ........ */
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* c8..cf    ........ */
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* d0..d7    ........ */
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* d8..df    ........ */
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* e0..e7    ........ */
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* e8..ef    ........ */
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* f0..f7    ........ */
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40  /* f8..ff    ........ */
+};
+
+static inline int sqlite3Isspace(char c) {
+  return isspace(c);
+}
+static inline int sqlite3Isdigit(char c) {
+  return isdigit(c);
+}
+static inline int sqlite3Isxdigit(char c) {
+  return isxdigit(c);
+}
+
+int sqlite3GetToken(const unsigned char* z, int* tokenType);
+
+static inline int sqlite3ParserFallback(int token) {
+  return 0;
+}
+
+#ifdef __cplusplus
+#define deliberate_fall_through [[fallthrough]];
+#else
+#define deliberate_fall_through
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_TOKENIZER_TOKENIZE_INTERNAL_HELPER_H_
diff --git a/src/trace_processor/read_trace.cc b/src/trace_processor/read_trace.cc
index cd4050a..e9f8f8c 100644
--- a/src/trace_processor/read_trace.cc
+++ b/src/trace_processor/read_trace.cc
@@ -26,8 +26,8 @@
 #include "perfetto/protozero/proto_utils.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "perfetto/trace_processor/trace_processor.h"
+#include "src/trace_processor/importers/archive/gzip_trace_parser.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
-#include "src/trace_processor/importers/gzip/gzip_trace_parser.h"
 #include "src/trace_processor/importers/proto/proto_trace_tokenizer.h"
 #include "src/trace_processor/read_trace_internal.h"
 #include "src/trace_processor/util/gzip_utils.h"
@@ -94,10 +94,7 @@
     std::unique_ptr<ChunkedTraceReader> reader(
         new SerializingProtoTraceReader(output));
     GzipTraceParser parser(std::move(reader));
-
     RETURN_IF_ERROR(parser.ParseUnowned(data, size));
-    if (parser.needs_more_input())
-      return base::ErrStatus("Cannot decompress partial trace file");
     return parser.NotifyEndOfFile();
   }
 
diff --git a/src/trace_processor/read_trace_internal.cc b/src/trace_processor/read_trace_internal.cc
index 3b537cd..f2b1a67 100644
--- a/src/trace_processor/read_trace_internal.cc
+++ b/src/trace_processor/read_trace_internal.cc
@@ -27,7 +27,6 @@
 #include "perfetto/trace_processor/trace_blob.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/forwarding_trace_parser.h"
-#include "src/trace_processor/importers/gzip/gzip_trace_parser.h"
 #include "src/trace_processor/importers/proto/proto_trace_tokenizer.h"
 #include "src/trace_processor/util/gzip_utils.h"
 #include "src/trace_processor/util/status_macros.h"
diff --git a/src/trace_processor/rpc/query_result_serializer_unittest.cc b/src/trace_processor/rpc/query_result_serializer_unittest.cc
index f28bec0..7e2f640 100644
--- a/src/trace_processor/rpc/query_result_serializer_unittest.cc
+++ b/src/trace_processor/rpc/query_result_serializer_unittest.cc
@@ -376,7 +376,7 @@
       insert_values += ",";
     } else {
       insert_values += "),";
-      if (insert_values.size() > 1024 * 1024 || i == kNumCells - 1) {
+      if (insert_values.size() > 100 * 1024 || i == kNumCells - 1) {
         insert_values[insert_values.size() - 1] = ';';
         auto query = "insert into tab (a,b,c) values " + insert_values;
         insert_values = "";
diff --git a/src/trace_processor/rpc/rpc.cc b/src/trace_processor/rpc/rpc.cc
index b7274ca..49feef3 100644
--- a/src/trace_processor/rpc/rpc.cc
+++ b/src/trace_processor/rpc/rpc.cc
@@ -32,6 +32,7 @@
 #include "perfetto/ext/protozero/proto_ring_buffer.h"
 #include "perfetto/ext/trace_processor/rpc/query_result_serializer.h"
 #include "perfetto/protozero/field.h"
+#include "perfetto/protozero/proto_utils.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
 #include "perfetto/trace_processor/basic_types.h"
 #include "perfetto/trace_processor/metatrace_config.h"
@@ -223,7 +224,18 @@
         resp.Send(rpc_response_fn_);
       } else {
         protozero::ConstBytes args = req.query_args();
-        auto it = QueryInternal(args.data, args.size);
+        protos::pbzero::QueryArgs::Decoder query(args.data, args.size);
+        std::string sql = query.sql_query().ToStdString();
+
+        PERFETTO_TP_TRACE(metatrace::Category::API_TIMELINE, "RPC_QUERY",
+                          [&](metatrace::Record* r) {
+                            r->AddArg("SQL", sql);
+                            if (query.has_tag()) {
+                              r->AddArg("tag", query.tag());
+                            }
+                          });
+
+        auto it = trace_processor_->ExecuteQuery(sql);
         QueryResultSerializer serializer(std::move(it));
         for (bool has_more = true; has_more;) {
           const auto seq_id = tx_seq_id_++;
@@ -312,6 +324,16 @@
       resp.Send(rpc_response_fn_);
       break;
     }
+    case RpcProto::TPM_REGISTER_SQL_PACKAGE: {
+      Response resp(tx_seq_id_++, req_type);
+      base::Status status = RegisterSqlPackage(req.register_sql_package_args());
+      auto* res = resp->set_register_sql_package_result();
+      if (!status.ok()) {
+        res->set_error(status.message());
+      }
+      resp.Send(rpc_response_fn_);
+      break;
+    }
     default: {
       // This can legitimately happen if the client is newer. We reply with a
       // generic "unkown request" response, so the client can do feature
@@ -385,9 +407,33 @@
             ? SoftDropFtraceDataBefore::kAllPerCpuBuffersValid
             : SoftDropFtraceDataBefore::kNoDrop;
   }
+  using Args = protos::pbzero::ResetTraceProcessorArgs;
+  switch (reset_trace_processor_args.parsing_mode()) {
+    case Args::ParsingMode::DEFAULT:
+      config.parsing_mode = ParsingMode::kDefault;
+      break;
+    case Args::ParsingMode::TOKENIZE_ONLY:
+      config.parsing_mode = ParsingMode::kTokenizeOnly;
+      break;
+    case Args::ParsingMode::TOKENIZE_AND_SORT:
+      config.parsing_mode = ParsingMode::kTokenizeAndSort;
+      break;
+  }
   ResetTraceProcessorInternal(config);
 }
 
+base::Status Rpc::RegisterSqlPackage(protozero::ConstBytes bytes) {
+  protos::pbzero::RegisterSqlPackageArgs::Decoder args(bytes);
+  SqlPackage package;
+  package.name = args.package_name().ToStdString();
+  package.allow_override = args.allow_override();
+  for (auto it = args.modules(); it; ++it) {
+    protos::pbzero::RegisterSqlPackageArgs::Module::Decoder m(*it);
+    package.modules.emplace_back(m.name().ToStdString(), m.sql().ToStdString());
+  }
+  return trace_processor_->RegisterSqlPackage(package);
+}
+
 void Rpc::MaybePrintProgress() {
   if (eof_ || bytes_parsed_ - bytes_last_progress_ > kProgressUpdateBytes) {
     bytes_last_progress_ = bytes_parsed_;
@@ -405,18 +451,6 @@
 void Rpc::Query(const uint8_t* args,
                 size_t len,
                 const QueryResultBatchCallback& result_callback) {
-  auto it = QueryInternal(args, len);
-  QueryResultSerializer serializer(std::move(it));
-
-  std::vector<uint8_t> res;
-  for (bool has_more = true; has_more;) {
-    has_more = serializer.Serialize(&res);
-    result_callback(res.data(), res.size(), has_more);
-    res.clear();
-  }
-}
-
-Iterator Rpc::QueryInternal(const uint8_t* args, size_t len) {
   protos::pbzero::QueryArgs::Decoder query(args, len);
   std::string sql = query.sql_query().ToStdString();
   PERFETTO_TP_TRACE(metatrace::Category::API_TIMELINE, "RPC_QUERY",
@@ -427,7 +461,16 @@
                       }
                     });
 
-  return trace_processor_->ExecuteQuery(sql);
+  auto it = trace_processor_->ExecuteQuery(sql);
+
+  QueryResultSerializer serializer(std::move(it));
+
+  std::vector<uint8_t> res;
+  for (bool has_more = true; has_more;) {
+    has_more = serializer.Serialize(&res);
+    result_callback(res.data(), res.size(), has_more);
+    res.clear();
+  }
 }
 
 void Rpc::RestoreInitialTables() {
diff --git a/src/trace_processor/rpc/rpc.h b/src/trace_processor/rpc/rpc.h
index 5890ef3..fe28748 100644
--- a/src/trace_processor/rpc/rpc.h
+++ b/src/trace_processor/rpc/rpc.h
@@ -27,6 +27,7 @@
 
 #include "perfetto/base/status.h"
 #include "perfetto/ext/protozero/proto_ring_buffer.h"
+#include "perfetto/protozero/field.h"
 #include "perfetto/trace_processor/basic_types.h"
 
 namespace perfetto {
@@ -104,7 +105,6 @@
 
   base::Status Parse(const uint8_t*, size_t);
   base::Status NotifyEndOfFile();
-  void ResetTraceProcessor(const uint8_t*, size_t);
   std::string GetCurrentTraceName();
   std::vector<uint8_t> ComputeMetric(const uint8_t*, size_t);
   void EnableMetatrace(const uint8_t*, size_t);
@@ -130,6 +130,8 @@
 
  private:
   void ParseRpcRequest(const uint8_t*, size_t);
+  void ResetTraceProcessor(const uint8_t*, size_t);
+  base::Status RegisterSqlPackage(protozero::ConstBytes);
   void ResetTraceProcessorInternal(const Config&);
   void MaybePrintProgress();
   Iterator QueryInternal(const uint8_t*, size_t);
diff --git a/src/trace_processor/sorter/BUILD.gn b/src/trace_processor/sorter/BUILD.gn
index a429c30..2653f44 100644
--- a/src/trace_processor/sorter/BUILD.gn
+++ b/src/trace_processor/sorter/BUILD.gn
@@ -30,10 +30,15 @@
     "../../../include/perfetto/trace_processor:storage",
     "../../base",
     "../importers/android_bugreport:android_log_event",
+    "../importers/art_method:art_method_event",
     "../importers/common:parser_types",
     "../importers/common:trace_parser_hdr",
     "../importers/fuchsia:fuchsia_record",
+    "../importers/gecko:gecko_event",
+    "../importers/instruments:row",
     "../importers/perf:record",
+    "../importers/perf_text:perf_text_event",
+    "../importers/proto:packet_sequence_state_generation_hdr",
     "../importers/systrace:systrace_line",
     "../storage",
     "../types",
diff --git a/src/trace_processor/sorter/trace_sorter.cc b/src/trace_processor/sorter/trace_sorter.cc
index b684e1a..4fff091 100644
--- a/src/trace_processor/sorter/trace_sorter.cc
+++ b/src/trace_processor/sorter/trace_sorter.cc
@@ -26,11 +26,16 @@
 #include "perfetto/base/compiler.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/public/compiler.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/android_bugreport/android_log_event.h"
+#include "src/trace_processor/importers/art_method/art_method_event.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_record.h"
+#include "src/trace_processor/importers/gecko/gecko_event.h"
+#include "src/trace_processor/importers/instruments/row.h"
 #include "src/trace_processor/importers/perf/record.h"
+#include "src/trace_processor/importers/perf_text/perf_text_event.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
 #include "src/trace_processor/sorter/trace_token_buffer.h"
 #include "src/trace_processor/storage/stats.h"
@@ -40,13 +45,12 @@
 namespace perfetto::trace_processor {
 
 TraceSorter::TraceSorter(TraceProcessorContext* context,
-                         SortingMode sorting_mode)
-    : sorting_mode_(sorting_mode), storage_(context->storage) {
+                         SortingMode sorting_mode,
+                         EventHandling event_handling)
+    : sorting_mode_(sorting_mode),
+      storage_(context->storage),
+      event_handling_(event_handling) {
   AddMachineContext(context);
-  const char* env = getenv("TRACE_PROCESSOR_SORT_ONLY");
-  bypass_next_stage_for_testing_ = env && !strcmp(env, "1");
-  if (bypass_next_stage_for_testing_)
-    PERFETTO_ELOG("TEST MODE: bypassing protobuf parsing stage");
 }
 
 TraceSorter::~TraceSorter() {
@@ -62,27 +66,44 @@
   }
 }
 
-void TraceSorter::Queue::Sort() {
+void TraceSorter::Queue::Sort(TraceTokenBuffer& buffer, bool use_slow_sorting) {
   PERFETTO_DCHECK(needs_sorting());
   PERFETTO_DCHECK(sort_start_idx_ < events_.size());
 
   // If sort_min_ts_ has been set, it will no long be max_int, and so will be
   // smaller than max_ts_.
-  PERFETTO_DCHECK(sort_min_ts_ < max_ts_);
+  PERFETTO_DCHECK(sort_min_ts_ < std::numeric_limits<int64_t>::max());
 
   // We know that all events between [0, sort_start_idx_] are sorted. Within
   // this range, perform a bound search and find the iterator for the min
   // timestamp that broke the monotonicity. Re-sort from there to the end.
   auto sort_end = events_.begin() + static_cast<ssize_t>(sort_start_idx_);
-  PERFETTO_DCHECK(std::is_sorted(events_.begin(), sort_end));
+  if (use_slow_sorting) {
+    PERFETTO_DCHECK(sort_min_ts_ <= max_ts_);
+    PERFETTO_DCHECK(std::is_sorted(events_.begin(), sort_end,
+                                   TimestampedEvent::SlowOperatorLess{buffer}));
+  } else {
+    PERFETTO_DCHECK(sort_min_ts_ < max_ts_);
+    PERFETTO_DCHECK(std::is_sorted(events_.begin(), sort_end));
+  }
   auto sort_begin = std::lower_bound(events_.begin(), sort_end, sort_min_ts_,
                                      &TimestampedEvent::Compare);
-  std::sort(sort_begin, events_.end());
+  if (use_slow_sorting) {
+    std::sort(sort_begin, events_.end(),
+              TimestampedEvent::SlowOperatorLess{buffer});
+  } else {
+    std::sort(sort_begin, events_.end());
+  }
   sort_start_idx_ = 0;
   sort_min_ts_ = 0;
 
   // At this point |events_| must be fully sorted
-  PERFETTO_DCHECK(std::is_sorted(events_.begin(), events_.end()));
+  if (use_slow_sorting) {
+    PERFETTO_DCHECK(std::is_sorted(events_.begin(), events_.end(),
+                                   TimestampedEvent::SlowOperatorLess{buffer}));
+  } else {
+    PERFETTO_DCHECK(std::is_sorted(events_.begin(), events_.end()));
+  }
 }
 
 // Removes all the events in |queues_| that are earlier than the given
@@ -148,7 +169,7 @@
     auto& queue = sorter_data.queues[min_queue_idx];
     auto& events = queue.events_;
     if (queue.needs_sorting())
-      queue.Sort();
+      queue.Sort(token_buffer_, use_slow_sorting_);
     PERFETTO_DCHECK(queue.min_ts_ == events.front().ts);
 
     // Now that we identified the min-queue, extract all events from it until
@@ -197,11 +218,15 @@
 void TraceSorter::ParseTracePacket(TraceProcessorContext& context,
                                    const TimestampedEvent& event) {
   TraceTokenBuffer::Id id = GetTokenBufferId(event);
-  switch (static_cast<TimestampedEvent::Type>(event.event_type)) {
+  switch (event.type()) {
     case TimestampedEvent::Type::kPerfRecord:
       context.perf_record_parser->ParsePerfRecord(
           event.ts, token_buffer_.Extract<perf_importer::Record>(id));
       return;
+    case TimestampedEvent::Type::kInstrumentsRow:
+      context.instruments_row_parser->ParseInstrumentsRow(
+          event.ts, token_buffer_.Extract<instruments_importer::Row>(id));
+      return;
     case TimestampedEvent::Type::kTracePacket:
       context.proto_trace_parser->ParseTracePacket(
           event.ts, token_buffer_.Extract<TracePacketData>(id));
@@ -218,6 +243,15 @@
       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));
+      return;
     case TimestampedEvent::Type::kSystraceLine:
       context.json_trace_parser->ParseSystraceLine(
           event.ts, token_buffer_.Extract<SystraceLine>(id));
@@ -226,6 +260,23 @@
       context.android_log_event_parser->ParseAndroidLogEvent(
           event.ts, token_buffer_.Extract<AndroidLogEvent>(id));
       return;
+    case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
+      context.json_trace_parser->ParseLegacyV8ProfileEvent(
+          event.ts, token_buffer_.Extract<LegacyV8CpuProfileEvent>(id));
+      return;
+    case TimestampedEvent::Type::kGeckoEvent:
+      context.gecko_trace_parser->ParseGeckoEvent(
+          event.ts, token_buffer_.Extract<gecko_importer::GeckoEvent>(id));
+      return;
+    case TimestampedEvent::Type::kArtMethodEvent:
+      context.art_method_parser->ParseArtMethodEvent(
+          event.ts, token_buffer_.Extract<art_method::ArtMethodEvent>(id));
+      return;
+    case TimestampedEvent::Type::kPerfTextEvent:
+      context.perf_text_parser->ParsePerfTextEvent(
+          event.ts,
+          token_buffer_.Extract<perf_text_importer::PerfTextEvent>(id));
+      return;
     case TimestampedEvent::Type::kInlineSchedSwitch:
     case TimestampedEvent::Type::kInlineSchedWaking:
     case TimestampedEvent::Type::kEtwEvent:
@@ -248,12 +299,19 @@
     case TimestampedEvent::Type::kInlineSchedWaking:
     case TimestampedEvent::Type::kFtraceEvent:
     case TimestampedEvent::Type::kTrackEvent:
+    case TimestampedEvent::Type::kSpeRecord:
     case TimestampedEvent::Type::kSystraceLine:
     case TimestampedEvent::Type::kTracePacket:
     case TimestampedEvent::Type::kPerfRecord:
+    case TimestampedEvent::Type::kInstrumentsRow:
     case TimestampedEvent::Type::kJsonValue:
+    case TimestampedEvent::Type::kJsonValueWithDur:
     case TimestampedEvent::Type::kFuchsiaRecord:
     case TimestampedEvent::Type::kAndroidLogEvent:
+    case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
+    case TimestampedEvent::Type::kGeckoEvent:
+    case TimestampedEvent::Type::kArtMethodEvent:
+    case TimestampedEvent::Type::kPerfTextEvent:
       PERFETTO_FATAL("Invalid event type");
   }
   PERFETTO_FATAL("For GCC");
@@ -278,12 +336,19 @@
       return;
     case TimestampedEvent::Type::kEtwEvent:
     case TimestampedEvent::Type::kTrackEvent:
+    case TimestampedEvent::Type::kSpeRecord:
     case TimestampedEvent::Type::kSystraceLine:
     case TimestampedEvent::Type::kTracePacket:
     case TimestampedEvent::Type::kPerfRecord:
+    case TimestampedEvent::Type::kInstrumentsRow:
     case TimestampedEvent::Type::kJsonValue:
+    case TimestampedEvent::Type::kJsonValueWithDur:
     case TimestampedEvent::Type::kFuchsiaRecord:
     case TimestampedEvent::Type::kAndroidLogEvent:
+    case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
+    case TimestampedEvent::Type::kGeckoEvent:
+    case TimestampedEvent::Type::kArtMethodEvent:
+    case TimestampedEvent::Type::kPerfTextEvent:
       PERFETTO_FATAL("Invalid event type");
   }
   PERFETTO_FATAL("For GCC");
@@ -307,6 +372,12 @@
     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;
     case TimestampedEvent::Type::kSystraceLine:
       base::ignore_result(token_buffer_.Extract<SystraceLine>(id));
       return;
@@ -319,9 +390,27 @@
     case TimestampedEvent::Type::kPerfRecord:
       base::ignore_result(token_buffer_.Extract<perf_importer::Record>(id));
       return;
+    case TimestampedEvent::Type::kInstrumentsRow:
+      base::ignore_result(token_buffer_.Extract<instruments_importer::Row>(id));
+      return;
     case TimestampedEvent::Type::kAndroidLogEvent:
       base::ignore_result(token_buffer_.Extract<AndroidLogEvent>(id));
       return;
+    case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
+      base::ignore_result(token_buffer_.Extract<LegacyV8CpuProfileEvent>(id));
+      return;
+    case TimestampedEvent::Type::kGeckoEvent:
+      base::ignore_result(
+          token_buffer_.Extract<gecko_importer::GeckoEvent>(id));
+      return;
+    case TimestampedEvent::Type::kArtMethodEvent:
+      base::ignore_result(
+          token_buffer_.Extract<art_method::ArtMethodEvent>(id));
+      return;
+    case TimestampedEvent::Type::kPerfTextEvent:
+      base::ignore_result(
+          token_buffer_.Extract<perf_text_importer::PerfTextEvent>(id));
+      return;
   }
   PERFETTO_FATAL("For GCC");
 }
@@ -337,12 +426,13 @@
 
   latest_pushed_event_ts_ = std::max(latest_pushed_event_ts_, timestamp);
 
-  if (PERFETTO_UNLIKELY(bypass_next_stage_for_testing_)) {
+  if (PERFETTO_UNLIKELY(event_handling_ == EventHandling::kSortAndDrop)) {
     // Parse* would extract this event and push it to the next stage. Since we
     // are skipping that, just extract and discard it.
     ExtractAndDiscardTokenizedObject(event);
     return;
   }
+  PERFETTO_DCHECK(event_handling_ == EventHandling::kSortAndPush);
 
   if (queue_idx == 0) {
     ParseTracePacket(*machine_context, event);
diff --git a/src/trace_processor/sorter/trace_sorter.h b/src/trace_processor/sorter/trace_sorter.h
index 656b283..cb6763c 100644
--- a/src/trace_processor/sorter/trace_sorter.h
+++ b/src/trace_processor/sorter/trace_sorter.h
@@ -35,10 +35,14 @@
 #include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/android_bugreport/android_log_event.h"
+#include "src/trace_processor/importers/art_method/art_method_event.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_record.h"
+#include "src/trace_processor/importers/gecko/gecko_event.h"
+#include "src/trace_processor/importers/instruments/row.h"
 #include "src/trace_processor/importers/perf/record.h"
+#include "src/trace_processor/importers/perf_text/perf_text_event.h"
 #include "src/trace_processor/importers/systrace/systrace_line.h"
 #include "src/trace_processor/sorter/trace_token_buffer.h"
 #include "src/trace_processor/storage/trace_storage.h"
@@ -99,111 +103,180 @@
     kDefault,
     kFullSort,
   };
+  enum class EventHandling {
+    // Indicates that events should be sorted and pushed to the parsing stage.
+    kSortAndPush,
 
-  TraceSorter(TraceProcessorContext* context, SortingMode sorting_mode);
+    // Indicates that events should be sorted but then dropped before pushing
+    // to the parsing stage.
+    // Used for performance analysis of the sorter.
+    kSortAndDrop,
+
+    // Indicates that the events should be dropped as soon as they enter the
+    // sorter.
+    // Used in cases where we only want to perform tokenization: dropping data
+    // when it hits the sorter is much cleaner than trying to handle this
+    // at every different tokenizer.
+    kDrop,
+  };
+
+  TraceSorter(TraceProcessorContext*,
+              SortingMode,
+              EventHandling = EventHandling::kSortAndPush);
 
   ~TraceSorter();
 
   SortingMode sorting_mode() const { return sorting_mode_; }
 
-  inline void AddMachineContext(TraceProcessorContext* context) {
+  void AddMachineContext(TraceProcessorContext* context) {
     sorter_data_by_machine_.emplace_back(context);
   }
 
-  inline void PushAndroidLogEvent(
-      int64_t timestamp,
-      AndroidLogEvent event,
-      std::optional<MachineId> machine_id = std::nullopt) {
-    TraceTokenBuffer::Id id = token_buffer_.Append(std::move(event));
+  void PushAndroidLogEvent(int64_t timestamp,
+                           const AndroidLogEvent& event,
+                           std::optional<MachineId> machine_id = std::nullopt) {
     AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kAndroidLogEvent,
-                         id, machine_id);
+                         event, machine_id);
   }
 
-  inline void PushPerfRecord(
-      int64_t timestamp,
-      perf_importer::Record record,
-      std::optional<MachineId> machine_id = std::nullopt) {
-    TraceTokenBuffer::Id id = token_buffer_.Append(std::move(record));
-    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kPerfRecord, id,
-                         machine_id);
+  void PushPerfRecord(int64_t timestamp,
+                      perf_importer::Record record,
+                      std::optional<MachineId> machine_id = std::nullopt) {
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kPerfRecord,
+                         std::move(record), machine_id);
   }
 
-  inline void PushTracePacket(
-      int64_t timestamp,
-      TracePacketData data,
-      std::optional<MachineId> machine_id = std::nullopt) {
-    TraceTokenBuffer::Id id = token_buffer_.Append(std::move(data));
-    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kTracePacket, id,
-                         machine_id);
+  void PushSpeRecord(int64_t timestamp,
+                     TraceBlobView record,
+                     std::optional<MachineId> machine_id = std::nullopt) {
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kSpeRecord,
+                         std::move(record), machine_id);
   }
 
-  inline void PushTracePacket(
-      int64_t timestamp,
-      RefPtr<PacketSequenceStateGeneration> state,
-      TraceBlobView tbv,
-      std::optional<MachineId> machine_id = std::nullopt) {
+  void PushInstrumentsRow(int64_t timestamp,
+                          const instruments_importer::Row& row,
+                          std::optional<MachineId> machine_id = std::nullopt) {
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kInstrumentsRow,
+                         row, machine_id);
+  }
+
+  void PushTracePacket(int64_t timestamp,
+                       TracePacketData data,
+                       std::optional<MachineId> machine_id = std::nullopt) {
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kTracePacket,
+                         std::move(data), machine_id);
+  }
+
+  void PushTracePacket(int64_t timestamp,
+                       RefPtr<PacketSequenceStateGeneration> state,
+                       TraceBlobView tbv,
+                       std::optional<MachineId> machine_id = std::nullopt) {
     PushTracePacket(timestamp,
                     TracePacketData{std::move(tbv), std::move(state)},
                     machine_id);
   }
 
-  inline void PushJsonValue(int64_t timestamp, std::string json_value) {
-    TraceTokenBuffer::Id id =
-        token_buffer_.Append(JsonEvent{std::move(json_value)});
-    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kJsonValue, id);
+  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.
+      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)});
   }
 
-  inline void PushFuchsiaRecord(int64_t timestamp,
-                                FuchsiaRecord fuchsia_record) {
-    TraceTokenBuffer::Id id = token_buffer_.Append(std::move(fuchsia_record));
-    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kFuchsiaRecord, id);
+  void PushFuchsiaRecord(int64_t timestamp, FuchsiaRecord fuchsia_record) {
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kFuchsiaRecord,
+                         std::move(fuchsia_record));
   }
 
-  inline void PushSystraceLine(SystraceLine systrace_line) {
-    TraceTokenBuffer::Id id = token_buffer_.Append(std::move(systrace_line));
+  void PushSystraceLine(SystraceLine systrace_line) {
     AppendNonFtraceEvent(systrace_line.ts,
-                         TimestampedEvent::Type::kSystraceLine, id);
+                         TimestampedEvent::Type::kSystraceLine,
+                         std::move(systrace_line));
   }
 
-  inline void PushTrackEventPacket(
+  void PushTrackEventPacket(
       int64_t timestamp,
       TrackEventData track_event,
       std::optional<MachineId> machine_id = std::nullopt) {
-    TraceTokenBuffer::Id id = token_buffer_.Append(std::move(track_event));
-    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kTrackEvent, id,
-                         machine_id);
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kTrackEvent,
+                         std::move(track_event), machine_id);
   }
 
-  inline void PushEtwEvent(uint32_t cpu,
-                           int64_t timestamp,
-                           TraceBlobView tbv,
-                           RefPtr<PacketSequenceStateGeneration> state,
-                           std::optional<MachineId> machine_id = std::nullopt) {
+  void PushLegacyV8CpuProfileEvent(int64_t timestamp,
+                                   uint64_t session_id,
+                                   uint32_t pid,
+                                   uint32_t tid,
+                                   uint32_t callsite_id) {
+    AppendNonFtraceEvent(
+        timestamp, TimestampedEvent::Type::kLegacyV8CpuProfileEvent,
+        LegacyV8CpuProfileEvent{session_id, pid, tid, callsite_id});
+  }
+
+  void PushGeckoEvent(int64_t timestamp,
+                      const gecko_importer::GeckoEvent& event) {
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kGeckoEvent, event);
+  }
+
+  void PushArtMethodEvent(int64_t timestamp,
+                          const art_method::ArtMethodEvent& event) {
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kArtMethodEvent,
+                         event);
+  }
+
+  void PushPerfTextEvent(int64_t timestamp,
+                         const perf_text_importer::PerfTextEvent& event) {
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kPerfTextEvent,
+                         event);
+  }
+
+  void PushEtwEvent(uint32_t cpu,
+                    int64_t timestamp,
+                    TraceBlobView tbv,
+                    RefPtr<PacketSequenceStateGeneration> state,
+                    std::optional<MachineId> machine_id = std::nullopt) {
+    if (PERFETTO_UNLIKELY(event_handling_ == EventHandling::kDrop)) {
+      return;
+    }
     TraceTokenBuffer::Id id =
         token_buffer_.Append(TracePacketData{std::move(tbv), std::move(state)});
     auto* queue = GetQueue(cpu + 1, machine_id);
-    queue->Append(timestamp, TimestampedEvent::Type::kEtwEvent, id);
+    queue->Append(timestamp, TimestampedEvent::Type::kEtwEvent, id,
+                  use_slow_sorting_);
     UpdateAppendMaxTs(queue);
   }
 
-  inline void PushFtraceEvent(
-      uint32_t cpu,
-      int64_t timestamp,
-      TraceBlobView tbv,
-      RefPtr<PacketSequenceStateGeneration> state,
-      std::optional<MachineId> machine_id = std::nullopt) {
+  void PushFtraceEvent(uint32_t cpu,
+                       int64_t timestamp,
+                       TraceBlobView tbv,
+                       RefPtr<PacketSequenceStateGeneration> state,
+                       std::optional<MachineId> machine_id = std::nullopt) {
+    if (PERFETTO_UNLIKELY(event_handling_ == EventHandling::kDrop)) {
+      return;
+    }
     TraceTokenBuffer::Id id =
         token_buffer_.Append(TracePacketData{std::move(tbv), std::move(state)});
     auto* queue = GetQueue(cpu + 1, machine_id);
-    queue->Append(timestamp, TimestampedEvent::Type::kFtraceEvent, id);
+    queue->Append(timestamp, TimestampedEvent::Type::kFtraceEvent, id,
+                  use_slow_sorting_);
     UpdateAppendMaxTs(queue);
   }
 
-  inline void PushInlineFtraceEvent(
+  void PushInlineFtraceEvent(
       uint32_t cpu,
       int64_t timestamp,
-      InlineSchedSwitch inline_sched_switch,
+      const InlineSchedSwitch& inline_sched_switch,
       std::optional<MachineId> machine_id = std::nullopt) {
+    if (PERFETTO_UNLIKELY(event_handling_ == EventHandling::kDrop)) {
+      return;
+    }
     // TODO(rsavitski): if a trace has a mix of normal & "compact" events
     // (being pushed through this function), the ftrace batches will no longer
     // be fully sorted by timestamp. In such situations, we will have to sort
@@ -211,22 +284,25 @@
     // sorted however. Consider adding extra queues, or pushing them in a
     // merge-sort fashion
     // // instead.
-    TraceTokenBuffer::Id id =
-        token_buffer_.Append(std::move(inline_sched_switch));
+    TraceTokenBuffer::Id id = token_buffer_.Append(inline_sched_switch);
     auto* queue = GetQueue(cpu + 1, machine_id);
-    queue->Append(timestamp, TimestampedEvent::Type::kInlineSchedSwitch, id);
+    queue->Append(timestamp, TimestampedEvent::Type::kInlineSchedSwitch, id,
+                  use_slow_sorting_);
     UpdateAppendMaxTs(queue);
   }
 
-  inline void PushInlineFtraceEvent(
+  void PushInlineFtraceEvent(
       uint32_t cpu,
       int64_t timestamp,
-      InlineSchedWaking inline_sched_waking,
+      const InlineSchedWaking& inline_sched_waking,
       std::optional<MachineId> machine_id = std::nullopt) {
-    TraceTokenBuffer::Id id =
-        token_buffer_.Append(std::move(inline_sched_waking));
+    if (PERFETTO_UNLIKELY(event_handling_ == EventHandling::kDrop)) {
+      return;
+    }
+    TraceTokenBuffer::Id id = token_buffer_.Append(inline_sched_waking);
     auto* queue = GetQueue(cpu + 1, machine_id);
-    queue->Append(timestamp, TimestampedEvent::Type::kInlineSchedWaking, id);
+    queue->Append(timestamp, TimestampedEvent::Type::kInlineSchedWaking, id,
+                  use_slow_sorting_);
     UpdateAppendMaxTs(queue);
   }
 
@@ -262,22 +338,29 @@
  private:
   struct TimestampedEvent {
     enum class Type : uint8_t {
+      kAndroidLogEvent,
+      kEtwEvent,
       kFtraceEvent,
-      kPerfRecord,
-      kTracePacket,
+      kFuchsiaRecord,
       kInlineSchedSwitch,
       kInlineSchedWaking,
+      kInstrumentsRow,
       kJsonValue,
-      kFuchsiaRecord,
-      kTrackEvent,
+      kJsonValueWithDur,
+      kLegacyV8CpuProfileEvent,
+      kPerfRecord,
+      kSpeRecord,
       kSystraceLine,
-      kEtwEvent,
-      kAndroidLogEvent,
-      kMax = kAndroidLogEvent,
+      kTracePacket,
+      kTrackEvent,
+      kGeckoEvent,
+      kArtMethodEvent,
+      kPerfTextEvent,
+      kMax = kPerfTextEvent,
     };
 
     // Number of bits required to store the max element in |Type|.
-    static constexpr uint32_t kMaxTypeBits = 4;
+    static constexpr uint32_t kMaxTypeBits = 6;
     static_assert(static_cast<uint8_t>(Type::kMax) <= (1 << kMaxTypeBits),
                   "Max type does not fit inside storage");
 
@@ -298,16 +381,38 @@
       return BumpAllocator::AllocId{chunk_index, chunk_offset};
     }
 
+    Type type() const { return static_cast<Type>(event_type); }
+
     // For std::lower_bound().
-    static inline bool Compare(const TimestampedEvent& x, int64_t ts) {
+    static bool Compare(const TimestampedEvent& x, int64_t ts) {
       return x.ts < ts;
     }
 
     // For std::sort().
-    inline bool operator<(const TimestampedEvent& evt) const {
+    bool operator<(const TimestampedEvent& evt) const {
       return std::tie(ts, chunk_index, chunk_offset) <
              std::tie(evt.ts, evt.chunk_index, evt.chunk_offset);
     }
+
+    struct SlowOperatorLess {
+      // For std::sort() in slow mode.
+      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();
+        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();
+        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);
+      }
+      TraceTokenBuffer& buffer;
+    };
   };
 
   static_assert(sizeof(TimestampedEvent) == 16,
@@ -324,7 +429,8 @@
   struct Queue {
     void Append(int64_t ts,
                 TimestampedEvent::Type type,
-                TraceTokenBuffer::Id id) {
+                TraceTokenBuffer::Id id,
+                bool use_slow_sorting) {
       {
         TimestampedEvent event;
         event.ts = ts;
@@ -335,7 +441,8 @@
       }
 
       // Events are often seen in order.
-      if (PERFETTO_LIKELY(ts >= max_ts_)) {
+      if (PERFETTO_LIKELY(ts > max_ts_ ||
+                          (!use_slow_sorting && ts == max_ts_))) {
         max_ts_ = ts;
       } else {
         // The event is breaking ordering. The first time it happens, keep
@@ -357,7 +464,7 @@
     }
 
     bool needs_sorting() const { return sort_start_idx_ != 0; }
-    void Sort();
+    void Sort(TraceTokenBuffer&, bool use_slow_sorting);
 
     base::CircularQueue<TimestampedEvent> events_;
     int64_t min_ts_ = std::numeric_limits<int64_t>::max();
@@ -368,8 +475,8 @@
 
   void SortAndExtractEventsUntilAllocId(BumpAllocator::AllocId alloc_id);
 
-  inline Queue* GetQueue(size_t index,
-                         std::optional<MachineId> machine_id = std::nullopt) {
+  Queue* GetQueue(size_t index,
+                  std::optional<MachineId> machine_id = std::nullopt) {
     // sorter_data_by_machine_[0] corresponds to the default machine.
     PERFETTO_DCHECK(sorter_data_by_machine_[0].machine_id == std::nullopt);
     auto* queues = &sorter_data_by_machine_[0].queues;
@@ -390,17 +497,29 @@
     return &queues->at(index);
   }
 
-  inline void AppendNonFtraceEvent(
+  template <typename E>
+  void AppendNonFtraceEvent(
       int64_t ts,
       TimestampedEvent::Type event_type,
-      TraceTokenBuffer::Id id,
+      E&& evt,
       std::optional<MachineId> machine_id = std::nullopt) {
+    if (PERFETTO_UNLIKELY(event_handling_ == EventHandling::kDrop)) {
+      return;
+    }
+    TraceTokenBuffer::Id id = token_buffer_.Append(std::forward<E>(evt));
+    AppendNonFtraceEventWithId(ts, event_type, id, machine_id);
+  }
+
+  void AppendNonFtraceEventWithId(int64_t ts,
+                                  TimestampedEvent::Type event_type,
+                                  TraceTokenBuffer::Id id,
+                                  std::optional<MachineId> machine_id) {
     Queue* queue = GetQueue(0, machine_id);
-    queue->Append(ts, event_type, id);
+    queue->Append(ts, event_type, id, use_slow_sorting_);
     UpdateAppendMaxTs(queue);
   }
 
-  inline void UpdateAppendMaxTs(Queue* queue) {
+  void UpdateAppendMaxTs(Queue* queue) {
     append_max_ts_ = std::max(append_max_ts_, queue->max_ts_);
   }
 
@@ -457,12 +576,15 @@
   // max(e.ts for e appended to the sorter)
   int64_t append_max_ts_ = 0;
 
-  // Used for performance tests. True when setting
-  // TRACE_PROCESSOR_SORT_ONLY=1.
-  bool bypass_next_stage_for_testing_ = false;
+  // How events pushed into the sorter should be handled.
+  EventHandling event_handling_ = EventHandling::kSortAndPush;
 
   // max(e.ts for e pushed to next stage)
   int64_t latest_pushed_event_ts_ = std::numeric_limits<int64_t>::min();
+
+  // Whether when std::sorting the queues, we should use the slow
+  // sorting algorithm
+  bool use_slow_sorting_ = false;
 };
 
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/sorter/trace_token_buffer.cc b/src/trace_processor/sorter/trace_token_buffer.cc
index 7541a4d..ab8b15d 100644
--- a/src/trace_processor/sorter/trace_token_buffer.cc
+++ b/src/trace_processor/sorter/trace_token_buffer.cc
@@ -16,24 +16,24 @@
 
 #include "src/trace_processor/sorter/trace_token_buffer.h"
 
-#include <stdint.h>
 #include <algorithm>
 #include <cstdint>
 #include <cstring>
 #include <functional>
 #include <limits>
 #include <optional>
-#include <type_traits>
 #include <utility>
 
 #include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/common/parser_types.h"
+#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/util/bump_allocator.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 namespace {
 
 struct alignas(8) TrackEventDataDescriptor {
@@ -111,7 +111,7 @@
   InternedIndex interned_index = GetInternedIndex(alloc_id);
 
   // Compute the interning information for the TrackBlob and the SequenceState.
-  const TracePacketData& tpd = ted.trace_packet_data;
+  TracePacketData& tpd = ted.trace_packet_data;
   desc.intern_blob_offset = InternTraceBlob(interned_index, tpd.packet);
   desc.intern_blob_index =
       static_cast<uint16_t>(interned_blobs_.at(interned_index).size() - 1);
@@ -280,5 +280,4 @@
   return static_cast<size_t>(interned_index);
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/sorter/trace_token_buffer.h b/src/trace_processor/sorter/trace_token_buffer.h
index 85af6ac..4ba332b 100644
--- a/src/trace_processor/sorter/trace_token_buffer.h
+++ b/src/trace_processor/sorter/trace_token_buffer.h
@@ -72,6 +72,14 @@
     return Append(TrackEventData(std::move(data)));
   }
 
+  // Gets a pointer an object of type |T| from the token buffer using an id
+  // previously returned by |Append|. This type *must* match the type added
+  // using Append. Mismatching types will caused undefined behaviour.
+  template <typename T>
+  PERFETTO_WARN_UNUSED_RESULT T* Get(Id id) {
+    return static_cast<T*>(allocator_.GetPointer(id.alloc_id));
+  }
+
   // Extracts an object of type |T| from the token buffer using an id previously
   // returned by |Append|. This type *must* match the type added using Append.
   // Mismatching types will caused undefined behaviour.
diff --git a/src/trace_processor/sqlite/BUILD.gn b/src/trace_processor/sqlite/BUILD.gn
index fda30ba..06150e0 100644
--- a/src/trace_processor/sqlite/BUILD.gn
+++ b/src/trace_processor/sqlite/BUILD.gn
@@ -28,8 +28,6 @@
     "sql_stats_table.h",
     "sqlite_engine.cc",
     "sqlite_engine.h",
-    "sqlite_tokenizer.cc",
-    "sqlite_tokenizer.h",
     "sqlite_utils.cc",
     "sqlite_utils.h",
     "stats_table.cc",
@@ -64,7 +62,6 @@
   sources = [
     "db_sqlite_table_unittest.cc",
     "sql_source_unittest.cc",
-    "sqlite_tokenizer_unittest.cc",
     "sqlite_utils_unittest.cc",
   ]
   deps = [
diff --git a/src/trace_processor/sqlite/sqlite_engine.cc b/src/trace_processor/sqlite/sqlite_engine.cc
index fcd68e8..fbb3fb1 100644
--- a/src/trace_processor/sqlite/sqlite_engine.cc
+++ b/src/trace_processor/sqlite/sqlite_engine.cc
@@ -54,7 +54,25 @@
     // in trace processor when multiple instances are used in parallel.
     // Fix this by disabling the memstatus API which we don't make use of in
     // any case. See b/335019324 for more info on this.
-    PERFETTO_CHECK(sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0) == SQLITE_OK);
+    int ret = sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0);
+
+    // As much as it is painful, we need to catch instances of SQLITE_MISUSE
+    // here against all the advice of the SQLite developers and lalitm@'s
+    // intuition: SQLITE_MISUSE for sqlite3_config really means: that someone
+    // else has already initialized SQLite. As we are an embeddable library,
+    // it's very possible that the process embedding us has initialized SQLite
+    // in a different way to what we want to do and, if so, we should respect
+    // their choice.
+    //
+    // TODO(lalitm): ideally we would have an sqlite3_is_initialized API we
+    // could use to gate the above check but that doesn't exist: report this
+    // issue to SQLite developers and see if such an API could be added. If so
+    // we can remove this check.
+    if (ret == SQLITE_MISUSE) {
+      return true;
+    }
+
+    PERFETTO_CHECK(ret == SQLITE_OK);
     return sqlite3_initialize() == SQLITE_OK;
   }();
   PERFETTO_CHECK(init_once);
diff --git a/src/trace_processor/sqlite/sqlite_tokenizer.cc b/src/trace_processor/sqlite/sqlite_tokenizer.cc
deleted file mode 100644
index 5a17f30..0000000
--- a/src/trace_processor/sqlite/sqlite_tokenizer.cc
+++ /dev/null
@@ -1,507 +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.
- */
-
-#include "src/trace_processor/sqlite/sqlite_tokenizer.h"
-
-#include <ctype.h>
-#include <sqlite3.h>
-#include <cstdint>
-#include <optional>
-#include <string_view>
-
-#include "perfetto/base/compiler.h"
-#include "perfetto/base/logging.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-// The contents of this file are ~copied from SQLite with some modifications to
-// minimize the amount copied: i.e. if we can call a libc function/public SQLite
-// API instead of a private one.
-//
-// The changes are as follows:
-// 1. Remove all ifdefs to only keep branches we actually use
-// 2. Change handling of |CC_KYWD0| to remove distinction between different
-//    SQLite kewords, reducing how many things we need to copy over.
-// 3. Constants are changed from be macro defines to be values in
-//    |SqliteTokenType|.
-
-namespace {
-
-const unsigned char sqlite3CtypeMap[256] = {
-    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 00..07    ........ */
-    0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, /* 08..0f    ........ */
-    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 10..17    ........ */
-    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 18..1f    ........ */
-    0x01, 0x00, 0x80, 0x00, 0x40, 0x00, 0x00, 0x80, /* 20..27     !"#$%&' */
-    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 28..2f    ()*+,-./ */
-    0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, /* 30..37    01234567 */
-    0x0c, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 38..3f    89:;<=>? */
-
-    0x00, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x02, /* 40..47    @ABCDEFG */
-    0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, /* 48..4f    HIJKLMNO */
-    0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, /* 50..57    PQRSTUVW */
-    0x02, 0x02, 0x02, 0x80, 0x00, 0x00, 0x00, 0x40, /* 58..5f    XYZ[\]^_ */
-    0x80, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x22, /* 60..67    `abcdefg */
-    0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, /* 68..6f    hijklmno */
-    0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, /* 70..77    pqrstuvw */
-    0x22, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, /* 78..7f    xyz{|}~. */
-
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* 80..87    ........ */
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* 88..8f    ........ */
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* 90..97    ........ */
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* 98..9f    ........ */
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* a0..a7    ........ */
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* a8..af    ........ */
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* b0..b7    ........ */
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* b8..bf    ........ */
-
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* c0..c7    ........ */
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* c8..cf    ........ */
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* d0..d7    ........ */
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* d8..df    ........ */
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* e0..e7    ........ */
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* e8..ef    ........ */
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* f0..f7    ........ */
-    0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40  /* f8..ff    ........ */
-};
-
-#define CC_X 0        /* The letter 'x', or start of BLOB literal */
-#define CC_KYWD0 1    /* First letter of a keyword */
-#define CC_KYWD 2     /* Alphabetics or '_'.  Usable in a keyword */
-#define CC_DIGIT 3    /* Digits */
-#define CC_DOLLAR 4   /* '$' */
-#define CC_VARALPHA 5 /* '@', '#', ':'.  Alphabetic SQL variables */
-#define CC_VARNUM 6   /* '?'.  Numeric SQL variables */
-#define CC_SPACE 7    /* Space characters */
-#define CC_QUOTE 8    /* '"', '\'', or '`'.  String literals, quoted ids */
-#define CC_QUOTE2 9   /* '['.   [...] style quoted ids */
-#define CC_PIPE 10    /* '|'.   Bitwise OR or concatenate */
-#define CC_MINUS 11   /* '-'.  Minus or SQL-style comment */
-#define CC_LT 12      /* '<'.  Part of < or <= or <> */
-#define CC_GT 13      /* '>'.  Part of > or >= */
-#define CC_EQ 14      /* '='.  Part of = or == */
-#define CC_BANG 15    /* '!'.  Part of != */
-#define CC_SLASH 16   /* '/'.  / or c-style comment */
-#define CC_LP 17      /* '(' */
-#define CC_RP 18      /* ')' */
-#define CC_SEMI 19    /* ';' */
-#define CC_PLUS 20    /* '+' */
-#define CC_STAR 21    /* '*' */
-#define CC_PERCENT 22 /* '%' */
-#define CC_COMMA 23   /* ',' */
-#define CC_AND 24     /* '&' */
-#define CC_TILDA 25   /* '~' */
-#define CC_DOT 26     /* '.' */
-#define CC_ID 27      /* unicode characters usable in IDs */
-#define CC_NUL 29     /* 0x00 */
-#define CC_BOM 30     /* First byte of UTF8 BOM:  0xEF 0xBB 0xBF */
-
-// clang-format off
-static const unsigned char aiClass[] = {
-/*         x0  x1  x2  x3  x4  x5  x6  x7  x8  x9  xa  xb  xc  xd  xe  xf */
-/* 0x */   29, 28, 28, 28, 28, 28, 28, 28, 28,  7,  7, 28,  7,  7, 28, 28,
-/* 1x */   28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
-/* 2x */    7, 15,  8,  5,  4, 22, 24,  8, 17, 18, 21, 20, 23, 11, 26, 16,
-/* 3x */    3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  5, 19, 12, 14, 13,  6,
-/* 4x */    5,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,
-/* 5x */    1,  1,  1,  1,  1,  1,  1,  1,  0,  2,  2,  9, 28, 28, 28,  2,
-/* 6x */    8,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,
-/* 7x */    1,  1,  1,  1,  1,  1,  1,  1,  0,  2,  2, 28, 10, 28, 25, 28,
-/* 8x */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
-/* 9x */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
-/* Ax */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
-/* Bx */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
-/* Cx */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
-/* Dx */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
-/* Ex */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 30,
-/* Fx */   27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27
-};
-// clang-format on
-
-#define IdChar(C) ((sqlite3CtypeMap[static_cast<unsigned char>(C)] & 0x46) != 0)
-
-// Copy of |sqlite3GetToken| for use by the PerfettoSql transpiler.
-//
-// We copy this function because |sqlite3GetToken| is static to sqlite3.c
-// in most distributions of SQLite so we cannot call it from our code.
-//
-// While we could redefine SQLITE_PRIVATE, pragmatically that will not fly in
-// all the places we build trace processor so we need to resort to making a
-// copy.
-int GetSqliteToken(const unsigned char* z, SqliteTokenType* tokenType) {
-  int i, c;
-  switch (aiClass[*z]) { /* Switch on the character-class of the first byte
-                         ** of the token. See the comment on the CC_ defines
-                         ** above. */
-    case CC_SPACE: {
-      for (i = 1; isspace(z[i]); i++) {
-      }
-      *tokenType = SqliteTokenType::TK_SPACE;
-      return i;
-    }
-    case CC_MINUS: {
-      if (z[1] == '-') {
-        for (i = 2; (c = z[i]) != 0 && c != '\n'; i++) {
-        }
-        *tokenType = SqliteTokenType::TK_SPACE; /* IMP: R-22934-25134 */
-        return i;
-      } else if (z[1] == '>') {
-        *tokenType = SqliteTokenType::TK_PTR;
-        return 2 + (z[2] == '>');
-      }
-      *tokenType = SqliteTokenType::TK_MINUS;
-      return 1;
-    }
-    case CC_LP: {
-      *tokenType = SqliteTokenType::TK_LP;
-      return 1;
-    }
-    case CC_RP: {
-      *tokenType = SqliteTokenType::TK_RP;
-      return 1;
-    }
-    case CC_SEMI: {
-      *tokenType = SqliteTokenType::TK_SEMI;
-      return 1;
-    }
-    case CC_PLUS: {
-      *tokenType = SqliteTokenType::TK_PLUS;
-      return 1;
-    }
-    case CC_STAR: {
-      *tokenType = SqliteTokenType::TK_STAR;
-      return 1;
-    }
-    case CC_SLASH: {
-      if (z[1] != '*' || z[2] == 0) {
-        *tokenType = SqliteTokenType::TK_SLASH;
-        return 1;
-      }
-      for (i = 3, c = z[2]; (c != '*' || z[i] != '/') && (c = z[i]) != 0; i++) {
-      }
-      if (c)
-        i++;
-      *tokenType = SqliteTokenType::TK_SPACE; /* IMP: R-22934-25134 */
-      return i;
-    }
-    case CC_PERCENT: {
-      *tokenType = SqliteTokenType::TK_REM;
-      return 1;
-    }
-    case CC_EQ: {
-      *tokenType = SqliteTokenType::TK_EQ;
-      return 1 + (z[1] == '=');
-    }
-    case CC_LT: {
-      if ((c = z[1]) == '=') {
-        *tokenType = SqliteTokenType::TK_LE;
-        return 2;
-      } else if (c == '>') {
-        *tokenType = SqliteTokenType::TK_NE;
-        return 2;
-      } else if (c == '<') {
-        *tokenType = SqliteTokenType::TK_LSHIFT;
-        return 2;
-      } else {
-        *tokenType = SqliteTokenType::TK_LT;
-        return 1;
-      }
-    }
-    case CC_GT: {
-      if ((c = z[1]) == '=') {
-        *tokenType = SqliteTokenType::TK_GE;
-        return 2;
-      } else if (c == '>') {
-        *tokenType = SqliteTokenType::TK_RSHIFT;
-        return 2;
-      } else {
-        *tokenType = SqliteTokenType::TK_GT;
-        return 1;
-      }
-    }
-    case CC_BANG: {
-      if (z[1] != '=') {
-        *tokenType = SqliteTokenType::TK_ILLEGAL;
-        return 1;
-      } else {
-        *tokenType = SqliteTokenType::TK_NE;
-        return 2;
-      }
-    }
-    case CC_PIPE: {
-      if (z[1] != '|') {
-        *tokenType = SqliteTokenType::TK_BITOR;
-        return 1;
-      } else {
-        *tokenType = SqliteTokenType::TK_CONCAT;
-        return 2;
-      }
-    }
-    case CC_COMMA: {
-      *tokenType = SqliteTokenType::TK_COMMA;
-      return 1;
-    }
-    case CC_AND: {
-      *tokenType = SqliteTokenType::TK_BITAND;
-      return 1;
-    }
-    case CC_TILDA: {
-      *tokenType = SqliteTokenType::TK_BITNOT;
-      return 1;
-    }
-    case CC_QUOTE: {
-      int delim = z[0];
-      for (i = 1; (c = z[i]) != 0; i++) {
-        if (c == delim) {
-          if (z[i + 1] == delim) {
-            i++;
-          } else {
-            break;
-          }
-        }
-      }
-      if (c == '\'') {
-        *tokenType = SqliteTokenType::TK_STRING;
-        return i + 1;
-      } else if (c != 0) {
-        *tokenType = SqliteTokenType::TK_ID;
-        return i + 1;
-      } else {
-        *tokenType = SqliteTokenType::TK_ILLEGAL;
-        return i;
-      }
-    }
-    case CC_DOT: {
-      if (!isdigit(z[1])) {
-        *tokenType = SqliteTokenType::TK_DOT;
-        return 1;
-      }
-      [[fallthrough]];
-    }
-    case CC_DIGIT: {
-      *tokenType = SqliteTokenType::TK_INTEGER;
-      if (z[0] == '0' && (z[1] == 'x' || z[1] == 'X') && isxdigit(z[2])) {
-        for (i = 3; isxdigit(z[i]); i++) {
-        }
-        return i;
-      }
-      for (i = 0; isxdigit(z[i]); i++) {
-      }
-      if (z[i] == '.') {
-        i++;
-        while (isxdigit(z[i])) {
-          i++;
-        }
-        *tokenType = SqliteTokenType::TK_FLOAT;
-      }
-      if ((z[i] == 'e' || z[i] == 'E') &&
-          (isdigit(z[i + 1]) ||
-           ((z[i + 1] == '+' || z[i + 1] == '-') && isdigit(z[i + 2])))) {
-        i += 2;
-        while (isdigit(z[i])) {
-          i++;
-        }
-        *tokenType = SqliteTokenType::TK_FLOAT;
-      }
-      while (IdChar(z[i])) {
-        *tokenType = SqliteTokenType::TK_ILLEGAL;
-        i++;
-      }
-      return i;
-    }
-    case CC_QUOTE2: {
-      for (i = 1, c = z[0]; c != ']' && (c = z[i]) != 0; i++) {
-      }
-      *tokenType =
-          c == ']' ? SqliteTokenType::TK_ID : SqliteTokenType::TK_ILLEGAL;
-      return i;
-    }
-    case CC_VARNUM: {
-      *tokenType = SqliteTokenType::TK_VARIABLE;
-      for (i = 1; isdigit(z[i]); i++) {
-      }
-      return i;
-    }
-    case CC_DOLLAR:
-    case CC_VARALPHA: {
-      int n = 0;
-      *tokenType = SqliteTokenType::TK_VARIABLE;
-      for (i = 1; (c = z[i]) != 0; i++) {
-        if (IdChar(c)) {
-          n++;
-        } else if (c == '(' && n > 0) {
-          do {
-            i++;
-          } while ((c = z[i]) != 0 && !isspace(c) && c != ')');
-          if (c == ')') {
-            i++;
-          } else {
-            *tokenType = SqliteTokenType::TK_ILLEGAL;
-          }
-          break;
-        } else if (c == ':' && z[i + 1] == ':') {
-          i++;
-        } else {
-          break;
-        }
-      }
-      if (n == 0)
-        *tokenType = SqliteTokenType::TK_ILLEGAL;
-      return i;
-    }
-    case CC_KYWD0: {
-      for (i = 1; aiClass[z[i]] <= CC_KYWD; i++) {
-      }
-      if (IdChar(z[i])) {
-        /* This token started out using characters that can appear in keywords,
-        ** but z[i] is a character not allowed within keywords, so this must
-        ** be an identifier instead */
-        i++;
-        break;
-      }
-      if (sqlite3_keyword_check(reinterpret_cast<const char*>(z), i)) {
-        *tokenType = SqliteTokenType::TK_GENERIC_KEYWORD;
-      } else {
-        *tokenType = SqliteTokenType::TK_ID;
-      }
-      return i;
-    }
-    case CC_X: {
-      if (z[1] == '\'') {
-        *tokenType = SqliteTokenType::TK_BLOB;
-        for (i = 2; isdigit(z[i]); i++) {
-        }
-        if (z[i] != '\'' || i % 2) {
-          *tokenType = SqliteTokenType::TK_ILLEGAL;
-          while (z[i] && z[i] != '\'') {
-            i++;
-          }
-        }
-        if (z[i])
-          i++;
-        return i;
-      }
-      [[fallthrough]];
-    }
-    case CC_KYWD:
-    case CC_ID: {
-      i = 1;
-      break;
-    }
-    case CC_BOM: {
-      if (z[1] == 0xbb && z[2] == 0xbf) {
-        *tokenType = SqliteTokenType::TK_SPACE;
-        return 3;
-      }
-      i = 1;
-      break;
-    }
-    case CC_NUL: {
-      *tokenType = SqliteTokenType::TK_ILLEGAL;
-      return 0;
-    }
-    default: {
-      *tokenType = SqliteTokenType::TK_ILLEGAL;
-      return 1;
-    }
-  }
-  while (IdChar(z[i])) {
-    i++;
-  }
-  *tokenType = SqliteTokenType::TK_ID;
-  return i;
-}
-
-}  // namespace
-
-SqliteTokenizer::SqliteTokenizer(SqlSource sql) : source_(std::move(sql)) {}
-
-SqliteTokenizer::Token SqliteTokenizer::Next() {
-  Token token;
-  const char* start = source_.sql().data() + offset_;
-  int n = GetSqliteToken(reinterpret_cast<const unsigned char*>(start),
-                         &token.token_type);
-  offset_ += static_cast<uint32_t>(n);
-  token.str = std::string_view(start, static_cast<uint32_t>(n));
-  return token;
-}
-
-SqliteTokenizer::Token SqliteTokenizer::NextNonWhitespace() {
-  Token t;
-  for (t = Next(); t.token_type == SqliteTokenType::TK_SPACE; t = Next()) {
-  }
-  return t;
-}
-
-SqliteTokenizer::Token SqliteTokenizer::NextTerminal() {
-  Token tok = Next();
-  while (!tok.IsTerminal()) {
-    tok = Next();
-  }
-  return tok;
-}
-
-SqlSource SqliteTokenizer::Substr(const Token& start, const Token& end) const {
-  uint32_t offset =
-      static_cast<uint32_t>(start.str.data() - source_.sql().c_str());
-  uint32_t len = static_cast<uint32_t>(end.str.data() - start.str.data());
-  return source_.Substr(offset, len);
-}
-
-SqlSource SqliteTokenizer::SubstrToken(const Token& token) const {
-  uint32_t offset =
-      static_cast<uint32_t>(token.str.data() - source_.sql().c_str());
-  uint32_t len = static_cast<uint32_t>(token.str.size());
-  return source_.Substr(offset, len);
-}
-
-std::string SqliteTokenizer::AsTraceback(const Token& token) const {
-  PERFETTO_CHECK(source_.sql().c_str() <= token.str.data());
-  PERFETTO_CHECK(token.str.data() <=
-                 source_.sql().c_str() + source_.sql().size());
-  uint32_t offset =
-      static_cast<uint32_t>(token.str.data() - source_.sql().c_str());
-  return source_.AsTraceback(offset);
-}
-
-void SqliteTokenizer::Rewrite(SqlSource::Rewriter& rewriter,
-                              const Token& start,
-                              const Token& end,
-                              SqlSource rewrite,
-                              EndToken end_token) const {
-  uint32_t s_off =
-      static_cast<uint32_t>(start.str.data() - source_.sql().c_str());
-  uint32_t e_off =
-      static_cast<uint32_t>(end.str.data() - source_.sql().c_str());
-  uint32_t e_diff = end_token == EndToken::kInclusive
-                        ? static_cast<uint32_t>(end.str.size())
-                        : 0;
-  rewriter.Rewrite(s_off, e_off + e_diff, std::move(rewrite));
-}
-
-void SqliteTokenizer::RewriteToken(SqlSource::Rewriter& rewriter,
-                                   const Token& token,
-                                   SqlSource rewrite) const {
-  uint32_t s_off =
-      static_cast<uint32_t>(token.str.data() - source_.sql().c_str());
-  uint32_t e_off = static_cast<uint32_t>(token.str.data() + token.str.size() -
-                                         source_.sql().c_str());
-  rewriter.Rewrite(s_off, e_off, std::move(rewrite));
-}
-
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/sqlite/sqlite_tokenizer.h b/src/trace_processor/sqlite/sqlite_tokenizer.h
deleted file mode 100644
index f4e9f79..0000000
--- a/src/trace_processor/sqlite/sqlite_tokenizer.h
+++ /dev/null
@@ -1,166 +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.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_SQLITE_SQLITE_TOKENIZER_H_
-#define SRC_TRACE_PROCESSOR_SQLITE_SQLITE_TOKENIZER_H_
-
-#include <optional>
-#include <string_view>
-#include "src/trace_processor/sqlite/sql_source.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-// List of token types returnable by |SqliteTokenizer|
-// 1:1 matches the defintions in SQLite.
-enum class SqliteTokenType : uint32_t {
-  TK_SEMI = 1,
-  TK_LP = 22,
-  TK_RP = 23,
-  TK_COMMA = 25,
-  TK_NE = 52,
-  TK_EQ = 53,
-  TK_GT = 54,
-  TK_LE = 55,
-  TK_LT = 56,
-  TK_GE = 57,
-  TK_ID = 59,
-  TK_BITAND = 102,
-  TK_BITOR = 103,
-  TK_LSHIFT = 104,
-  TK_RSHIFT = 105,
-  TK_PLUS = 106,
-  TK_MINUS = 107,
-  TK_STAR = 108,
-  TK_SLASH = 109,
-  TK_REM = 110,
-  TK_CONCAT = 111,
-  TK_PTR = 112,
-  TK_BITNOT = 114,
-  TK_STRING = 117,
-  TK_DOT = 141,
-  TK_FLOAT = 153,
-  TK_BLOB = 154,
-  TK_INTEGER = 155,
-  TK_VARIABLE = 156,
-  TK_SPACE = 183,
-  TK_ILLEGAL = 184,
-
-  // Generic constant which replaces all the keywords in SQLite as we do not
-  // care about the distinguishing between the vast majority of them.
-  TK_GENERIC_KEYWORD = 1000,
-};
-
-// Tokenizes SQL statements according to SQLite SQL language specification:
-// https://www2.sqlite.org/hlr40000.html
-//
-// Usage of this class:
-// SqliteTokenizer tzr(std::move(my_sql_source));
-// for (auto t = tzr.Next(); t.token_type != TK_SEMI; t = tzr.Next()) {
-//   // Handle t here
-// }
-class SqliteTokenizer {
- public:
-  // A single SQL token according to the SQLite standard.
-  struct Token {
-    // The string contents of the token.
-    std::string_view str;
-
-    // The type of the token.
-    SqliteTokenType token_type = SqliteTokenType::TK_ILLEGAL;
-
-    bool operator==(const Token& o) const {
-      return str == o.str && token_type == o.token_type;
-    }
-
-    // Returns if the token is empty or semicolon.
-    bool IsTerminal() {
-      return token_type == SqliteTokenType::TK_SEMI || str.empty();
-    }
-  };
-
-  enum class EndToken {
-    kExclusive,
-    kInclusive,
-  };
-
-  // Creates a tokenizer which tokenizes |sql|.
-  explicit SqliteTokenizer(SqlSource sql);
-
-  // Returns the next SQL token.
-  Token Next();
-
-  // Returns the next SQL token which is not of type TK_SPACE.
-  Token NextNonWhitespace();
-
-  // Returns the next SQL token which is terminal.
-  Token NextTerminal();
-
-  // Returns an SqlSource containing all the tokens between |start| and |end|.
-  //
-  // Note: |start| and |end| must both have been previously returned by this
-  // tokenizer.
-  SqlSource Substr(const Token& start, const Token& end) const;
-
-  // Returns an SqlSource containing only the SQL backing |token|.
-  //
-  // Note: |token| must have been previously returned by this tokenizer.
-  SqlSource SubstrToken(const Token& token) const;
-
-  // Returns a traceback error message for the SqlSource backing this tokenizer
-  // pointing to |token|. See SqlSource::AsTraceback for more information about
-  // this method.
-  //
-  // Note: |token| must have been previously returned by this tokenizer.
-  std::string AsTraceback(const Token&) const;
-
-  // Replaces the SQL in |rewriter| between |start| and |end| with the contents
-  // of |rewrite|. If |end_token| == kInclusive, the end token is also included
-  // in the rewrite.
-  void Rewrite(SqlSource::Rewriter& rewriter,
-               const Token& start,
-               const Token& end,
-               SqlSource rewrite,
-               EndToken end_token = EndToken::kExclusive) const;
-
-  // Replaces the SQL in |rewriter| backing |token| with the contents of
-  // |rewrite|.
-  void RewriteToken(SqlSource::Rewriter&,
-                    const Token&,
-                    SqlSource rewrite) const;
-
-  // Resets this tokenizer to tokenize |source|. Any previous returned tokens
-  // are invalidated.
-  void Reset(SqlSource source) {
-    source_ = std::move(source);
-    offset_ = 0;
-  }
-
- private:
-  SqliteTokenizer(const SqliteTokenizer&) = delete;
-  SqliteTokenizer& operator=(const SqliteTokenizer&) = delete;
-
-  SqliteTokenizer(SqliteTokenizer&&) = delete;
-  SqliteTokenizer& operator=(SqliteTokenizer&&) = delete;
-
-  SqlSource source_;
-  uint32_t offset_ = 0;
-};
-
-}  // namespace trace_processor
-}  // namespace perfetto
-
-#endif  // SRC_TRACE_PROCESSOR_SQLITE_SQLITE_TOKENIZER_H_
diff --git a/src/trace_processor/sqlite/sqlite_tokenizer_unittest.cc b/src/trace_processor/sqlite/sqlite_tokenizer_unittest.cc
deleted file mode 100644
index e7aa4cf..0000000
--- a/src/trace_processor/sqlite/sqlite_tokenizer_unittest.cc
+++ /dev/null
@@ -1,82 +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.
- */
-
-#include "src/trace_processor/sqlite/sqlite_tokenizer.h"
-#include <vector>
-
-#include "perfetto/base/logging.h"
-#include "src/trace_processor/sqlite/sql_source.h"
-#include "test/gtest_and_gmock.h"
-
-namespace perfetto {
-namespace trace_processor {
-namespace {
-
-using Token = SqliteTokenizer::Token;
-using Type = SqliteTokenType;
-
-class SqliteTokenizerTest : public ::testing::Test {
- protected:
-  std::vector<SqliteTokenizer::Token> Tokenize(const char* ptr) {
-    tokenizer_.Reset(SqlSource::FromTraceProcessorImplementation(ptr));
-    std::vector<SqliteTokenizer::Token> tokens;
-    for (auto t = tokenizer_.Next(); !t.str.empty(); t = tokenizer_.Next()) {
-      tokens.push_back(t);
-    }
-    return tokens;
-  }
-
-  SqliteTokenizer tokenizer_{SqlSource::FromTraceProcessorImplementation("")};
-};
-
-TEST_F(SqliteTokenizerTest, EmptyString) {
-  ASSERT_THAT(Tokenize(""), testing::IsEmpty());
-}
-
-TEST_F(SqliteTokenizerTest, OnlySpace) {
-  ASSERT_THAT(Tokenize(" "), testing::ElementsAre(Token{" ", Type::TK_SPACE}));
-}
-
-TEST_F(SqliteTokenizerTest, SpaceColon) {
-  ASSERT_THAT(Tokenize(" ;"), testing::ElementsAre(Token{" ", Type::TK_SPACE},
-                                                   Token{";", Type::TK_SEMI}));
-}
-
-TEST_F(SqliteTokenizerTest, Select) {
-  ASSERT_THAT(
-      Tokenize("SELECT * FROM slice;"),
-      testing::ElementsAre(
-          Token{"SELECT", Type::TK_GENERIC_KEYWORD}, Token{" ", Type::TK_SPACE},
-          Token{"*", Type::TK_STAR}, Token{" ", Type::TK_SPACE},
-          Token{"FROM", Type::TK_GENERIC_KEYWORD}, Token{" ", Type::TK_SPACE},
-          Token{"slice", Type::TK_ID}, Token{";", Type::TK_SEMI}));
-}
-
-TEST_F(SqliteTokenizerTest, PastEndErrorToken) {
-  tokenizer_.Reset(SqlSource::FromTraceProcessorImplementation("S"));
-  ASSERT_EQ(tokenizer_.Next(), (Token{"S", Type::TK_ID}));
-
-  auto end_token = tokenizer_.Next();
-  ASSERT_EQ(end_token, (Token{"", Type::TK_ILLEGAL}));
-  ASSERT_EQ(tokenizer_.AsTraceback(end_token),
-            "  Trace Processor Internal line 1 col 2\n"
-            "    S\n"
-            "     ^\n");
-}
-
-}  // namespace
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/storage/metadata.h b/src/trace_processor/storage/metadata.h
index 36c781b..a690f84 100644
--- a/src/trace_processor/storage/metadata.h
+++ b/src/trace_processor/storage/metadata.h
@@ -31,9 +31,13 @@
   F(all_data_source_flushed_ns,        KeyType::kMulti,   Variadic::kInt),    \
   F(all_data_source_started_ns,        KeyType::kSingle,  Variadic::kInt),    \
   F(android_build_fingerprint,         KeyType::kSingle,  Variadic::kString), \
+  F(android_device_manufacturer,       KeyType::kSingle,  Variadic::kString), \
   F(android_sdk_version,               KeyType::kSingle,  Variadic::kInt),    \
   F(android_soc_model,                 KeyType::kSingle,  Variadic::kString), \
+  F(android_guest_soc_model,           KeyType::kSingle,  Variadic::kString), \
   F(android_hardware_revision,         KeyType::kSingle,  Variadic::kString), \
+  F(android_storage_model,             KeyType::kSingle,  Variadic::kString), \
+  F(android_ram_model,                 KeyType::kSingle,  Variadic::kString), \
   F(benchmark_description,             KeyType::kSingle,  Variadic::kString), \
   F(benchmark_had_failures,            KeyType::kSingle,  Variadic::kInt),    \
   F(benchmark_label,                   KeyType::kSingle,  Variadic::kString), \
@@ -46,6 +50,7 @@
   F(ftrace_setup_errors,               KeyType::kMulti,   Variadic::kString), \
   F(ftrace_latest_data_start_ns,       KeyType::kSingle,  Variadic::kInt),    \
   F(range_of_interest_start_us,        KeyType::kSingle,  Variadic::kInt),    \
+  F(slow_start_data_source,            KeyType::kMulti,   Variadic::kString), \
   F(statsd_triggering_subscription_id, KeyType::kSingle,  Variadic::kInt),    \
   F(system_machine,                    KeyType::kSingle,  Variadic::kString), \
   F(system_name,                       KeyType::kSingle,  Variadic::kString), \
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h
index a19e5be..cb24633 100644
--- a/src/trace_processor/storage/stats.h
+++ b/src/trace_processor/storage/stats.h
@@ -17,11 +17,9 @@
 #ifndef SRC_TRACE_PROCESSOR_STORAGE_STATS_H_
 #define SRC_TRACE_PROCESSOR_STORAGE_STATS_H_
 
-#include <stddef.h>
+#include <cstddef>
 
-namespace perfetto {
-namespace trace_processor {
-namespace stats {
+namespace perfetto::trace_processor::stats {
 
 // Compile time list of parsing and processing stats.
 // clang-format off
@@ -69,6 +67,21 @@
        "unreliable. The kernel buffer overwrote events between our reads "     \
        "in userspace. Try re-recording the trace with a bigger buffer "        \
        "(ftrace_config.buffer_size_kb), or with fewer enabled ftrace events."),\
+  F(ftrace_kprobe_hits_begin,             kSingle,  kInfo,     kTrace,         \
+       "The number of kretprobe hits at the beginning of the trace."),         \
+  F(ftrace_kprobe_hits_end,               kSingle,  kInfo,     kTrace,         \
+       "The number of kretprobe hits at the end of the trace."),               \
+  F(ftrace_kprobe_hits_delta,             kSingle,  kInfo,     kTrace,         \
+       "The number of kprobe hits encountered during the collection of the"    \
+       "trace."),                                                              \
+  F(ftrace_kprobe_misses_begin,           kSingle,  kInfo,     kTrace,         \
+       "The number of kretprobe missed events at the beginning of the trace."),\
+  F(ftrace_kprobe_misses_end,             kSingle,  kInfo,     kTrace,         \
+       "The number of kretprobe missed events at the end of the trace."),      \
+  F(ftrace_kprobe_misses_delta,           kSingle,  kDataLoss, kTrace,         \
+       "The number of kretprobe missed events encountered during the "         \
+       "collection of the trace. A value greater than zero is due to the "     \
+       "maxactive parameter for the kretprobe being too small"),               \
   F(ftrace_setup_errors,                  kSingle,  kInfo,     kTrace,         \
        "One or more atrace/ftrace categories were not found or failed to "     \
        "enable. See ftrace_setup_errors in the metadata table for details."),  \
@@ -161,7 +174,17 @@
       "the RING_BUFFER start or after the DISCARD buffer end."),               \
   F(traced_buf_sequence_packet_loss,      kIndexed, kDataLoss, kAnalysis,      \
       "The number of groups of consecutive packets lost in each sequence for " \
-      "this buffer"), \
+      "this buffer"),                                                          \
+  F(traced_buf_incremental_sequences_dropped, kIndexed, kDataLoss, kAnalysis,  \
+      "For a given buffer, indicates the number of sequences where all the "   \
+      "packets on that sequence were dropped due to lack of a valid "          \
+      "incremental state (i.e. interned data). This is usually a strong sign " \
+      "that either: "                                                          \
+      "1) incremental state invalidation is disabled. "                        \
+      "2) the incremental state invalidation interval is too low. "            \
+      "In either case, see "                                                   \
+      "https://perfetto.dev/docs/concepts/buffers"                             \
+      "#incremental-state-in-trace-packets"),                                  \
   F(traced_buf_write_wrap_count,          kIndexed, kInfo,     kTrace,    ""), \
   F(traced_chunks_discarded,              kSingle,  kInfo,     kTrace,    ""), \
   F(traced_data_sources_registered,       kSingle,  kInfo,     kTrace,    ""), \
@@ -264,19 +287,57 @@
   F(compact_sched_waking_skipped,         kSingle,  kInfo,     kAnalysis, ""), \
   F(empty_chrome_metadata,                kSingle,  kError,    kTrace,    ""), \
   F(ninja_parse_errors,                   kSingle,  kError,    kTrace,    ""), \
-  F(perf_cpu_lost_records,                kIndexed, kDataLoss, kTrace,    ""), \
+  F(perf_cpu_lost_records,                kIndexed, kDataLoss, kTrace,         \
+      "Count of perf samples lost due to kernel buffer overruns. The trace "   \
+      "is missing information, but it's not known which processes are "        \
+      "affected. Consider lowering the sampling frequency or raising "         \
+      "the ring_buffer_pages config option."),                                 \
   F(perf_process_shard_count,             kIndexed, kInfo,     kTrace,    ""), \
   F(perf_chosen_process_shard,            kIndexed, kInfo,     kTrace,    ""), \
   F(perf_guardrail_stop_ts,               kIndexed, kDataLoss, kTrace,    ""), \
   F(perf_unknown_record_type,             kIndexed, kInfo,     kAnalysis, ""), \
-  F(perf_record_skipped,                  kSingle,  kError,    kAnalysis, ""), \
-  F(perf_samples_skipped,                 kSingle,  kError,    kAnalysis, ""), \
+  F(perf_record_skipped,                  kIndexed, kError,    kAnalysis, ""), \
+  F(perf_samples_skipped,                 kSingle,  kError,    kAnalysis,      \
+      "Count of skipped perf samples that otherwise matched the tracing "      \
+      "config. This will cause a process to be completely absent from the "    \
+      "trace, but does *not* imply data loss for processes that do have "      \
+      "samples in this trace."),                                               \
   F(perf_counter_skipped_because_no_cpu,  kSingle,  kError,    kAnalysis, ""), \
   F(perf_features_skipped,                kIndexed, kInfo,     kAnalysis, ""), \
   F(perf_samples_cpu_mode_unknown,        kSingle,  kError,    kAnalysis, ""), \
-  F(perf_samples_skipped_dataloss,        kSingle,  kDataLoss, kTrace,    ""), \
+  F(perf_samples_skipped_dataloss,        kSingle,  kDataLoss, kTrace,         \
+      "Count of perf samples lost within the profiler (traced_perf), likely "  \
+      "due to load shedding. This may impact any traced processes. The trace " \
+      "protobuf needs to be inspected manually to confirm which processes "    \
+      "are affected."),                                                        \
   F(perf_dummy_mapping_used,              kSingle,  kInfo,     kAnalysis, ""), \
-  F(perf_invalid_event_id,                kSingle,  kError,    kTrace,    ""), \
+  F(perf_aux_missing,                     kSingle,  kDataLoss, kTrace,         \
+      "Number of bytes missing in AUX data streams due to missing "            \
+      "PREF_RECORD_AUX messages."),                                            \
+  F(perf_aux_ignored,                     kSingle,  kInfo,     kTrace,         \
+       "AUX data was ignored because the proper parser is not implemented."), \
+  F(perf_aux_lost,                        kSingle,  kDataLoss, kTrace,         \
+      "Gaps in the AUX data stream pased to the tokenizer."), \
+  F(perf_aux_truncated,                   kSingle,  kDataLoss, kTrace,         \
+      "Data was truncated when being written to the AUX stream at the "        \
+      "source."),\
+  F(perf_aux_partial,                     kSingle,  kDataLoss, kTrace,         \
+      "The PERF_RECORD_AUX contained partial data."), \
+  F(perf_aux_collision,                   kSingle,  kDataLoss, kTrace,         \
+      "The collection of a sample colliden with another. You should reduce "   \
+      "the rate at which samples are collected."),                             \
+  F(perf_auxtrace_missing,                kSingle,  kDataLoss, kTrace,         \
+      "Number of bytes missing in AUX data streams due to missing "            \
+      "PREF_RECORD_AUXTRACE messages."),                                       \
+  F(perf_unknown_aux_data,                kIndexed, kDataLoss, kTrace,         \
+      "AUX data type encountered for which there is no known parser."),        \
+  F(perf_no_tsc_data,                     kSingle,  kInfo,     kTrace,         \
+      "TSC data unavailable. Will be unable to translate HW clocks."),         \
+  F(spe_no_timestamp,                     kSingle,  kInfo,     kTrace,         \
+      "SPE record with no timestamp. Will try our best to assign a "           \
+      "timestamp."),                                                           \
+  F(spe_record_droped,                    kSingle,  kDataLoss, kTrace,         \
+      "SPE record dropped. E.g. Unable to assign it a timestamp."),            \
   F(memory_snapshot_parser_failure,       kSingle,  kError,    kAnalysis, ""), \
   F(thread_time_in_state_out_of_order,    kSingle,  kError,    kAnalysis, ""), \
   F(thread_time_in_state_unknown_cpu_freq,                                     \
@@ -355,6 +416,12 @@
   F(winscope_protolog_missing_interned_stacktrace_parse_errors,                \
                                           kSingle,  kInfo,     kAnalysis,      \
       "Failed to find interned ProtoLog stacktrace."),                         \
+  F(winscope_protolog_message_decoding_failed,                                 \
+                                          kSingle,  kInfo,     kAnalysis,      \
+      "Failed to decode ProtoLog message."),                                   \
+  F(winscope_protolog_view_config_collision,                                   \
+                                          kSingle,  kInfo,     kAnalysis,      \
+      "Got a viewer config collision!"),                                       \
   F(winscope_viewcapture_parse_errors,                                         \
                                           kSingle,  kInfo,     kAnalysis,      \
       "ViewCapture packet has unknown fields, which results in some "          \
@@ -379,7 +446,24 @@
       "in some arguments missing. You may need a newer version of trace "      \
       "processor to parse them."),                                             \
   F(mali_unknown_mcu_state_id,            kSingle,  kError,   kAnalysis,       \
-      "An invalid Mali GPU MCU state ID was detected.")
+      "An invalid Mali GPU MCU state ID was detected."),                       \
+  F(pixel_modem_negative_timestamp,       kSingle,  kError,   kAnalysis,       \
+      "A negative timestamp was received from a Pixel modem event."),          \
+  F(legacy_v8_cpu_profile_invalid_callsite, kSingle,  kInfo,  kAnalysis,       \
+      "Indicates a callsite in legacy v8 CPU profiling is invalid."),          \
+  F(legacy_v8_cpu_profile_invalid_sample, kSingle,  kError,  kAnalysis,        \
+      "Indicates a sample in legacy v8 CPU profile is invalid. This will "     \
+      "cause CPU samples to be missing in the UI."),                           \
+  F(config_write_into_file_no_flush,      kSingle,  kError,  kTrace,           \
+      "The trace was collected with the `write_into_file` option set but "     \
+      "*without* `flush_period_ms` being set. This will cause the trace to "   \
+      "be fully loaded into memory and use significantly more memory than "    \
+      "necessary."),                                                           \
+  F(config_write_into_file_discard,        kIndexed,  kDataLoss,  kTrace,      \
+      "The trace was collected with the `write_into_file` option set but "     \
+      "uses a `DISCARD` buffer. This configuration is strongly discouraged "   \
+      "and can cause mysterious data loss in the trace. Please use "           \
+      "`RING_BUFFER` buffers instead.")
 // clang-format on
 
 enum Type {
@@ -433,8 +517,6 @@
 constexpr char const* kDescriptions[] = {
     PERFETTO_TP_STATS(PERFETTO_TP_STATS_DESCRIPTION)};
 
-}  // namespace stats
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor::stats
 
 #endif  // SRC_TRACE_PROCESSOR_STORAGE_STATS_H_
diff --git a/src/trace_processor/storage/trace_storage.h b/src/trace_processor/storage/trace_storage.h
index 649c28d..f590be0 100644
--- a/src/trace_processor/storage/trace_storage.h
+++ b/src/trace_processor/storage/trace_storage.h
@@ -36,7 +36,6 @@
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/trace_processor/basic_types.h"
-#include "perfetto/trace_processor/status.h"
 #include "src/trace_processor/containers/null_term_string_view.h"
 #include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/db/column/types.h"
@@ -48,6 +47,7 @@
 #include "src/trace_processor/tables/jit_tables_py.h"
 #include "src/trace_processor/tables/memory_tables_py.h"
 #include "src/trace_processor/tables/metadata_tables_py.h"
+#include "src/trace_processor/tables/perf_tables_py.h"
 #include "src/trace_processor/tables/profiler_tables_py.h"
 #include "src/trace_processor/tables/sched_tables_py.h"
 #include "src/trace_processor/tables/slice_tables_py.h"
@@ -57,8 +57,7 @@
 #include "src/trace_processor/tables/winscope_tables_py.h"
 #include "src/trace_processor/types/variadic.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 // UniquePid is an offset into |unique_processes_|. This is necessary because
 // Unix pids are reused and thus not guaranteed to be unique over a long
@@ -230,6 +229,12 @@
   virtual StringId InternString(base::StringView str) {
     return string_pool_.InternString(str);
   }
+  virtual StringId InternString(const char* str) {
+    return InternString(base::StringView(str));
+  }
+  virtual StringId InternString(const std::string& str) {
+    return InternString(base::StringView(str));
+  }
 
   // Example usage: SetStats(stats::android_log_num_failed, 42);
   void SetStats(size_t key, int64_t value) {
@@ -270,6 +275,12 @@
     return std::nullopt;
   }
 
+  int64_t GetStats(size_t key) {
+    PERFETTO_DCHECK(key < stats::kNumKeys);
+    PERFETTO_DCHECK(stats::kTypes[key] == stats::kSingle);
+    return stats_[key].value;
+  }
+
   class ScopedStatsTracer {
    public:
     ScopedStatsTracer(TraceStorage* storage, size_t key)
@@ -331,6 +342,8 @@
     sched_slice_table_.ShrinkToFit();
     thread_state_table_.ShrinkToFit();
     arg_table_.ShrinkToFit();
+    heap_graph_object_table_.ShrinkToFit();
+    heap_graph_reference_table_.ShrinkToFit();
   }
 
   const tables::ThreadTable& thread_table() const { return thread_table_; }
@@ -377,43 +390,6 @@
     return &gpu_counter_track_table_;
   }
 
-  const tables::EnergyCounterTrackTable& energy_counter_track_table() const {
-    return energy_counter_track_table_;
-  }
-  tables::EnergyCounterTrackTable* mutable_energy_counter_track_table() {
-    return &energy_counter_track_table_;
-  }
-
-  const tables::LinuxDeviceTrackTable& linux_device_track_table() const {
-    return linux_device_track_table_;
-  }
-  tables::LinuxDeviceTrackTable* mutable_linux_device_track_table() {
-    return &linux_device_track_table_;
-  }
-
-  const tables::UidCounterTrackTable& uid_counter_track_table() const {
-    return uid_counter_track_table_;
-  }
-  tables::UidCounterTrackTable* mutable_uid_counter_track_table() {
-    return &uid_counter_track_table_;
-  }
-
-  const tables::EnergyPerUidCounterTrackTable&
-  energy_per_uid_counter_track_table() const {
-    return energy_per_uid_counter_track_table_;
-  }
-  tables::EnergyPerUidCounterTrackTable*
-  mutable_energy_per_uid_counter_track_table() {
-    return &energy_per_uid_counter_track_table_;
-  }
-
-  const tables::IrqCounterTrackTable& irq_counter_track_table() const {
-    return irq_counter_track_table_;
-  }
-  tables::IrqCounterTrackTable* mutable_irq_counter_track_table() {
-    return &irq_counter_track_table_;
-  }
-
   const tables::PerfCounterTrackTable& perf_counter_track_table() const {
     return perf_counter_track_table_;
   }
@@ -456,13 +432,6 @@
     return &thread_counter_track_table_;
   }
 
-  const tables::SoftirqCounterTrackTable& softirq_counter_track_table() const {
-    return softirq_counter_track_table_;
-  }
-  tables::SoftirqCounterTrackTable* mutable_softirq_counter_track_table() {
-    return &softirq_counter_track_table_;
-  }
-
   const tables::SchedSliceTable& sched_slice_table() const {
     return sched_slice_table_;
   }
@@ -635,13 +604,6 @@
     return &trace_file_table_;
   }
 
-  const tables::StackSampleTable& stack_sample_table() const {
-    return stack_sample_table_;
-  }
-  tables::StackSampleTable* mutable_stack_sample_table() {
-    return &stack_sample_table_;
-  }
-
   const tables::CpuProfileStackSampleTable& cpu_profile_stack_sample_table()
       const {
     return cpu_profile_stack_sample_table_;
@@ -664,6 +626,13 @@
     return &perf_sample_table_;
   }
 
+  const tables::InstrumentsSampleTable& instruments_sample_table() const {
+    return instruments_sample_table_;
+  }
+  tables::InstrumentsSampleTable* mutable_instruments_sample_table() {
+    return &instruments_sample_table_;
+  }
+
   const tables::SymbolTable& symbol_table() const { return symbol_table_; }
 
   tables::SymbolTable* mutable_symbol_table() { return &symbol_table_; }
@@ -701,18 +670,6 @@
   }
   tables::GpuTrackTable* mutable_gpu_track_table() { return &gpu_track_table_; }
 
-  const tables::UidTrackTable& uid_track_table() const {
-    return uid_track_table_;
-  }
-  tables::UidTrackTable* mutable_uid_track_table() { return &uid_track_table_; }
-
-  const tables::GpuWorkPeriodTrackTable& gpu_work_period_track_table() const {
-    return gpu_work_period_track_table_;
-  }
-  tables::GpuWorkPeriodTrackTable* mutable_gpu_work_period_track_table() {
-    return &gpu_work_period_track_table_;
-  }
-
   const tables::VulkanMemoryAllocationsTable& vulkan_memory_allocations_table()
       const {
     return vulkan_memory_allocations_table_;
@@ -844,6 +801,13 @@
   }
   tables::JitFrameTable* mutable_jit_frame_table() { return &jit_frame_table_; }
 
+  const tables::SpeRecordTable& spe_record_table() const {
+    return spe_record_table_;
+  }
+  tables::SpeRecordTable* mutable_spe_record_table() {
+    return &spe_record_table_;
+  }
+
   const tables::InputMethodClientsTable& inputmethod_clients_table() const {
     return inputmethod_clients_table_;
   }
@@ -1063,13 +1027,8 @@
   tables::ThreadStateTable thread_state_table_{&string_pool_};
   tables::CpuTrackTable cpu_track_table_{&string_pool_, &track_table_};
   tables::GpuTrackTable gpu_track_table_{&string_pool_, &track_table_};
-  tables::UidTrackTable uid_track_table_{&string_pool_, &track_table_};
-  tables::GpuWorkPeriodTrackTable gpu_work_period_track_table_{
-      &string_pool_, &uid_track_table_};
   tables::ProcessTrackTable process_track_table_{&string_pool_, &track_table_};
   tables::ThreadTrackTable thread_track_table_{&string_pool_, &track_table_};
-  tables::LinuxDeviceTrackTable linux_device_track_table_{&string_pool_,
-                                                          &track_table_};
 
   // Track tables for counter events.
   tables::CounterTrackTable counter_track_table_{&string_pool_, &track_table_};
@@ -1079,18 +1038,8 @@
       &string_pool_, &counter_track_table_};
   tables::CpuCounterTrackTable cpu_counter_track_table_{&string_pool_,
                                                         &counter_track_table_};
-  tables::IrqCounterTrackTable irq_counter_track_table_{&string_pool_,
-                                                        &counter_track_table_};
-  tables::SoftirqCounterTrackTable softirq_counter_track_table_{
-      &string_pool_, &counter_track_table_};
   tables::GpuCounterTrackTable gpu_counter_track_table_{&string_pool_,
                                                         &counter_track_table_};
-  tables::EnergyCounterTrackTable energy_counter_track_table_{
-      &string_pool_, &counter_track_table_};
-  tables::UidCounterTrackTable uid_counter_track_table_{&string_pool_,
-                                                        &counter_track_table_};
-  tables::EnergyPerUidCounterTrackTable energy_per_uid_counter_track_table_{
-      &string_pool_, &uid_counter_track_table_};
   tables::GpuCounterGroupTable gpu_counter_group_table_{&string_pool_};
   tables::PerfCounterTrackTable perf_counter_track_table_{
       &string_pool_, &counter_track_table_};
@@ -1150,13 +1099,13 @@
   tables::StackProfileFrameTable stack_profile_frame_table_{&string_pool_};
   tables::StackProfileCallsiteTable stack_profile_callsite_table_{
       &string_pool_};
-  tables::StackSampleTable stack_sample_table_{&string_pool_};
   tables::HeapProfileAllocationTable heap_profile_allocation_table_{
       &string_pool_};
   tables::CpuProfileStackSampleTable cpu_profile_stack_sample_table_{
-      &string_pool_, &stack_sample_table_};
+      &string_pool_};
   tables::PerfSessionTable perf_session_table_{&string_pool_};
   tables::PerfSampleTable perf_sample_table_{&string_pool_};
+  tables::InstrumentsSampleTable instruments_sample_table_{&string_pool_};
   tables::PackageListTable package_list_table_{&string_pool_};
   tables::AndroidGameInterventionListTable
       android_game_intervention_list_table_{&string_pool_};
@@ -1207,6 +1156,9 @@
   tables::JitCodeTable jit_code_table_{&string_pool_};
   tables::JitFrameTable jit_frame_table_{&string_pool_};
 
+  // Perf tables
+  tables::SpeRecordTable spe_record_table_{&string_pool_};
+
   // Winscope tables
   tables::InputMethodClientsTable inputmethod_clients_table_{&string_pool_};
   tables::InputMethodManagerServiceTable inputmethod_manager_service_table_{
@@ -1238,8 +1190,7 @@
   std::array<StringId, Variadic::kMaxType + 1> variadic_type_ids_;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 template <>
 struct std::hash<::perfetto::trace_processor::BaseId> {
diff --git a/src/trace_processor/tables/BUILD.gn b/src/trace_processor/tables/BUILD.gn
index 72796e5..5b4a133 100644
--- a/src/trace_processor/tables/BUILD.gn
+++ b/src/trace_processor/tables/BUILD.gn
@@ -23,6 +23,7 @@
     "jit_tables.py",
     "memory_tables.py",
     "metadata_tables.py",
+    "perf_tables.py",
     "profiler_tables.py",
     "sched_tables.py",
     "slice_tables.py",
diff --git a/src/trace_processor/tables/android_tables.py b/src/trace_processor/tables/android_tables.py
index e086d3d..47fe3ea 100644
--- a/src/trace_processor/tables/android_tables.py
+++ b/src/trace_processor/tables/android_tables.py
@@ -24,6 +24,7 @@
 from python.generators.trace_processor_table.public import TableDoc
 from python.generators.trace_processor_table.public import CppTableId
 from python.generators.trace_processor_table.public import CppUint32
+from python.generators.trace_processor_table.public import WrappingSqlView
 
 from src.trace_processor.tables.metadata_tables import THREAD_TABLE
 
@@ -164,6 +165,8 @@
         C('event_id', CppUint32()),
         C('ts', CppInt64()),
         C('arg_set_id', CppUint32()),
+        C('base64_proto', CppString()),
+        C('base64_proto_id', CppOptional(CppUint32())),
     ],
     tabledoc=TableDoc(
         doc='Contains Android MotionEvents processed by the system',
@@ -180,6 +183,8 @@
                 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',
         }))
 
 ANDROID_KEY_EVENTS_TABLE = Table(
@@ -190,6 +195,8 @@
         C('event_id', CppUint32()),
         C('ts', CppInt64()),
         C('arg_set_id', CppUint32()),
+        C('base64_proto', CppString()),
+        C('base64_proto_id', CppOptional(CppUint32())),
     ],
     tabledoc=TableDoc(
         doc='Contains Android KeyEvents processed by the system',
@@ -206,6 +213,8 @@
                 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',
         }))
 
 ANDROID_INPUT_EVENT_DISPATCH_TABLE = Table(
@@ -217,10 +226,11 @@
         C('arg_set_id', CppUint32()),
         C('vsync_id', CppInt64()),
         C('window_id', CppInt32()),
+        C('base64_proto', CppString()),
+        C('base64_proto_id', CppOptional(CppUint32())),
     ],
     tabledoc=TableDoc(
-        doc=
-            '''
+        doc='''
                 Contains records of Android input events being dispatched to input windows
                 by the Android Framework.
             ''',
@@ -242,6 +252,8 @@
                 ''',
             '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',
         }))
 
 # Keep this list sorted.
diff --git a/src/trace_processor/tables/jit_tables.py b/src/trace_processor/tables/jit_tables.py
index 44707a6..6f0637f 100644
--- a/src/trace_processor/tables/jit_tables.py
+++ b/src/trace_processor/tables/jit_tables.py
@@ -27,6 +27,7 @@
 from python.generators.trace_processor_table.public import ColumnFlag
 from python.generators.trace_processor_table.public import Table
 from python.generators.trace_processor_table.public import TableDoc
+from python.generators.trace_processor_table.public import WrappingSqlView
 from .profiler_tables import STACK_PROFILE_FRAME_TABLE
 
 JIT_CODE_TABLE = Table(
diff --git a/src/trace_processor/tables/metadata_tables.py b/src/trace_processor/tables/metadata_tables.py
index b55e147..10da09b 100644
--- a/src/trace_processor/tables/metadata_tables.py
+++ b/src/trace_processor/tables/metadata_tables.py
@@ -201,7 +201,9 @@
         C('processor', CppString()),
         C('machine_id', CppOptional(CppTableId(MACHINE_TABLE))),
         C('capacity', CppOptional(CppUint32())),
+        C('arg_set_id', CppOptional(CppUint32())),
     ],
+    wrapping_sql_view=WrappingSqlView('cpu'),
     tabledoc=TableDoc(
         doc='''
           Contains information of processes seen during the trace
@@ -211,8 +213,7 @@
             'cpu':
                 '''the index (0-based) of the CPU core on the device''',
             'cluster_id':
-                '''the cluster id is shared by CPUs in
-the same cluster''',
+                '''the cluster id is shared by CPUs in the same cluster''',
             'processor':
                 '''a string describing this core''',
             'machine_id':
@@ -223,9 +224,11 @@
                 '''
                   Capacity of a CPU of a device, a metric which indicates the
                   relative performance of a CPU on a device
-                  For details see: 
+                  For details see:
                   https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/cpu-capacity.txt
                 ''',
+            'arg_set_id':
+                '''Extra args associated with the CPU''',
         }))
 
 RAW_TABLE = Table(
@@ -240,11 +243,14 @@
         C('common_flags', CppUint32()),
         C('ucpu', CppTableId(CPU_TABLE))
     ],
+    wrapping_sql_view=WrappingSqlView('track'),
     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.
         ''',
         group='Events',
         columns={
@@ -278,6 +284,7 @@
     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
@@ -398,6 +405,7 @@
         C('ucpu', CppTableId(CPU_TABLE)),
         C('freq', CppUint32()),
     ],
+    wrapping_sql_view=WrappingSqlView('cpu_freq'),
     tabledoc=TableDoc(
         doc='''''', group='Misc', columns={
             'ucpu': '''''',
@@ -452,7 +460,9 @@
         C('name', CppOptional(CppString())),
         C('size', CppInt64()),
         C('trace_type', CppString()),
+        C('processing_order', CppOptional(CppInt64())),
     ],
+    wrapping_sql_view=WrappingSqlView('trace_file'),
     tabledoc=TableDoc(
         doc='''
             Metadata related to the trace file parsed. Note the order in which
@@ -472,6 +482,8 @@
                 '''Size in bytes''',
             'trace_type':
                 '''Trace type''',
+            'processing_order':
+                '''In which order where the files were processed.''',
         }))
 
 # Keep this list sorted.
diff --git a/src/trace_processor/tables/perf_tables.py b/src/trace_processor/tables/perf_tables.py
new file mode 100644
index 0000000..60b10f7
--- /dev/null
+++ b/src/trace_processor/tables/perf_tables.py
@@ -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.
+"""
+Contains tables related to perf data ingestion.
+"""
+
+from python.generators.trace_processor_table.public import Column as C
+from python.generators.trace_processor_table.public import ColumnDoc
+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 CppOptional
+from python.generators.trace_processor_table.public import CppString
+from python.generators.trace_processor_table.public import CppTableId
+from python.generators.trace_processor_table.public import CppUint32
+from python.generators.trace_processor_table.public import Table
+from python.generators.trace_processor_table.public import TableDoc
+from .profiler_tables import STACK_PROFILE_FRAME_TABLE
+from .metadata_tables import THREAD_TABLE
+
+SPE_RECORD_TABLE = Table(
+    python_module=__file__,
+    class_name='SpeRecordTable',
+    sql_name='__intrinsic_spe_record',
+    columns=[
+        C('ts', CppInt64(), ColumnFlag.SORTED),
+        C('utid', CppOptional(CppTableId(THREAD_TABLE))),
+        C('exception_level', CppString()),
+        C('instruction_frame_id',
+          CppOptional(CppTableId(STACK_PROFILE_FRAME_TABLE))),
+        C('operation', CppString()),
+        C('data_virtual_address', CppInt64()),
+        C('data_physical_address', CppInt64()),
+        C('total_latency', CppUint32()),
+        C('issue_latency', CppUint32()),
+        C('translation_latency', CppUint32()),
+        C('events_bitmask', CppInt64()),
+        C('data_source', CppString()),
+    ],
+    tabledoc=TableDoc(
+        doc='''
+          This table has a row for each sampled operation in an ARM Statistical
+          Profiling Extension trace.
+        ''',
+        group='Perf',
+        columns={
+            'ts':
+                'Time the operation was sampled',
+            'utid':
+                'EXecuting thread',
+            'exception_level':
+                'Exception level the operation executed in',
+            'instruction_frame_id':
+                ColumnDoc(
+                    'Instruction virtual address',
+                    joinable='stack_profile_frame.id'),
+            'operation':
+                'Operation executed',
+            'data_virtual_address':
+                'Virtual address of accesses data (if any)',
+            'data_physical_address':
+                '''
+                  Physical address of accesses data (if any)
+                ''',
+            'total_latency':
+                '''
+                  Cycle count from the operation being dispatched for issue to
+                  the operation being complete.
+                ''',
+            'issue_latency':
+                '''
+                  Cycle count from the operation being dispatched for issue to
+                  the operation being issued for execution.
+                ''',
+            'translation_latency':
+                '''
+                  Cycle count from a virtual address being passed to the MMU for
+                  translation to the result of the translation being available.
+                ''',
+            'events_bitmask':
+                'Events generated by the operation',
+            'data_source':
+                '''
+                  Where the data returned for a load operation was sourced
+                ''',
+        },
+    ),
+)
+
+# Keep this list sorted.
+ALL_TABLES = [SPE_RECORD_TABLE]
diff --git a/src/trace_processor/tables/profiler_tables.py b/src/trace_processor/tables/profiler_tables.py
index 2c55640..eafd5a9 100644
--- a/src/trace_processor/tables/profiler_tables.py
+++ b/src/trace_processor/tables/profiler_tables.py
@@ -24,6 +24,7 @@
 from python.generators.trace_processor_table.public import TableDoc
 from python.generators.trace_processor_table.public import CppTableId
 from python.generators.trace_processor_table.public import CppUint32
+from python.generators.trace_processor_table.public import WrappingSqlView
 
 from src.trace_processor.tables.track_tables import TRACK_TABLE, COUNTER_TRACK_TABLE
 
@@ -209,39 +210,22 @@
                 '''Frame at this position in the callstack.'''
         }))
 
-STACK_SAMPLE_TABLE = Table(
-    python_module=__file__,
-    class_name='StackSampleTable',
-    sql_name='stack_sample',
-    columns=[
-        C('ts', CppInt64(), flags=ColumnFlag.SORTED),
-        C('callsite_id', CppTableId(STACK_PROFILE_CALLSITE_TABLE)),
-    ],
-    tabledoc=TableDoc(
-        doc='''
-          Root table for timestamped stack samples.
-        ''',
-        group='Callstack profilers',
-        columns={
-            'ts': '''timestamp of the sample.''',
-            'callsite_id': '''unwound callstack.'''
-        }))
-
 CPU_PROFILE_STACK_SAMPLE_TABLE = Table(
     python_module=__file__,
     class_name='CpuProfileStackSampleTable',
     sql_name='cpu_profile_stack_sample',
     columns=[
+        C('ts', CppInt64(), flags=ColumnFlag.SORTED),
+        C('callsite_id', CppTableId(STACK_PROFILE_CALLSITE_TABLE)),
         C('utid', CppUint32()),
         C('process_priority', CppInt32()),
     ],
-    parent=STACK_SAMPLE_TABLE,
     tabledoc=TableDoc(
-        doc='''
-          Samples from the Chromium stack sampler.
-        ''',
+        doc='Table containing stack samples from CPU profiling.',
         group='Callstack profilers',
         columns={
+            'ts': '''timestamp of the sample.''',
+            'callsite_id': '''unwound callstack.''',
             'utid': '''thread that was active when the sample was taken.''',
             'process_priority': ''''''
         }))
@@ -253,10 +237,9 @@
     columns=[
         C('cmdline', CppOptional(CppString())),
     ],
+    wrapping_sql_view=WrappingSqlView('perf_session'),
     tabledoc=TableDoc(
-        doc='''
-          Perf sessions.
-        ''',
+        doc='''Perf sessions.''',
         group='Callstack profilers',
         columns={
             'cmdline': '''Command line used to collect the data.''',
@@ -276,9 +259,7 @@
         C('perf_session_id', CppTableId(PERF_SESSION_TABLE)),
     ],
     tabledoc=TableDoc(
-        doc='''
-          Samples from the traced_perf profiler.
-        ''',
+        doc='''Samples from the traced_perf profiler.''',
         group='Callstack profilers',
         columns={
             'ts':
@@ -302,6 +283,32 @@
                 streams (i.e. multiple data sources).'''
         }))
 
+INSTRUMENTS_SAMPLE_TABLE = Table(
+    python_module=__file__,
+    class_name='InstrumentsSampleTable',
+    sql_name='instruments_sample',
+    columns=[
+        C('ts', CppInt64(), flags=ColumnFlag.SORTED),
+        C('utid', CppUint32()),
+        C('cpu', CppOptional(CppUint32())),
+        C('callsite_id', CppOptional(CppTableId(STACK_PROFILE_CALLSITE_TABLE))),
+    ],
+    tabledoc=TableDoc(
+        doc='''
+          Samples from MacOS Instruments.
+        ''',
+        group='Callstack profilers',
+        columns={
+            'ts':
+                '''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.''',
+        }))
+
 SYMBOL_TABLE = Table(
     python_module=__file__,
     class_name='SymbolTable',
@@ -659,6 +666,7 @@
     HEAP_GRAPH_CLASS_TABLE,
     HEAP_GRAPH_OBJECT_TABLE,
     HEAP_GRAPH_REFERENCE_TABLE,
+    INSTRUMENTS_SAMPLE_TABLE,
     HEAP_PROFILE_ALLOCATION_TABLE,
     PACKAGE_LIST_TABLE,
     PERF_SAMPLE_TABLE,
@@ -667,7 +675,6 @@
     STACK_PROFILE_CALLSITE_TABLE,
     STACK_PROFILE_FRAME_TABLE,
     STACK_PROFILE_MAPPING_TABLE,
-    STACK_SAMPLE_TABLE,
     SYMBOL_TABLE,
     VULKAN_MEMORY_ALLOCATIONS_TABLE,
     PERF_COUNTER_TRACK_TABLE,
diff --git a/src/trace_processor/tables/sched_tables.py b/src/trace_processor/tables/sched_tables.py
index 3539543..b16d014 100644
--- a/src/trace_processor/tables/sched_tables.py
+++ b/src/trace_processor/tables/sched_tables.py
@@ -41,6 +41,7 @@
         C('priority', CppInt32()),
         C('ucpu', CppTableId(CPU_TABLE)),
     ],
+    wrapping_sql_view=WrappingSqlView('sched'),
     tabledoc=TableDoc(
         doc='''
           This table holds slices with kernel thread scheduling information.
@@ -133,6 +134,7 @@
         C('irq_context', CppOptional(CppUint32())),
         C('ucpu', CppOptional(CppTableId(CPU_TABLE))),
     ],
+    wrapping_sql_view=WrappingSqlView('thread_state'),
     tabledoc=TableDoc(
         doc='''
           This table contains the scheduling state of every thread on the
diff --git a/src/trace_processor/tables/slice_tables.py b/src/trace_processor/tables/slice_tables.py
index dd394a3..99f64c8 100644
--- a/src/trace_processor/tables/slice_tables.py
+++ b/src/trace_processor/tables/slice_tables.py
@@ -352,25 +352,37 @@
         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)',
+            '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_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':
diff --git a/src/trace_processor/tables/table_destructors.cc b/src/trace_processor/tables/table_destructors.cc
index 229c724..b01364ae 100644
--- a/src/trace_processor/tables/table_destructors.cc
+++ b/src/trace_processor/tables/table_destructors.cc
@@ -20,6 +20,7 @@
 #include "src/trace_processor/tables/jit_tables_py.h"
 #include "src/trace_processor/tables/memory_tables_py.h"
 #include "src/trace_processor/tables/metadata_tables_py.h"
+#include "src/trace_processor/tables/perf_tables_py.h"
 #include "src/trace_processor/tables/profiler_tables_py.h"
 #include "src/trace_processor/tables/sched_tables_py.h"
 #include "src/trace_processor/tables/slice_tables_py.h"
@@ -66,14 +67,17 @@
 MachineTable::~MachineTable() = default;
 TraceFileTable::~TraceFileTable() = default;
 
+// perf_tables.py
+SpeRecordTable::~SpeRecordTable() = default;
+
 // profiler_tables_py.h
 StackProfileMappingTable::~StackProfileMappingTable() = default;
 StackProfileFrameTable::~StackProfileFrameTable() = default;
 StackProfileCallsiteTable::~StackProfileCallsiteTable() = default;
-StackSampleTable::~StackSampleTable() = default;
 CpuProfileStackSampleTable::~CpuProfileStackSampleTable() = default;
 PerfSessionTable::~PerfSessionTable() = default;
 PerfSampleTable::~PerfSampleTable() = default;
+InstrumentsSampleTable::~InstrumentsSampleTable() = default;
 SymbolTable::~SymbolTable() = default;
 HeapProfileAllocationTable::~HeapProfileAllocationTable() = default;
 ExperimentalFlamegraphTable::~ExperimentalFlamegraphTable() = default;
@@ -106,20 +110,12 @@
 ThreadTrackTable::~ThreadTrackTable() = default;
 CpuTrackTable::~CpuTrackTable() = default;
 GpuTrackTable::~GpuTrackTable() = default;
-UidTrackTable::~UidTrackTable() = default;
-GpuWorkPeriodTrackTable::~GpuWorkPeriodTrackTable() = default;
 CounterTrackTable::~CounterTrackTable() = default;
 ThreadCounterTrackTable::~ThreadCounterTrackTable() = default;
 ProcessCounterTrackTable::~ProcessCounterTrackTable() = default;
 CpuCounterTrackTable::~CpuCounterTrackTable() = default;
-IrqCounterTrackTable::~IrqCounterTrackTable() = default;
-SoftirqCounterTrackTable::~SoftirqCounterTrackTable() = default;
 GpuCounterTrackTable::~GpuCounterTrackTable() = default;
 PerfCounterTrackTable::~PerfCounterTrackTable() = default;
-EnergyCounterTrackTable::~EnergyCounterTrackTable() = default;
-UidCounterTrackTable::~UidCounterTrackTable() = default;
-EnergyPerUidCounterTrackTable::~EnergyPerUidCounterTrackTable() = default;
-LinuxDeviceTrackTable::~LinuxDeviceTrackTable() = default;
 
 // trace_proto_tables_py.h
 ExperimentalProtoPathTable::~ExperimentalProtoPathTable() = default;
diff --git a/src/trace_processor/tables/track_tables.py b/src/trace_processor/tables/track_tables.py
index 3cf55a8..1ed6df8 100644
--- a/src/trace_processor/tables/track_tables.py
+++ b/src/trace_processor/tables/track_tables.py
@@ -14,29 +14,34 @@
 """Contains tables for tracks."""
 
 from python.generators.trace_processor_table.public import Column as C
+from python.generators.trace_processor_table.public import ColumnDoc
+from python.generators.trace_processor_table.public import ColumnFlag
 from python.generators.trace_processor_table.public import CppInt32
 from python.generators.trace_processor_table.public import CppInt64
 from python.generators.trace_processor_table.public import CppOptional
-from python.generators.trace_processor_table.public import CppString
-from python.generators.trace_processor_table.public import Table
-from python.generators.trace_processor_table.public import TableDoc
-from python.generators.trace_processor_table.public import ColumnDoc
 from python.generators.trace_processor_table.public import CppSelfTableId
+from python.generators.trace_processor_table.public import CppString
 from python.generators.trace_processor_table.public import CppTableId
 from python.generators.trace_processor_table.public import CppUint32
+from python.generators.trace_processor_table.public import Table
+from python.generators.trace_processor_table.public import TableDoc
+from python.generators.trace_processor_table.public import WrappingSqlView
 
 from src.trace_processor.tables.metadata_tables import CPU_TABLE, MACHINE_TABLE
 
 TRACK_TABLE = Table(
     python_module=__file__,
     class_name="TrackTable",
-    sql_name="track",
+    sql_name="__intrinsic_track",
     columns=[
         C("name", CppString()),
         C("parent_id", CppOptional(CppSelfTableId())),
         C("source_arg_set_id", CppOptional(CppUint32())),
         C('machine_id', CppOptional(CppTableId(MACHINE_TABLE))),
+        C("classification", CppString()),
+        C("dimension_arg_set_id", CppOptional(CppUint32())),
     ],
+    wrapping_sql_view=WrappingSqlView('track'),
     tabledoc=TableDoc(
         doc='''
           Tracks are a fundamental concept in trace processor and represent a
@@ -56,18 +61,35 @@
                   The track which is the "parent" of this track. Only non-null
                   for tracks created using Perfetto's track_event API.
                 ''',
-            'source_arg_set_id':
-                ColumnDoc(
-                    doc='''
-                      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.
-                    ''',
-                    joinable='args.arg_set_id'),
             'machine_id':
                 '''
                   Machine identifier, non-null for tracks on a remote machine.
                 ''',
+            'classification':
+                '''
+                  Classification of this track. Responsible for grouping
+                  similar tracks together.
+                ''',
+            'dimension_arg_set_id':
+                ColumnDoc(
+                    doc='''
+                      The dimensions of the track which uniquely identify the
+                      track within a given classification.
+
+                      Join with the `args` table or use the `EXTRACT_ARG` helper
+                      function to expand the args.
+                    ''',
+                    joinable='args.arg_set_id'),
+            'source_arg_set_id':
+                ColumnDoc(
+                    doc='''
+                      Generic key-value pairs containing extra information about
+                      the track.
+
+                      Join with the `args` table or use the `EXTRACT_ARG` helper
+                      function to expand the args.
+                    ''',
+                    joinable='args.arg_set_id'),
         }))
 
 PROCESS_TRACK_TABLE = Table(
@@ -118,6 +140,7 @@
     columns=[
         C('ucpu', CppTableId(CPU_TABLE)),
     ],
+    wrapping_sql_view=WrappingSqlView('cpu_track'),
     parent=TRACK_TABLE,
     tabledoc=TableDoc(
         doc='Tracks which are associated to a single CPU',
@@ -148,36 +171,6 @@
                 'The context id for the GPU this track is associated to.'
         }))
 
-UID_TRACK_TABLE = Table(
-    python_module=__file__,
-    class_name='UidTrackTable',
-    sql_name='uid_track',
-    columns=[
-        C('uid', CppInt32()),
-    ],
-    parent=TRACK_TABLE,
-    tabledoc=TableDoc(
-        doc='Tracks associated to a UID.',
-        group='Tracks',
-        columns={
-            'uid': 'The uid associated with this track.',
-        }))
-
-GPU_WORK_PERIOD_TRACK_TABLE = Table(
-    python_module=__file__,
-    class_name='GpuWorkPeriodTrackTable',
-    sql_name='gpu_work_period_track',
-    columns=[
-        C('gpu_id', CppUint32()),
-    ],
-    parent=UID_TRACK_TABLE,
-    tabledoc=TableDoc(
-        doc='Tracks containing gpu_work_period events.',
-        group='Tracks',
-        columns={
-            'gpu_id': 'The identifier for the GPU.',
-        }))
-
 COUNTER_TRACK_TABLE = Table(
     python_module=__file__,
     class_name='CounterTrackTable',
@@ -247,6 +240,7 @@
     columns=[
         C('ucpu', CppTableId(CPU_TABLE)),
     ],
+    wrapping_sql_view=WrappingSqlView('cpu_counter_track'),
     parent=COUNTER_TRACK_TABLE,
     tabledoc=TableDoc(
         doc='Tracks containing counter-like events associated to a CPU.',
@@ -255,32 +249,6 @@
             'ucpu': 'The unique CPU identifier associated with this track.'
         }))
 
-IRQ_COUNTER_TRACK_TABLE = Table(
-    python_module=__file__,
-    class_name='IrqCounterTrackTable',
-    sql_name='irq_counter_track',
-    columns=[
-        C('irq', CppInt32()),
-    ],
-    parent=COUNTER_TRACK_TABLE,
-    tabledoc=TableDoc(
-        doc='Tracks containing counter-like events associated to an hardirq',
-        group='Counter Tracks',
-        columns={'irq': 'The identifier for the hardirq.'}))
-
-SOFTIRQ_COUNTER_TRACK_TABLE = Table(
-    python_module=__file__,
-    class_name='SoftirqCounterTrackTable',
-    sql_name='softirq_counter_track',
-    columns=[
-        C('softirq', CppInt32()),
-    ],
-    parent=COUNTER_TRACK_TABLE,
-    tabledoc=TableDoc(
-        doc='Tracks containing counter-like events associated to a softirq',
-        group='Counter Tracks',
-        columns={'softirq': 'The identifier for the softirq.'}))
-
 GPU_COUNTER_TRACK_TABLE = Table(
     python_module=__file__,
     class_name='GpuCounterTrackTable',
@@ -295,90 +263,16 @@
         columns={'gpu_id': 'The identifier for the GPU.'}))
 
 
-ENERGY_COUNTER_TRACK_TABLE = Table(
-    python_module=__file__,
-    class_name='EnergyCounterTrackTable',
-    sql_name='energy_counter_track',
-    columns=[
-        C('consumer_id', CppInt32()),
-        C('consumer_type', CppString()),
-        C('ordinal', CppInt32()),
-    ],
-    parent=COUNTER_TRACK_TABLE,
-    tabledoc=TableDoc(
-        doc='''
-          Energy consumers' values for energy descriptors in
-          energy_estimation_breakdown packet
-        ''',
-        group='Counter Tracks',
-        columns={
-            'consumer_id': 'id of a distinct energy consumer',
-            'consumer_type': 'type of energy consumer',
-            'ordinal': 'ordinal of energy consumer'
-        }))
-
-LINUX_DEVICE_TRACK_TABLE = Table(
-    python_module=__file__,
-    class_name='LinuxDeviceTrackTable',
-    sql_name='linux_device_track',
-    columns=[],
-    parent=TRACK_TABLE,
-    tabledoc=TableDoc(
-        doc='''
-          Slice data corresponding to runtime power state transitions
-          associated with Linux devices (where a Linux device is anything
-          managed by a Linux driver). The name of each track corresponds to the
-          device name as recognized by the linux kernel running on the system.
-        ''',
-        group='Tracks',
-        # No additional columns are needed because the track name implicitly
-        # serves as the device name, providing all required information.
-        columns={}))
-
-UID_COUNTER_TRACK_TABLE = Table(
-    python_module=__file__,
-    class_name='UidCounterTrackTable',
-    sql_name='uid_counter_track',
-    columns=[
-        C('uid', CppInt32()),
-    ],
-    parent=COUNTER_TRACK_TABLE,
-    tabledoc=TableDoc(
-        doc='The uid associated with this track',
-        group='Counter Tracks',
-        columns={'uid': 'uid of process for which breakdowns are emitted'}))
-
-ENERGY_PER_UID_COUNTER_TRACK_TABLE = Table(
-    python_module=__file__,
-    class_name='EnergyPerUidCounterTrackTable',
-    sql_name='energy_per_uid_counter_track',
-    columns=[
-        C('consumer_id', CppInt32()),
-    ],
-    parent=UID_COUNTER_TRACK_TABLE,
-    tabledoc=TableDoc(
-        doc='Energy consumer values for per uid in uid_counter_track',
-        group='Counter Tracks',
-        columns={'consumer_id': 'id of the consumer process'}))
-
 # Keep this list sorted.
 ALL_TABLES = [
     COUNTER_TRACK_TABLE,
     CPU_COUNTER_TRACK_TABLE,
     CPU_TRACK_TABLE,
-    ENERGY_COUNTER_TRACK_TABLE,
-    ENERGY_PER_UID_COUNTER_TRACK_TABLE,
     GPU_COUNTER_TRACK_TABLE,
     GPU_TRACK_TABLE,
-    GPU_WORK_PERIOD_TRACK_TABLE,
-    IRQ_COUNTER_TRACK_TABLE,
-    LINUX_DEVICE_TRACK_TABLE,
     PROCESS_COUNTER_TRACK_TABLE,
     PROCESS_TRACK_TABLE,
-    SOFTIRQ_COUNTER_TRACK_TABLE,
     THREAD_COUNTER_TRACK_TABLE,
     THREAD_TRACK_TABLE,
     TRACK_TABLE,
-    UID_COUNTER_TRACK_TABLE,
-    UID_TRACK_TABLE,
 ]
diff --git a/src/trace_processor/tables/v8_tables.py b/src/trace_processor/tables/v8_tables.py
index 28ed330..25592f0 100644
--- a/src/trace_processor/tables/v8_tables.py
+++ b/src/trace_processor/tables/v8_tables.py
@@ -29,6 +29,8 @@
 from python.generators.trace_processor_table.public import CppUint32 as CppBool
 from python.generators.trace_processor_table.public import Table
 from python.generators.trace_processor_table.public import TableDoc
+from python.generators.trace_processor_table.public import WrappingSqlView
+
 from .jit_tables import JIT_CODE_TABLE
 
 V8_ISOLATE = Table(
diff --git a/src/trace_processor/tables/winscope_tables.py b/src/trace_processor/tables/winscope_tables.py
index 8887eb5..a218e6b 100644
--- a/src/trace_processor/tables/winscope_tables.py
+++ b/src/trace_processor/tables/winscope_tables.py
@@ -17,9 +17,11 @@
 from python.generators.trace_processor_table.public import ColumnFlag
 from python.generators.trace_processor_table.public import Table
 from python.generators.trace_processor_table.public import CppTableId
+from python.generators.trace_processor_table.public import CppOptional
 from python.generators.trace_processor_table.public import TableDoc
 from python.generators.trace_processor_table.public import CppUint32
 from python.generators.trace_processor_table.public import CppString
+from python.generators.trace_processor_table.public import WrappingSqlView
 
 INPUTMETHOD_CLIENTS_TABLE = Table(
     python_module=__file__,
@@ -28,6 +30,8 @@
     columns=[
         C('ts', CppInt64(), ColumnFlag.SORTED),
         C('arg_set_id', CppUint32()),
+        C('base64_proto', CppString()),
+        C('base64_proto_id', CppOptional(CppUint32())),
     ],
     tabledoc=TableDoc(
         doc='InputMethod clients',
@@ -35,6 +39,8 @@
         columns={
             'ts': 'The timestamp the dump was triggered',
             'arg_set_id': 'Extra args parsed from the proto message',
+            'base64_proto': 'Raw proto message encoded in base64',
+            'base64_proto_id': 'String id for raw proto message',
         }))
 
 INPUTMETHOD_MANAGER_SERVICE_TABLE = Table(
@@ -44,6 +50,8 @@
     columns=[
         C('ts', CppInt64(), ColumnFlag.SORTED),
         C('arg_set_id', CppUint32()),
+        C('base64_proto', CppString()),
+        C('base64_proto_id', CppOptional(CppUint32())),
     ],
     tabledoc=TableDoc(
         doc='InputMethod manager service',
@@ -51,6 +59,8 @@
         columns={
             'ts': 'The timestamp the dump was triggered',
             'arg_set_id': 'Extra args parsed from the proto message',
+            'base64_proto': 'Raw proto message encoded in base64',
+            'base64_proto_id': 'String id for raw proto message',
         }))
 
 INPUTMETHOD_SERVICE_TABLE = Table(
@@ -60,6 +70,8 @@
     columns=[
         C('ts', CppInt64(), ColumnFlag.SORTED),
         C('arg_set_id', CppUint32()),
+        C('base64_proto', CppString()),
+        C('base64_proto_id', CppOptional(CppUint32())),
     ],
     tabledoc=TableDoc(
         doc='InputMethod service',
@@ -67,6 +79,8 @@
         columns={
             'ts': 'The timestamp the dump was triggered',
             'arg_set_id': 'Extra args parsed from the proto message',
+            'base64_proto': 'Raw proto message encoded in base64',
+            'base64_proto_id': 'String id for raw proto message',
         }))
 
 SURFACE_FLINGER_LAYERS_SNAPSHOT_TABLE = Table(
@@ -76,6 +90,8 @@
     columns=[
         C('ts', CppInt64(), ColumnFlag.SORTED),
         C('arg_set_id', CppUint32()),
+        C('base64_proto', CppString()),
+        C('base64_proto_id', CppOptional(CppUint32())),
     ],
     tabledoc=TableDoc(
         doc='SurfaceFlinger layers snapshot',
@@ -83,6 +99,8 @@
         columns={
             'ts': 'Timestamp of the snapshot',
             'arg_set_id': 'Extra args parsed from the proto message',
+            'base64_proto': 'Raw proto message encoded in base64',
+            'base64_proto_id': 'String id for raw proto message',
         }))
 
 SURFACE_FLINGER_LAYER_TABLE = Table(
@@ -92,6 +110,8 @@
     columns=[
         C('snapshot_id', CppTableId(SURFACE_FLINGER_LAYERS_SNAPSHOT_TABLE)),
         C('arg_set_id', CppUint32()),
+        C('base64_proto', CppString()),
+        C('base64_proto_id', CppOptional(CppUint32())),
     ],
     tabledoc=TableDoc(
         doc='SurfaceFlinger layer',
@@ -99,6 +119,8 @@
         columns={
             'snapshot_id': 'The snapshot that generated this layer',
             'arg_set_id': 'Extra args parsed from the proto message',
+            'base64_proto': 'Raw proto message encoded in base64',
+            'base64_proto_id': 'String id for raw proto message',
         }))
 
 SURFACE_FLINGER_TRANSACTIONS_TABLE = Table(
@@ -108,6 +130,8 @@
     columns=[
         C('ts', CppInt64(), ColumnFlag.SORTED),
         C('arg_set_id', CppUint32()),
+        C('base64_proto', CppString()),
+        C('base64_proto_id', CppOptional(CppUint32())),
     ],
     tabledoc=TableDoc(
         doc='SurfaceFlinger transactions. Each row contains a set of ' +
@@ -116,6 +140,8 @@
         columns={
             'ts': 'Timestamp of the transactions commit',
             'arg_set_id': 'Extra args parsed from the proto message',
+            'base64_proto': 'Raw proto message encoded in base64',
+            'base64_proto_id': 'String id for raw proto message',
         }))
 
 VIEWCAPTURE_TABLE = Table(
@@ -125,6 +151,8 @@
     columns=[
         C('ts', CppInt64(), ColumnFlag.SORTED),
         C('arg_set_id', CppUint32()),
+        C('base64_proto', CppString()),
+        C('base64_proto_id', CppOptional(CppUint32())),
     ],
     tabledoc=TableDoc(
         doc='ViewCapture',
@@ -132,6 +160,8 @@
         columns={
             'ts': 'The timestamp the views were captured',
             'arg_set_id': 'Extra args parsed from the proto message',
+            'base64_proto': 'Raw proto message encoded in base64',
+            'base64_proto_id': 'String id for raw proto message',
         }))
 
 WINDOW_MANAGER_SHELL_TRANSITIONS_TABLE = Table(
@@ -142,6 +172,8 @@
         C('ts', CppInt64()),
         C('transition_id', CppInt64(), ColumnFlag.SORTED),
         C('arg_set_id', CppUint32()),
+        C('base64_proto', CppString()),
+        C('base64_proto_id', CppOptional(CppUint32())),
     ],
     tabledoc=TableDoc(
         doc='Window Manager Shell Transitions',
@@ -150,6 +182,8 @@
             'ts': 'The timestamp the transition started playing',
             'transition_id': 'The id of the transition',
             'arg_set_id': 'Extra args parsed from the proto message',
+            'base64_proto': 'Raw proto message encoded in base64',
+            'base64_proto_id': 'String id for raw proto message',
         }))
 
 WINDOW_MANAGER_SHELL_TRANSITION_HANDLERS_TABLE = Table(
@@ -159,6 +193,8 @@
     columns=[
         C('handler_id', CppInt64()),
         C('handler_name', CppString()),
+        C('base64_proto', CppString()),
+        C('base64_proto_id', CppOptional(CppUint32())),
     ],
     tabledoc=TableDoc(
         doc='Window Manager Shell Transition Handlers',
@@ -166,6 +202,8 @@
         columns={
             'handler_id': 'The id of the handler',
             'handler_name': 'The name of the handler',
+            'base64_proto': 'Raw proto message encoded in base64',
+            'base64_proto_id': 'String id for raw proto message',
         }))
 
 WINDOW_MANAGER_TABLE = Table(
@@ -175,13 +213,18 @@
     columns=[
         C('ts', CppInt64(), ColumnFlag.SORTED),
         C('arg_set_id', CppUint32()),
+        C('base64_proto', CppString()),
+        C('base64_proto_id', CppOptional(CppUint32())),
     ],
+    wrapping_sql_view=WrappingSqlView('windowmanager'),
     tabledoc=TableDoc(
         doc='WindowManager',
         group='Winscope',
         columns={
             'ts': 'The timestamp the state snapshot was captured',
             'arg_set_id': 'Extra args parsed from the proto message',
+            'base64_proto': 'Raw proto message encoded in base64',
+            'base64_proto_id': 'String id for raw proto message',
         }))
 
 PROTOLOG_TABLE = Table(
diff --git a/src/trace_processor/trace_database_integrationtest.cc b/src/trace_processor/trace_database_integrationtest.cc
index f4da05f..b9d4c47 100644
--- a/src/trace_processor/trace_database_integrationtest.cc
+++ b/src/trace_processor/trace_database_integrationtest.cc
@@ -124,8 +124,9 @@
       if (!status.ok())
         return status;
     }
-    return processor_->NotifyEndOfFile();
+    return NotifyEndOfFile();
   }
+  base::Status NotifyEndOfFile() { return processor_->NotifyEndOfFile(); }
 
   Iterator Query(const std::string& query) {
     return processor_->ExecuteQuery(query);
@@ -289,6 +290,8 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, ComputeMetricsFormattedExtension) {
+  ASSERT_OK(NotifyEndOfFile());
+
   std::string metric_output;
   base::Status status = Processor()->ComputeMetricText(
       std::vector<std::string>{"test_chrome_metric"},
@@ -302,11 +305,12 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, ComputeMetricsFormattedNoExtension) {
+  ASSERT_OK(NotifyEndOfFile());
+
   std::string metric_output;
-  base::Status status = Processor()->ComputeMetricText(
+  ASSERT_OK(Processor()->ComputeMetricText(
       std::vector<std::string>{"trace_metadata"},
-      TraceProcessor::MetricResultFormat::kProtoText, &metric_output);
-  ASSERT_TRUE(status.ok());
+      TraceProcessor::MetricResultFormat::kProtoText, &metric_output));
   // Check that metric result starts with trace_metadata field. Since this is
   // not an extension field, the field name is not fully qualified.
   ASSERT_TRUE(metric_output.rfind("trace_metadata {") == 0);
@@ -410,13 +414,13 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, RestoreInitialTablesInvariant) {
-  ASSERT_OK(Processor()->NotifyEndOfFile());
+  ASSERT_OK(NotifyEndOfFile());
   uint64_t first_restore = RestoreInitialTables();
   ASSERT_EQ(RestoreInitialTables(), first_restore);
 }
 
 TEST_F(TraceProcessorIntegrationTest, RestoreInitialTablesPerfettoSql) {
-  ASSERT_OK(Processor()->NotifyEndOfFile());
+  ASSERT_OK(NotifyEndOfFile());
   RestoreInitialTables();
 
   for (int repeat = 0; repeat < 3; repeat++) {
@@ -465,7 +469,7 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, RestoreInitialTablesStandardSqlite) {
-  ASSERT_OK(Processor()->NotifyEndOfFile());
+  ASSERT_OK(NotifyEndOfFile());
   RestoreInitialTables();
 
   for (int repeat = 0; repeat < 3; repeat++) {
@@ -491,13 +495,13 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, RestoreInitialTablesModules) {
-  ASSERT_OK(Processor()->NotifyEndOfFile());
+  ASSERT_OK(NotifyEndOfFile());
   RestoreInitialTables();
 
   for (int repeat = 0; repeat < 3; repeat++) {
     ASSERT_EQ(RestoreInitialTables(), 0u);
     {
-      auto it = Query("INCLUDE PERFETTO MODULE common.timestamps;");
+      auto it = Query("INCLUDE PERFETTO MODULE time.conversion;");
       it.Next();
       ASSERT_TRUE(it.Status().ok());
     }
@@ -511,7 +515,7 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, RestoreInitialTablesSpanJoin) {
-  ASSERT_OK(Processor()->NotifyEndOfFile());
+  ASSERT_OK(NotifyEndOfFile());
   RestoreInitialTables();
 
   for (int repeat = 0; repeat < 3; repeat++) {
@@ -550,7 +554,7 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, RestoreInitialTablesWithClause) {
-  ASSERT_OK(Processor()->NotifyEndOfFile());
+  ASSERT_OK(NotifyEndOfFile());
   RestoreInitialTables();
 
   for (int repeat = 0; repeat < 3; repeat++) {
@@ -567,7 +571,7 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, RestoreInitialTablesIndex) {
-  ASSERT_OK(Processor()->NotifyEndOfFile());
+  ASSERT_OK(NotifyEndOfFile());
   RestoreInitialTables();
 
   for (int repeat = 0; repeat < 3; repeat++) {
@@ -605,7 +609,7 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, RestoreInitialTablesDependents) {
-  ASSERT_OK(Processor()->NotifyEndOfFile());
+  ASSERT_OK(NotifyEndOfFile());
   {
     auto it = Query("create perfetto table foo as select 1 as x");
     ASSERT_FALSE(it.Next());
@@ -625,7 +629,7 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, RestoreDependentFunction) {
-  ASSERT_OK(Processor()->NotifyEndOfFile());
+  ASSERT_OK(NotifyEndOfFile());
   {
     auto it =
         Query("create perfetto function foo0() returns INT as select 1 as x");
@@ -645,7 +649,7 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, RestoreDependentTableFunction) {
-  ASSERT_OK(Processor()->NotifyEndOfFile());
+  ASSERT_OK(NotifyEndOfFile());
   {
     auto it = Query(
         "create perfetto function foo0() returns TABLE(x INT) "
@@ -735,6 +739,7 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, ErrorMessageExecuteQuery) {
+  ASSERT_OK(NotifyEndOfFile());
   auto it = Query("select t from slice");
   ASSERT_FALSE(it.Next());
   ASSERT_FALSE(it.Status().ok());
@@ -748,6 +753,7 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, ErrorMessageMetricFile) {
+  ASSERT_OK(NotifyEndOfFile());
   ASSERT_TRUE(
       Processor()->RegisterMetric("foo/bar.sql", "select t from slice").ok());
 
@@ -758,7 +764,7 @@
   ASSERT_EQ(it.Status().message(),
             R"(Traceback (most recent call last):
   File "stdin" line 1 col 1
-    select RUN_METRIC('foo/bar.sql')
+    select RUN_METRIC('foo/bar.sql');
     ^
   Metric file "foo/bar.sql" line 1 col 8
     select t from slice
@@ -767,11 +773,12 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, ErrorMessageModule) {
-  SqlModule module;
+  ASSERT_OK(NotifyEndOfFile());
+  SqlPackage module;
   module.name = "foo";
-  module.files.push_back(std::make_pair("foo.bar", "select t from slice"));
+  module.modules.push_back(std::make_pair("foo.bar", "select t from slice"));
 
-  ASSERT_TRUE(Processor()->RegisterSqlModule(module).ok());
+  ASSERT_TRUE(Processor()->RegisterSqlPackage(module).ok());
 
   auto it = Query("include perfetto module foo.bar;");
   ASSERT_FALSE(it.Next());
@@ -819,7 +826,7 @@
                    ->Parse(TraceBlobView(
                        TraceBlob::CopyFrom(kBadData, sizeof(kBadData))))
                    .ok());
-  Processor()->NotifyEndOfFile();
+  NotifyEndOfFile();
 }
 
 TEST_F(TraceProcessorIntegrationTest, NoNotifyEndOfFileCalled) {
diff --git a/src/trace_processor/trace_processor_context.cc b/src/trace_processor/trace_processor_context.cc
index 0da4693..53876b0 100644
--- a/src/trace_processor/trace_processor_context.cc
+++ b/src/trace_processor/trace_processor_context.cc
@@ -15,21 +15,22 @@
  */
 
 #include "src/trace_processor/types/trace_processor_context.h"
+
 #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"
 #include "src/trace_processor/importers/common/async_track_set_tracker.h"
-#include "src/trace_processor/importers/common/chunked_trace_reader.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/cpu_tracker.h"
-#include "src/trace_processor/importers/common/deobfuscation_mapping_table.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/legacy_v8_cpu_profile_tracker.h"
 #include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/mapping_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
@@ -41,20 +42,16 @@
 #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_tracker.h"
-#include "src/trace_processor/importers/ftrace/ftrace_module.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/importers/proto/track_event_module.h"
-#include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/trace_reader_registry.h"
-#include "src/trace_processor/types/destructible.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 TraceProcessorContext::TraceProcessorContext(const InitArgs& args)
     : config(args.config), storage(args.storage) {
@@ -109,11 +106,17 @@
       });
 
   trace_file_tracker = std::make_unique<TraceFileTracker>(this);
+  legacy_v8_cpu_profile_tracker =
+      std::make_unique<LegacyV8CpuProfileTracker>(this);
 }
 
 TraceProcessorContext::TraceProcessorContext() = default;
 TraceProcessorContext::~TraceProcessorContext() = default;
 
+TraceProcessorContext::TraceProcessorContext(TraceProcessorContext&&) = default;
+TraceProcessorContext& TraceProcessorContext::operator=(
+    TraceProcessorContext&&) = default;
+
 std::optional<MachineId> TraceProcessorContext::machine_id() const {
   if (!machine_tracker) {
     // Doesn't require that |machine_tracker| is initialzed, e.g. in unit tests.
@@ -122,5 +125,4 @@
   return machine_tracker->machine_id();
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index c4a1cb2..cd04da5 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -29,9 +29,12 @@
 #include <utility>
 #include <vector>
 
+#include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
+#include "perfetto/base/thread_utils.h"
 #include "perfetto/base/time.h"
+#include "perfetto/ext/base/clock_snapshots.h"
 #include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/small_vector.h"
 #include "perfetto/ext/base/status_or.h"
@@ -45,22 +48,30 @@
 #include "perfetto/trace_processor/trace_processor.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"
+#include "src/trace_processor/importers/archive/tar_trace_reader.h"
+#include "src/trace_processor/importers/archive/zip_trace_reader.h"
+#include "src/trace_processor/importers/art_method/art_method_parser_impl.h"
+#include "src/trace_processor/importers/art_method/art_method_tokenizer.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/trace_file_tracker.h"
 #include "src/trace_processor/importers/common/trace_parser.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/gzip/gzip_trace_parser.h"
+#include "src/trace_processor/importers/gecko/gecko_trace_parser_impl.h"
+#include "src/trace_processor/importers/gecko/gecko_trace_tokenizer.h"
 #include "src/trace_processor/importers/json/json_trace_parser_impl.h"
 #include "src/trace_processor/importers/json/json_trace_tokenizer.h"
 #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/record_parser.h"
+#include "src/trace_processor/importers/perf/spe_record_parser.h"
+#include "src/trace_processor/importers/perf_text/perf_text_trace_parser_impl.h"
+#include "src/trace_processor/importers/perf_text/perf_text_trace_tokenizer.h"
 #include "src/trace_processor/importers/proto/additional_modules.h"
 #include "src/trace_processor/importers/proto/content_analyzer.h"
 #include "src/trace_processor/importers/systrace/systrace_trace_parser.h"
-#include "src/trace_processor/importers/zip/zip_trace_reader.h"
 #include "src/trace_processor/iterator_impl.h"
 #include "src/trace_processor/metrics/all_chrome_metrics.descriptor.h"
 #include "src/trace_processor/metrics/all_webview_metrics.descriptor.h"
@@ -71,6 +82,7 @@
 #include "src/trace_processor/perfetto_sql/engine/table_pointer_module.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/base64.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/clock_functions.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/functions/counter_intervals.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/create_function.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/create_view_function.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/dominator_tree.h"
@@ -89,7 +101,6 @@
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/utils.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/window_functions.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/operators/interval_intersect_operator.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.h"
@@ -104,6 +115,7 @@
 #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"
 #include "src/trace_processor/perfetto_sql/stdlib/stdlib.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_aggregate_function.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_result.h"
@@ -124,6 +136,11 @@
 #include "src/trace_processor/util/status_macros.h"
 #include "src/trace_processor/util/trace_type.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"
+#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"
@@ -145,32 +162,45 @@
     PERFETTO_ELOG("%s", status.c_message());
 }
 
-void RegisterAllProtoBuilderFunctions(DescriptorPool* pool,
-                                      PerfettoSqlEngine* engine,
-                                      TraceProcessor* tp) {
+base::Status RegisterAllProtoBuilderFunctions(
+    DescriptorPool* pool,
+    std::unordered_map<std::string, std::string>* proto_fn_name_to_path,
+    PerfettoSqlEngine* engine,
+    TraceProcessor* tp) {
   for (uint32_t i = 0; i < pool->descriptors().size(); ++i) {
     // Convert the full name (e.g. .perfetto.protos.TraceMetrics.SubMetric)
     // into a function name of the form (TraceMetrics_SubMetric).
     const auto& desc = pool->descriptors()[i];
     auto fn_name = desc.full_name().substr(desc.package_name().size() + 1);
     std::replace(fn_name.begin(), fn_name.end(), '.', '_');
+    auto registered_fn = proto_fn_name_to_path->find(fn_name);
+    if (registered_fn != proto_fn_name_to_path->end() &&
+        registered_fn->second != desc.full_name()) {
+      return base::ErrStatus(
+          "Attempt to create new metric function '%s' for different descriptor "
+          "'%s' that conflicts with '%s'",
+          fn_name.c_str(), desc.full_name().c_str(),
+          registered_fn->second.c_str());
+    }
     RegisterFunction<metrics::BuildProto>(
         engine, fn_name.c_str(), -1,
         std::make_unique<metrics::BuildProto::Context>(
             metrics::BuildProto::Context{tp, pool, i}));
+    proto_fn_name_to_path->emplace(fn_name, desc.full_name());
   }
+  return base::OkStatus();
 }
 
 void BuildBoundsTable(sqlite3* db, std::pair<int64_t, int64_t> bounds) {
   char* error = nullptr;
-  sqlite3_exec(db, "DELETE FROM trace_bounds", nullptr, nullptr, &error);
+  sqlite3_exec(db, "DELETE FROM _trace_bounds", nullptr, nullptr, &error);
   if (error) {
     PERFETTO_ELOG("Error deleting from bounds table: %s", error);
     sqlite3_free(error);
     return;
   }
 
-  base::StackString<1024> sql("INSERT INTO trace_bounds VALUES(%" PRId64
+  base::StackString<1024> sql("INSERT INTO _trace_bounds VALUES(%" PRId64
                               ", %" PRId64 ")",
                               bounds.first, bounds.second);
   sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &error);
@@ -282,7 +312,7 @@
 
 void InsertIntoTraceMetricsTable(sqlite3* db, const std::string& metric_name) {
   char* insert_sql = sqlite3_mprintf(
-      "INSERT INTO trace_metrics(name) VALUES('%q')", metric_name.c_str());
+      "INSERT INTO _trace_metrics(name) VALUES('%q')", metric_name.c_str());
   char* insert_error = nullptr;
   sqlite3_exec(db, insert_sql, nullptr, nullptr, &insert_error);
   sqlite3_free(insert_sql);
@@ -292,14 +322,15 @@
   }
 }
 
-sql_modules::NameToModule GetStdlibModules() {
-  sql_modules::NameToModule modules;
+sql_modules::NameToPackage GetStdlibPackages() {
+  sql_modules::NameToPackage packages;
   for (const auto& file_to_sql : stdlib::kFileToSql) {
-    std::string import_key = sql_modules::GetIncludeKey(file_to_sql.path);
-    std::string module = sql_modules::GetModuleName(import_key);
-    modules.Insert(module, {}).first->push_back({import_key, file_to_sql.sql});
+    std::string module_name = sql_modules::GetIncludeKey(file_to_sql.path);
+    std::string package_name = sql_modules::GetPackageName(module_name);
+    packages.Insert(package_name, {})
+        .first->push_back({module_name, file_to_sql.sql});
   }
-  return modules;
+  return packages;
 }
 
 std::pair<int64_t, int64_t> GetTraceTimestampBoundsNs(
@@ -343,6 +374,10 @@
     start_ns = std::min(it.ts(), start_ns);
     end_ns = std::max(it.ts(), end_ns);
   }
+  for (auto it = storage.instruments_sample_table().IterateRows(); it; ++it) {
+    start_ns = std::min(it.ts(), start_ns);
+    end_ns = std::max(it.ts(), end_ns);
+  }
   for (auto it = storage.cpu_profile_stack_sample_table().IterateRows(); it;
        ++it) {
     start_ns = std::min(it.ts(), start_ns);
@@ -381,8 +416,18 @@
           kPerfDataTraceType);
   context_.perf_record_parser =
       std::make_unique<perf_importer::RecordParser>(&context_);
+  context_.spe_record_parser =
+      std::make_unique<perf_importer::SpeRecordParserImpl>(&context_);
 
-  if (util::IsGzipSupported()) {
+#if PERFETTO_BUILDFLAG(PERFETTO_TP_INSTRUMENTS)
+  context_.reader_registry
+      ->RegisterTraceReader<instruments_importer::InstrumentsXmlTokenizer>(
+          kInstrumentsXmlTraceType);
+  context_.instruments_row_parser =
+      std::make_unique<instruments_importer::RowParser>(&context_);
+#endif
+
+  if constexpr (util::IsGzipSupported()) {
     context_.reader_registry->RegisterTraceReader<GzipTraceParser>(
         kGzipTraceType);
     context_.reader_registry->RegisterTraceReader<GzipTraceParser>(
@@ -390,13 +435,32 @@
     context_.reader_registry->RegisterTraceReader<ZipTraceReader>(kZipFile);
   }
 
-  if (json::IsJsonSupported()) {
+  if constexpr (json::IsJsonSupported()) {
     context_.reader_registry->RegisterTraceReader<JsonTraceTokenizer>(
         kJsonTraceType);
     context_.json_trace_parser =
         std::make_unique<JsonTraceParserImpl>(&context_);
+
+    context_.reader_registry
+        ->RegisterTraceReader<gecko_importer::GeckoTraceTokenizer>(
+            kGeckoTraceType);
+    context_.gecko_trace_parser =
+        std::make_unique<gecko_importer::GeckoTraceParserImpl>(&context_);
   }
 
+  context_.reader_registry->RegisterTraceReader<art_method::ArtMethodTokenizer>(
+      kArtMethodTraceType);
+  context_.art_method_parser =
+      std::make_unique<art_method::ArtMethodParserImpl>(&context_);
+
+  context_.reader_registry
+      ->RegisterTraceReader<perf_text_importer::PerfTextTraceTokenizer>(
+          kPerfTextTraceType);
+  context_.perf_text_parser =
+      std::make_unique<perf_text_importer::PerfTextTraceParserImpl>(&context_);
+
+  context_.reader_registry->RegisterTraceReader<TarTraceReader>(kTarTraceType);
+
   if (context_.config.analyze_trace_proto_content) {
     context_.content_analyzer =
         std::make_unique<ProtoContentAnalyzer>(&context_);
@@ -422,8 +486,7 @@
   RegisterAdditionalModules(&context_);
   InitPerfettoSqlEngine();
 
-  sqlite_objects_post_constructor_initialization_ =
-      engine_->SqliteRegisteredObjectCount();
+  sqlite_objects_post_prelude_ = engine_->SqliteRegisteredObjectCount();
 
   bool skip_all_sql = std::find(config_.skip_builtin_metric_paths.begin(),
                                 config_.skip_builtin_metric_paths.end(),
@@ -489,6 +552,9 @@
                    GetTraceTimestampBoundsNs(*context_.storage));
 
   TraceProcessorStorageImpl::DestroyContext();
+
+  IncludeAfterEofPrelude();
+  sqlite_objects_post_prelude_ = engine_->SqliteRegisteredObjectCount();
   return base::OkStatus();
 }
 
@@ -496,20 +562,19 @@
   // 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_constructor_initialization_);
+  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_constructor_initialization_);
+  PERFETTO_CHECK(registered_count_after == sqlite_objects_post_prelude_);
   return static_cast<size_t>(registered_count_before - registered_count_after);
 }
 
 Iterator TraceProcessorImpl::ExecuteQuery(const std::string& sql) {
-  PERFETTO_TP_TRACE(metatrace::Category::API_TIMELINE, "EXECUTE_QUERY");
+  PERFETTO_TP_TRACE(metatrace::Category::API_TIMELINE, "EXECUTE_QUERY",
+                    [&](metatrace::Record* r) { r->AddArg("query", sql); });
 
   uint32_t sql_stats_row =
       context_.storage->mutable_sql_stats()->RecordQueryBegin(
@@ -540,30 +605,30 @@
   return field_idx != nullptr;
 }
 
-base::Status TraceProcessorImpl::RegisterSqlModule(SqlModule sql_module) {
-  sql_modules::RegisteredModule new_module;
-  std::string name = sql_module.name;
-  if (engine_->FindModule(name) && !sql_module.allow_module_override) {
+base::Status TraceProcessorImpl::RegisterSqlPackage(SqlPackage sql_package) {
+  sql_modules::RegisteredPackage new_package;
+  std::string name = sql_package.name;
+  if (engine_->FindPackage(name) && !sql_package.allow_override) {
     return base::ErrStatus(
-        "Module '%s' is already registered. Choose a different name.\n"
-        "If you want to replace the existing module using trace processor "
+        "Package '%s' is already registered. Choose a different name.\n"
+        "If you want to replace the existing package using trace processor "
         "shell, you need to pass the --dev flag and use "
         "--override-sql-module "
         "to pass the module path.",
         name.c_str());
   }
-  for (auto const& name_and_sql : sql_module.files) {
-    if (sql_modules::GetModuleName(name_and_sql.first) != name) {
+  for (auto const& module_name_and_sql : sql_package.modules) {
+    if (sql_modules::GetPackageName(module_name_and_sql.first) != name) {
       return base::ErrStatus(
-          "File import key doesn't match the module name. First part of "
-          "import "
-          "key should be module name. Import key: %s, module name: %s.",
-          name_and_sql.first.c_str(), name.c_str());
+          "Module name doesn't match the package name. First part of module "
+          "name should be package name. Import key: '%s', package name: '%s'.",
+          module_name_and_sql.first.c_str(), name.c_str());
     }
-    new_module.include_key_to_file.Insert(name_and_sql.first,
-                                          {name_and_sql.second, false});
+    new_package.modules.Insert(module_name_and_sql.first,
+                               {module_name_and_sql.second, false});
   }
-  engine_->RegisterModule(name, std::move(new_module));
+  manually_registered_sql_packages_.push_back(SqlPackage(sql_package));
+  engine_->RegisterPackage(name, std::move(new_package));
   return base::OkStatus();
 }
 
@@ -633,7 +698,8 @@
     size_t size,
     const std::vector<std::string>& skip_prefixes) {
   RETURN_IF_ERROR(pool_.AddFromFileDescriptorSet(data, size, skip_prefixes));
-  RegisterAllProtoBuilderFunctions(&pool_, engine_.get(), this);
+  RETURN_IF_ERROR(RegisterAllProtoBuilderFunctions(
+      &pool_, &proto_fn_name_to_path_, engine_.get(), this));
   return base::OkStatus();
 }
 
@@ -784,6 +850,10 @@
     base::Status status = perfetto_sql::RegisterIntervalIntersectFunctions(
         *engine_, context_.storage->mutable_string_pool());
   }
+  {
+    base::Status status = perfetto_sql::RegisterCounterIntervalsFunctions(
+        *engine_, context_.storage->mutable_string_pool());
+  }
 
   TraceStorage* storage = context_.storage.get();
 
@@ -805,16 +875,13 @@
   engine_->sqlite_engine()->RegisterVirtualTableModule<SliceMipmapOperator>(
       "__intrinsic_slice_mipmap",
       std::make_unique<SliceMipmapOperator::Context>(engine_.get()));
-  engine_->sqlite_engine()
-      ->RegisterVirtualTableModule<IntervalIntersectOperator>(
-          "__intrinsic_ii_with_interval_tree",
-          std::make_unique<IntervalIntersectOperator::Context>(engine_.get()));
 
-  // Register stdlib modules.
-  auto stdlib_modules = GetStdlibModules();
-  for (auto module_it = stdlib_modules.GetIterator(); module_it; ++module_it) {
+  // Register stdlib packages.
+  auto packages = GetStdlibPackages();
+  for (auto package = packages.GetIterator(); package; ++package) {
     base::Status status =
-        RegisterSqlModule({module_it.key(), module_it.value(), false});
+        RegisterSqlPackage({/*name=*/package.key(), /*modules=*/package.value(),
+                            /*allow_override=*/false});
     if (!status.ok())
       PERFETTO_ELOG("%s", status.c_message());
   }
@@ -869,8 +936,6 @@
   RegisterStaticTable(storage->mutable_process_track_table());
   RegisterStaticTable(storage->mutable_cpu_track_table());
   RegisterStaticTable(storage->mutable_gpu_track_table());
-  RegisterStaticTable(storage->mutable_uid_track_table());
-  RegisterStaticTable(storage->mutable_gpu_work_period_track_table());
 
   RegisterStaticTable(storage->mutable_counter_table());
 
@@ -878,15 +943,9 @@
   RegisterStaticTable(storage->mutable_process_counter_track_table());
   RegisterStaticTable(storage->mutable_thread_counter_track_table());
   RegisterStaticTable(storage->mutable_cpu_counter_track_table());
-  RegisterStaticTable(storage->mutable_irq_counter_track_table());
-  RegisterStaticTable(storage->mutable_softirq_counter_track_table());
   RegisterStaticTable(storage->mutable_gpu_counter_track_table());
   RegisterStaticTable(storage->mutable_gpu_counter_group_table());
   RegisterStaticTable(storage->mutable_perf_counter_track_table());
-  RegisterStaticTable(storage->mutable_energy_counter_track_table());
-  RegisterStaticTable(storage->mutable_linux_device_track_table());
-  RegisterStaticTable(storage->mutable_uid_counter_track_table());
-  RegisterStaticTable(storage->mutable_energy_per_uid_counter_track_table());
 
   RegisterStaticTable(storage->mutable_heap_graph_object_table());
   RegisterStaticTable(storage->mutable_heap_graph_reference_table());
@@ -897,6 +956,7 @@
   RegisterStaticTable(storage->mutable_cpu_profile_stack_sample_table());
   RegisterStaticTable(storage->mutable_perf_session_table());
   RegisterStaticTable(storage->mutable_perf_sample_table());
+  RegisterStaticTable(storage->mutable_instruments_sample_table());
   RegisterStaticTable(storage->mutable_stack_profile_callsite_table());
   RegisterStaticTable(storage->mutable_stack_profile_mapping_table());
   RegisterStaticTable(storage->mutable_stack_profile_frame_table());
@@ -931,6 +991,8 @@
   RegisterStaticTable(storage->mutable_jit_code_table());
   RegisterStaticTable(storage->mutable_jit_frame_table());
 
+  RegisterStaticTable(storage->mutable_spe_record_table());
+
   RegisterStaticTable(storage->mutable_inputmethod_clients_table());
   RegisterStaticTable(storage->mutable_inputmethod_manager_service_table());
   RegisterStaticTable(storage->mutable_inputmethod_service_table());
@@ -1000,6 +1062,9 @@
       std::make_unique<ExperimentalFlatSlice>(&context_));
   engine_->RegisterStaticTableFunction(std::make_unique<DfsWeightBounded>(
       context_.storage->mutable_string_pool()));
+  engine_->RegisterStaticTableFunction(
+      std::make_unique<WinscopeProtoToArgsWithDefaults>(
+          context_.storage->mutable_string_pool(), engine_.get(), &context_));
 
   // Value table aggregate functions.
   engine_->RegisterSqliteAggregateFunction<DominatorTree>(
@@ -1008,18 +1073,20 @@
       context_.storage->mutable_string_pool());
 
   // Metrics.
-  RegisterAllProtoBuilderFunctions(&pool_, engine_.get(), this);
-
-  // Import prelude module.
   {
-    auto result = engine_->Execute(SqlSource::FromTraceProcessorImplementation(
-        "INCLUDE PERFETTO MODULE prelude.*"));
-    if (!result.status().ok()) {
-      PERFETTO_FATAL("Failed to import prelude: %s",
-                     result.status().c_message());
+    auto status = RegisterAllProtoBuilderFunctions(
+        &pool_, &proto_fn_name_to_path_, engine_.get(), this);
+    if (!status.ok()) {
+      PERFETTO_FATAL("%s", status.c_message());
     }
   }
 
+  // Import prelude package.
+  IncludeBeforeEofPrelude();
+  if (notify_eof_called_) {
+    IncludeAfterEofPrelude();
+  }
+
   for (const auto& metric : sql_metrics_) {
     if (metric.proto_field_name) {
       InsertIntoTraceMetricsTable(db, *metric.proto_field_name);
@@ -1028,6 +1095,27 @@
 
   // Fill trace bounds table.
   BuildBoundsTable(db, GetTraceTimestampBoundsNs(*context_.storage));
+
+  // Reregister manually added stdlib packages.
+  for (const auto& package : manually_registered_sql_packages_) {
+    RegisterSqlPackage(package);
+  }
+}
+
+void TraceProcessorImpl::IncludeBeforeEofPrelude() {
+  auto result = engine_->Execute(SqlSource::FromTraceProcessorImplementation(
+      "INCLUDE PERFETTO MODULE prelude.before_eof.*"));
+  if (!result.status().ok()) {
+    PERFETTO_FATAL("Failed to import prelude: %s", result.status().c_message());
+  }
+}
+
+void TraceProcessorImpl::IncludeAfterEofPrelude() {
+  auto result = engine_->Execute(SqlSource::FromTraceProcessorImplementation(
+      "INCLUDE PERFETTO MODULE prelude.after_eof.*"));
+  if (!result.status().ok()) {
+    PERFETTO_FATAL("Failed to import prelude: %s", result.status().c_message());
+  }
 }
 
 namespace {
@@ -1068,54 +1156,42 @@
     std::vector<uint8_t>* trace_proto) {
   protozero::HeapBuffered<protos::pbzero::Trace> trace;
 
-  {
-    uint64_t realtime_timestamp = static_cast<uint64_t>(
-        std::chrono::system_clock::now().time_since_epoch() /
-        std::chrono::nanoseconds(1));
-    uint64_t boottime_timestamp = metatrace::TraceTimeNowNs();
-    auto* clock_snapshot = trace->add_packet()->set_clock_snapshot();
-    {
-      auto* realtime_clock = clock_snapshot->add_clocks();
-      realtime_clock->set_clock_id(
-          protos::pbzero::BuiltinClock::BUILTIN_CLOCK_REALTIME);
-      realtime_clock->set_timestamp(realtime_timestamp);
-    }
-    {
-      auto* boottime_clock = clock_snapshot->add_clocks();
-      boottime_clock->set_clock_id(
-          protos::pbzero::BuiltinClock::BUILTIN_CLOCK_BOOTTIME);
-      boottime_clock->set_timestamp(boottime_timestamp);
-    }
+  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](
-                                      metatrace::Record* record) {
-    auto* packet = trace->add_packet();
-    packet->set_timestamp(record->timestamp_ns);
-    auto* evt = packet->set_perfetto_metatrace();
+  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);
+        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(1);  // Not really important, just required for the ui.
+        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;
+        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()));
+        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()));
-    }
-  });
+          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();
 }
diff --git a/src/trace_processor/trace_processor_impl.h b/src/trace_processor/trace_processor_impl.h
index 7b480d2..3c86da0 100644
--- a/src/trace_processor/trace_processor_impl.h
+++ b/src/trace_processor/trace_processor_impl.h
@@ -67,7 +67,7 @@
   base::Status RegisterMetric(const std::string& path,
                               const std::string& sql) override;
 
-  base::Status RegisterSqlModule(SqlModule sql_module) override;
+  base::Status RegisterSqlPackage(SqlPackage) override;
 
   base::Status ExtendMetricsProto(const uint8_t* data, size_t size) override;
 
@@ -97,6 +97,15 @@
   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);
+  }
+
  private:
   // Needed for iterators to be able to access the context.
   friend class IteratorImpl;
@@ -110,6 +119,8 @@
   bool IsRootMetricField(const std::string& metric_name);
 
   void InitPerfettoSqlEngine();
+  void IncludeBeforeEofPrelude();
+  void IncludeAfterEofPrelude();
 
   const Config config_;
   std::unique_ptr<PerfettoSqlEngine> engine_;
@@ -117,14 +128,20 @@
   DescriptorPool pool_;
 
   std::vector<metrics::SqlMetricFile> sql_metrics_;
+
+  // Manually registeres SQL packages are stored here, to be able to restore
+  // them when running |RestoreInitialTables()|.
+  std::vector<SqlPackage> manually_registered_sql_packages_;
+
   std::unordered_map<std::string, std::string> proto_field_to_sql_metric_path_;
+  std::unordered_map<std::string, std::string> proto_fn_name_to_path_;
 
   // This is atomic because it is set by the CTRL-C signal handler and we need
   // to prevent single-flow compiler optimizations in ExecuteQuery().
   std::atomic<bool> query_interrupted_{false};
 
-  // Track the number of objects registered with SQLite after the constructor.
-  uint64_t sqlite_objects_post_constructor_initialization_ = 0;
+  // Track the number of objects registered with SQLite post prelude.
+  uint64_t sqlite_objects_post_prelude_ = 0;
 
   std::string current_trace_name_;
   uint64_t bytes_parsed_ = 0;
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc
index a790884..dac39a9 100644
--- a/src/trace_processor/trace_processor_shell.cc
+++ b/src/trace_processor/trace_processor_shell.cc
@@ -14,22 +14,29 @@
  * limitations under the License.
  */
 
-#include <errno.h>
 #include <fcntl.h>
 #include <stdio.h>
 #include <sys/stat.h>
+#include <algorithm>
 #include <cctype>
+#include <cerrno>
+#include <chrono>
 #include <cinttypes>
-#include <functional>
-#include <iostream>
+#include <cstdint>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <memory>
 #include <optional>
 #include <string>
-#include <unordered_map>
 #include <unordered_set>
+#include <utility>
 #include <vector>
 
 #include <google/protobuf/compiler/parser.h>
+#include <google/protobuf/descriptor.pb.h>
 #include <google/protobuf/dynamic_message.h>
+#include <google/protobuf/io/tokenizer.h>
 #include <google/protobuf/io/zero_copy_stream_impl.h>
 #include <google/protobuf/text_format.h>
 
@@ -38,16 +45,20 @@
 #include "perfetto/base/status.h"
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/file_utils.h"
-#include "perfetto/ext/base/flat_hash_map.h"
-#include "perfetto/ext/base/getopt.h"
+#include "perfetto/ext/base/getopt.h"  // IWYU pragma: keep
 #include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/status_or.h"
 #include "perfetto/ext/base/string_splitter.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/version.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/read_trace.h"
 #include "perfetto/trace_processor/trace_processor.h"
+#include "src/profiling/deobfuscator.h"
+#include "src/profiling/symbolizer/local_symbolizer.h"
+#include "src/profiling/symbolizer/symbolize_database.h"
 #include "src/trace_processor/metrics/all_chrome_metrics.descriptor.h"
 #include "src/trace_processor/metrics/all_webview_metrics.descriptor.h"
 #include "src/trace_processor/metrics/metrics.descriptor.h"
@@ -61,10 +72,6 @@
 #if PERFETTO_BUILDFLAG(PERFETTO_TP_HTTPD)
 #include "src/trace_processor/rpc/httpd.h"
 #endif
-#include "src/profiling/deobfuscator.h"
-#include "src/profiling/symbolizer/local_symbolizer.h"
-#include "src/profiling/symbolizer/symbolize_database.h"
-#include "src/profiling/symbolizer/symbolizer.h"
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) ||   \
     PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
@@ -96,8 +103,7 @@
 #include <unistd.h>  // For getuid() in GetConfigPath().
 #endif
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 namespace {
 TraceProcessor* g_tp;
@@ -496,41 +502,68 @@
          static_cast<double>((t_end - t_start).count()) / 1E6);
 }
 
-base::Status PrintQueryResultAsCsv(Iterator* it, bool has_more, FILE* output) {
+struct QueryResult {
+  std::vector<std::string> column_names;
+  std::vector<std::vector<std::string>> rows;
+};
+
+base::StatusOr<QueryResult> ExtractQueryResult(Iterator* it, bool has_more) {
+  QueryResult result;
+
   for (uint32_t c = 0; c < it->ColumnCount(); c++) {
+    fprintf(stderr, "column %d = %s\n", c, it->GetColumnName(c).c_str());
+    result.column_names.push_back(it->GetColumnName(c));
+  }
+
+  for (; has_more; has_more = it->Next()) {
+    std::vector<std::string> row;
+    for (uint32_t c = 0; c < it->ColumnCount(); c++) {
+      SqlValue value = it->Get(c);
+      std::string str_value;
+      switch (value.type) {
+        case SqlValue::Type::kNull:
+          str_value = "\"[NULL]\"";
+          break;
+        case SqlValue::Type::kDouble:
+          str_value =
+              base::StackString<256>("%f", value.double_value).ToStdString();
+          break;
+        case SqlValue::Type::kLong:
+          str_value = base::StackString<256>("%" PRIi64, value.long_value)
+                          .ToStdString();
+          break;
+        case SqlValue::Type::kString:
+          str_value = '"' + std::string(value.string_value) + '"';
+          break;
+        case SqlValue::Type::kBytes:
+          str_value = "\"<raw bytes>\"";
+          break;
+      }
+
+      row.push_back(std::move(str_value));
+    }
+    result.rows.push_back(std::move(row));
+  }
+  RETURN_IF_ERROR(it->Status());
+  return result;
+}
+
+void PrintQueryResultAsCsv(const QueryResult& result, FILE* output) {
+  for (uint32_t c = 0; c < result.column_names.size(); c++) {
     if (c > 0)
       fprintf(output, ",");
-    fprintf(output, "\"%s\"", it->GetColumnName(c).c_str());
+    fprintf(output, "\"%s\"", result.column_names[c].c_str());
   }
   fprintf(output, "\n");
 
-  for (; has_more; has_more = it->Next()) {
-    for (uint32_t c = 0; c < it->ColumnCount(); c++) {
+  for (const auto& row : result.rows) {
+    for (uint32_t c = 0; c < result.column_names.size(); c++) {
       if (c > 0)
         fprintf(output, ",");
-
-      auto value = it->Get(c);
-      switch (value.type) {
-        case SqlValue::Type::kNull:
-          fprintf(output, "\"%s\"", "[NULL]");
-          break;
-        case SqlValue::Type::kDouble:
-          fprintf(output, "%f", value.double_value);
-          break;
-        case SqlValue::Type::kLong:
-          fprintf(output, "%" PRIi64, value.long_value);
-          break;
-        case SqlValue::Type::kString:
-          fprintf(output, "\"%s\"", value.string_value);
-          break;
-        case SqlValue::Type::kBytes:
-          fprintf(output, "\"%s\"", "<raw bytes>");
-          break;
-      }
+      fprintf(output, "%s", row[c].c_str());
     }
     fprintf(output, "\n");
   }
-  return it->Status();
 }
 
 base::Status RunQueriesWithoutOutput(const std::string& sql_query) {
@@ -575,8 +608,16 @@
     return base::OkStatus();
   }
 
+  auto query_result = ExtractQueryResult(&it, has_more);
+  RETURN_IF_ERROR(query_result.status());
+
+  // We want to include the query iteration time (as it's a part of executing
+  // SQL and can be non-trivial), and we want to exclude the time spent printing
+  // the result (which can be significant for large results), so we materialise
+  // the results first, then take the measurement, then print them.
   auto query_end = std::chrono::steady_clock::now();
-  RETURN_IF_ERROR(PrintQueryResultAsCsv(&it, has_more, output));
+
+  PrintQueryResultAsCsv(query_result.value(), output);
 
   auto dur = query_end - query_start;
   PERFETTO_ILOG(
@@ -1114,10 +1155,9 @@
 base::Status RunQueriesFromFile(const std::string& query_file_path,
                                 bool expect_output) {
   std::string queries;
-  if (!base::ReadFile(query_file_path.c_str(), &queries)) {
+  if (!base::ReadFile(query_file_path, &queries)) {
     return base::ErrStatus("Unable to read file %s", query_file_path.c_str());
   }
-
   return RunQueries(queries, expect_output);
 }
 
@@ -1195,7 +1235,7 @@
   // Get module name
   size_t last_slash = root.rfind('/');
   if ((last_slash == std::string::npos) ||
-      (root.find(".") != std::string::npos))
+      (root.find('.') != std::string::npos))
     return base::ErrStatus("Module path must point to the directory: %s",
                            root.c_str());
 
@@ -1203,7 +1243,7 @@
 
   std::vector<std::string> paths;
   RETURN_IF_ERROR(base::ListFilesRecursive(root, paths));
-  sql_modules::NameToModule modules;
+  sql_modules::NameToPackage modules;
   for (const auto& path : paths) {
     if (base::GetFileExtension(path) != ".sql")
       continue;
@@ -1219,8 +1259,9 @@
         .first->push_back({import_key, file_contents});
   }
   for (auto module_it = modules.GetIterator(); module_it; ++module_it) {
-    auto status = g_tp->RegisterSqlModule(
-        {module_it.key(), module_it.value(), allow_override});
+    auto status = g_tp->RegisterSqlPackage({/*name=*/module_it.key(),
+                                            /*files=*/module_it.value(),
+                                            /*allow_override=*/allow_override});
     if (!status.ok())
       return status;
   }
@@ -1235,27 +1276,30 @@
   }
 
   if (!base::FileExists(root)) {
-    return base::ErrStatus("Directory %s does not exist.", root.c_str());
+    return base::ErrStatus("Directory '%s' does not exist.", root.c_str());
   }
 
   std::vector<std::string> paths;
   RETURN_IF_ERROR(base::ListFilesRecursive(root, paths));
-  sql_modules::NameToModule modules;
+  sql_modules::NameToPackage packages;
   for (const auto& path : paths) {
     if (base::GetFileExtension(path) != ".sql") {
       continue;
     }
     std::string filename = root + "/" + path;
-    std::string file_contents;
-    if (!base::ReadFile(filename, &file_contents)) {
-      return base::ErrStatus("Cannot read file %s", filename.c_str());
+    std::string module_file;
+    if (!base::ReadFile(filename, &module_file)) {
+      return base::ErrStatus("Cannot read file '%s'", filename.c_str());
     }
-    std::string import_key = sql_modules::GetIncludeKey(path);
-    std::string module = sql_modules::GetModuleName(import_key);
-    modules.Insert(module, {}).first->push_back({import_key, file_contents});
+    std::string module_name = sql_modules::GetIncludeKey(path);
+    std::string package_name = sql_modules::GetPackageName(module_name);
+    packages.Insert(package_name, {})
+        .first->push_back({module_name, module_file});
   }
-  for (auto module_it = modules.GetIterator(); module_it; ++module_it) {
-    g_tp->RegisterSqlModule({module_it.key(), module_it.value(), true});
+  for (auto package = packages.GetIterator(); package; ++package) {
+    g_tp->RegisterSqlPackage({/*name=*/package.key(),
+                              /*files=*/package.value(),
+                              /*allow_override=*/true});
   }
 
   return base::OkStatus();
@@ -1476,7 +1520,8 @@
       sscanf(line.get() + 1, "%31s %1023s", command, arg);
       if (strcmp(command, "quit") == 0 || strcmp(command, "q") == 0) {
         break;
-      } else if (strcmp(command, "help") == 0) {
+      }
+      if (strcmp(command, "help") == 0) {
         PrintShellUsage();
       } else if (strcmp(command, "dump") == 0 && strlen(arg)) {
         if (!ExportTraceToDatabase(arg).ok())
@@ -1747,8 +1792,7 @@
 
 }  // namespace
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 int main(int argc, char** argv) {
   auto status = perfetto::trace_processor::TraceProcessorMain(argc, argv);
diff --git a/src/trace_processor/trace_processor_storage_impl.cc b/src/trace_processor/trace_processor_storage_impl.cc
index 7478dd9..d409320 100644
--- a/src/trace_processor/trace_processor_storage_impl.cc
+++ b/src/trace_processor/trace_processor_storage_impl.cc
@@ -20,40 +20,40 @@
 #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"
+#include "perfetto/trace_processor/basic_types.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"
 #include "src/trace_processor/importers/common/async_track_set_tracker.h"
-#include "src/trace_processor/importers/common/clock_converter.h"
+#include "src/trace_processor/importers/common/clock_converter.h"  // IWYU pragma: keep
 #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 "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/sched_event_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_file_tracker.h"
-#include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/perf/dso_tracker.h"
-#include "src/trace_processor/importers/proto/chrome_track_event.descriptor.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"
-#include "src/trace_processor/importers/proto/track_event.descriptor.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/trace_reader_registry.h"
+#include "src/trace_processor/types/variadic.h"
 #include "src/trace_processor/util/descriptors.h"
+#include "src/trace_processor/util/status_macros.h"
 #include "src/trace_processor/util/trace_type.h"
 
 namespace perfetto::trace_processor {
@@ -78,8 +78,8 @@
     return base::ErrStatus(
         "Failed unrecoverably while parsing in a previous Parse call");
   if (!parser_) {
-    active_file_ = context_.trace_file_tracker->StartNewFile();
-    auto parser = std::make_unique<ForwardingTraceParser>(&context_);
+    auto parser = std::make_unique<ForwardingTraceParser>(
+        &context_, context_.trace_file_tracker->AddFile());
     parser_ = parser.get();
     context_.chunk_readers.push_back(std::move(parser));
   }
@@ -99,7 +99,6 @@
                                            Variadic::String(id_for_uuid));
   }
 
-  active_file_->AddSize(blob.size());
   base::Status status = parser_->Parse(std::move(blob));
   unrecoverable_parse_error_ |= !status.ok();
   return status;
@@ -123,9 +122,6 @@
   }
   Flush();
   RETURN_IF_ERROR(parser_->NotifyEndOfFile());
-  PERFETTO_CHECK(active_file_.has_value());
-  active_file_->SetTraceType(parser_->trace_type());
-  active_file_.reset();
   // NotifyEndOfFile might have pushed packets to the sorter.
   Flush();
   for (std::unique_ptr<ProtoImporterModule>& module : context_.modules) {
@@ -146,8 +142,6 @@
 }
 
 void TraceProcessorStorageImpl::DestroyContext() {
-  // End any active files. Eg. when NotifyEndOfFile is not called.
-  active_file_.reset();
   TraceProcessorContext context;
   context.storage = std::move(context_.storage);
 
@@ -159,6 +153,9 @@
   // kernel version (inside system_info_tracker) to know how to textualise
   // sched_switch.prev_state bitflags.
   context.system_info_tracker = std::move(context_.system_info_tracker);
+  // "__intrinsic_winscope_proto_to_args_with_defaults" requires proto
+  // descriptors.
+  context.descriptor_pool_ = std::move(context_.descriptor_pool_);
 
   context_ = std::move(context);
 
diff --git a/src/trace_processor/trace_processor_storage_impl.h b/src/trace_processor/trace_processor_storage_impl.h
index bab8961..9198d2f 100644
--- a/src/trace_processor/trace_processor_storage_impl.h
+++ b/src/trace_processor/trace_processor_storage_impl.h
@@ -17,9 +17,6 @@
 #ifndef SRC_TRACE_PROCESSOR_TRACE_PROCESSOR_STORAGE_IMPL_H_
 #define SRC_TRACE_PROCESSOR_TRACE_PROCESSOR_STORAGE_IMPL_H_
 
-#include <memory>
-#include <optional>
-
 #include "perfetto/ext/base/hash.h"
 #include "perfetto/trace_processor/basic_types.h"
 #include "perfetto/trace_processor/status.h"
@@ -51,7 +48,6 @@
   bool unrecoverable_parse_error_ = false;
   size_t hash_input_size_remaining_ = 4096;
   ForwardingTraceParser* parser_ = nullptr;
-  std::optional<ScopedActiveTraceFile> active_file_;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/trace_reader_registry.cc b/src/trace_processor/trace_reader_registry.cc
index b071295..fe108c8 100644
--- a/src/trace_processor/trace_reader_registry.cc
+++ b/src/trace_processor/trace_reader_registry.cc
@@ -15,7 +15,13 @@
  */
 
 #include "src/trace_processor/trace_reader_registry.h"
+
+#include <memory>
+#include <utility>
+
 #include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/util/gzip_utils.h"
@@ -37,6 +43,7 @@
     case kNinjaLogTraceType:
     case kSystraceTraceType:
     case kPerfDataTraceType:
+    case kInstrumentsXmlTraceType:
     case kUnknownTraceType:
     case kJsonTraceType:
     case kFuchsiaTraceType:
@@ -44,6 +51,10 @@
     case kSymbolsTraceType:
     case kAndroidLogcatTraceType:
     case kAndroidDumpstateTraceType:
+    case kGeckoTraceType:
+    case kArtMethodTraceType:
+    case kPerfTextTraceType:
+    case kTarTraceType:
       return false;
   }
   PERFETTO_FATAL("For GCC");
@@ -57,7 +68,7 @@
 
 base::StatusOr<std::unique_ptr<ChunkedTraceReader>>
 TraceReaderRegistry::CreateTraceReader(TraceType type) {
-  if (auto it = factories_.Find(type); it) {
+  if (auto* it = factories_.Find(type); it) {
     return (*it)(context_);
   }
 
diff --git a/src/trace_processor/trace_reader_registry.h b/src/trace_processor/trace_reader_registry.h
index 8e9b039..3d140bf 100644
--- a/src/trace_processor/trace_reader_registry.h
+++ b/src/trace_processor/trace_reader_registry.h
@@ -19,13 +19,9 @@
 
 #include <functional>
 #include <memory>
-#include <optional>
 
 #include "perfetto/ext/base/flat_hash_map.h"
-
 #include "perfetto/ext/base/status_or.h"
-#include "src/trace_processor/importers/common/trace_parser.h"
-#include "src/trace_processor/sorter/trace_sorter.h"
 #include "src/trace_processor/util/trace_type.h"
 
 namespace perfetto {
diff --git a/src/trace_processor/types/trace_processor_context.h b/src/trace_processor/types/trace_processor_context.h
index 909959c..3d8ef29 100644
--- a/src/trace_processor/types/trace_processor_context.h
+++ b/src/trace_processor/types/trace_processor_context.h
@@ -17,6 +17,7 @@
 #ifndef SRC_TRACE_PROCESSOR_TYPES_TRACE_PROCESSOR_CONTEXT_H_
 #define SRC_TRACE_PROCESSOR_TYPES_TRACE_PROCESSOR_CONTEXT_H_
 
+#include <cstdint>
 #include <memory>
 #include <optional>
 #include <vector>
@@ -24,14 +25,13 @@
 #include "perfetto/trace_processor/basic_types.h"
 #include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/types/destructible.h"
-#include "src/trace_processor/util/trace_type.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class AndroidLogEventParser;
 class ArgsTracker;
 class ArgsTranslationTable;
+class ArtMethodParser;
 class AsyncTrackSetTracker;
 class ChunkedTraceReader;
 class ClockConverter;
@@ -45,9 +45,12 @@
 class ForwardingTraceParser;
 class FtraceModule;
 class FuchsiaRecordParser;
+class GeckoTraceParser;
 class GlobalArgsTracker;
 class HeapGraphTracker;
+class InstrumentsRowParser;
 class JsonTraceParser;
+class LegacyV8CpuProfileTracker;
 class MachineTracker;
 class MappingTracker;
 class MetadataTracker;
@@ -55,6 +58,7 @@
 class PacketAnalyzer;
 class PerfRecordParser;
 class PerfSampleTracker;
+class PerfTextTraceParser;
 class ProcessTracker;
 class ProcessTrackTranslationTable;
 class ProtoImporterModule;
@@ -62,6 +66,7 @@
 class SchedEventTracker;
 class SliceTracker;
 class SliceTranslationTable;
+class SpeRecordParser;
 class StackProfileTracker;
 class TraceFileTracker;
 class TraceReaderRegistry;
@@ -79,13 +84,15 @@
     std::shared_ptr<TraceStorage> storage;
     uint32_t raw_machine_id = 0;
   };
+
   explicit TraceProcessorContext(const InitArgs&);
+
   // The default constructor is used in testing.
   TraceProcessorContext();
   ~TraceProcessorContext();
 
-  TraceProcessorContext(TraceProcessorContext&&) = default;
-  TraceProcessorContext& operator=(TraceProcessorContext&&) = default;
+  TraceProcessorContext(TraceProcessorContext&&);
+  TraceProcessorContext& operator=(TraceProcessorContext&&);
 
   Config config;
 
@@ -128,6 +135,7 @@
   std::unique_ptr<MetadataTracker> metadata_tracker;
   std::unique_ptr<CpuTracker> cpu_tracker;
   std::unique_ptr<TraceFileTracker> trace_file_tracker;
+  std::unique_ptr<LegacyV8CpuProfileTracker> legacy_v8_cpu_profile_tracker;
 
   // These fields are stored as pointers to Destructible objects rather than
   // their actual type (a subclass of Destructible), as the concrete subclass
@@ -135,25 +143,26 @@
   // the GetOrCreate() method on their subclass type, e.g.
   // SyscallTracker::GetOrCreate(context)
   // clang-format off
-  std::unique_ptr<Destructible> android_probes_tracker;    // AndroidProbesTracker
-  std::unique_ptr<Destructible> binder_tracker;            // BinderTracker
-  std::unique_ptr<Destructible> heap_graph_tracker;        // HeapGraphTracker
-  std::unique_ptr<Destructible> syscall_tracker;           // SyscallTracker
-  std::unique_ptr<Destructible> system_info_tracker;       // SystemInfoTracker
-  std::unique_ptr<Destructible> v4l2_tracker;              // V4l2Tracker
-  std::unique_ptr<Destructible> virtio_video_tracker;      // VirtioVideoTracker
-  std::unique_ptr<Destructible> systrace_parser;           // SystraceParser
-  std::unique_ptr<Destructible> thread_state_tracker;      // ThreadStateTracker
-  std::unique_ptr<Destructible> i2c_tracker;               // I2CTracker
-  std::unique_ptr<Destructible> perf_data_tracker;         // PerfDataTracker
-  std::unique_ptr<Destructible> content_analyzer;          // ProtoContentAnalyzer
-  std::unique_ptr<Destructible> shell_transitions_tracker; // ShellTransitionsTracker
-  std::unique_ptr<Destructible> protolog_messages_tracker; // ProtoLogMessagesTracker
-  std::unique_ptr<Destructible> ftrace_sched_tracker;      // FtraceSchedEventTracker
-  std::unique_ptr<Destructible> v8_tracker;                // V8Tracker
-  std::unique_ptr<Destructible> jit_tracker;               // JitTracker
-  std::unique_ptr<Destructible> perf_dso_tracker;          // DsoTracker
-  std::unique_ptr<Destructible> protolog_message_decoder;  // ProtoLogMessageDecoder
+  std::unique_ptr<Destructible> android_probes_tracker;       // AndroidProbesTracker
+  std::unique_ptr<Destructible> binder_tracker;               // BinderTracker
+  std::unique_ptr<Destructible> heap_graph_tracker;           // HeapGraphTracker
+  std::unique_ptr<Destructible> syscall_tracker;              // SyscallTracker
+  std::unique_ptr<Destructible> system_info_tracker;          // SystemInfoTracker
+  std::unique_ptr<Destructible> v4l2_tracker;                 // V4l2Tracker
+  std::unique_ptr<Destructible> virtio_video_tracker;         // VirtioVideoTracker
+  std::unique_ptr<Destructible> systrace_parser;              // SystraceParser
+  std::unique_ptr<Destructible> thread_state_tracker;         // ThreadStateTracker
+  std::unique_ptr<Destructible> i2c_tracker;                  // I2CTracker
+  std::unique_ptr<Destructible> perf_data_tracker;            // PerfDataTracker
+  std::unique_ptr<Destructible> content_analyzer;             // ProtoContentAnalyzer
+  std::unique_ptr<Destructible> shell_transitions_tracker;    // ShellTransitionsTracker
+  std::unique_ptr<Destructible> protolog_messages_tracker;    // ProtoLogMessagesTracker
+  std::unique_ptr<Destructible> ftrace_sched_tracker;         // FtraceSchedEventTracker
+  std::unique_ptr<Destructible> v8_tracker;                   // V8Tracker
+  std::unique_ptr<Destructible> jit_tracker;                  // JitTracker
+  std::unique_ptr<Destructible> perf_dso_tracker;             // DsoTracker
+  std::unique_ptr<Destructible> protolog_message_decoder;     // ProtoLogMessageDecoder
+  std::unique_ptr<Destructible> instruments_row_data_tracker; // RowDataTracker
   // clang-format on
 
   std::unique_ptr<ProtoTraceParser> proto_trace_parser;
@@ -164,7 +173,12 @@
   std::unique_ptr<JsonTraceParser> json_trace_parser;
   std::unique_ptr<FuchsiaRecordParser> fuchsia_record_parser;
   std::unique_ptr<PerfRecordParser> perf_record_parser;
+  std::unique_ptr<SpeRecordParser> spe_record_parser;
+  std::unique_ptr<InstrumentsRowParser> instruments_row_parser;
   std::unique_ptr<AndroidLogEventParser> android_log_event_parser;
+  std::unique_ptr<GeckoTraceParser> gecko_trace_parser;
+  std::unique_ptr<ArtMethodParser> art_method_parser;
+  std::unique_ptr<PerfTextTraceParser> perf_text_parser;
 
   // This field contains the list of proto descriptors that can be used by
   // reflection-based parsers.
@@ -192,7 +206,6 @@
   std::unique_ptr<MultiMachineTraceManager> multi_machine_trace_manager;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_TYPES_TRACE_PROCESSOR_CONTEXT_H_
diff --git a/src/trace_processor/util/BUILD.gn b/src/trace_processor/util/BUILD.gn
index b857bd0..73bdbcb 100644
--- a/src/trace_processor/util/BUILD.gn
+++ b/src/trace_processor/util/BUILD.gn
@@ -278,7 +278,18 @@
     "../../../gn:default_deps",
     "../../../include/perfetto/ext/base",
     "../../../protos/perfetto/trace:non_minimal_zero",
+    "../../protozero",
     "../importers/android_bugreport:android_log_event",
+    "../importers/perf_text:perf_text_sample_line_parser",
+  ]
+}
+
+source_set("winscope_proto_mapping") {
+  sources = ["winscope_proto_mapping.h"]
+  deps = [
+    "../../../gn:default_deps",
+    "../../../include/perfetto/ext/base:base",
+    "../tables",
   ]
 }
 
diff --git a/src/trace_processor/util/build_id.cc b/src/trace_processor/util/build_id.cc
index 20d76dd..d037e0e 100644
--- a/src/trace_processor/util/build_id.cc
+++ b/src/trace_processor/util/build_id.cc
@@ -83,6 +83,10 @@
   }
 
   while (it != hex.end()) {
+    if (*it == '-') {
+      ++it;
+      continue;
+    }
     int v = (HexToBinary(*it++) << 4);
     v += HexToBinary(*it++);
     res.push_back(static_cast<char>(v));
diff --git a/src/trace_processor/util/bump_allocator.h b/src/trace_processor/util/bump_allocator.h
index 985b5bc..9e92471 100644
--- a/src/trace_processor/util/bump_allocator.h
+++ b/src/trace_processor/util/bump_allocator.h
@@ -54,7 +54,7 @@
  public:
   // The limit on the total number of bits which can be used to represent
   // the chunk id.
-  static constexpr uint64_t kMaxIdBits = 60;
+  static constexpr uint64_t kMaxIdBits = 58;
 
   // The limit on the total amount of memory which can be allocated.
   static constexpr uint64_t kAllocLimit = 1ull << kMaxIdBits;
diff --git a/src/trace_processor/util/debug_annotation_parser.cc b/src/trace_processor/util/debug_annotation_parser.cc
index 0c2792e..6669b4d 100644
--- a/src/trace_processor/util/debug_annotation_parser.cc
+++ b/src/trace_processor/util/debug_annotation_parser.cc
@@ -36,7 +36,7 @@
   return result;
 }
 
-bool IsJsonSupported() {
+constexpr bool IsJsonSupported() {
 #if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
   return true;
 #else
diff --git a/src/trace_processor/util/descriptors.cc b/src/trace_processor/util/descriptors.cc
index 96833ed..8a1d35f 100644
--- a/src/trace_processor/util/descriptors.cc
+++ b/src/trace_processor/util/descriptors.cc
@@ -48,13 +48,17 @@
           ? static_cast<uint32_t>(f_decoder.type())
           : static_cast<uint32_t>(FieldDescriptorProto::TYPE_MESSAGE);
   protos::pbzero::FieldOptions::Decoder opt(f_decoder.options());
+  std::optional<std::string> default_value;
+  if (f_decoder.has_default_value()) {
+    default_value = f_decoder.default_value().ToStdString();
+  }
   return FieldDescriptor(
       base::StringView(f_decoder.name()).ToStdString(),
       static_cast<uint32_t>(f_decoder.number()), type, std::move(type_name),
       std::vector<uint8_t>(f_decoder.options().data,
                            f_decoder.options().data + f_decoder.options().size),
-      f_decoder.label() == FieldDescriptorProto::LABEL_REPEATED, opt.packed(),
-      is_extension);
+      default_value, f_decoder.label() == FieldDescriptorProto::LABEL_REPEATED,
+      opt.packed(), is_extension);
 }
 
 base::Status CheckExtensionField(const ProtoDescriptor& proto_descriptor,
@@ -443,6 +447,7 @@
                                  uint32_t type,
                                  std::string raw_type_name,
                                  std::vector<uint8_t> options,
+                                 std::optional<std::string> default_value,
                                  bool is_repeated,
                                  bool is_packed,
                                  bool is_extension)
@@ -451,6 +456,7 @@
       type_(type),
       raw_type_name_(std::move(raw_type_name)),
       options_(std::move(options)),
+      default_value_(std::move(default_value)),
       is_repeated_(is_repeated),
       is_packed_(is_packed),
       is_extension_(is_extension) {}
diff --git a/src/trace_processor/util/descriptors.h b/src/trace_processor/util/descriptors.h
index c99a72b..075455c 100644
--- a/src/trace_processor/util/descriptors.h
+++ b/src/trace_processor/util/descriptors.h
@@ -41,7 +41,8 @@
                   uint32_t number,
                   uint32_t type,
                   std::string raw_type_name,
-                  std::vector<uint8_t>,
+                  std::vector<uint8_t> options,
+                  std::optional<std::string> default_value,
                   bool is_repeated,
                   bool is_packed,
                   bool is_extension = false);
@@ -57,6 +58,9 @@
 
   const std::vector<uint8_t>& options() const { return options_; }
   std::vector<uint8_t>* mutable_options() { return &options_; }
+  const std::optional<std::string>& default_value() const {
+    return default_value_;
+  }
 
   void set_resolved_type_name(const std::string& resolved_type_name) {
     resolved_type_name_ = resolved_type_name;
@@ -69,6 +73,7 @@
   std::string raw_type_name_;
   std::string resolved_type_name_;
   std::vector<uint8_t> options_;
+  std::optional<std::string> default_value_;
   bool is_repeated_;
   bool is_packed_;
   bool is_extension_;
diff --git a/src/trace_processor/util/gzip_utils.cc b/src/trace_processor/util/gzip_utils.cc
index 3d76c33..50ea9db 100644
--- a/src/trace_processor/util/gzip_utils.cc
+++ b/src/trace_processor/util/gzip_utils.cc
@@ -32,14 +32,6 @@
 
 namespace perfetto::trace_processor::util {
 
-bool IsGzipSupported() {
-#if PERFETTO_BUILDFLAG(PERFETTO_ZLIB)
-  return true;
-#else
-  return false;
-#endif
-}
-
 #if PERFETTO_BUILDFLAG(PERFETTO_ZLIB)  // Real Implementation
 
 GzipDecompressor::GzipDecompressor(InputMode mode)
diff --git a/src/trace_processor/util/gzip_utils.h b/src/trace_processor/util/gzip_utils.h
index c9c9c32..8719bd8 100644
--- a/src/trace_processor/util/gzip_utils.h
+++ b/src/trace_processor/util/gzip_utils.h
@@ -22,15 +22,21 @@
 #include <memory>
 #include <vector>
 
+#include "perfetto/base/build_config.h"
+
 struct z_stream_s;
 
-namespace perfetto {
-namespace trace_processor {
-namespace util {
+namespace perfetto::trace_processor::util {
 
 // Returns whether gzip related functioanlity is supported with the current
 // build flags.
-bool IsGzipSupported();
+constexpr bool IsGzipSupported() {
+#if PERFETTO_BUILDFLAG(PERFETTO_ZLIB)
+  return true;
+#else
+  return false;
+#endif
+}
 
 // Usage: To decompress in a streaming way, there are two ways of using it:
 // 1. [Commonly used] - Feed the sequence of mem-blocks in 'FeedAndExtract' one
@@ -130,8 +136,6 @@
   std::unique_ptr<z_stream_s, Deleter> z_stream_;
 };
 
-}  // namespace util
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor::util
 
 #endif  // SRC_TRACE_PROCESSOR_UTIL_GZIP_UTILS_H_
diff --git a/src/trace_processor/util/proto_to_args_parser.cc b/src/trace_processor/util/proto_to_args_parser.cc
index e09359c..da2f15c 100644
--- a/src/trace_processor/util/proto_to_args_parser.cc
+++ b/src/trace_processor/util/proto_to_args_parser.cc
@@ -16,9 +16,11 @@
 
 #include "src/trace_processor/util/proto_to_args_parser.h"
 
-#include <stdint.h>
+#include <unordered_set>
 
 #include "perfetto/base/status.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/protozero/field.h"
 #include "perfetto/protozero/proto_decoder.h"
 #include "perfetto/protozero/proto_utils.h"
 #include "protos/perfetto/common/descriptor.pbzero.h"
@@ -40,6 +42,15 @@
   target += value;
 }
 
+bool IsFieldAllowed(const FieldDescriptor& field,
+                    const std::vector<uint32_t>* allowed_fields) {
+  // If allowlist is not provided, reflect all fields. Otherwise, check if the
+  // current field either an extension or is in allowlist.
+  return field.is_extension() || !allowed_fields ||
+         std::find(allowed_fields->begin(), allowed_fields->end(),
+                   field.number()) != allowed_fields->end();
+}
+
 }  // namespace
 
 ProtoToArgsParser::Key::Key() = default;
@@ -88,10 +99,11 @@
     const std::string& type,
     const std::vector<uint32_t>* allowed_fields,
     Delegate& delegate,
-    int* unknown_extensions) {
+    int* unknown_extensions,
+    bool add_defaults) {
   ScopedNestedKeyContext key_context(key_prefix_);
   return ParseMessageInternal(key_context, cb, type, allowed_fields, delegate,
-                              unknown_extensions);
+                              unknown_extensions, add_defaults);
 }
 
 base::Status ProtoToArgsParser::ParseMessageInternal(
@@ -100,7 +112,8 @@
     const std::string& type,
     const std::vector<uint32_t>* allowed_fields,
     Delegate& delegate,
-    int* unknown_extensions) {
+    int* unknown_extensions,
+    bool add_defaults) {
   if (auto override_result =
           MaybeApplyOverrideForType(type, key_context, cb, delegate)) {
     return override_result.value();
@@ -116,6 +129,7 @@
   std::unordered_map<size_t, int> repeated_field_index;
   bool empty_message = true;
   protozero::ProtoDecoder decoder(cb);
+  std::unordered_set<std::string_view> existing_fields;
   for (protozero::Field f = decoder.ReadField(); f.valid();
        f = decoder.ReadField()) {
     empty_message = false;
@@ -128,13 +142,11 @@
       continue;
     }
 
-    // If allowlist is not provided, reflect all fields. Otherwise, check if the
-    // current field either an extension or is in allowlist.
-    bool is_allowed = field->is_extension() || !allowed_fields ||
-                      std::find(allowed_fields->begin(), allowed_fields->end(),
-                                f.id()) != allowed_fields->end();
+    if (add_defaults) {
+      existing_fields.insert(field->name());
+    }
 
-    if (!is_allowed) {
+    if (!IsFieldAllowed(*field, allowed_fields)) {
       // Field is neither an extension, nor is allowed to be
       // reflected.
       continue;
@@ -143,12 +155,13 @@
     // Packed fields need to be handled specially because
     if (field->is_packed()) {
       RETURN_IF_ERROR(ParsePackedField(*field, repeated_field_index, f,
-                                       delegate, unknown_extensions));
+                                       delegate, unknown_extensions,
+                                       add_defaults));
       continue;
     }
 
     RETURN_IF_ERROR(ParseField(*field, repeated_field_index[f.id()], f,
-                               delegate, unknown_extensions));
+                               delegate, unknown_extensions, add_defaults));
     if (field->is_repeated()) {
       repeated_field_index[f.id()]++;
     }
@@ -156,6 +169,22 @@
 
   if (empty_message) {
     delegate.AddNull(key_prefix_);
+  } else if (add_defaults) {
+    for (const auto& [id, field] : descriptor.fields()) {
+      if (!IsFieldAllowed(field, allowed_fields)) {
+        continue;
+      }
+      const std::string& field_name = field.name();
+      bool field_exists =
+          existing_fields.find(field_name) != existing_fields.cend();
+      if (field_exists) {
+        continue;
+      }
+      ScopedNestedKeyContext key_context_default(key_prefix_);
+      AppendProtoType(key_prefix_.flat_key, field_name);
+      AppendProtoType(key_prefix_.key, field_name);
+      RETURN_IF_ERROR(AddDefault(field, delegate));
+    }
   }
 
   return base::OkStatus();
@@ -166,7 +195,8 @@
     int repeated_field_number,
     protozero::Field field,
     Delegate& delegate,
-    int* unknown_extensions) {
+    int* unknown_extensions,
+    bool add_defaults) {
   std::string prefix_part = field_descriptor.name();
   if (field_descriptor.is_repeated()) {
     std::string number = std::to_string(repeated_field_number);
@@ -197,7 +227,7 @@
       protos::pbzero::FieldDescriptorProto::TYPE_MESSAGE) {
     return ParseMessageInternal(key_context, field.as_bytes(),
                                 field_descriptor.resolved_type_name(), nullptr,
-                                delegate, unknown_extensions);
+                                delegate, unknown_extensions, add_defaults);
   }
   return ParseSimpleField(field_descriptor, field, delegate);
 }
@@ -207,7 +237,8 @@
     std::unordered_map<size_t, int>& repeated_field_index,
     protozero::Field field,
     Delegate& delegate,
-    int* unknown_extensions) {
+    int* unknown_extensions,
+    bool add_defaults) {
   using FieldDescriptorProto = protos::pbzero::FieldDescriptorProto;
   using PWT = protozero::proto_utils::ProtoWireType;
 
@@ -225,7 +256,7 @@
     protozero::Field f;
     f.initialize(field.id(), static_cast<uint8_t>(wire_type), new_value, 0);
     return ParseField(field_descriptor, repeated_field_index[field.id()]++, f,
-                      delegate, unknown_extensions);
+                      delegate, unknown_extensions, add_defaults);
   };
 
   const uint8_t* data = field.as_bytes().data;
@@ -335,30 +366,12 @@
     case FieldDescriptorProto::TYPE_STRING:
       delegate.AddString(key_prefix_, field.as_string());
       return base::OkStatus();
-    case FieldDescriptorProto::TYPE_ENUM: {
-      auto opt_enum_descriptor_idx =
-          pool_.FindDescriptorIdx(descriptor.resolved_type_name());
-      if (!opt_enum_descriptor_idx) {
-        delegate.AddInteger(key_prefix_, field.as_int32());
-        return base::OkStatus();
-      }
-      auto opt_enum_string =
-          pool_.descriptors()[*opt_enum_descriptor_idx].FindEnumString(
-              field.as_int32());
-      if (!opt_enum_string) {
-        // Fall back to the integer representation of the field.
-        delegate.AddInteger(key_prefix_, field.as_int32());
-        return base::OkStatus();
-      }
-      delegate.AddString(key_prefix_,
-                         protozero::ConstChars{opt_enum_string->data(),
-                                               opt_enum_string->size()});
-      return base::OkStatus();
-    }
+    case FieldDescriptorProto::TYPE_ENUM:
+      return AddEnum(descriptor, field.as_int32(), delegate);
     default:
       return base::ErrStatus(
           "Tried to write value of type field %s (in proto type "
-          "%s) which has type enum %d",
+          "%s) which has type enum %u",
           descriptor.name().c_str(), descriptor.resolved_type_name().c_str(),
           descriptor.type());
   }
@@ -379,6 +392,108 @@
   return context;
 }
 
+base::Status ProtoToArgsParser::AddDefault(const FieldDescriptor& descriptor,
+                                           Delegate& delegate) {
+  using FieldDescriptorProto = protos::pbzero::FieldDescriptorProto;
+  if (descriptor.is_repeated()) {
+    delegate.AddNull(key_prefix_);
+    return base::OkStatus();
+  }
+  const auto& default_value = descriptor.default_value();
+  const auto& default_value_if_number =
+      default_value ? default_value.value() : "0";
+  switch (descriptor.type()) {
+    case FieldDescriptorProto::TYPE_INT32:
+    case FieldDescriptorProto::TYPE_SFIXED32:
+      delegate.AddInteger(key_prefix_,
+                          base::StringToInt32(default_value_if_number).value());
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_SINT32:
+      delegate.AddInteger(
+          key_prefix_,
+          protozero::proto_utils::ZigZagDecode(
+              base::StringToInt64(default_value_if_number).value()));
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_INT64:
+    case FieldDescriptorProto::TYPE_SFIXED64:
+      delegate.AddInteger(key_prefix_,
+                          base::StringToInt64(default_value_if_number).value());
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_SINT64:
+      delegate.AddInteger(
+          key_prefix_,
+          protozero::proto_utils::ZigZagDecode(
+              base::StringToInt64(default_value_if_number).value()));
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_UINT32:
+    case FieldDescriptorProto::TYPE_FIXED32:
+      delegate.AddUnsignedInteger(
+          key_prefix_, base::StringToUInt32(default_value_if_number).value());
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_UINT64:
+    case FieldDescriptorProto::TYPE_FIXED64:
+      delegate.AddUnsignedInteger(
+          key_prefix_, base::StringToUInt64(default_value_if_number).value());
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_BOOL:
+      delegate.AddBoolean(key_prefix_, default_value == "true");
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_DOUBLE:
+    case FieldDescriptorProto::TYPE_FLOAT:
+      delegate.AddDouble(key_prefix_,
+                         base::StringToDouble(default_value_if_number).value());
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_BYTES:
+      delegate.AddBytes(key_prefix_, protozero::ConstBytes{});
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_STRING:
+      if (default_value) {
+        delegate.AddString(key_prefix_, default_value.value());
+      } else {
+        delegate.AddNull(key_prefix_);
+      }
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_MESSAGE:
+      delegate.AddNull(key_prefix_);
+      return base::OkStatus();
+    case FieldDescriptorProto::TYPE_ENUM:
+      return AddEnum(descriptor,
+                     base::StringToInt32(default_value_if_number).value(),
+                     delegate);
+    default:
+      return base::ErrStatus(
+          "Tried to write default value of type field %s (in proto type "
+          "%s) which has type enum %u",
+          descriptor.name().c_str(), descriptor.resolved_type_name().c_str(),
+          descriptor.type());
+  }
+}
+
+base::Status ProtoToArgsParser::AddEnum(const FieldDescriptor& descriptor,
+                                        int32_t value,
+                                        Delegate& delegate) {
+  auto opt_enum_descriptor_idx =
+      pool_.FindDescriptorIdx(descriptor.resolved_type_name());
+  if (!opt_enum_descriptor_idx) {
+    delegate.AddInteger(key_prefix_, value);
+    return base::OkStatus();
+  }
+  auto opt_enum_string =
+      pool_.descriptors()[*opt_enum_descriptor_idx].FindEnumString(value);
+  if (!opt_enum_string) {
+    // Fall back to the integer representation of the field.
+    // We add the string representation of the int value here in order that
+    // EXTRACT_ARG() should return consistent types under error conditions and
+    // that CREATE PERFETTO TABLE AS EXTRACT_ARG(...) should be generally safe
+    // to use.
+    delegate.AddString(key_prefix_, std::to_string(value));
+    return base::OkStatus();
+  }
+  delegate.AddString(
+      key_prefix_,
+      protozero::ConstChars{opt_enum_string->data(), opt_enum_string->size()});
+  return base::OkStatus();
+}
 }  // namespace util
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/util/proto_to_args_parser.h b/src/trace_processor/util/proto_to_args_parser.h
index b709a40..427ed41 100644
--- a/src/trace_processor/util/proto_to_args_parser.h
+++ b/src/trace_processor/util/proto_to_args_parser.h
@@ -147,7 +147,8 @@
                             const std::string& type,
                             const std::vector<uint32_t>* allowed_fields,
                             Delegate& delegate,
-                            int* unknown_extensions = nullptr);
+                            int* unknown_extensions = nullptr,
+                            bool add_defaults = false);
 
   // This class is responsible for resetting the current key prefix to the old
   // value when deleted or reset.
@@ -249,14 +250,16 @@
                           int repeated_field_number,
                           protozero::Field field,
                           Delegate& delegate,
-                          int* unknown_extensions);
+                          int* unknown_extensions,
+                          bool add_defaults);
 
   base::Status ParsePackedField(
       const FieldDescriptor& field_descriptor,
       std::unordered_map<size_t, int>& repeated_field_index,
       protozero::Field field,
       Delegate& delegate,
-      int* unknown_extensions);
+      int* unknown_extensions,
+      bool add_defaults);
 
   std::optional<base::Status> MaybeApplyOverrideForField(
       const protozero::Field&,
@@ -275,12 +278,19 @@
                                     const std::string& type,
                                     const std::vector<uint32_t>* fields,
                                     Delegate& delegate,
-                                    int* unknown_extensions);
+                                    int* unknown_extensions,
+                                    bool add_defaults = false);
 
   base::Status ParseSimpleField(const FieldDescriptor& desciptor,
                                 const protozero::Field& field,
                                 Delegate& delegate);
 
+  base::Status AddDefault(const FieldDescriptor& desciptor, Delegate& delegate);
+
+  base::Status AddEnum(const FieldDescriptor& descriptor,
+                       int32_t value,
+                       Delegate& delegate);
+
   std::unordered_map<std::string, ParsingOverrideForField> field_overrides_;
   std::unordered_map<std::string, ParsingOverrideForType> type_overrides_;
   const DescriptorPool& pool_;
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 5baf8f0..3dae057 100644
--- a/src/trace_processor/util/proto_to_args_parser_unittest.cc
+++ b/src/trace_processor/util/proto_to_args_parser_unittest.cc
@@ -662,6 +662,54 @@
           "field_double field_double[3] 1.79769e+308"));
 }
 
+TEST_F(ProtoToArgsParserTest, AddsDefaults) {
+  using namespace protozero::test::protos::pbzero;
+  protozero::HeapBuffered<EveryField> msg{kChunkSize, kChunkSize};
+  msg->set_field_int32(-1);
+  msg->add_repeated_string("test");
+  msg->add_repeated_sfixed32(1);
+  msg->add_repeated_fixed64(1);
+  msg->set_nested_enum(EveryField::PONG);
+
+  auto binary_proto = msg.SerializeAsArray();
+
+  DescriptorPool pool;
+  auto status = pool.AddFromFileDescriptorSet(kTestMessagesDescriptor.data(),
+                                              kTestMessagesDescriptor.size());
+  ProtoToArgsParser parser(pool);
+  ASSERT_TRUE(status.ok()) << "Failed to parse kTestMessagesDescriptor: "
+                           << status.message();
+
+  status = parser.ParseMessage(
+      protozero::ConstBytes{binary_proto.data(), binary_proto.size()},
+      ".protozero.test.protos.EveryField", nullptr, *this, nullptr, true);
+
+  EXPECT_TRUE(status.ok()) << "AddsDefaults failed with error: "
+                           << status.message();
+
+  EXPECT_THAT(
+      args(),
+      testing::UnorderedElementsAre(
+          "field_int32 field_int32 -1",  // exists in message
+          "repeated_string repeated_string[0] test",
+          "repeated_sfixed32 repeated_sfixed32[0] 1",
+          "repeated_fixed64 repeated_fixed64[0] 1",
+          "nested_enum nested_enum PONG",
+          "field_bytes field_bytes <bytes size=0>",
+          "field_string field_string [NULL]",  // null if no string default
+          "field_nested field_nested [NULL]",  // no defaults for inner fields
+          "field_bool field_bool false",
+          "repeated_int32 repeated_int32 [NULL]",  // null for repeated fields
+          "field_double field_double 0", "field_float field_float 0",
+          "field_sfixed64 field_sfixed64 0", "field_sfixed32 field_sfixed32 0",
+          "field_fixed64 field_fixed64 0", "field_sint64 field_sint64 0",
+          "big_enum big_enum 0", "field_fixed32 field_fixed32 0",
+          "field_sint32 field_sint32 0",
+          "signed_enum signed_enum NEUTRAL",  // translates default enum
+          "small_enum small_enum NOT_TO_BE", "field_uint64 field_uint64 0",
+          "field_uint32 field_uint32 0", "field_int64 field_int64 0"));
+}
+
 }  // namespace
 }  // namespace util
 }  // namespace trace_processor
diff --git a/src/trace_processor/util/sql_modules.h b/src/trace_processor/util/sql_modules.h
index 997acb0..e060fb0 100644
--- a/src/trace_processor/util/sql_modules.h
+++ b/src/trace_processor/util/sql_modules.h
@@ -20,26 +20,22 @@
 #include <string>
 
 #include "perfetto/ext/base/flat_hash_map.h"
-#include "perfetto/ext/base/string_splitter.h"
 #include "perfetto/ext/base/string_view.h"
-#include "perfetto/trace_processor/basic_types.h"
 
-namespace perfetto {
-namespace trace_processor {
-namespace sql_modules {
+namespace perfetto ::trace_processor::sql_modules {
 
-using NameToModule =
+using NameToPackage =
     base::FlatHashMap<std::string,
                       std::vector<std::pair<std::string, std::string>>>;
 
 // Map from include key to sql file. Include key is the string used in INCLUDE
 // function.
-struct RegisteredModule {
+struct RegisteredPackage {
   struct ModuleFile {
     std::string sql;
     bool included;
   };
-  base::FlatHashMap<std::string, ModuleFile> include_key_to_file;
+  base::FlatHashMap<std::string, ModuleFile> modules;
 };
 
 inline std::string ReplaceSlashWithDot(std::string str) {
@@ -51,13 +47,13 @@
   return str;
 }
 
-inline std::string GetIncludeKey(std::string path) {
+inline std::string GetIncludeKey(const std::string& path) {
   base::StringView path_view(path);
   auto path_no_extension = path_view.substr(0, path_view.rfind('.'));
   return ReplaceSlashWithDot(path_no_extension.ToStdString());
 }
 
-inline std::string GetModuleName(std::string str) {
+inline std::string GetPackageName(const std::string& str) {
   size_t found = str.find('.');
   if (found == std::string::npos) {
     return str;
@@ -65,7 +61,5 @@
   return str.substr(0, found);
 }
 
-}  // namespace sql_modules
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor::sql_modules
 #endif  // SRC_TRACE_PROCESSOR_UTIL_SQL_MODULES_H_
diff --git a/src/trace_processor/util/trace_blob_view_reader.cc b/src/trace_processor/util/trace_blob_view_reader.cc
index 4ba5b13..acdc898 100644
--- a/src/trace_processor/util/trace_blob_view_reader.cc
+++ b/src/trace_processor/util/trace_blob_view_reader.cc
@@ -21,8 +21,10 @@
 #include <cstddef>
 #include <cstdint>
 #include <cstring>
+#include <iterator>
 #include <optional>
 #include <utility>
+#include <vector>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/public/compiler.h"
@@ -59,9 +61,15 @@
   return target_offset == end_offset_;
 }
 
-std::optional<TraceBlobView> TraceBlobViewReader::SliceOff(
-    size_t offset,
-    size_t length) const {
+template <typename Visitor>
+auto TraceBlobViewReader::SliceOffImpl(const size_t offset,
+                                       const size_t length,
+                                       Visitor& visitor) const {
+  // If the length is zero, then a zero-sized blob view is always appropiate.
+  if (PERFETTO_UNLIKELY(length == 0)) {
+    return visitor.OneSlice(TraceBlobView());
+  }
+
   PERFETTO_DCHECK(offset >= start_offset());
 
   // Fast path: the slice fits entirely inside the first TBV, we can just slice
@@ -71,52 +79,105 @@
       !data_.empty() &&
       offset + length <= data_.front().start_offset + data_.front().data.size();
   if (PERFETTO_LIKELY(is_fast_path)) {
-    return data_.front().data.slice_off(offset - data_.front().start_offset,
-                                        length);
-  }
-
-  // If the length is zero, then a zero-sized blob view is always approrpriate.
-  if (PERFETTO_UNLIKELY(length == 0)) {
-    return TraceBlobView();
+    return visitor.OneSlice(data_.front().data.slice_off(
+        offset - data_.front().start_offset, length));
   }
 
   // If we don't have any TBVs or the end of the slice does not fit, then we
   // cannot possibly return a full slice.
   if (PERFETTO_UNLIKELY(data_.empty() || offset + length > end_offset_)) {
-    return std::nullopt;
+    return visitor.NoData();
   }
 
   // Find the first block finishes *after* start_offset i.e. there is at least
   // one byte in that block which will end up in the slice. We know this *must*
   // exist because of the above check.
-  auto rit = std::upper_bound(
+  auto it = std::upper_bound(
       data_.begin(), data_.end(), offset, [](size_t offset, const Entry& rhs) {
         return offset < rhs.start_offset + rhs.data.size();
       });
-  PERFETTO_CHECK(rit != data_.end());
+  PERFETTO_CHECK(it != data_.end());
 
   // If the slice fits entirely in the block we found, then just slice that
   // block avoiding any copies.
-  size_t rel_off = offset - rit->start_offset;
-  if (rel_off + length <= rit->data.size()) {
-    return rit->data.slice_off(rel_off, length);
+  size_t rel_off = offset - it->start_offset;
+  if (rel_off + length <= it->data.size()) {
+    return visitor.OneSlice(it->data.slice_off(rel_off, length));
   }
 
-  // Otherwise, allocate some memory and make a copy.
-  auto buffer = TraceBlob::Allocate(length);
-  uint8_t* ptr = buffer.data();
-  uint8_t* end = buffer.data() + buffer.size();
+  auto res = visitor.StartMultiSlice(length);
 
-  // Copy all bytes in this block which overlap with the slice.
-  memcpy(ptr, rit->data.data() + rel_off, rit->data.length() - rel_off);
-  ptr += rit->data.length() - rel_off;
+  size_t res_offset = 0;
+  size_t left = length;
 
-  for (auto it = rit + 1; ptr != end; ++it) {
-    auto len = std::min(static_cast<size_t>(end - ptr), it->data.size());
-    memcpy(ptr, it->data.data(), len);
-    ptr += len;
+  size_t size = it->data.length() - rel_off;
+  visitor.AddSlice(res, res_offset, it->data.slice_off(rel_off, size));
+  left -= size;
+  res_offset += size;
+
+  for (++it; left != 0; ++it) {
+    size = std::min(left, it->data.size());
+    visitor.AddSlice(res, res_offset, it->data.slice_off(0, size));
+    left -= size;
+    res_offset += size;
   }
-  return TraceBlobView(std::move(buffer));
+
+  return visitor.Finalize(std::move(res));
+}
+
+std::optional<TraceBlobView> TraceBlobViewReader::SliceOff(
+    size_t offset,
+    size_t length) const {
+  struct Visitor {
+    std::optional<TraceBlobView> NoData() { return std::nullopt; }
+
+    std::optional<TraceBlobView> OneSlice(TraceBlobView tbv) {
+      return std::move(tbv);
+    }
+
+    TraceBlob StartMultiSlice(size_t length) {
+      return TraceBlob::Allocate(length);
+    }
+
+    void AddSlice(TraceBlob& blob, size_t offset, TraceBlobView tbv) {
+      memcpy(blob.data() + offset, tbv.data(), tbv.size());
+    }
+
+    std::optional<TraceBlobView> Finalize(TraceBlob blob) {
+      return TraceBlobView(std::move(blob));
+    }
+
+  } visitor;
+
+  return SliceOffImpl(offset, length, visitor);
+}
+
+std::vector<TraceBlobView> TraceBlobViewReader::MultiSliceOff(
+    size_t offset,
+    size_t length) const {
+  struct Visitor {
+    std::vector<TraceBlobView> NoData() { return {}; }
+
+    std::vector<TraceBlobView> OneSlice(TraceBlobView tbv) {
+      std::vector<TraceBlobView> res;
+      res.reserve(1);
+      res.push_back(std::move(tbv));
+      return res;
+    }
+
+    std::vector<TraceBlobView> StartMultiSlice(size_t) { return {}; }
+
+    void AddSlice(std::vector<TraceBlobView>& vec, size_t, TraceBlobView tbv) {
+      vec.push_back(std::move(tbv));
+    }
+
+    std::vector<TraceBlobView> Finalize(std::vector<TraceBlobView> vec) {
+      return vec;
+    }
+
+  } visitor;
+
+  return SliceOffImpl(offset, length, visitor);
 }
 
 }  // namespace perfetto::trace_processor::util
diff --git a/src/trace_processor/util/trace_blob_view_reader.h b/src/trace_processor/util/trace_blob_view_reader.h
index c39ffab..76fac2d 100644
--- a/src/trace_processor/util/trace_blob_view_reader.h
+++ b/src/trace_processor/util/trace_blob_view_reader.h
@@ -18,9 +18,15 @@
 #define SRC_TRACE_PROCESSOR_UTIL_TRACE_BLOB_VIEW_READER_H_
 
 #include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <cstring>
 #include <optional>
+#include <string_view>
 
+#include "perfetto/base/logging.h"
 #include "perfetto/ext/base/circular_queue.h"
+#include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 
 namespace perfetto::trace_processor::util {
@@ -31,7 +37,129 @@
 //  2) Stitching together the cross-chunk spanning pieces.
 //  3) Dropping data when it is no longer necessary to be buffered.
 class TraceBlobViewReader {
+ private:
+  struct Entry {
+    // File offset of the first byte in `data`.
+    size_t start_offset;
+    TraceBlobView data;
+    size_t end_offset() const { return start_offset + data.size(); }
+  };
+
  public:
+  class Iterator {
+   public:
+    ~Iterator() = default;
+
+    Iterator(const Iterator&) = default;
+    Iterator& operator=(const Iterator&) = default;
+
+    Iterator(Iterator&&) = default;
+    Iterator& operator=(Iterator&&) = default;
+
+    // Tries to advance the iterator `size` bytes forward. Returns true if
+    // the advance was successful and false if it would overflow the iterator.
+    // If false is returned, the state of the iterator is not changed.
+    bool MaybeAdvance(size_t delta) {
+      file_offset_ += delta;
+      if (PERFETTO_LIKELY(file_offset_ < iter_->end_offset())) {
+        return true;
+      }
+      if (file_offset_ == end_offset_) {
+        return true;
+      }
+      if (file_offset_ > end_offset_) {
+        file_offset_ -= delta;
+        return false;
+      }
+      do {
+        ++iter_;
+      } while (file_offset_ >= iter_->end_offset());
+      return true;
+    }
+
+    // Tries to read `size` bytes from the iterator.  Returns a TraceBlobView
+    // containing the data if `size` bytes were available and std::nullopt
+    // otherwise. If std::nullopt is returned, the state of the iterator is not
+    // changed.
+    std::optional<TraceBlobView> MaybeRead(size_t delta) {
+      std::optional<TraceBlobView> tbv =
+          reader_->SliceOff(file_offset(), delta);
+      if (PERFETTO_LIKELY(tbv)) {
+        PERFETTO_CHECK(MaybeAdvance(delta));
+      }
+      return tbv;
+    }
+
+    // Tries to find a byte equal to |chr| in the iterator and, if found,
+    // advance to it. Returns a TraceBlobView containing the data if the byte
+    // was found and could be advanced to and std::nullopt if no such byte was
+    // found before the end of the iterator. If std::nullopt is returned, the
+    // state of the iterator is not changed.
+    std::optional<TraceBlobView> MaybeFindAndRead(uint8_t chr) {
+      size_t begin = file_offset();
+      if (!MaybeFindAndAdvanceInner(chr)) {
+        return std::nullopt;
+      }
+      std::optional<TraceBlobView> tbv =
+          reader_->SliceOff(begin, file_offset() - begin);
+      PERFETTO_CHECK(tbv);
+      PERFETTO_CHECK(MaybeAdvance(1));
+      return tbv;
+    }
+
+    uint8_t operator*() const {
+      PERFETTO_DCHECK(file_offset_ < iter_->end_offset());
+      return iter_->data.data()[file_offset_ - iter_->start_offset];
+    }
+
+    explicit operator bool() const { return file_offset_ != end_offset_; }
+
+    size_t file_offset() const { return file_offset_; }
+
+   private:
+    friend TraceBlobViewReader;
+
+    Iterator(const TraceBlobViewReader* reader,
+             base::CircularQueue<Entry>::Iterator iter,
+             size_t file_offset,
+             size_t end_offset)
+        : reader_(reader),
+          iter_(iter),
+          file_offset_(file_offset),
+          end_offset_(end_offset) {}
+
+    // Tries to find a byte equal to |chr| in the iterator and, if found,
+    // advance to it. Returns true if the byte was found and could be advanced
+    // to and false if no such byte was found before the end of the iterator. If
+    // false is returned, the state of the iterator is not changed.
+    bool MaybeFindAndAdvanceInner(uint8_t chr) {
+      size_t off = file_offset_;
+      while (off < end_offset_) {
+        size_t iter_off = off - iter_->start_offset;
+        size_t iter_rem = iter_->data.size() - iter_off;
+        const auto* p = reinterpret_cast<const uint8_t*>(
+            memchr(iter_->data.data() + iter_off, chr, iter_rem));
+        if (p) {
+          file_offset_ =
+              iter_->start_offset + static_cast<size_t>(p - iter_->data.data());
+          return true;
+        }
+        off = iter_->end_offset();
+        ++iter_;
+      }
+      return false;
+    }
+
+    const TraceBlobViewReader* reader_;
+    base::CircularQueue<Entry>::Iterator iter_;
+    size_t file_offset_;
+    size_t end_offset_;
+  };
+
+  Iterator GetIterator() const {
+    return {this, data_.begin(), start_offset(), end_offset()};
+  }
+
   // Adds a `TraceBlobView` at the back.
   void PushBack(TraceBlobView);
 
@@ -39,12 +167,18 @@
   // given offset is reached. If not enough data is present as much data as
   // possible will be dropped and `false` will be returned.
   //
-  // NOTE: If `offset` < 'file_offset()' this method will CHECK fail.
+  // Note:
+  //  * if `offset` < 'file_offset()' this method will CHECK fail.
+  //  * calling this function invalidates all iterators created from this
+  //  reader.
   bool PopFrontUntil(size_t offset);
 
   // Shrinks the buffer by dropping `bytes` from the front of the buffer. If not
   // enough data is present as much data as possible will be dropped and `false`
   // will be returned.
+  //
+  // Note: calling this function invalidates all iterators created from this
+  // reader.
   bool PopFrontBytes(size_t bytes) {
     return PopFrontUntil(start_offset() + bytes);
   }
@@ -59,6 +193,11 @@
   // NOTE: If `offset` < 'file_offset()' this method will CHECK fail.
   std::optional<TraceBlobView> SliceOff(size_t offset, size_t length) const;
 
+  // Similar to SliceOff but this method will not combine slices but instead
+  // potentially return multiple chunks. Useful if we are extracting slices to
+  // forward them to a `ChunkedTraceReader`.
+  std::vector<TraceBlobView> MultiSliceOff(size_t offset, size_t length) const;
+
   // Returns the offset to the start of the available data.
   size_t start_offset() const {
     return data_.empty() ? end_offset_ : data_.front().start_offset;
@@ -73,12 +212,8 @@
   bool empty() const { return data_.empty(); }
 
  private:
-  struct Entry {
-    // File offset of the first byte in `data`.
-    size_t start_offset;
-    TraceBlobView data;
-  };
-  using Iterator = base::CircularQueue<Entry>::Iterator;
+  template <typename Visitor>
+  auto SliceOffImpl(size_t offset, size_t length, Visitor& visitor) const;
 
   // CircularQueue has no const_iterator, so mutable is needed to access it from
   // const methods.
diff --git a/src/trace_processor/util/trace_type.cc b/src/trace_processor/util/trace_type.cc
index 4c4b042..bec7279 100644
--- a/src/trace_processor/util/trace_type.cc
+++ b/src/trace_processor/util/trace_type.cc
@@ -17,14 +17,16 @@
 #include "src/trace_processor/util/trace_type.h"
 
 #include <algorithm>
+#include <cctype>
 #include <cstddef>
 #include <cstdint>
 #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_utils.h"
 #include "src/trace_processor/importers/android_bugreport/android_log_event.h"
+#include "src/trace_processor/importers/perf_text/perf_text_sample_line_parser.h"
 
 #include "protos/perfetto/trace/trace.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
@@ -36,10 +38,12 @@
 constexpr char kFuchsiaMagic[] = {'\x10', '\x00', '\x04', '\x46',
                                   '\x78', '\x54', '\x16', '\x00'};
 constexpr char kPerfMagic[] = {'P', 'E', 'R', 'F', 'I', 'L', 'E', '2'};
-
 constexpr char kZipMagic[] = {'P', 'K', '\x03', '\x04'};
-
 constexpr char kGzipMagic[] = {'\x1f', '\x8b'};
+constexpr char kArtMethodStreamingMagic[] = {'S', 'L', 'O', 'W'};
+constexpr char kTarPosixMagic[] = {'u', 's', 't', 'a', 'r', '\0'};
+constexpr char kTarGnuMagic[] = {'u', 's', 't', 'a', 'r', ' ', ' ', '\0'};
+constexpr size_t kTarMagicOffset = 257;
 
 constexpr uint8_t kTracePacketTag =
     protozero::proto_utils::MakeTagLengthDelimited(
@@ -58,11 +62,15 @@
 }
 
 template <size_t N>
-bool MatchesMagic(const uint8_t* data, size_t size, const char (&magic)[N]) {
-  if (size < N) {
+bool MatchesMagic(const uint8_t* data,
+                  size_t size,
+                  const char (&magic)[N],
+                  size_t offset = 0) {
+  if (size < N + offset) {
     return false;
   }
-  return memcmp(data, magic, N) == 0;
+
+  return memcmp(data + offset, magic, N) == 0;
 }
 
 bool IsProtoTraceWithSymbols(const uint8_t* ptr, size_t size) {
@@ -119,14 +127,24 @@
       return "zip";
     case kPerfDataTraceType:
       return "perf";
+    case kInstrumentsXmlTraceType:
+      return "instruments_xml";
     case kAndroidLogcatTraceType:
       return "android_logcat";
     case kAndroidDumpstateTraceType:
       return "android_dumpstate";
     case kAndroidBugreportTraceType:
       return "android_bugreport";
+    case kGeckoTraceType:
+      return "gecko";
+    case kArtMethodTraceType:
+      return "art_method";
+    case kPerfTextTraceType:
+      return "perf_text";
     case kUnknownTraceType:
       return "unknown";
+    case kTarTraceType:
+      return "tar";
   }
   PERFETTO_FATAL("For GCC");
 }
@@ -136,6 +154,14 @@
     return kUnknownTraceType;
   }
 
+  if (MatchesMagic(data, size, kTarPosixMagic, kTarMagicOffset)) {
+    return kTarTraceType;
+  }
+
+  if (MatchesMagic(data, size, kTarGnuMagic, kTarMagicOffset)) {
+    return kTarTraceType;
+  }
+
   if (MatchesMagic(data, size, kFuchsiaMagic)) {
     return kFuchsiaTraceType;
   }
@@ -152,15 +178,29 @@
     return kGzipTraceType;
   }
 
+  if (MatchesMagic(data, size, kArtMethodStreamingMagic)) {
+    return kArtMethodTraceType;
+  }
+
   std::string start(reinterpret_cast<const char*>(data),
                     std::min<size_t>(size, kGuessTraceMaxLookahead));
 
   std::string start_minus_white_space = RemoveWhitespace(start);
+  // Generated by the Gecko conversion script built into perf.
+  if (base::StartsWith(start_minus_white_space, "{\"meta\""))
+    return kGeckoTraceType;
+  // Generated by the simpleperf conversion script.
+  if (base::StartsWith(start_minus_white_space, "{\"libs\""))
+    return kGeckoTraceType;
   if (base::StartsWith(start_minus_white_space, "{\""))
     return kJsonTraceType;
   if (base::StartsWith(start_minus_white_space, "[{\""))
     return kJsonTraceType;
 
+  // ART method traces (non-streaming).
+  if (base::StartsWith(start, "*version\n"))
+    return kArtMethodTraceType;
+
   // Systrace with header but no leading HTML.
   if (base::Contains(start, "# tracer"))
     return kSystraceTraceType;
@@ -172,6 +212,10 @@
       base::StartsWith(lower_start, "<html>"))
     return kSystraceTraceType;
 
+  // MacOS Instruments XML export.
+  if (base::StartsWith(start, "<?xml version=\"1.0\"?>\n<trace-query-result>"))
+    return kInstrumentsXmlTraceType;
+
   // Traces obtained from atrace -z (compress).
   // They all have the string "TRACE:" followed by 78 9C which is a zlib header
   // for "deflate, default compression, window size=32K" (see b/208691037)
@@ -190,6 +234,10 @@
     return kAndroidLogcatTraceType;
   }
 
+  // Perf text format.
+  if (perf_text_importer::IsPerfTextFormatTrace(data, size))
+    return kPerfTextTraceType;
+
   // Systrace with no header or leading HTML.
   if (base::StartsWith(start, " "))
     return kSystraceTraceType;
diff --git a/src/trace_processor/util/trace_type.h b/src/trace_processor/util/trace_type.h
index 7e730fc..80d8ff4 100644
--- a/src/trace_processor/util/trace_type.h
+++ b/src/trace_processor/util/trace_type.h
@@ -37,6 +37,11 @@
   kSystraceTraceType,
   kUnknownTraceType,
   kZipFile,
+  kInstrumentsXmlTraceType,
+  kGeckoTraceType,
+  kArtMethodTraceType,
+  kPerfTextTraceType,
+  kTarTraceType,
 };
 
 constexpr size_t kGuessTraceMaxLookahead = 64;
diff --git a/src/trace_processor/util/winscope_proto_mapping.h b/src/trace_processor/util/winscope_proto_mapping.h
new file mode 100644
index 0000000..b9bd320
--- /dev/null
+++ b/src/trace_processor/util/winscope_proto_mapping.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_UTIL_WINSCOPE_PROTO_MAPPING_H_
+#define SRC_TRACE_PROCESSOR_UTIL_WINSCOPE_PROTO_MAPPING_H_
+
+#include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/tables/android_tables_py.h"
+#include "src/trace_processor/tables/winscope_tables_py.h"
+
+namespace perfetto::trace_processor {
+namespace util {
+namespace winscope_proto_mapping {
+inline base::StatusOr<const char* const> GetProtoName(
+    const std::string& table_name) {
+  if (table_name == tables::SurfaceFlingerLayerTable::Name()) {
+    return ".perfetto.protos.LayerProto";
+  }
+  if (table_name == tables::SurfaceFlingerLayersSnapshotTable::Name()) {
+    return ".perfetto.protos.LayersSnapshotProto";
+  }
+  if (table_name == tables::SurfaceFlingerTransactionsTable::Name()) {
+    return ".perfetto.protos.TransactionTraceEntry";
+  }
+  if (table_name == tables::WindowManagerShellTransitionsTable::Name()) {
+    return ".perfetto.protos.ShellTransition";
+  }
+  if (table_name == tables::InputMethodClientsTable::Name()) {
+    return ".perfetto.protos.InputMethodClientsTraceProto";
+  }
+  if (table_name == tables::InputMethodManagerServiceTable::Name()) {
+    return ".perfetto.protos.InputMethodManagerServiceTraceProto";
+  }
+  if (table_name == tables::InputMethodServiceTable::Name()) {
+    return ".perfetto.protos.InputMethodServiceTraceProto";
+  }
+  if (table_name == tables::ViewCaptureTable::Name()) {
+    return ".perfetto.protos.ViewCapture";
+  }
+  if (table_name == tables::WindowManagerTable::Name()) {
+    return ".perfetto.protos.WindowManagerTraceEntry";
+  }
+  if (table_name == tables::AndroidKeyEventsTable::Name()) {
+    return ".perfetto.protos.AndroidKeyEvent";
+  }
+  if (table_name == tables::AndroidMotionEventsTable::Name()) {
+    return ".perfetto.protos.AndroidMotionEvent";
+  }
+  if (table_name == tables::AndroidInputEventDispatchTable::Name()) {
+    return ".perfetto.protos.AndroidWindowInputDispatchEvent";
+  }
+  return base::ErrStatus("%s table does not have proto descriptor.",
+                         table_name.c_str());
+}
+
+inline std::optional<const std::vector<uint32_t>> GetAllowedFields(
+    const std::string& table_name) {
+  if (table_name == tables::SurfaceFlingerLayersSnapshotTable::Name()) {
+    return std::vector<uint32_t>({1, 2, 4, 5, 6, 7, 8});
+  }
+  return std::nullopt;
+}
+}  // namespace winscope_proto_mapping
+}  // namespace util
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_UTIL_WINSCOPE_PROTO_MAPPING_H_
diff --git a/src/trace_processor/util/zip_reader.cc b/src/trace_processor/util/zip_reader.cc
index 1ec0cf5..f65f0b7 100644
--- a/src/trace_processor/util/zip_reader.cc
+++ b/src/trace_processor/util/zip_reader.cc
@@ -60,7 +60,9 @@
   k8kSlidingDictionary = 1u << 1,
   kShannonFaro = 1u << 2,
   kDataDescriptor = 1u << 3,
-  kUnknown = ~((1u << 4) - 1),
+  kLangageEncoding = 1u << 11,
+  kUnknown = ~(kEncrypted | k8kSlidingDictionary | kShannonFaro |
+               kDataDescriptor | kLangageEncoding),
 };
 
 // Compression flags.
diff --git a/src/traceconv/trace_to_pprof_integrationtest.cc b/src/traceconv/trace_to_pprof_integrationtest.cc
index b852854..bff3e26 100644
--- a/src/traceconv/trace_to_pprof_integrationtest.cc
+++ b/src/traceconv/trace_to_pprof_integrationtest.cc
@@ -14,9 +14,14 @@
  * limitations under the License.
  */
 
+#include <unistd.h>
 #include "test/gtest_and_gmock.h"
 
 #include <fstream>
+#include <ostream>
+#include <sstream>
+#include <string>
+#include <vector>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/file_utils.h"
@@ -30,7 +35,7 @@
 
 using testing::Contains;
 
-pprof::PprofProfileReader convert_trace_to_pprof(
+pprof::PprofProfileReader ConvertTraceToPprof(
     const std::string& input_file_name) {
   const std::string trace_file = base::GetTestDataPath(input_file_name);
   std::ifstream file_istream;
@@ -84,8 +89,7 @@
 };
 
 TEST_F(TraceToPprofTest, SummaryValues) {
-  const auto pprof =
-      convert_trace_to_pprof("test/data/heap_graph/heap_graph.pb");
+  const auto pprof = ConvertTraceToPprof("test/data/heap_graph/heap_graph.pb");
 
   EXPECT_EQ(pprof.get_samples_value_sum("Foo", "Total allocation count"), 1);
   EXPECT_EQ(pprof.get_samples_value_sum("Foo", "Total allocation size"), 32);
@@ -100,7 +104,7 @@
 
 TEST_F(TraceToPprofTest, TreeLocationFunctionNames) {
   const auto pprof =
-      convert_trace_to_pprof("test/data/heap_graph/heap_graph_branching.pb");
+      ConvertTraceToPprof("test/data/heap_graph/heap_graph_branching.pb");
 
   EXPECT_THAT(get_samples_function_names(pprof, "LeftChild0"),
               Contains(std::vector<std::string>{"LeftChild0",
@@ -118,7 +122,7 @@
 
 TEST_F(TraceToPprofTest, HugeSizes) {
   const auto pprof =
-      convert_trace_to_pprof("test/data/heap_graph/heap_graph_huge_size.pb");
+      ConvertTraceToPprof("test/data/heap_graph/heap_graph_huge_size.pb");
   EXPECT_EQ(pprof.get_samples_value_sum("dev.perfetto.BigStuff",
                                         "Total allocation size"),
             3000000000);
@@ -138,7 +142,7 @@
 
 TEST_F(TraceToPprofRealTraceTest, AllocationCountForClass) {
   const auto pprof =
-      convert_trace_to_pprof("test/data/system-server-heap-graph-new.pftrace");
+      ConvertTraceToPprof("test/data/system-server-heap-graph-new.pftrace");
 
   EXPECT_EQ(pprof.get_samples_value_sum(
                 "android.content.pm.parsing.component.ParsedActivity",
diff --git a/src/traceconv/trace_to_profile.cc b/src/traceconv/trace_to_profile.cc
index b7676be..96b97fd 100644
--- a/src/traceconv/trace_to_profile.cc
+++ b/src/traceconv/trace_to_profile.cc
@@ -110,12 +110,11 @@
   tp->Flush();
   MaybeSymbolize(tp.get());
   MaybeDeobfuscate(tp.get());
-
-  TraceToPprof(tp.get(), &profiles, conversion_mode, conversion_flags, pid,
-               timestamps);
   if (auto status = tp->NotifyEndOfFile(); !status.ok()) {
     return -1;
   }
+  TraceToPprof(tp.get(), &profiles, conversion_mode, conversion_flags, pid,
+               timestamps);
   if (profiles.empty()) {
     return 0;
   }
diff --git a/src/traced/probes/ftrace/cpu_reader.cc b/src/traced/probes/ftrace/cpu_reader.cc
index 34c4078..850ff0e 100644
--- a/src/traced/probes/ftrace/cpu_reader.cc
+++ b/src/traced/probes/ftrace/cpu_reader.cc
@@ -47,6 +47,7 @@
 namespace {
 
 using FtraceParseStatus = protos::pbzero::FtraceParseStatus;
+using protos::pbzero::KprobeEvent;
 
 // If the compact_sched buffer accumulates more unique strings, the reader will
 // flush it to reset the interning state (and make it cheap again).
@@ -292,27 +293,24 @@
     }
   }  // end of metatrace::FTRACE_CPU_READ_BATCH
 
-  // Parse the pages and write to the trace for all relevant data
-  // sources.
+  // Parse the pages and write to the trace for all relevant data sources.
   if (pages_read == 0)
     return pages_read;
 
-  uint64_t last_read_ts = last_read_event_ts_;
   for (FtraceDataSource* data_source : started_data_sources) {
-    last_read_ts = last_read_event_ts_;
     ProcessPagesForDataSource(
         data_source->trace_writer(), data_source->mutable_metadata(), cpu_,
         data_source->parsing_config(), data_source->mutable_parse_errors(),
-        &last_read_ts, parsing_buf, pages_read, compact_sched_buf, table_,
-        symbolizer_, ftrace_clock_snapshot_, ftrace_clock_);
+        data_source->mutable_bundle_end_timestamp(cpu_), parsing_buf,
+        pages_read, compact_sched_buf, table_, symbolizer_,
+        ftrace_clock_snapshot_, ftrace_clock_);
   }
-  last_read_event_ts_ = last_read_ts;
-
   return pages_read;
 }
 
-void CpuReader::Bundler::StartNewPacket(bool lost_events,
-                                        uint64_t last_read_event_timestamp) {
+void CpuReader::Bundler::StartNewPacket(
+    bool lost_events,
+    uint64_t previous_bundle_end_timestamp) {
   FinalizeAndRunSymbolizer();
   packet_ = trace_writer_->NewTracePacket();
   bundle_ = packet_->set_ftrace_events();
@@ -325,7 +323,7 @@
   // note: set-to-zero is valid and expected for the first bundle per cpu
   // (outside of concurrent tracing), with the effective meaning of "all data is
   // valid since the data source was started".
-  bundle_->set_last_read_event_timestamp(last_read_event_timestamp);
+  bundle_->set_previous_bundle_end_timestamp(previous_bundle_end_timestamp);
 
   if (ftrace_clock_) {
     bundle_->set_ftrace_clock(ftrace_clock_);
@@ -410,13 +408,6 @@
 // event bundle proto with a timestamp, letting the trace processor decide
 // whether to discard or keep the post-error data. Previously, we crashed as
 // soon as we encountered such an error.
-// TODO(rsavitski, b/192586066): consider moving last_read_event_ts tracking to
-// be per-datasource. The current implementation can be pessimistic if there are
-// multiple concurrent data sources, one of which is only interested in sparse
-// events (imagine a print filter and one matching event every minute, while the
-// buffers are read - advancing the last read timestamp - multiple times per
-// second). Tracking the timestamp of the last event *written into the
-// datasource* can be more accurate.
 // static
 bool CpuReader::ProcessPagesForDataSource(
     TraceWriter* trace_writer,
@@ -424,7 +415,7 @@
     size_t cpu,
     const FtraceDataSourceConfig* ds_config,
     base::FlatSet<protos::pbzero::FtraceParseStatus>* parse_errors,
-    uint64_t* last_read_event_ts,
+    uint64_t* bundle_end_timestamp,
     const uint8_t* parsing_buf,
     const size_t pages_read,
     CompactSchedBuffer* compact_sched_buf,
@@ -436,7 +427,7 @@
   Bundler bundler(trace_writer, metadata,
                   ds_config->symbolize_ksyms ? symbolizer : nullptr, cpu,
                   ftrace_clock_snapshot, ftrace_clock, compact_sched_buf,
-                  ds_config->compact_sched.enabled, *last_read_event_ts);
+                  ds_config->compact_sched.enabled, *bundle_end_timestamp);
 
   bool success = true;
   size_t pages_parsed = 0;
@@ -472,14 +463,14 @@
             kCompactSchedInternerThreshold;
 
     if (page_header->lost_events || interner_past_threshold) {
-      // pass in an updated last_read_event_ts since we're starting a new
+      // pass in an updated bundle_end_timestamp since we're starting a new
       // bundle, which needs to reference the last timestamp from the prior one.
-      bundler.StartNewPacket(page_header->lost_events, *last_read_event_ts);
+      bundler.StartNewPacket(page_header->lost_events, *bundle_end_timestamp);
     }
 
     FtraceParseStatus status =
         ParsePagePayload(parse_pos, &page_header.value(), table, ds_config,
-                         &bundler, metadata, last_read_event_ts);
+                         &bundler, metadata, bundle_end_timestamp);
 
     if (status != FtraceParseStatus::FTRACE_STATUS_OK) {
       WriteAndSetParseError(&bundler, parse_errors, page_header->timestamp,
@@ -560,12 +551,12 @@
     const FtraceDataSourceConfig* ds_config,
     Bundler* bundler,
     FtraceMetadata* metadata,
-    uint64_t* last_read_event_ts) {
+    uint64_t* bundle_end_timestamp) {
   const uint8_t* ptr = start_of_payload;
   const uint8_t* const end = ptr + page_header->size;
 
   uint64_t timestamp = page_header->timestamp;
-  uint64_t last_data_record_ts = 0;
+  uint64_t last_written_event_ts = 0;
 
   while (ptr < end) {
     EventHeader event_header;
@@ -652,10 +643,12 @@
           const CompactSchedWakingFormat& sched_waking_format =
               table->compact_sched_format().sched_waking;
 
+          // Special-cased filtering of ftrace/print events to retain only the
+          // matching events.
+          bool event_written = true;
           bool ftrace_print_filter_enabled =
               ds_config->print_filter.has_value();
 
-          // compact sched_switch
           if (compact_sched_enabled &&
               ftrace_event_id == sched_switch_format.event_id) {
             if (event_size < sched_switch_format.size)
@@ -663,8 +656,6 @@
 
             ParseSchedSwitchCompact(start, timestamp, &sched_switch_format,
                                     bundler->compact_sched_buf(), metadata);
-
-            // compact sched_waking
           } else if (compact_sched_enabled &&
                      ftrace_event_id == sched_waking_format.event_id) {
             if (event_size < sched_waking_format.size)
@@ -672,7 +663,6 @@
 
             ParseSchedWakingCompact(start, timestamp, &sched_waking_format,
                                     bundler->compact_sched_buf(), metadata);
-
           } else if (ftrace_print_filter_enabled &&
                      ftrace_event_id == ds_config->print_filter->event_id()) {
             if (ds_config->print_filter->IsEventInteresting(start, next)) {
@@ -683,6 +673,8 @@
                               event, metadata)) {
                 return FtraceParseStatus::FTRACE_STATUS_INVALID_EVENT;
               }
+            } else {  // print event did NOT pass the filter
+              event_written = false;
             }
           } else {
             // Common case: parse all other types of enabled events.
@@ -694,14 +686,17 @@
               return FtraceParseStatus::FTRACE_STATUS_INVALID_EVENT;
             }
           }
-        }
-        last_data_record_ts = timestamp;
-        ptr = next;  // jump to next event
-      }              // default case
+          if (event_written) {
+            last_written_event_ts = timestamp;
+          }
+        }  // IsEventEnabled(id)
+        ptr = next;
+      }              // case (data_record)
     }                // switch (event_header.type_or_length)
   }                  // while (ptr < end)
-  if (last_data_record_ts)
-    *last_read_event_ts = last_data_record_ts;
+
+  if (last_written_event_ts)
+    *bundle_end_timestamp = last_written_event_ts;
   return FtraceParseStatus::FTRACE_STATUS_OK;
 }
 
@@ -752,6 +747,14 @@
                  info.proto_field_id ==
                  protos::pbzero::FtraceEvent::kSysExitFieldNumber)) {
     success &= ParseSysExit(info, start, end, ds_config, nested, metadata);
+  } else if (PERFETTO_UNLIKELY(
+                 info.proto_field_id ==
+                 protos::pbzero::FtraceEvent::kKprobeEventFieldNumber)) {
+    KprobeEvent::KprobeType* elem = ds_config->kprobes.Find(ftrace_event_id);
+    nested->AppendString(KprobeEvent::kNameFieldNumber, info.name);
+    if (elem) {
+      nested->AppendVarInt(KprobeEvent::kTypeFieldNumber, *elem);
+    }
   } else {  // Parse all other events.
     for (const Field& field : info.fields) {
       success &= ParseField(field, start, end, table, nested, metadata);
diff --git a/src/traced/probes/ftrace/cpu_reader.h b/src/traced/probes/ftrace/cpu_reader.h
index 821ba48..44a6f52 100644
--- a/src/traced/probes/ftrace/cpu_reader.h
+++ b/src/traced/probes/ftrace/cpu_reader.h
@@ -105,7 +105,10 @@
     std::unique_ptr<CompactSchedBuffer> compact_sched_;
   };
 
-  // Helper class to generate `TracePacket`s when needed. Public for testing.
+  // Facilitates lazy proto writing - not every event in the kernel ring buffer
+  // is serialised in the trace, so this class allows for trace packets to be
+  // written only if there's at least one relevant event in the ring buffer
+  // batch. Public for testing.
   class Bundler {
    public:
     Bundler(TraceWriter* trace_writer,
@@ -116,7 +119,7 @@
             protos::pbzero::FtraceClock ftrace_clock,
             CompactSchedBuffer* compact_sched_buf,
             bool compact_sched_enabled,
-            uint64_t last_read_event_ts)
+            uint64_t previous_bundle_end_ts)
         : trace_writer_(trace_writer),
           metadata_(metadata),
           symbolizer_(symbolizer),
@@ -125,7 +128,7 @@
           ftrace_clock_(ftrace_clock),
           compact_sched_enabled_(compact_sched_enabled),
           compact_sched_buf_(compact_sched_buf),
-          initial_last_read_event_ts_(last_read_event_ts) {
+          initial_previous_bundle_end_ts_(previous_bundle_end_ts) {
       if (compact_sched_enabled_)
         compact_sched_buf_->Reset();
     }
@@ -134,13 +137,14 @@
 
     protos::pbzero::FtraceEventBundle* GetOrCreateBundle() {
       if (!bundle_) {
-        StartNewPacket(false, initial_last_read_event_ts_);
+        StartNewPacket(false, initial_previous_bundle_end_ts_);
       }
       return bundle_;
     }
 
     // Forces the creation of a new TracePacket.
-    void StartNewPacket(bool lost_events, uint64_t last_read_event_timestamp);
+    void StartNewPacket(bool lost_events,
+                        uint64_t previous_bundle_end_timestamp);
 
     // This function is called after the contents of a FtraceBundle are written.
     void FinalizeAndRunSymbolizer();
@@ -161,7 +165,7 @@
     protos::pbzero::FtraceClock const ftrace_clock_;
     const bool compact_sched_enabled_;
     CompactSchedBuffer* const compact_sched_buf_;
-    uint64_t initial_last_read_event_ts_;
+    uint64_t initial_previous_bundle_end_ts_;
 
     TraceWriter::TracePacketHandle packet_;
     protos::pbzero::FtraceEventBundle* bundle_ = nullptr;
@@ -306,7 +310,7 @@
       const FtraceDataSourceConfig* ds_config,
       Bundler* bundler,
       FtraceMetadata* metadata,
-      uint64_t* last_read_event_ts);
+      uint64_t* bundle_end_timestamp);
 
   // Parse a single raw ftrace event beginning at |start| and ending at |end|
   // and write it into the provided bundle as a proto.
@@ -374,7 +378,7 @@
       size_t cpu,
       const FtraceDataSourceConfig* ds_config,
       base::FlatSet<protos::pbzero::FtraceParseStatus>* parse_errors,
-      uint64_t* last_read_event_ts,
+      uint64_t* bundle_end_timestamp,
       const uint8_t* parsing_buf,
       size_t pages_read,
       CompactSchedBuffer* compact_sched_buf,
@@ -402,7 +406,6 @@
   const ProtoTranslationTable* table_;
   LazyKernelSymbolizer* symbolizer_;
   base::ScopedFile trace_fd_;
-  uint64_t last_read_event_ts_ = 0;
   protos::pbzero::FtraceClock ftrace_clock_{};
   const FtraceClockSnapshot* ftrace_clock_snapshot_;
 };
diff --git a/src/traced/probes/ftrace/cpu_reader_unittest.cc b/src/traced/probes/ftrace/cpu_reader_unittest.cc
index fb653cb..daa90e5 100644
--- a/src/traced/probes/ftrace/cpu_reader_unittest.cc
+++ b/src/traced/probes/ftrace/cpu_reader_unittest.cc
@@ -25,6 +25,8 @@
 #include "perfetto/protozero/proto_utils.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
 #include "perfetto/protozero/scattered_stream_writer.h"
+#include "protos/perfetto/trace/ftrace/generic.pbzero.h"
+#include "src/base/test/tmp_dir_tree.h"
 #include "src/traced/probes/ftrace/event_info.h"
 #include "src/traced/probes/ftrace/ftrace_config_muxer.h"
 #include "src/traced/probes/ftrace/ftrace_procfs.h"
@@ -42,6 +44,7 @@
 #include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
 #include "protos/perfetto/trace/ftrace/ftrace_stats.gen.h"
 #include "protos/perfetto/trace/ftrace/ftrace_stats.pbzero.h"
+#include "protos/perfetto/trace/ftrace/generic.gen.h"
 #include "protos/perfetto/trace/ftrace/power.gen.h"
 #include "protos/perfetto/trace/ftrace/raw_syscalls.gen.h"
 #include "protos/perfetto/trace/ftrace/sched.gen.h"
@@ -1022,7 +1025,7 @@
   EXPECT_EQ(last_read_event_ts_, 1'045'157'726'697'236ULL);
 
   auto bundle = GetBundle();
-  EXPECT_EQ(0u, bundle.last_read_event_timestamp());
+  EXPECT_EQ(0u, bundle.previous_bundle_end_timestamp());
   ASSERT_EQ(bundle.event().size(), 6u);
   {
     const protos::gen::FtraceEvent& event = bundle.event()[1];
@@ -1081,7 +1084,7 @@
   auto bundle = GetBundle();
 
   const auto& compact_sched = bundle.compact_sched();
-  EXPECT_EQ(0u, bundle.last_read_event_timestamp());
+  EXPECT_EQ(0u, bundle.previous_bundle_end_timestamp());
 
   EXPECT_EQ(6u, compact_sched.switch_timestamp().size());
   EXPECT_EQ(6u, compact_sched.switch_prev_state().size());
@@ -1225,6 +1228,82 @@
   EXPECT_EQ("kworker/u16:17", comm);
 }
 
+TEST_F(CpuReaderParsePagePayloadTest, ParseKprobeAndKretprobe) {
+  char kprobe_fuse_file_write_iter_page[] =
+      R"(
+    00000000: b31b bfe2 a513 0000 1400 0000 0000 0000  ................
+    00000010: 0400 0000 ff05 48ff 8a33 0000 443d 0e91  ......H..3..D=..
+    00000020: ffff ffff 0000 0000 0000 0000 0000 0000  ................
+    )";
+
+  std::unique_ptr<uint8_t[]> page =
+      PageFromXxd(kprobe_fuse_file_write_iter_page);
+
+  base::TmpDirTree ftrace;
+  ftrace.AddFile("available_events", "perfetto_kprobes:fuse_file_write_iter\n");
+  ftrace.AddDir("events");
+  ftrace.AddFile(
+      "events/header_page",
+      R"(        field: u64 timestamp;   offset:0;       size:8; signed:0;
+        field: local_t commit;  offset:8;       size:8; signed:1;
+        field: int overwrite;   offset:8;       size:1; signed:1;
+        field: char data;       offset:16;      size:4080;      signed:1;
+)");
+  ftrace.AddDir("events/perfetto_kprobes");
+  ftrace.AddDir("events/perfetto_kprobes/fuse_file_write_iter");
+  ftrace.AddFile("events/perfetto_kprobes/fuse_file_write_iter/format",
+                 R"format(name: fuse_file_write_iter
+ID: 1535
+format:
+        field:unsigned short common_type;       offset:0;       size:2; signed:0;
+        field:unsigned char common_flags;       offset:2;       size:1; signed:0;
+        field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
+        field:int common_pid;   offset:4;       size:4; signed:1;
+
+        field:unsigned long __probe_ip; offset:8;       size:8; signed:0;
+
+print fmt: "(%lx)", REC->__probe_ip
+)format");
+  ftrace.AddFile("trace", "");
+
+  std::unique_ptr<FtraceProcfs> ftrace_procfs =
+      FtraceProcfs::Create(ftrace.path() + "/");
+  ASSERT_NE(ftrace_procfs.get(), nullptr);
+  std::unique_ptr<ProtoTranslationTable> table = ProtoTranslationTable::Create(
+      ftrace_procfs.get(), GetStaticEventInfo(), GetStaticCommonFieldsInfo());
+  table->GetOrCreateKprobeEvent(
+      GroupAndName("perfetto_kprobes", "fuse_file_write_iter"));
+
+  auto ftrace_evt_id = static_cast<uint32_t>(table->EventToFtraceId(
+      GroupAndName("perfetto_kprobes", "fuse_file_write_iter")));
+  FtraceDataSourceConfig ds_config = EmptyConfig();
+  ds_config.event_filter.AddEnabledEvent(ftrace_evt_id);
+  ds_config.kprobes[ftrace_evt_id] =
+      protos::pbzero::KprobeEvent::KprobeType::KPROBE_TYPE_INSTANT;
+
+  const uint8_t* parse_pos = page.get();
+  std::optional<CpuReader::PageHeader> page_header =
+      CpuReader::ParsePageHeader(&parse_pos, table->page_header_size_len());
+
+  const uint8_t* page_end = page.get() + base::GetSysPageSize();
+  ASSERT_TRUE(page_header.has_value());
+  EXPECT_FALSE(page_header->lost_events);
+  EXPECT_LE(parse_pos + page_header->size, page_end);
+
+  FtraceParseStatus status = CpuReader::ParsePagePayload(
+      parse_pos, &page_header.value(), table.get(), &ds_config,
+      CreateBundler(ds_config), &metadata_, &last_read_event_ts_);
+
+  EXPECT_EQ(status, FtraceParseStatus::FTRACE_STATUS_OK);
+
+  // Write the buffer out & check the serialized format:
+  auto bundle = GetBundle();
+  ASSERT_EQ(bundle.event_size(), 1);
+  EXPECT_EQ(bundle.event()[0].kprobe_event().name(), "fuse_file_write_iter");
+  EXPECT_EQ(bundle.event()[0].kprobe_event().type(),
+            protos::gen::KprobeEvent::KPROBE_TYPE_INSTANT);
+}
+
 TEST_F(CpuReaderTableTest, ParseAllFields) {
   using FakeEventProvider =
       ProtoProvider<pbzero::FakeFtraceEvent, gen::FakeFtraceEvent>;
@@ -3569,9 +3648,9 @@
     00000250: 645f 7072 696e 740a 0000 0000 0000 0000  d_print.........
   )";
 
-// Tests that |last_read_event_timestamp| is correctly updated in cases where a
-// single ProcessPagesForDataSource call produces multiple ftrace bundle packets
-// (due to splitting on data loss markers).
+// Tests that |previous_bundle_end_timestamp| is correctly updated in cases
+// where a single ProcessPagesForDataSource call produces multiple ftrace bundle
+// packets (due to splitting on data loss markers).
 TEST(CpuReaderTest, LastReadEventTimestampWithSplitBundles) {
   // build test buffer with 3 pages
   ProtoTranslationTable* table = GetTable("synthetic");
@@ -3615,8 +3694,8 @@
   // Therefore we expect two bundles, as we start a new one whenever we
   // encounter data loss (to set the |lost_events| field in the bundle proto).
   //
-  // In terms of |last_read_event_timestamp|, the first bundle will emit zero
-  // since that's our initial input. The second bundle needs to emit the
+  // In terms of |previous_bundle_end_timestamp|, the first bundle will emit
+  // zero since that's our initial input. The second bundle needs to emit the
   // timestamp of the last event in the first bundle.
   auto packets = trace_writer.GetAllTracePackets();
   ASSERT_EQ(2u, packets.size());
@@ -3625,18 +3704,18 @@
   auto const& first_bundle = packets[0].ftrace_events();
   EXPECT_FALSE(first_bundle.lost_events());
   ASSERT_EQ(2u, first_bundle.event().size());
-  EXPECT_TRUE(first_bundle.has_last_read_event_timestamp());
-  EXPECT_EQ(0u, first_bundle.last_read_event_timestamp());
+  EXPECT_TRUE(first_bundle.has_previous_bundle_end_timestamp());
+  EXPECT_EQ(0u, first_bundle.previous_bundle_end_timestamp());
 
   const uint64_t kSecondPrintTs = 1308020252356549ULL;
   EXPECT_EQ(kSecondPrintTs, first_bundle.event()[1].timestamp());
-  EXPECT_EQ(0u, first_bundle.last_read_event_timestamp());
+  EXPECT_EQ(0u, first_bundle.previous_bundle_end_timestamp());
 
-  // 1 print + lost_events + updated last_read_event_timestamp
+  // 1 print + lost_events + updated previous_bundle_end_timestamp
   auto const& second_bundle = packets[1].ftrace_events();
   EXPECT_TRUE(second_bundle.lost_events());
   EXPECT_EQ(1u, second_bundle.event().size());
-  EXPECT_EQ(kSecondPrintTs, second_bundle.last_read_event_timestamp());
+  EXPECT_EQ(kSecondPrintTs, second_bundle.previous_bundle_end_timestamp());
 }
 
 }  // namespace
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index 4bc6a0c..2cd9b20 100644
--- a/src/traced/probes/ftrace/event_info.cc
+++ b/src/traced/probes/ftrace/event_info.cc
@@ -1294,6 +1294,22 @@
        kUnsetFtraceId,
        112,
        kUnsetSize},
+      {"param_set_value_cpm",
+       "cpm_trace",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "body", 1, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "value", 2, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "timestamp", 3, ProtoSchemaType::kInt64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       543,
+       kUnsetSize},
       {"cpuhp_exit",
        "cpuhp",
        {
@@ -1427,6 +1443,28 @@
        kUnsetFtraceId,
        508,
        kUnsetSize},
+      {"devfreq_frequency",
+       "devfreq",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "dev_name", 1, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "freq", 2, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "prev_freq", 3, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "busy_time", 4, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "total_time", 5, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       541,
+       kUnsetSize},
       {"dma_fence_init",
        "dma_fence",
        {
@@ -8227,19 +8265,19 @@
             "new_pid", 2, ProtoSchemaType::kInt32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "cctr", 3, ProtoSchemaType::kUint32,
+            "cctr", 3, ProtoSchemaType::kUint64,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr0", 4, ProtoSchemaType::kUint32,
+            "ctr0", 4, ProtoSchemaType::kUint64,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr1", 5, ProtoSchemaType::kUint32,
+            "ctr1", 5, ProtoSchemaType::kUint64,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr2", 6, ProtoSchemaType::kUint32,
+            "ctr2", 6, ProtoSchemaType::kUint64,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr3", 7, ProtoSchemaType::kUint32,
+            "ctr3", 7, ProtoSchemaType::kUint64,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
             "lctr0", 8, ProtoSchemaType::kUint32,
@@ -8248,10 +8286,10 @@
             "lctr1", 9, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr4", 10, ProtoSchemaType::kUint32,
+            "ctr4", 10, ProtoSchemaType::kUint64,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr5", 11, ProtoSchemaType::kUint32,
+            "ctr5", 11, ProtoSchemaType::kUint64,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
             "prev_comm", 12, ProtoSchemaType::kString,
@@ -8271,10 +8309,51 @@
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
             "l3dm", 17, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "next_pid", 18, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "next_comm", 19, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "prev_state", 20, ProtoSchemaType::kInt64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "amu0", 21, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "amu1", 22, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "amu2", 23, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
        },
        kUnsetFtraceId,
        487,
        kUnsetSize},
+      {"pixel_mm_kswapd_wake",
+       "pixel_mm",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "whatever", 1, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       538,
+       kUnsetSize},
+      {"pixel_mm_kswapd_done",
+       "pixel_mm",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "delta_nr_scanned", 1, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "delta_nr_reclaimed", 2, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       539,
+       kUnsetSize},
       {"cpu_frequency",
        "power",
        {
@@ -8954,6 +9033,28 @@
        kUnsetFtraceId,
        491,
        kUnsetSize},
+      {"sched_wakeup_task_attr",
+       "sched",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "pid", 1, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "cpu_affinity", 2, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "task_util", 3, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "uclamp_min", 4, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "vruntime", 5, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       540,
+       kUnsetSize},
       {"scm_call_start",
        "scm",
        {
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.cc b/src/traced/probes/ftrace/ftrace_config_muxer.cc
index ee9a3a2..e05d6f2 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.cc
@@ -27,6 +27,7 @@
 
 #include "perfetto/base/compiler.h"
 #include "perfetto/ext/base/utils.h"
+#include "protos/perfetto/trace/ftrace/generic.pbzero.h"
 #include "src/traced/probes/ftrace/atrace_wrapper.h"
 #include "src/traced/probes/ftrace/compact_sched.h"
 #include "src/traced/probes/ftrace/ftrace_config_utils.h"
@@ -37,6 +38,8 @@
 namespace perfetto {
 namespace {
 
+using protos::pbzero::KprobeEvent;
+
 constexpr uint64_t kDefaultLowRamPerCpuBufferSizeKb = 2 * (1ULL << 10);   // 2mb
 constexpr uint64_t kDefaultHighRamPerCpuBufferSizeKb = 8 * (1ULL << 10);  // 8mb
 
@@ -132,6 +135,43 @@
   dst->insert(GroupAndName(group, name));
 }
 
+std::map<GroupAndName, KprobeEvent::KprobeType> GetFtraceKprobeEvents(
+    const FtraceConfig& request) {
+  std::map<GroupAndName, KprobeEvent::KprobeType> events;
+  for (const auto& config_value : request.kprobe_events()) {
+    switch (config_value.type()) {
+      case protos::gen::FtraceConfig::KprobeEvent::KPROBE_TYPE_KPROBE:
+        events[GroupAndName(kKprobeGroup, config_value.probe().c_str())] =
+            KprobeEvent::KprobeType::KPROBE_TYPE_INSTANT;
+        break;
+      case protos::gen::FtraceConfig::KprobeEvent::KPROBE_TYPE_KRETPROBE:
+        events[GroupAndName(kKretprobeGroup, config_value.probe().c_str())] =
+            KprobeEvent::KprobeType::KPROBE_TYPE_INSTANT;
+        break;
+      case protos::gen::FtraceConfig::KprobeEvent::KPROBE_TYPE_BOTH:
+        events[GroupAndName(kKprobeGroup, config_value.probe().c_str())] =
+            KprobeEvent::KprobeType::KPROBE_TYPE_BEGIN;
+        events[GroupAndName(kKretprobeGroup, config_value.probe().c_str())] =
+            KprobeEvent::KprobeType::KPROBE_TYPE_END;
+        break;
+      case protos::gen::FtraceConfig::KprobeEvent::KPROBE_TYPE_UNKNOWN:
+        PERFETTO_DLOG("Unknown kprobe event");
+        break;
+    }
+    PERFETTO_DLOG("Added kprobe event: %s", config_value.probe().c_str());
+  }
+  return events;
+}
+
+bool ValidateKprobeName(const std::string& name) {
+  for (const char& c : name) {
+    if (!std::isalnum(c) && c != '_') {
+      return false;
+    }
+  }
+  return true;
+}
+
 }  // namespace
 
 std::set<GroupAndName> FtraceConfigMuxer::GetFtraceEvents(
@@ -588,6 +628,29 @@
   return true;
 }
 
+void FtraceConfigMuxer::EnableFtraceEvent(const Event* event,
+                                          const GroupAndName& group_and_name,
+                                          EventFilter* filter,
+                                          FtraceSetupErrors* errors) {
+  // Note: ftrace events are always implicitly enabled (and don't have an
+  // "enable" file). So they aren't tracked by the central event filter (but
+  // still need to be added to the per data source event filter to retain
+  // the events during parsing).
+  if (current_state_.ftrace_events.IsEventEnabled(event->ftrace_event_id) ||
+      std::string("ftrace") == event->group) {
+    filter->AddEnabledEvent(event->ftrace_event_id);
+    return;
+  }
+  if (ftrace_->EnableEvent(event->group, event->name)) {
+    current_state_.ftrace_events.AddEnabledEvent(event->ftrace_event_id);
+    filter->AddEnabledEvent(event->ftrace_event_id);
+  } else {
+    PERFETTO_DPLOG("Failed to enable %s.", group_and_name.ToString().c_str());
+    if (errors)
+      errors->failed_ftrace_events.push_back(group_and_name.ToString());
+  }
+}
+
 FtraceConfigMuxer::FtraceConfigMuxer(
     FtraceProcfs* ftrace,
     AtraceWrapper* atrace_wrapper,
@@ -646,6 +709,8 @@
   }
 
   std::set<GroupAndName> events = GetFtraceEvents(request, table_);
+  std::map<GroupAndName, KprobeEvent::KprobeType> events_kprobes =
+      GetFtraceKprobeEvents(request);
 
   // Vendors can provide a set of extra ftrace categories to be enabled when a
   // specific atrace category is used (e.g. "gfx" -> ["my_hw/my_custom_event",
@@ -675,7 +740,47 @@
     UpdateAtrace(request, errors ? &errors->atrace_errors : nullptr);
   }
 
+  base::FlatHashMap<uint32_t, KprobeEvent::KprobeType> kprobes;
+  for (const auto& [group_and_name, type] : events_kprobes) {
+    if (!ValidateKprobeName(group_and_name.name())) {
+      PERFETTO_ELOG("Invalid kprobes event %s", group_and_name.name().c_str());
+      if (errors)
+        errors->failed_ftrace_events.push_back(group_and_name.ToString());
+      continue;
+    }
+    // Kprobes events are created after their definition is written in the
+    // kprobe_events file
+    if (!ftrace_->CreateKprobeEvent(
+            group_and_name.group(), group_and_name.name(),
+            group_and_name.group() == kKretprobeGroup)) {
+      PERFETTO_ELOG("Failed creation of kprobes event %s",
+                    group_and_name.name().c_str());
+      if (errors)
+        errors->failed_ftrace_events.push_back(group_and_name.ToString());
+      continue;
+    }
+
+    const Event* event = table_->GetOrCreateKprobeEvent(group_and_name);
+    if (!event) {
+      PERFETTO_ELOG("Can't enable kprobe %s",
+                    group_and_name.ToString().c_str());
+      if (errors)
+        errors->unknown_ftrace_events.push_back(group_and_name.ToString());
+      continue;
+    }
+    EnableFtraceEvent(event, group_and_name, &filter, errors);
+    kprobes[event->ftrace_event_id] = type;
+  }
+
   for (const auto& group_and_name : events) {
+    if (group_and_name.group() == kKprobeGroup ||
+        group_and_name.group() == kKretprobeGroup) {
+      PERFETTO_DLOG("Can't enable %s, group reserved for kprobes",
+                    group_and_name.ToString().c_str());
+      if (errors)
+        errors->failed_ftrace_events.push_back(group_and_name.ToString());
+      continue;
+    }
     const Event* event = table_->GetOrCreateEvent(group_and_name);
     if (!event) {
       PERFETTO_DLOG("Can't enable %s, event not known",
@@ -684,6 +789,7 @@
         errors->unknown_ftrace_events.push_back(group_and_name.ToString());
       continue;
     }
+
     // Niche option to skip events that are in the config, but don't have a
     // dedicated proto for the event in perfetto. Otherwise such events will be
     // encoded as GenericFtraceEvent.
@@ -694,23 +800,8 @@
         errors->failed_ftrace_events.push_back(group_and_name.ToString());
       continue;
     }
-    // Note: ftrace events are always implicitly enabled (and don't have an
-    // "enable" file). So they aren't tracked by the central event filter (but
-    // still need to be added to the per data source event filter to retain
-    // the events during parsing).
-    if (current_state_.ftrace_events.IsEventEnabled(event->ftrace_event_id) ||
-        std::string("ftrace") == event->group) {
-      filter.AddEnabledEvent(event->ftrace_event_id);
-      continue;
-    }
-    if (ftrace_->EnableEvent(event->group, event->name)) {
-      current_state_.ftrace_events.AddEnabledEvent(event->ftrace_event_id);
-      filter.AddEnabledEvent(event->ftrace_event_id);
-    } else {
-      PERFETTO_DPLOG("Failed to enable %s.", group_and_name.ToString().c_str());
-      if (errors)
-        errors->failed_ftrace_events.push_back(group_and_name.ToString());
-    }
+
+    EnableFtraceEvent(event, group_and_name, &filter, errors);
   }
 
   EventFilter syscall_filter = BuildSyscallFilter(filter, request);
@@ -773,7 +864,7 @@
   std::vector<std::string> categories(request.atrace_categories());
   std::vector<std::string> categories_sdk_optout = Subtract(
       request.atrace_categories(), request.atrace_categories_prefer_sdk());
-  ds_configs_.emplace(
+  auto [it, inserted] = ds_configs_.emplace(
       std::piecewise_construct, std::forward_as_tuple(id),
       std::forward_as_tuple(
           std::move(filter), std::move(syscall_filter), compact_sched,
@@ -781,6 +872,9 @@
           std::move(categories), std::move(categories_sdk_optout),
           request.symbolize_ksyms(), request.drain_buffer_percent(),
           GetSyscallsReturningFds(syscalls_)));
+  if (inserted) {
+    it->second.kprobes = std::move(kprobes);
+  }
   return true;
 }
 
@@ -866,6 +960,11 @@
     PERFETTO_DCHECK(event);
     if (ftrace_->DisableEvent(event->group, event->name))
       current_state_.ftrace_events.DisableEvent(event->ftrace_event_id);
+
+    if (event->group == kKprobeGroup || event->group == kKretprobeGroup) {
+      ftrace_->RemoveKprobeEvent(event->group, event->name);
+      table_->RemoveEvent({event->group, event->name});
+    }
   }
 
   auto active_it = active_configs_.find(config_id);
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.h b/src/traced/probes/ftrace/ftrace_config_muxer.h
index e654eed..ea3ef24 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.h
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.h
@@ -21,6 +21,8 @@
 #include <optional>
 #include <set>
 
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "protos/perfetto/trace/ftrace/generic.pbzero.h"
 #include "src/kernel_utils/syscall_table.h"
 #include "src/traced/probes/ftrace/atrace_wrapper.h"
 #include "src/traced/probes/ftrace/compact_sched.h"
@@ -31,6 +33,9 @@
 
 namespace perfetto {
 
+constexpr std::string_view kKprobeGroup = "perfetto_kprobes";
+constexpr std::string_view kKretprobeGroup = "perfetto_kretprobes";
+
 namespace protos {
 namespace pbzero {
 enum FtraceClock : int32_t;
@@ -92,6 +97,9 @@
 
   // List of syscalls monitored to return a new filedescriptor upon success
   base::FlatSet<int64_t> syscalls_returning_fd;
+
+  // Keep track of the kprobe type for the given tracefs event id
+  base::FlatHashMap<uint32_t, protos::pbzero::KprobeEvent::KprobeType> kprobes;
 };
 
 // Ftrace is a bunch of globally modifiable persistent state.
@@ -221,6 +229,11 @@
   std::set<GroupAndName> GetFtraceEvents(const FtraceConfig& request,
                                          const ProtoTranslationTable*);
 
+  void EnableFtraceEvent(const Event*,
+                         const GroupAndName& group_and_name,
+                         EventFilter* filter,
+                         FtraceSetupErrors* errors);
+
   // Returns true if the event filter has at least one event from group.
   bool FilterHasGroup(const EventFilter& filter, const std::string& group);
 
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
index 53fa696..39c3ea7 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
@@ -20,6 +20,7 @@
 
 #include "ftrace_config_muxer.h"
 #include "perfetto/ext/base/utils.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
 #include "src/traced/probes/ftrace/atrace_wrapper.h"
 #include "src/traced/probes/ftrace/compact_sched.h"
 #include "src/traced/probes/ftrace/ftrace_procfs.h"
@@ -30,10 +31,12 @@
 using testing::_;
 using testing::AnyNumber;
 using testing::Contains;
+using testing::ElementsAre;
 using testing::ElementsAreArray;
 using testing::Eq;
 using testing::Invoke;
 using testing::IsEmpty;
+using testing::IsSupersetOf;
 using testing::MatchesRegex;
 using testing::NiceMock;
 using testing::Not;
@@ -64,6 +67,7 @@
   MockFtraceProcfs() : FtraceProcfs("/root/") {
     ON_CALL(*this, NumberOfCpus()).WillByDefault(Return(1));
     ON_CALL(*this, WriteToFile(_, _)).WillByDefault(Return(true));
+    ON_CALL(*this, AppendToFile(_, _)).WillByDefault(Return(true));
     ON_CALL(*this, ClearFile(_)).WillByDefault(Return(true));
     EXPECT_CALL(*this, NumberOfCpus()).Times(AnyNumber());
   }
@@ -117,6 +121,10 @@
               GetOrCreateEvent,
               (const GroupAndName& group_and_name),
               (override));
+  MOCK_METHOD(Event*,
+              GetOrCreateKprobeEvent,
+              (const GroupAndName& group_and_name),
+              (override));
   MOCK_METHOD(const Event*,
               GetEvent,
               (const GroupAndName& group_and_name),
@@ -1204,6 +1212,28 @@
   ASSERT_TRUE(model_.SetupConfig(id, config));
 }
 
+TEST_F(FtraceConfigMuxerFakeTableTest, KprobeNamesReserved) {
+  FtraceConfig config = CreateFtraceConfig(
+      {"perfetto_kprobes/fuse_file_write_iter",
+       "perfetto_kretprobes/fuse_file_write_iter", "unknown/unknown"});
+
+  ON_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillByDefault(Return("nop"));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/events/enable"))
+      .WillByDefault(Return("0"));
+
+  FtraceSetupErrors errors{};
+  FtraceConfigId id_a = 23;
+  ASSERT_TRUE(model_.SetupConfig(id_a, config, &errors));
+  // These event fail because the names "perfetto_kprobes" and
+  // "perfetto_kretprobes" are used internally by perfetto.
+  EXPECT_THAT(errors.failed_ftrace_events,
+              IsSupersetOf({"perfetto_kprobes/fuse_file_write_iter",
+                            "perfetto_kretprobes/fuse_file_write_iter"}));
+  // This event is just unknown
+  EXPECT_THAT(errors.unknown_ftrace_events, ElementsAre("unknown/unknown"));
+}
+
 // Fixture that constructs a FtraceConfigMuxer with a mock
 // ProtoTranslationTable.
 class FtraceConfigMuxerMockTableTest : public FtraceConfigMuxerTest {
@@ -1265,6 +1295,158 @@
               ElementsAreArray({kExpectedEventId}));
 }
 
+class FtraceConfigMuxerMockTableParamTest
+    : public FtraceConfigMuxerMockTableTest,
+      public testing::WithParamInterface<
+          std::pair<perfetto::protos::gen::FtraceConfig_KprobeEvent_KprobeType,
+                    std::string>> {};
+
+TEST_P(FtraceConfigMuxerMockTableParamTest, AddKprobeEvent) {
+  auto kprobe_type = std::get<0>(GetParam());
+  std::string group_name(std::get<1>(GetParam()));
+
+  FtraceConfig config;
+  FtraceConfig::KprobeEvent kprobe_event;
+
+  kprobe_event.set_probe("fuse_file_write_iter");
+  kprobe_event.set_type(kprobe_type);
+  *config.add_kprobe_events() = kprobe_event;
+
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", _));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/trace_clock", "boot"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/" + group_name +
+                                       "/fuse_file_write_iter/enable",
+                                   "1"));
+  EXPECT_CALL(*mock_table_, GetEvent(GroupAndName("power", "cpu_frequency")))
+      .Times(AnyNumber());
+
+  static constexpr int kExpectedEventId = 77;
+  Event event_to_return_kprobe;
+  event_to_return_kprobe.name = "fuse_file_write_iter";
+  event_to_return_kprobe.group = group_name.c_str();
+  event_to_return_kprobe.ftrace_event_id = kExpectedEventId;
+  EXPECT_CALL(*mock_table_, GetOrCreateKprobeEvent(GroupAndName(
+                                group_name, "fuse_file_write_iter")))
+      .WillOnce(Return(&event_to_return_kprobe));
+
+  FtraceConfigId id = 7;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "1"));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              ElementsAre(kExpectedEventId));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  ASSERT_THAT(central_filter->GetEnabledEvents(),
+              ElementsAre(kExpectedEventId));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    KprobeTypes,
+    FtraceConfigMuxerMockTableParamTest,
+    testing::Values(
+        std::make_pair(
+            protos::gen::FtraceConfig::KprobeEvent::KPROBE_TYPE_KPROBE,
+            kKprobeGroup),
+        std::make_pair(
+            protos::gen::FtraceConfig::KprobeEvent::KPROBE_TYPE_KRETPROBE,
+            kKretprobeGroup)));
+
+TEST_F(FtraceConfigMuxerMockTableTest, AddKprobeBothEvent) {
+  FtraceConfig config;
+  FtraceConfig::KprobeEvent kprobe_event;
+
+  kprobe_event.set_probe("fuse_file_write_iter");
+  kprobe_event.set_type(
+      protos::gen::FtraceConfig::KprobeEvent::KPROBE_TYPE_BOTH);
+  *config.add_kprobe_events() = kprobe_event;
+
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/current_tracer"))
+      .WillOnce(Return("nop"));
+  EXPECT_CALL(ftrace_, ReadOneCharFromFile("/root/tracing_on"))
+      .WillOnce(Return('1'));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "0"));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/events/enable", "0"));
+  EXPECT_CALL(ftrace_, ClearFile("/root/trace"));
+  EXPECT_CALL(ftrace_, ClearFile(MatchesRegex("/root/per_cpu/cpu[0-9]/trace")));
+  ON_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .WillByDefault(Return("[local] global boot"));
+  EXPECT_CALL(ftrace_, ReadFileIntoString("/root/trace_clock"))
+      .Times(AnyNumber());
+  EXPECT_CALL(ftrace_, WriteToFile("/root/buffer_size_kb", _));
+  EXPECT_CALL(ftrace_, WriteToFile("/root/trace_clock", "boot"));
+  EXPECT_CALL(
+      ftrace_,
+      WriteToFile("/root/events/perfetto_kprobes/fuse_file_write_iter/enable",
+                  "1"));
+  EXPECT_CALL(
+      ftrace_,
+      WriteToFile(
+          "/root/events/perfetto_kretprobes/fuse_file_write_iter/enable", "1"));
+  EXPECT_CALL(
+      ftrace_,
+      AppendToFile(
+          "/root/kprobe_events",
+          "p:perfetto_kprobes/fuse_file_write_iter fuse_file_write_iter"));
+  EXPECT_CALL(
+      ftrace_,
+      AppendToFile("/root/kprobe_events",
+                   std::string("r") + std::string(kKretprobeDefaultMaxactives) +
+                       ":perfetto_kretprobes/fuse_file_write_iter "
+                       "fuse_file_write_iter"));
+
+  std::string g1(kKprobeGroup);
+  static constexpr int kExpectedEventId = 77;
+  Event event_to_return_kprobe;
+  event_to_return_kprobe.name = "fuse_file_write_iter";
+  event_to_return_kprobe.group = g1.c_str();
+  event_to_return_kprobe.ftrace_event_id = kExpectedEventId;
+  EXPECT_CALL(*mock_table_, GetOrCreateKprobeEvent(GroupAndName(
+                                "perfetto_kprobes", "fuse_file_write_iter")))
+      .WillOnce(Return(&event_to_return_kprobe));
+
+  std::string g2(kKretprobeGroup);
+  static constexpr int kExpectedEventId2 = 78;
+  Event event_to_return_kretprobe;
+  event_to_return_kretprobe.name = "fuse_file_write_iter";
+  event_to_return_kretprobe.group = g2.c_str();
+  event_to_return_kretprobe.ftrace_event_id = kExpectedEventId2;
+  EXPECT_CALL(*mock_table_, GetOrCreateKprobeEvent(GroupAndName(
+                                "perfetto_kretprobes", "fuse_file_write_iter")))
+      .WillOnce(Return(&event_to_return_kretprobe));
+
+  FtraceConfigId id = 7;
+  ASSERT_TRUE(model_.SetupConfig(id, config));
+
+  EXPECT_CALL(ftrace_, WriteToFile("/root/tracing_on", "1"));
+  ASSERT_TRUE(model_.ActivateConfig(id));
+
+  const FtraceDataSourceConfig* ds_config = model_.GetDataSourceConfig(id);
+  ASSERT_TRUE(ds_config);
+  ASSERT_THAT(ds_config->event_filter.GetEnabledEvents(),
+              UnorderedElementsAre(kExpectedEventId, kExpectedEventId2));
+
+  const EventFilter* central_filter = model_.GetCentralEventFilterForTesting();
+  ASSERT_THAT(central_filter->GetEnabledEvents(),
+              UnorderedElementsAre(kExpectedEventId, kExpectedEventId2));
+}
+
 TEST_F(FtraceConfigMuxerMockTableTest, AddAllEvents) {
   FtraceConfig config = CreateFtraceConfig({"sched/*"});
 
diff --git a/src/traced/probes/ftrace/ftrace_controller.cc b/src/traced/probes/ftrace/ftrace_controller.cc
index 750629d..837a5e1 100644
--- a/src/traced/probes/ftrace/ftrace_controller.cc
+++ b/src/traced/probes/ftrace/ftrace_controller.cc
@@ -39,6 +39,7 @@
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/metatrace.h"
 #include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/string_splitter.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/tracing/core/trace_writer.h"
 #include "src/kallsyms/kernel_symbol_map.h"
@@ -65,7 +66,7 @@
 constexpr uint32_t kMinTickPeriodMs = 1;
 constexpr uint32_t kMaxTickPeriodMs = 1000 * 60;
 constexpr int kPollRequiredMajorVersion = 6;
-constexpr int kPollRequiredMinorVersion = 1;
+constexpr int kPollRequiredMinorVersion = 9;
 
 // Read at most this many pages of data per cpu per read task. If we hit this
 // limit on at least one cpu, we stop and repost the read task, letting other
@@ -630,6 +631,33 @@
   StopIfNeeded(instance);
 }
 
+bool DumpKprobeStats(const std::string& text, FtraceStats* ftrace_stats) {
+  int64_t hits = 0;
+  int64_t misses = 0;
+
+  base::StringSplitter line(std::move(text), '\n');
+  while (line.Next()) {
+    base::StringSplitter tok(line.cur_token(), line.cur_token_size() + 1, ' ');
+
+    if (!tok.Next())
+      return false;
+    // Skip the event name field
+
+    if (!tok.Next())
+      return false;
+    hits += static_cast<int64_t>(std::strtoll(tok.cur_token(), nullptr, 10));
+
+    if (!tok.Next())
+      return false;
+    misses += static_cast<int64_t>(std::strtoll(tok.cur_token(), nullptr, 10));
+  }
+
+  ftrace_stats->kprobe_stats.hits = hits;
+  ftrace_stats->kprobe_stats.misses = misses;
+
+  return true;
+}
+
 void FtraceController::DumpFtraceStats(FtraceDataSource* data_source,
                                        FtraceStats* stats_out) {
   FtraceInstanceState* instance =
@@ -646,6 +674,11 @@
     stats_out->kernel_symbols_mem_kb =
         static_cast<uint32_t>(symbol_map->size_bytes() / 1024);
   }
+
+  if (data_source->parsing_config()->kprobes.size() > 0) {
+    DumpKprobeStats(instance->ftrace_procfs.get()->ReadKprobeStats(),
+                    stats_out);
+  }
 }
 
 void FtraceController::MaybeSnapshotFtraceClock() {
@@ -693,14 +726,8 @@
 }
 
 // Check kernel version since the poll implementation has historical bugs.
-// We're looking for at least 6.1 for the following:
-//   42fb0a1e84ff tracing/ring-buffer: Have polling block on watermark
-// Otherwise the poll will wake us up as soon as a single byte is in the
-// buffer. A more conservative check would look for 6.6 for an extra fix that
-// reduces excessive kernel-space wakeups:
-//   1e0cb399c765 ring-buffer: Update "shortest_full" in polling
-// However that doesn't break functionality, so we'll still use poll if
-// requested by the config.
+// We're looking for at least 6.9 for the following:
+//   ffe3986fece6 ring-buffer: Only update pages_touched when a new page...
 // static
 bool FtraceController::PollSupportedOnKernelVersion(const char* uts_release) {
   int major = 0, minor = 0;
@@ -711,21 +738,18 @@
       (major == kPollRequiredMajorVersion &&
        minor < kPollRequiredMinorVersion)) {
     // Android: opportunistically detect a few select GKI kernels that are known
-    // to have the fixes. Note: 6.1 and 6.6 GKIs are already covered by the
-    // outer check.
+    // to have the fixes.
     std::optional<AndroidGkiVersion> gki = ParseAndroidGkiVersion(uts_release);
     if (!gki.has_value())
       return false;
-    // android13-5.10.197 or higher sublevel:
-    //   ef47f25e98de ring-buffer: Update "shortest_full" in polling
-    // android13-5.15.133 and
-    // android14-5.15.133 or higher sublevel:
-    //   b5d00cd7db66 ring-buffer: Update "shortest_full" in polling
-    bool gki_patched =
-        (gki->release == 13 && gki->version == 5 && gki->patch_level == 10 &&
-         gki->sub_level >= 197) ||
-        ((gki->release == 13 || gki->release == 14) && gki->version == 5 &&
-         gki->patch_level == 15 && gki->sub_level >= 133);
+    // android14-6.1.86 or higher sublevel:
+    //   2d5f12de4cf5 ring-buffer: Only update pages_touched when a new page...
+    // android15-6.6.27 or higher sublevel:
+    //   a9cd92bc051f ring-buffer: Only update pages_touched when a new page...
+    bool gki_patched = (gki->release == 14 && gki->version == 6 &&
+                        gki->patch_level == 1 && gki->sub_level >= 86) ||
+                       (gki->release == 15 && gki->version == 6 &&
+                        gki->patch_level == 6 && gki->sub_level >= 27);
     return gki_patched;
   }
   return true;
diff --git a/src/traced/probes/ftrace/ftrace_controller.h b/src/traced/probes/ftrace/ftrace_controller.h
index a858dfe..41bb59c 100644
--- a/src/traced/probes/ftrace/ftrace_controller.h
+++ b/src/traced/probes/ftrace/ftrace_controller.h
@@ -189,6 +189,8 @@
   base::WeakPtrFactory<FtraceController> weak_factory_;  // Keep last.
 };
 
+bool DumpKprobeStats(const std::string& text, FtraceStats* ftrace_stats);
+
 }  // namespace perfetto
 
 #endif  // SRC_TRACED_PROBES_FTRACE_FTRACE_CONTROLLER_H_
diff --git a/src/traced/probes/ftrace/ftrace_controller_unittest.cc b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
index e0d4a1f..549f81a 100644
--- a/src/traced/probes/ftrace/ftrace_controller_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
@@ -656,6 +656,72 @@
   EXPECT_EQ(result.cpu(), 0u);
   EXPECT_EQ(result.entries(), 1u);
   EXPECT_EQ(result.overrun(), 2u);
+  auto kprobe_stats = result_packet.ftrace_stats().kprobe_stats();
+  EXPECT_EQ(kprobe_stats.hits(), 0u);
+  EXPECT_EQ(kprobe_stats.misses(), 0u);
+}
+
+TEST(FtraceStatsTest, WriteKprobeStats) {
+  FtraceStats stats{};
+  FtraceKprobeStats kprobe_stats{};
+  kprobe_stats.hits = 1;
+  kprobe_stats.misses = 2;
+  stats.kprobe_stats = kprobe_stats;
+
+  std::unique_ptr<TraceWriterForTesting> writer =
+      std::unique_ptr<TraceWriterForTesting>(new TraceWriterForTesting());
+  {
+    auto packet = writer->NewTracePacket();
+    auto* out = packet->set_ftrace_stats();
+    stats.Write(out);
+  }
+
+  protos::gen::TracePacket result_packet = writer->GetOnlyTracePacket();
+  auto result = result_packet.ftrace_stats();
+  EXPECT_EQ(result.kprobe_stats().hits(), 1u);
+  EXPECT_EQ(result.kprobe_stats().misses(), 2u);
+}
+
+TEST(FtraceStatsTest, KprobeProfileParseEmpty) {
+  std::string text = "";
+
+  FtraceStats stats{};
+  EXPECT_TRUE(DumpKprobeStats(text, &stats));
+}
+
+TEST(FtraceStatsTest, KprobeProfileParseEmptyLines) {
+  std::string text = R"(
+
+)";
+
+  FtraceStats stats{};
+  EXPECT_TRUE(DumpKprobeStats(text, &stats));
+}
+
+TEST(FtraceStatsTest, KprobeProfileParseValid) {
+  std::string text = R"(  _binder_inner_proc_lock  1   8
+  _binder_inner_proc_unlock                        2   9
+  _binder_node_inner_unlock                        3  10
+  _binder_node_unlock                              4  11
+)";
+
+  FtraceStats stats{};
+  EXPECT_TRUE(DumpKprobeStats(text, &stats));
+
+  EXPECT_EQ(stats.kprobe_stats.hits, 10u);
+  EXPECT_EQ(stats.kprobe_stats.misses, 38u);
+}
+
+TEST(FtraceStatsTest, KprobeProfileMissingValuesParseInvalid) {
+  std::string text = R"(  _binder_inner_proc_lock  1   8
+  _binder_inner_proc_unlock                        2
+)";
+
+  FtraceStats stats{};
+  EXPECT_FALSE(DumpKprobeStats(text, &stats));
+
+  EXPECT_EQ(stats.kprobe_stats.hits, 0u);
+  EXPECT_EQ(stats.kprobe_stats.misses, 0u);
 }
 
 TEST(FtraceControllerTest, OnlySecondaryInstance) {
@@ -822,27 +888,24 @@
   auto test = [](auto s) {
     return FtraceController::PollSupportedOnKernelVersion(s);
   };
-  // Linux 6.1 or above are ok
-  EXPECT_TRUE(test("6.5.13-1-amd64"));
-  EXPECT_TRUE(test("6.1.0-1-amd64"));
-  EXPECT_TRUE(test("6.1.25-android14-11-g"));
-  // before 6.1
+  // Linux 6.9 or above are ok
+  EXPECT_TRUE(test("6.9.13-1-amd64"));
+  EXPECT_TRUE(test("6.9.0-1-amd64"));
+  EXPECT_TRUE(test("6.9.25-android14-11-g"));
+  // before 6.9
   EXPECT_FALSE(test("5.15.200-1-amd"));
 
   // Android: check allowlisted GKI versions
 
   // sublevel matters:
-  EXPECT_TRUE(test("5.10.198-android13-4-0"));
-  EXPECT_FALSE(test("5.10.189-android13-4-0"));
+  EXPECT_TRUE(test("6.1.87-android14-4-0"));
+  EXPECT_FALSE(test("6.1.80-android14-4-0"));
   // sublevel matters:
-  EXPECT_TRUE(test("5.15.137-android14-8-suffix"));
-  EXPECT_FALSE(test("5.15.130-android14-8-suffix"));
-  // sublevel matters:
-  EXPECT_TRUE(test("5.15.137-android13-8-0"));
-  EXPECT_FALSE(test("5.15.129-android13-8-0"));
-  // android12 instead of android13 (clarification: this is part of the kernel
+  EXPECT_TRUE(test("6.6.27-android15-8-suffix"));
+  EXPECT_FALSE(test("6.6.26-android15-8-suffix"));
+  // android13 instead of android14 (clarification: this is part of the kernel
   // version, and is unrelated to the system image version).
-  EXPECT_FALSE(test("5.10.198-android12-4-0"));
+  EXPECT_FALSE(test("6.1.87-android13-4-0"));
 }
 
 }  // namespace perfetto
diff --git a/src/traced/probes/ftrace/ftrace_data_source.h b/src/traced/probes/ftrace/ftrace_data_source.h
index a78b01b..c74cc23 100644
--- a/src/traced/probes/ftrace/ftrace_data_source.h
+++ b/src/traced/probes/ftrace/ftrace_data_source.h
@@ -62,6 +62,12 @@
                    std::unique_ptr<TraceWriter>);
   ~FtraceDataSource() override;
 
+  // Hands out internal pointers to callbacks.
+  FtraceDataSource(const FtraceDataSource&) = delete;
+  FtraceDataSource& operator=(const FtraceDataSource&) = delete;
+  FtraceDataSource(FtraceDataSource&&) = delete;
+  FtraceDataSource& operator=(FtraceDataSource&&) = delete;
+
   // Called by FtraceController soon after ProbesProducer creates the data
   // source, to inject ftrace dependencies.
   void Initialize(FtraceConfigId, const FtraceDataSourceConfig* parsing_config);
@@ -89,13 +95,13 @@
   }
   TraceWriter* trace_writer() { return writer_.get(); }
 
- private:
-  // Hands out internal pointers to callbacks.
-  FtraceDataSource(const FtraceDataSource&) = delete;
-  FtraceDataSource& operator=(const FtraceDataSource&) = delete;
-  FtraceDataSource(FtraceDataSource&&) = delete;
-  FtraceDataSource& operator=(FtraceDataSource&&) = delete;
+  uint64_t* mutable_bundle_end_timestamp(size_t cpu) {
+    if (cpu >= bundle_end_ts_by_cpu_.size())
+      bundle_end_ts_by_cpu_.resize(cpu + 1);
+    return &bundle_end_ts_by_cpu_[cpu];
+  }
 
+ private:
   void WriteStats();
 
   const FtraceConfig config_;
@@ -107,6 +113,8 @@
   // data disagreeing with our understanding of the ring buffer ABI):
   base::FlatSet<protos::pbzero::FtraceParseStatus> parse_errors_;
   std::map<FlushRequestID, std::function<void()>> pending_flushes_;
+  // Remembers, for each per-cpu buffer, the last written event's timestamp.
+  std::vector<uint64_t> bundle_end_ts_by_cpu_;
 
   // -- Fields initialized by the Initialize() call:
   FtraceConfigId config_id_ = 0;
diff --git a/src/traced/probes/ftrace/ftrace_procfs.cc b/src/traced/probes/ftrace/ftrace_procfs.cc
index b7adf48..fd2c948 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.cc
+++ b/src/traced/probes/ftrace/ftrace_procfs.cc
@@ -144,6 +144,46 @@
   return AppendToFile(path, group + ":" + name);
 }
 
+bool FtraceProcfs::CreateKprobeEvent(const std::string& group,
+                                     const std::string& name,
+                                     bool is_retprobe) {
+  std::string path = root_ + "kprobe_events";
+  std::string probe =
+      (is_retprobe ? std::string("r") + std::string(kKretprobeDefaultMaxactives)
+                   : "p") +
+      std::string(":") + group + "/" + name + " " + name;
+
+  PERFETTO_DLOG("Writing \"%s >> %s\"", probe.c_str(), path.c_str());
+
+  bool ret = AppendToFile(path, probe);
+  if (!ret) {
+    if (errno == EEXIST) {
+      // The kprobe event defined by group/name already exists.
+      // TODO maybe because the /sys/kernel/tracing/kprobe_events file has not
+      // been properly cleaned up after tracing
+      PERFETTO_DLOG("Kprobe event %s::%s already exists", group.c_str(),
+                    name.c_str());
+      return true;
+    }
+    PERFETTO_PLOG("Failed writing '%s' to '%s'", probe.c_str(), path.c_str());
+  }
+
+  return ret;
+}
+
+// Utility function to remove kprobe event from the system
+bool FtraceProcfs::RemoveKprobeEvent(const std::string& group,
+                                     const std::string& name) {
+  PERFETTO_DLOG("RemoveKprobeEvent %s::%s", group.c_str(), name.c_str());
+  std::string path = root_ + "kprobe_events";
+  return AppendToFile(path, "-:" + group + "/" + name);
+}
+
+std::string FtraceProcfs::ReadKprobeStats() const {
+  std::string path = root_ + "/kprobe_profile";
+  return ReadFileIntoString(path);
+}
+
 bool FtraceProcfs::DisableEvent(const std::string& group,
                                 const std::string& name) {
   std::string path = root_ + "events/" + group + "/" + name + "/enable";
diff --git a/src/traced/probes/ftrace/ftrace_procfs.h b/src/traced/probes/ftrace/ftrace_procfs.h
index 42b1f83..2a0593f 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.h
+++ b/src/traced/probes/ftrace/ftrace_procfs.h
@@ -26,6 +26,8 @@
 
 namespace perfetto {
 
+constexpr std::string_view kKretprobeDefaultMaxactives = "1024";
+
 class FtraceProcfs {
  public:
   static const char* const kTracingPaths[];
@@ -49,6 +51,19 @@
   // Enable the event under with the given |group| and |name|.
   bool EnableEvent(const std::string& group, const std::string& name);
 
+  // Create the kprobe event for the function |name|. The event will be in
+  // |group|/|name|. Depending on the value of |is_retprobe|, installs a kprobe
+  // or a kretprobe.
+  bool CreateKprobeEvent(const std::string& group,
+                         const std::string& name,
+                         bool is_retprobe);
+
+  // Remove kprobe event from the system
+  bool RemoveKprobeEvent(const std::string& group, const std::string& name);
+
+  // Read the "kprobe_profile" file.
+  std::string ReadKprobeStats() const;
+
   // Disable the event under with the given |group| and |name|.
   bool DisableEvent(const std::string& group, const std::string& name);
 
diff --git a/src/traced/probes/ftrace/ftrace_stats.cc b/src/traced/probes/ftrace/ftrace_stats.cc
index 01c5890..1f7b053 100644
--- a/src/traced/probes/ftrace/ftrace_stats.cc
+++ b/src/traced/probes/ftrace/ftrace_stats.cc
@@ -32,6 +32,12 @@
     writer->add_unknown_ftrace_events(err);
   for (const std::string& err : setup_errors.failed_ftrace_events)
     writer->add_failed_ftrace_events(err);
+
+  if (kprobe_stats.hits || kprobe_stats.misses) {
+    auto* kprobe_stats_pb = writer->set_kprobe_stats();
+    kprobe_stats_pb->set_hits(kprobe_stats.hits);
+    kprobe_stats_pb->set_misses(kprobe_stats.misses);
+  }
 }
 
 void FtraceCpuStats::Write(protos::pbzero::FtraceCpuStats* writer) const {
diff --git a/src/traced/probes/ftrace/ftrace_stats.h b/src/traced/probes/ftrace/ftrace_stats.h
index 127b4f4..ccabe4a 100644
--- a/src/traced/probes/ftrace/ftrace_stats.h
+++ b/src/traced/probes/ftrace/ftrace_stats.h
@@ -27,6 +27,7 @@
 namespace pbzero {
 class FtraceStats;
 class FtraceCpuStats;
+class FtraceKprobeStats;
 }  // namespace pbzero
 }  // namespace protos
 
@@ -44,6 +45,11 @@
   void Write(protos::pbzero::FtraceCpuStats*) const;
 };
 
+struct FtraceKprobeStats {
+  int64_t hits;
+  int64_t misses;
+};
+
 struct FtraceSetupErrors {
   std::string atrace_errors;
   std::vector<std::string> unknown_ftrace_events;
@@ -55,6 +61,7 @@
   FtraceSetupErrors setup_errors;
   uint32_t kernel_symbols_parsed = 0;
   uint32_t kernel_symbols_mem_kb = 0;
+  FtraceKprobeStats kprobe_stats = {};
 
   void Write(protos::pbzero::FtraceStats*) const;
 };
diff --git a/src/traced/probes/ftrace/proto_translation_table.cc b/src/traced/probes/ftrace/proto_translation_table.cc
index 7e9092e..ef3b557 100644
--- a/src/traced/probes/ftrace/proto_translation_table.cc
+++ b/src/traced/probes/ftrace/proto_translation_table.cc
@@ -540,11 +540,9 @@
   }
 }
 
-const Event* ProtoTranslationTable::GetOrCreateEvent(
-    const GroupAndName& group_and_name) {
-  const Event* event = GetEvent(group_and_name);
-  if (event)
-    return event;
+const Event* ProtoTranslationTable::CreateEventWithProtoId(
+    const GroupAndName& group_and_name,
+    uint32_t proto_field_id) {
   // The ftrace event does not already exist so a new one will be created
   // by parsing the format file.
   std::string contents = ftrace_procfs_->ReadEventFormat(group_and_name.group(),
@@ -563,7 +561,7 @@
   // Set known event variables
   Event* e = &events_.at(ftrace_event.id);
   e->ftrace_event_id = ftrace_event.id;
-  e->proto_field_id = protos::pbzero::FtraceEvent::kGenericFieldNumber;
+  e->proto_field_id = proto_field_id;
   e->name = InternString(group_and_name.name());
   e->group = InternString(group_and_name.group());
 
@@ -584,6 +582,57 @@
   return e;
 }
 
+const Event* ProtoTranslationTable::GetOrCreateEvent(
+    const GroupAndName& group_and_name) {
+  const Event* event = GetEvent(group_and_name);
+  if (event)
+    return event;
+  return CreateEventWithProtoId(
+      group_and_name, protos::pbzero::FtraceEvent::kGenericFieldNumber);
+}
+
+const Event* ProtoTranslationTable::GetOrCreateKprobeEvent(
+    const GroupAndName& group_and_name) {
+  const Event* event = GetEvent(group_and_name);
+  const uint32_t proto_field_id =
+      protos::pbzero::FtraceEvent::kKprobeEventFieldNumber;
+  if (event) {
+    if (event->proto_field_id != proto_field_id) {
+      return nullptr;
+    }
+    return event;
+  }
+  return CreateEventWithProtoId(group_and_name, proto_field_id);
+}
+
+void ProtoTranslationTable::RemoveEvent(const GroupAndName& group_and_name) {
+  const std::string& group = group_and_name.group();
+  const std::string& name = group_and_name.name();
+  auto it = group_and_name_to_event_.find(group_and_name);
+  if (it == group_and_name_to_event_.end()) {
+    return;
+  }
+  Event* event = &events_[it->second->ftrace_event_id];
+  event->ftrace_event_id = 0;
+  if (auto it2 = name_to_events_.find(name); it2 != name_to_events_.end()) {
+    std::vector<const Event*>& events = it2->second;
+    events.erase(std::remove(events.begin(), events.end(), event),
+                 events.end());
+    if (events.empty()) {
+      name_to_events_.erase(it2);
+    }
+  }
+  if (auto it2 = group_to_events_.find(group); it2 != group_to_events_.end()) {
+    std::vector<const Event*>& events = it2->second;
+    events.erase(std::remove(events.begin(), events.end(), event),
+                 events.end());
+    if (events.empty()) {
+      group_to_events_.erase(it2);
+    }
+  }
+  group_and_name_to_event_.erase(it);
+}
+
 const char* ProtoTranslationTable::InternString(const std::string& str) {
   auto it_and_inserted = interned_strings_.insert(str);
   return it_and_inserted.first->c_str();
diff --git a/src/traced/probes/ftrace/proto_translation_table.h b/src/traced/probes/ftrace/proto_translation_table.h
index 7b71380..482525d 100644
--- a/src/traced/probes/ftrace/proto_translation_table.h
+++ b/src/traced/probes/ftrace/proto_translation_table.h
@@ -48,7 +48,7 @@
 // ftrace event.
 class GroupAndName {
  public:
-  GroupAndName(const std::string& group, const std::string& name)
+  GroupAndName(std::string_view group, std::string_view name)
       : group_(group), name_(name) {}
 
   bool operator==(const GroupAndName& other) const {
@@ -162,6 +162,14 @@
   // new event with the proto id set to generic. Virtual for testing.
   virtual const Event* GetOrCreateEvent(const GroupAndName&);
 
+  // Retrieves the ftrace event, that's going to be translated to a kprobe, from
+  // the proto translation table. If the event is already known and used for
+  // something other than a kprobe, returns nullptr.
+  virtual const Event* GetOrCreateKprobeEvent(const GroupAndName&);
+
+  // Removes the ftrace event from the proto translation table.
+  virtual void RemoveEvent(const GroupAndName&);
+
   // This is for backwards compatibility. If a group is not specified in the
   // config then the first event with that name will be returned.
   const Event* GetEventByName(const std::string& name) const {
@@ -185,6 +193,10 @@
   // Store strings so they can be read when writing the trace output.
   const char* InternString(const std::string& str);
 
+  // The event must not already exist.
+  const Event* CreateEventWithProtoId(const GroupAndName& group_and_name,
+                                      uint32_t proto_field_id);
+
   uint16_t CreateGenericEventField(const FtraceEvent::Field& ftrace_field,
                                    Event& event);
 
diff --git a/src/traced/probes/ftrace/proto_translation_table_unittest.cc b/src/traced/probes/ftrace/proto_translation_table_unittest.cc
index 40df967..1ea829c 100644
--- a/src/traced/probes/ftrace/proto_translation_table_unittest.cc
+++ b/src/traced/probes/ftrace/proto_translation_table_unittest.cc
@@ -16,7 +16,6 @@
 
 #include "src/traced/probes/ftrace/proto_translation_table.h"
 
-#include "src/base/test/gtest_test_suite.h"
 #include "src/base/test/utils.h"
 #include "src/traced/probes/ftrace/compact_sched.h"
 #include "src/traced/probes/ftrace/event_info.h"
@@ -30,8 +29,10 @@
 using testing::AllOf;
 using testing::AnyNumber;
 using testing::Contains;
+using testing::ElementsAre;
 using testing::Eq;
 using testing::IsNull;
+using testing::NiceMock;
 using testing::Pointee;
 using testing::Return;
 using testing::StrEq;
@@ -601,5 +602,70 @@
   }
 }
 
+TEST(TranslationTableTest, CreateRemoveKprobeEvent) {
+  NiceMock<MockFtraceProcfs> ftrace;
+  ON_CALL(ftrace, ReadEventFormat(_, _)).WillByDefault(Return(""));
+  ON_CALL(ftrace, ReadPageHeaderFormat())
+      .WillByDefault(Return(
+          R"(	field: u64 timestamp;	offset:0;	size:8;	signed:0;
+	field: local_t commit;	offset:8;	size:4;	signed:1;
+	field: int overwrite;	offset:8;	size:1;	signed:1;
+	field: char data;	offset:16;	size:4080;	signed:0;)"));
+  auto table = ProtoTranslationTable::Create(&ftrace, GetStaticEventInfo(),
+                                             GetStaticCommonFieldsInfo());
+  PERFETTO_CHECK(table);
+
+  EXPECT_CALL(ftrace,
+              ReadEventFormat("perfetto_kprobe", "fuse_file_write_iter"))
+      .WillOnce(Return(R"format(name: fuse_file_write_iter
+ID: 1535
+format:
+        field:unsigned short common_type;       offset:0;       size:2; signed:0;
+        field:unsigned char common_flags;       offset:2;       size:1; signed:0;
+        field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
+        field:int common_pid;   offset:4;       size:4; signed:1;
+
+        field:unsigned long __probe_ip; offset:8;       size:8; signed:0;
+
+print fmt: "(%lx)", REC->__probe_ip
+)format"));
+  const Event* event = table->GetOrCreateKprobeEvent(
+      {"perfetto_kprobe", "fuse_file_write_iter"});
+  ASSERT_NE(event, nullptr);
+  EXPECT_EQ(event->ftrace_event_id, 1535u);
+  EXPECT_EQ(table->GetEventByName("fuse_file_write_iter"), event);
+  EXPECT_THAT(table->GetEventsByGroup("perfetto_kprobe"),
+              Pointee(ElementsAre(event)));
+  EXPECT_EQ(table->GetEventById(1535), event);
+
+  table->RemoveEvent({"perfetto_kprobe", "fuse_file_write_iter"});
+  EXPECT_EQ(table->GetEventByName("fuse_file_write_iter"), nullptr);
+  EXPECT_EQ(table->GetEventsByGroup("perfetto_kprobe"), nullptr);
+  EXPECT_EQ(table->GetEventById(1535), nullptr);
+
+  EXPECT_CALL(ftrace,
+              ReadEventFormat("perfetto_kprobe", "fuse_file_write_iter"))
+      .WillOnce(Return(R"format(name: fuse_file_write_iter
+ID: 1536
+format:
+        field:unsigned short common_type;       offset:0;       size:2; signed:0;
+        field:unsigned char common_flags;       offset:2;       size:1; signed:0;
+        field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
+        field:int common_pid;   offset:4;       size:4; signed:1;
+
+        field:unsigned long __probe_ip; offset:8;       size:8; signed:0;
+
+print fmt: "(%lx)", REC->__probe_ip
+)format"));
+  event = table->GetOrCreateKprobeEvent(
+      {"perfetto_kprobe", "fuse_file_write_iter"});
+  ASSERT_NE(event, nullptr);
+  EXPECT_EQ(event->ftrace_event_id, 1536u);
+  EXPECT_EQ(table->GetEventByName("fuse_file_write_iter"), event);
+  EXPECT_THAT(table->GetEventsByGroup("perfetto_kprobe"),
+              Pointee(ElementsAre(event)));
+  EXPECT_EQ(table->GetEventById(1536), event);
+}
+
 }  // namespace
 }  // namespace perfetto
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/cpm_trace/param_set_value_cpm/format b/src/traced/probes/ftrace/test/data/synthetic/events/cpm_trace/param_set_value_cpm/format
new file mode 100644
index 0000000..a7a2876
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/cpm_trace/param_set_value_cpm/format
@@ -0,0 +1,13 @@
+name: param_set_value_cpm
+ID: 1125
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:__data_loc char[] body;	offset:8;	size:4;	signed:0;
+	field:unsigned int value;	offset:12;	size:4;	signed:0;
+	field:long long timestamp;	offset:16;	size:8;	signed:1;
+
+print fmt: "%s state=%u timestamp=%lld", __get_str(body), REC->value, REC->timestamp
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/devfreq/devfreq_frequency/format b/src/traced/probes/ftrace/test/data/synthetic/events/devfreq/devfreq_frequency/format
new file mode 100644
index 0000000..1b021f2
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/devfreq/devfreq_frequency/format
@@ -0,0 +1,15 @@
+name: devfreq_frequency
+ID: 898
+format:
+        field:unsigned short common_type;       offset:0;       size:2; signed:0;
+        field:unsigned char common_flags;       offset:2;       size:1; signed:0;
+        field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
+        field:int common_pid;   offset:4;       size:4; signed:1;
+
+        field:__data_loc char[] dev_name;       offset:8;       size:4; signed:0;
+        field:unsigned long freq;       offset:16;      size:8; signed:0;
+        field:unsigned long prev_freq;  offset:24;      size:8; signed:0;
+        field:unsigned long busy_time;  offset:32;      size:8; signed:0;
+        field:unsigned long total_time; offset:40;      size:8; signed:0;
+
+print fmt: "dev_name=%-30s freq=%-12lu prev_freq=%-12lu load=%-2lu", __get_str(dev_name), REC->freq, REC->prev_freq, REC->total_time == 0 ? 0 : (100 * REC->busy_time) / REC->total_time
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/pixel_mm/pixel_mm_kswapd_done/format b/src/traced/probes/ftrace/test/data/synthetic/events/pixel_mm/pixel_mm_kswapd_done/format
new file mode 100644
index 0000000..8a06a55
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/pixel_mm/pixel_mm_kswapd_done/format
@@ -0,0 +1,12 @@
+name: pixel_mm_kswapd_done
+ID: 1092
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:unsigned long delta_nr_scanned;	offset:8;	size:8;	signed:0;
+	field:unsigned long delta_nr_reclaimed;	offset:16;	size:8;	signed:0;
+
+print fmt: "delta_nr_scanned=%lu, delta_nr_reclaimed=%lu", REC->delta_nr_scanned, REC->delta_nr_reclaimed
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/pixel_mm/pixel_mm_kswapd_wake/format b/src/traced/probes/ftrace/test/data/synthetic/events/pixel_mm/pixel_mm_kswapd_wake/format
new file mode 100644
index 0000000..6697281
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/pixel_mm/pixel_mm_kswapd_wake/format
@@ -0,0 +1,11 @@
+name: pixel_mm_kswapd_wake
+ID: 1091
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:int whatever;	offset:8;	size:4;	signed:1;
+
+print fmt: "%s", ""
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/sched/sched_wakeup_task_attr/format b/src/traced/probes/ftrace/test/data/synthetic/events/sched/sched_wakeup_task_attr/format
new file mode 100644
index 0000000..738d532
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/sched/sched_wakeup_task_attr/format
@@ -0,0 +1,15 @@
+name: sched_wakeup_task_attr
+ID: 1062
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:pid_t pid;	offset:8;	size:4;	signed:1;
+	field:unsigned long cpu_affinity;	offset:16;	size:8;	signed:0;
+	field:unsigned long task_util;	offset:24;	size:8;	signed:0;
+	field:unsigned long uclamp_min;	offset:32;	size:8;	signed:0;
+	field:u64 vruntime;	offset:40;	size:8;	signed:0;
+
+print fmt: "pid=%d cpu_affinity=0x%lx, task_util=%lu, uclamp.min=%lu vruntime=%Lu [ns]", REC->pid, REC->cpu_affinity, REC->task_util, REC->uclamp_min, (unsigned long long)REC->vruntime
diff --git a/src/traced/probes/ftrace/test/data/synthetic_alt/events/perf_trace_counters/sched_switch_with_ctrs/format b/src/traced/probes/ftrace/test/data/synthetic_alt/events/perf_trace_counters/sched_switch_with_ctrs/format
new file mode 100644
index 0000000..1e27eaf
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic_alt/events/perf_trace_counters/sched_switch_with_ctrs/format
@@ -0,0 +1,25 @@
+name: sched_switch_with_ctrs
+ID: 1241
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:pid_t prev_pid;	offset:8;	size:4;	signed:1;
+	field:pid_t next_pid;	offset:12;	size:4;	signed:1;
+	field:char prev_comm[16];	offset:16;	size:16;	signed:0;
+	field:char next_comm[16];	offset:32;	size:16;	signed:0;
+	field:long prev_state;	offset:48;	size:8;	signed:1;
+	field:unsigned long cctr;	offset:56;	size:8;	signed:0;
+	field:unsigned long ctr0;	offset:64;	size:8;	signed:0;
+	field:unsigned long ctr1;	offset:72;	size:8;	signed:0;
+	field:unsigned long ctr2;	offset:80;	size:8;	signed:0;
+	field:unsigned long ctr3;	offset:88;	size:8;	signed:0;
+	field:unsigned long ctr4;	offset:96;	size:8;	signed:0;
+	field:unsigned long ctr5;	offset:104;	size:8;	signed:0;
+	field:unsigned long amu0;	offset:112;	size:8;	signed:0;
+	field:unsigned long amu1;	offset:120;	size:8;	signed:0;
+	field:unsigned long amu2;	offset:128;	size:8;	signed:0;
+
+print fmt: "prev_comm=%s prev_pid=%d prev_state=%s%s ==> next_comm=%s next_pid=%d CCNTR=%lu CTR0=%lu CTR1=%lu CTR2=%lu CTR3=%lu CTR4=%lu CTR5=%lu, CYC: %lu, INST: %lu, STALL: %lu", REC->prev_comm, REC->prev_pid, (REC->prev_state & ((((0x00000000 | 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000010 | 0x00000020 | 0x00000040) + 1) << 1) - 1)) ? __print_flags(REC->prev_state & ((((0x00000000 | 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000010 | 0x00000020 | 0x00000040) + 1) << 1) - 1), "|", { 0x00000001, "S" }, { 0x00000002, "D" }, { 0x00000004, "T" }, { 0x00000008, "t" }, { 0x00000010, "X" }, { 0x00000020, "Z" }, { 0x00000040, "P" }, { 0x00000080, "I" }) : "R", REC->prev_state & (((0x00000000 | 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000010 | 0x00000020 | 0x00000040) + 1) << 1) ? "+" : "", REC->next_comm, REC->next_pid, REC->cctr, REC->ctr0, REC->ctr1, REC->ctr2, REC->ctr3, REC->ctr4, REC->ctr5, REC->amu0, REC->amu1, REC->amu2
diff --git a/src/traced/probes/probes_producer.cc b/src/traced/probes/probes_producer.cc
index ab75b35..7b95f09 100644
--- a/src/traced/probes/probes_producer.cc
+++ b/src/traced/probes/probes_producer.cc
@@ -661,17 +661,8 @@
 }
 
 void ProbesProducer::ActivateTrigger(std::string trigger) {
-  android_stats::MaybeLogTriggerEvent(
-      PerfettoTriggerAtom::kProbesProducerTrigger, trigger);
-
-  task_runner_->PostTask([this, trigger]() {
-    if (!endpoint_) {
-      android_stats::MaybeLogTriggerEvent(
-          PerfettoTriggerAtom::kProbesProducerTriggerFail, trigger);
-      return;
-    }
-    endpoint_->ActivateTriggers({trigger});
-  });
+  task_runner_->PostTask(
+      [this, trigger]() { endpoint_->ActivateTriggers({trigger}); });
 }
 
 }  // namespace perfetto
diff --git a/src/traced/probes/sys_stats/sys_stats_data_source.cc b/src/traced/probes/sys_stats/sys_stats_data_source.cc
index 393be27..490881b 100644
--- a/src/traced/probes/sys_stats/sys_stats_data_source.cc
+++ b/src/traced/probes/sys_stats/sys_stats_data_source.cc
@@ -146,8 +146,8 @@
     stat_enabled_fields_ |= 1ul << static_cast<uint32_t>(*counter);
   }
 
-  std::array<uint32_t, 10> periods_ms{};
-  std::array<uint32_t, 10> ticks{};
+  std::array<uint32_t, 11> periods_ms{};
+  std::array<uint32_t, 11> ticks{};
   static_assert(periods_ms.size() == ticks.size(), "must have same size");
 
   periods_ms[0] = ClampTo10Ms(cfg.meminfo_period_ms(), "meminfo_period_ms");
@@ -160,6 +160,7 @@
   periods_ms[7] = ClampTo10Ms(cfg.psi_period_ms(), "psi_period_ms");
   periods_ms[8] = ClampTo10Ms(cfg.thermal_period_ms(), "thermal_period_ms");
   periods_ms[9] = ClampTo10Ms(cfg.cpuidle_period_ms(), "cpuidle_period_ms");
+  periods_ms[10] = ClampTo10Ms(cfg.gpufreq_period_ms(), "gpufreq_period_ms");
 
   tick_period_ms_ = 0;
   for (uint32_t ms : periods_ms) {
@@ -188,6 +189,7 @@
   psi_ticks_ = ticks[7];
   thermal_ticks_ = ticks[8];
   cpuidle_ticks_ = ticks[9];
+  gpufreq_ticks_ = ticks[10];
 }
 
 void SysStatsDataSource::Start() {
@@ -249,6 +251,9 @@
   if (cpuidle_ticks_ && tick_ % cpuidle_ticks_ == 0)
     ReadCpuIdleStates(sys_stats);
 
+  if (gpufreq_ticks_ && tick_ % gpufreq_ticks_ == 0)
+    ReadGpuFrequency(sys_stats);
+
   sys_stats->set_collection_end_timestamp(
       static_cast<uint64_t>(base::GetBootTimeNs().count()));
 
@@ -376,6 +381,44 @@
   }
 }
 
+std::optional<uint64_t> SysStatsDataSource::ReadAMDGpuFreq() {
+  std::optional<std::string> amd_gpu_freq =
+      ReadFileToString("/sys/class/drm/card0/device/pp_dpm_sclk");
+  if (!amd_gpu_freq) {
+    return std::nullopt;
+  }
+  for (base::StringSplitter lines(*amd_gpu_freq, '\n'); lines.Next();) {
+    base::StringView line(lines.cur_token(), lines.cur_token_size());
+    // Current frequency indicated with asterisk.
+    if (line.EndsWith("*")) {
+      for (base::StringSplitter words(line.ToStdString(), ' '); words.Next();) {
+        if (!base::EndsWith(words.cur_token(), "Mhz"))
+          continue;
+        // Strip suffix "Mhz".
+        std::string maybe_freq = std::string(words.cur_token())
+                                     .substr(0, words.cur_token_size() - 3);
+        auto freq = base::StringToUInt32(maybe_freq);
+        return freq;
+      }
+    }
+  }
+  return std::nullopt;
+}
+
+void SysStatsDataSource::ReadGpuFrequency(protos::pbzero::SysStats* sys_stats) {
+  std::optional<uint64_t> freq;
+  // Intel GPU Frequency.
+  freq = ReadFileToUInt64("/sys/class/drm/card0/gt_act_freq_mhz");
+  if (freq) {
+    sys_stats->add_gpufreq_mhz((*freq));
+    return;
+  }
+  freq = ReadAMDGpuFreq();
+  if (freq) {
+    sys_stats->add_gpufreq_mhz((*freq));
+  }
+}
+
 void SysStatsDataSource::ReadDiskStat(protos::pbzero::SysStats* sys_stats) {
   size_t rsize = ReadFile(&diskstat_fd_, "/proc/diskstats");
   if (!rsize) {
diff --git a/src/traced/probes/sys_stats/sys_stats_data_source.h b/src/traced/probes/sys_stats/sys_stats_data_source.h
index e09cd8a..5e212f5 100644
--- a/src/traced/probes/sys_stats/sys_stats_data_source.h
+++ b/src/traced/probes/sys_stats/sys_stats_data_source.h
@@ -21,6 +21,7 @@
 
 #include <map>
 #include <memory>
+#include <optional>
 #include <string>
 
 #include "perfetto/ext/base/paged_memory.h"
@@ -101,6 +102,8 @@
   void ReadPsi(protos::pbzero::SysStats* sys_stats);
   void ReadThermalZones(protos::pbzero::SysStats* sys_stats);
   void ReadCpuIdleStates(protos::pbzero::SysStats* sys_stats);
+  void ReadGpuFrequency(protos::pbzero::SysStats* sys_stats);
+  std::optional<uint64_t> ReadAMDGpuFreq();
 
   size_t ReadFile(base::ScopedFile*, const char* path);
 
@@ -132,6 +135,7 @@
   uint32_t psi_ticks_ = 0;
   uint32_t thermal_ticks_ = 0;
   uint32_t cpuidle_ticks_ = 0;
+  uint32_t gpufreq_ticks_ = 0;
 
   std::unique_ptr<CpuFreqInfo> cpu_freq_info_;
 
diff --git a/src/traced/probes/sys_stats/sys_stats_data_source_unittest.cc b/src/traced/probes/sys_stats/sys_stats_data_source_unittest.cc
index fd5dcb2..7a934c1 100644
--- a/src/traced/probes/sys_stats/sys_stats_data_source_unittest.cc
+++ b/src/traced/probes/sys_stats/sys_stats_data_source_unittest.cc
@@ -212,7 +212,13 @@
 const char kMockThermalType[] = "TSR0";
 const uint64_t kMockCpuIdleStateTime = 10000;
 const char kMockCpuIdleStateName[] = "MOCK_STATE_NAME";
-
+const uint64_t kMockIntelGpuFreq = 300;
+// kMockAMDGpuFreq whitespace is intentional.
+const char kMockAMDGpuFreq[] = R"(
+0: 200Mhz 
+1: 400Mhz *
+2: 2000Mhz 
+)";
 class TestSysStatsDataSource : public SysStatsDataSource {
  public:
   TestSysStatsDataSource(base::TaskRunner* task_runner,
@@ -581,6 +587,51 @@
   }
 }
 
+TEST_F(SysStatsDataSourceTest, IntelGpuFrequency) {
+  DataSourceConfig config;
+  protos::gen::SysStatsConfig sys_cfg;
+  sys_cfg.set_gpufreq_period_ms(10);
+  config.set_sys_stats_config_raw(sys_cfg.SerializeAsString());
+  auto data_source = GetSysStatsDataSource(config);
+
+  EXPECT_CALL(*data_source,
+              ReadFileToUInt64("/sys/class/drm/card0/gt_act_freq_mhz"))
+      .WillRepeatedly(Return(std::optional<uint64_t>(kMockIntelGpuFreq)));
+
+  WaitTick(data_source.get());
+
+  protos::gen::TracePacket packet = writer_raw_->GetOnlyTracePacket();
+  ASSERT_TRUE(packet.has_sys_stats());
+  const auto& sys_stats = packet.sys_stats();
+  EXPECT_EQ(sys_stats.gpufreq_mhz_size(), 1);
+  uint32_t intel_gpufreq = 300;
+  EXPECT_EQ(sys_stats.gpufreq_mhz()[0], intel_gpufreq);
+}
+
+TEST_F(SysStatsDataSourceTest, AMDGpuFrequency) {
+  DataSourceConfig config;
+  protos::gen::SysStatsConfig sys_cfg;
+  sys_cfg.set_gpufreq_period_ms(10);
+  config.set_sys_stats_config_raw(sys_cfg.SerializeAsString());
+  auto data_source = GetSysStatsDataSource(config);
+
+  // Ignore other GPU freq calls.
+  EXPECT_CALL(*data_source,
+              ReadFileToUInt64("/sys/class/drm/card0/gt_act_freq_mhz"));
+  EXPECT_CALL(*data_source,
+              ReadFileToString("/sys/class/drm/card0/device/pp_dpm_sclk"))
+      .WillRepeatedly(Return(std::optional<std::string>(kMockAMDGpuFreq)));
+
+  WaitTick(data_source.get());
+
+  protos::gen::TracePacket packet = writer_raw_->GetOnlyTracePacket();
+  ASSERT_TRUE(packet.has_sys_stats());
+  const auto& sys_stats = packet.sys_stats();
+  EXPECT_EQ(sys_stats.gpufreq_mhz_size(), 1);
+  uint32_t amd_gpufreq = 400;
+  EXPECT_EQ(sys_stats.gpufreq_mhz()[0], amd_gpufreq);
+}
+
 TEST_F(SysStatsDataSourceTest, DevfreqAll) {
   DataSourceConfig config;
   protos::gen::SysStatsConfig sys_cfg;
diff --git a/src/traced/probes/system_info/system_info_data_source.cc b/src/traced/probes/system_info/system_info_data_source.cc
index eba4df8..7c5aa84 100644
--- a/src/traced/probes/system_info/system_info_data_source.cc
+++ b/src/traced/probes/system_info/system_info_data_source.cc
@@ -18,6 +18,7 @@
 
 #include <optional>
 
+#include "perfetto/base/logging.h"
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/string_splitter.h"
@@ -38,6 +39,21 @@
 // of lines describes a CPU.
 const char kProcessor[] = "processor";
 
+// Key for CPU implementer in /proc/cpuinfo. Arm only.
+const char kImplementer[] = "CPU implementer";
+
+// Key for CPU architecture in /proc/cpuinfo. Arm only.
+const char kArchitecture[] = "CPU architecture";
+
+// Key for CPU variant in /proc/cpuinfo. Arm only.
+const char kVariant[] = "CPU variant";
+
+// Key for CPU part in /proc/cpuinfo. Arm only.
+const char kPart[] = "CPU part";
+
+// Key for CPU revision in /proc/cpuinfo. Arm only.
+const char kRevision[] = "CPU revision";
+
 }  // namespace
 
 // static
@@ -68,6 +84,13 @@
   std::string::iterator line_end = proc_cpu_info.end();
   std::string default_processor = "unknown";
   std::string cpu_index = "";
+
+  std::optional<uint32_t> implementer;
+  std::optional<uint32_t> architecture;
+  std::optional<uint32_t> variant;
+  std::optional<uint32_t> part;
+  std::optional<uint32_t> revision;
+
   uint32_t next_cpu_index = 0;
   while (line_start != proc_cpu_info.end()) {
     line_end = find(line_start, proc_cpu_info.end(), '\n');
@@ -95,6 +118,28 @@
         cpu->add_frequencies(*it);
       }
       cpu_index = "";
+
+      // Set Arm CPU identifier if available
+      if (implementer || architecture || part || variant || revision) {
+        if (implementer && architecture && part && variant && revision) {
+          auto* identifier = cpu->set_arm_identifier();
+          identifier->set_implementer(implementer.value());
+          identifier->set_architecture(architecture.value());
+          identifier->set_variant(variant.value());
+          identifier->set_part(part.value());
+          identifier->set_revision(revision.value());
+        } else {
+          PERFETTO_ILOG(
+              "Failed to parse Arm specific fields from /proc/cpuinfo");
+        }
+      }
+
+      implementer = std::nullopt;
+      architecture = std::nullopt;
+      variant = std::nullopt;
+      part = std::nullopt;
+      revision = std::nullopt;
+
       next_cpu_index++;
       continue;
     }
@@ -108,6 +153,16 @@
       default_processor = value;
     else if (key == kProcessor)
       cpu_index = value;
+    else if (key == kImplementer)
+      implementer = base::CStringToUInt32(value.data(), 16);
+    else if (key == kArchitecture)
+      architecture = base::CStringToUInt32(value.data(), 10);
+    else if (key == kVariant)
+      variant = base::CStringToUInt32(value.data(), 16);
+    else if (key == kPart)
+      part = base::CStringToUInt32(value.data(), 16);
+    else if (key == kRevision)
+      revision = base::CStringToUInt32(value.data(), 10);
   }
 
   packet->Finalize();
diff --git a/src/traced/probes/system_info/system_info_data_source_unittest.cc b/src/traced/probes/system_info/system_info_data_source_unittest.cc
index 83d5a72..ca7fe80 100644
--- a/src/traced/probes/system_info/system_info_data_source_unittest.cc
+++ b/src/traced/probes/system_info/system_info_data_source_unittest.cc
@@ -162,14 +162,37 @@
   ASSERT_THAT(cpu.frequencies(),
               ElementsAre(300000, 576000, 748800, 998400, 1209600, 1324800,
                           1516800, 1612800, 1708800));
+  ASSERT_TRUE(cpu.has_arm_identifier());
+  auto id = cpu.arm_identifier();
+  ASSERT_EQ(id.implementer(), 0x51U);
+  ASSERT_EQ(id.architecture(), 8U);
+  ASSERT_EQ(id.variant(), 0x7U);
+  ASSERT_EQ(id.part(), 0x803U);
+  ASSERT_EQ(id.revision(), 12U);
+
   ASSERT_EQ(cpu.capacity(), static_cast<uint32_t>(200));
   cpu = cpu_info.cpus()[1];
   ASSERT_EQ(cpu.processor(), "AArch64 Processor rev 13 (aarch64)");
   ASSERT_THAT(cpu.frequencies(),
               ElementsAre(300000, 652800, 825600, 979200, 1132800, 1363200,
                           1536000, 1747200, 1843200, 1996800, 2803200));
+  ASSERT_TRUE(cpu.has_arm_identifier());
+  id = cpu.arm_identifier();
+  ASSERT_EQ(id.implementer(), 0x51U);
+  ASSERT_EQ(id.architecture(), 8U);
+  ASSERT_EQ(id.variant(), 0x7U);
+  ASSERT_EQ(id.part(), 0x803U);
+  ASSERT_EQ(id.revision(), 12U);
+
   cpu = cpu_info.cpus()[7];
   ASSERT_EQ(cpu.capacity(), static_cast<uint32_t>(1024));
+  ASSERT_TRUE(cpu.has_arm_identifier());
+  id = cpu.arm_identifier();
+  ASSERT_EQ(id.implementer(), 0x51U);
+  ASSERT_EQ(id.architecture(), 8U);
+  ASSERT_EQ(id.variant(), 0x6U);
+  ASSERT_EQ(id.part(), 0x802U);
+  ASSERT_EQ(id.revision(), 13U);
 }
 
 }  // namespace
diff --git a/src/traced_relay/BUILD.gn b/src/traced_relay/BUILD.gn
index 6799e34..346c17f 100644
--- a/src/traced_relay/BUILD.gn
+++ b/src/traced_relay/BUILD.gn
@@ -22,6 +22,7 @@
     "../../gn:default_deps",
     "../../include/perfetto/ext/traced",
     "../base",
+    "../base:clock_snapshots",
     "../base:unix_socket",
     "../base:version",
     "../ipc:perfetto_ipc",
diff --git a/src/traced_relay/relay_service.cc b/src/traced_relay/relay_service.cc
index 3eb4214..2954cfd 100644
--- a/src/traced_relay/relay_service.cc
+++ b/src/traced_relay/relay_service.cc
@@ -22,13 +22,13 @@
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/base/task_runner.h"
+#include "perfetto/ext/base/clock_snapshots.h"
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/hash.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/unix_socket.h"
 #include "perfetto/ext/base/utils.h"
 #include "perfetto/ext/ipc/client.h"
-#include "perfetto/tracing/core/clock_snapshots.h"
 #include "perfetto/tracing/core/forward_decls.h"
 #include "protos/perfetto/ipc/wire_protocol.gen.h"
 #include "src/ipc/buffered_frame_deserializer.h"
@@ -143,7 +143,7 @@
       break;
   }
 
-  ClockSnapshotVector snapshot_data = CaptureClockSnapshots();
+  base::ClockSnapshotVector snapshot_data = base::CaptureClockSnapshots();
   for (auto& clock : snapshot_data) {
     auto* clock_proto = request.add_clocks();
     clock_proto->set_clock_id(clock.clock_id);
diff --git a/src/tracing/core/BUILD.gn b/src/tracing/core/BUILD.gn
index 1441c97..53deb50 100644
--- a/src/tracing/core/BUILD.gn
+++ b/src/tracing/core/BUILD.gn
@@ -29,7 +29,6 @@
     "../../base",
   ]
   sources = [
-    "clock_snapshots.cc",
     "id_allocator.cc",
     "id_allocator.h",
     "in_process_shared_memory.cc",
diff --git a/src/tracing/core/clock_snapshots.cc b/src/tracing/core/clock_snapshots.cc
deleted file mode 100644
index a4fe6c0..0000000
--- a/src/tracing/core/clock_snapshots.cc
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "perfetto/tracing/core/clock_snapshots.h"
-
-#include "perfetto/base/build_config.h"
-#include "perfetto/base/time.h"
-#include "protos/perfetto/common/builtin_clock.pbzero.h"
-
-namespace perfetto {
-
-ClockSnapshotVector CaptureClockSnapshots() {
-  ClockSnapshotVector snapshot_data;
-#if !PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE) && \
-    !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) &&   \
-    !PERFETTO_BUILDFLAG(PERFETTO_OS_NACL)
-  struct {
-    clockid_t id;
-    protos::pbzero::BuiltinClock type;
-    struct timespec ts;
-  } clocks[] = {
-      {CLOCK_BOOTTIME, protos::pbzero::BUILTIN_CLOCK_BOOTTIME, {0, 0}},
-      {CLOCK_REALTIME_COARSE,
-       protos::pbzero::BUILTIN_CLOCK_REALTIME_COARSE,
-       {0, 0}},
-      {CLOCK_MONOTONIC_COARSE,
-       protos::pbzero::BUILTIN_CLOCK_MONOTONIC_COARSE,
-       {0, 0}},
-      {CLOCK_REALTIME, protos::pbzero::BUILTIN_CLOCK_REALTIME, {0, 0}},
-      {CLOCK_MONOTONIC, protos::pbzero::BUILTIN_CLOCK_MONOTONIC, {0, 0}},
-      {CLOCK_MONOTONIC_RAW,
-       protos::pbzero::BUILTIN_CLOCK_MONOTONIC_RAW,
-       {0, 0}},
-  };
-  // First snapshot all the clocks as atomically as we can.
-  for (auto& clock : clocks) {
-    if (clock_gettime(clock.id, &clock.ts) == -1)
-      PERFETTO_DLOG("clock_gettime failed for clock %d", clock.id);
-  }
-  for (auto& clock : clocks) {
-    snapshot_data.push_back(ClockReading(
-        static_cast<uint32_t>(clock.type),
-        static_cast<uint64_t>(base::FromPosixTimespec(clock.ts).count())));
-  }
-#else  // OS_APPLE || OS_WIN && OS_NACL
-  auto wall_time_ns = static_cast<uint64_t>(base::GetWallTimeNs().count());
-  // The default trace clock is boot time, so we always need to emit a path to
-  // it. However since we don't actually have a boot time source on these
-  // platforms, pretend that wall time equals boot time.
-  snapshot_data.push_back(
-      ClockReading(protos::pbzero::BUILTIN_CLOCK_BOOTTIME, wall_time_ns));
-  snapshot_data.push_back(
-      ClockReading(protos::pbzero::BUILTIN_CLOCK_MONOTONIC, wall_time_ns));
-#endif
-
-#if PERFETTO_BUILDFLAG(PERFETTO_ARCH_CPU_X86_64)
-  // X86-specific but OS-independent TSC clocksource
-  snapshot_data.push_back(
-      ClockReading(protos::pbzero::BUILTIN_CLOCK_TSC, base::Rdtsc()));
-#endif  // PERFETTO_BUILDFLAG(PERFETTO_ARCH_CPU_X86_64)
-
-  return snapshot_data;
-}
-
-}  // namespace perfetto
diff --git a/src/tracing/core/in_process_shared_memory.cc b/src/tracing/core/in_process_shared_memory.cc
index 65ae53a..435bcdb 100644
--- a/src/tracing/core/in_process_shared_memory.cc
+++ b/src/tracing/core/in_process_shared_memory.cc
@@ -21,7 +21,7 @@
 InProcessSharedMemory::~InProcessSharedMemory() = default;
 InProcessSharedMemory::Factory::~Factory() = default;
 
-void* InProcessSharedMemory::start() const {
+const void* InProcessSharedMemory::start() const {
   return mem_.Get();
 }
 size_t InProcessSharedMemory::size() const {
diff --git a/src/tracing/core/in_process_shared_memory.h b/src/tracing/core/in_process_shared_memory.h
index 353d44b..9ac692f 100644
--- a/src/tracing/core/in_process_shared_memory.h
+++ b/src/tracing/core/in_process_shared_memory.h
@@ -42,7 +42,8 @@
   }
 
   // SharedMemory implementation.
-  void* start() const override;
+  using SharedMemory::start;  // Equal priority to const and non-const versions
+  const void* start() const override;
   size_t size() const override;
 
   class Factory : public SharedMemory::Factory {
diff --git a/src/tracing/core/shared_memory_abi_unittest.cc b/src/tracing/core/shared_memory_abi_unittest.cc
index d3e1754..5e39a93 100644
--- a/src/tracing/core/shared_memory_abi_unittest.cc
+++ b/src/tracing/core/shared_memory_abi_unittest.cc
@@ -17,7 +17,6 @@
 #include "perfetto/ext/tracing/core/shared_memory_abi.h"
 
 #include "perfetto/ext/tracing/core/basic_types.h"
-#include "src/base/test/gtest_test_suite.h"
 #include "src/tracing/test/aligned_buffer_test.h"
 #include "test/gtest_and_gmock.h"
 
diff --git a/src/tracing/core/shared_memory_arbiter_impl.cc b/src/tracing/core/shared_memory_arbiter_impl.cc
index d9d774e..d611dfd 100644
--- a/src/tracing/core/shared_memory_arbiter_impl.cc
+++ b/src/tracing/core/shared_memory_arbiter_impl.cc
@@ -861,6 +861,7 @@
 
 void SharedMemoryArbiterImpl::ReleaseWriterID(WriterID id) {
   base::TaskRunner* task_runner = nullptr;
+  base::WeakPtr<SharedMemoryArbiterImpl> weak_this;
   {
     std::lock_guard<std::mutex> scoped_lock(lock_);
     active_writer_ids_.Free(id);
@@ -879,12 +880,15 @@
     if (!task_runner_)
       return;
 
+    // If `active_writer_ids_` is empty, `TryShutdown()` can return true
+    // and `*this` can be deleted. Let's grab everything we need from `*this`
+    // before releasing the lock.
+    weak_this = weak_ptr_factory_.GetWeakPtr();
     task_runner = task_runner_;
   }  // scoped_lock
 
   // We shouldn't post tasks while locked. |task_runner| remains valid after
   // unlocking, because |task_runner_| is never reset.
-  auto weak_this = weak_ptr_factory_.GetWeakPtr();
   task_runner->PostTask([weak_this, id] {
     if (weak_this)
       weak_this->producer_endpoint_->UnregisterTraceWriter(id);
diff --git a/src/tracing/core/shared_memory_arbiter_impl_unittest.cc b/src/tracing/core/shared_memory_arbiter_impl_unittest.cc
index 8f7184c..c269897 100644
--- a/src/tracing/core/shared_memory_arbiter_impl_unittest.cc
+++ b/src/tracing/core/shared_memory_arbiter_impl_unittest.cc
@@ -24,7 +24,6 @@
 #include "perfetto/ext/tracing/core/trace_packet.h"
 #include "perfetto/ext/tracing/core/trace_writer.h"
 #include "perfetto/ext/tracing/core/tracing_service.h"
-#include "src/base/test/gtest_test_suite.h"
 #include "src/base/test/test_task_runner.h"
 #include "src/tracing/core/in_process_shared_memory.h"
 #include "src/tracing/core/patch_list.h"
diff --git a/src/tracing/core/trace_writer_impl.h b/src/tracing/core/trace_writer_impl.h
index d72f15b..3ee4d01 100644
--- a/src/tracing/core/trace_writer_impl.h
+++ b/src/tracing/core/trace_writer_impl.h
@@ -67,10 +67,6 @@
     return protobuf_stream_writer_.written();
   }
 
-  void ResetChunkForTesting() {
-    cur_chunk_ = SharedMemoryABI::Chunk();
-    cur_chunk_packet_count_inflated_ = false;
-  }
   bool drop_packets_for_testing() const { return drop_packets_; }
 
  private:
diff --git a/src/tracing/core/trace_writer_impl_unittest.cc b/src/tracing/core/trace_writer_impl_unittest.cc
index b345c55..f0a14e6 100644
--- a/src/tracing/core/trace_writer_impl_unittest.cc
+++ b/src/tracing/core/trace_writer_impl_unittest.cc
@@ -26,7 +26,6 @@
 #include "perfetto/protozero/message.h"
 #include "perfetto/protozero/proto_utils.h"
 #include "perfetto/protozero/scattered_stream_writer.h"
-#include "src/base/test/gtest_test_suite.h"
 #include "src/base/test/test_task_runner.h"
 #include "src/tracing/core/shared_memory_arbiter_impl.h"
 #include "src/tracing/test/aligned_buffer_test.h"
diff --git a/src/tracing/internal/tracing_backend_fake.cc b/src/tracing/internal/tracing_backend_fake.cc
index ff896a2..226a558 100644
--- a/src/tracing/internal/tracing_backend_fake.cc
+++ b/src/tracing/internal/tracing_backend_fake.cc
@@ -134,7 +134,7 @@
   void QueryCapabilities(QueryCapabilitiesCallback) override {}
 
   void SaveTraceForBugreport(SaveTraceForBugreportCallback) override {}
-  void CloneSession(TracingSessionID, CloneSessionArgs) override {}
+  void CloneSession(CloneSessionArgs) override {}
 
  private:
   Consumer* const consumer_;
diff --git a/src/tracing/internal/tracing_muxer_impl.cc b/src/tracing/internal/tracing_muxer_impl.cc
index 04832f1..03771b0 100644
--- a/src/tracing/internal/tracing_muxer_impl.cc
+++ b/src/tracing/internal/tracing_muxer_impl.cc
@@ -442,6 +442,11 @@
     query_service_state_callback_ = nullptr;
     muxer_->QueryServiceState(session_id_, std::move(callback));
   }
+  if (session_to_clone_) {
+    service_->CloneSession(*session_to_clone_);
+    session_to_clone_ = std::nullopt;
+  }
+
   if (stop_pending_)
     muxer_->StopTracingSession(session_id_);
 }
@@ -613,9 +618,17 @@
 }
 
 void TracingMuxerImpl::ConsumerImpl::OnSessionCloned(
-    const OnSessionClonedArgs&) {
-  // CloneSession is not exposed in the SDK. This should never happen.
-  PERFETTO_DCHECK(false);
+    const OnSessionClonedArgs& args) {
+  if (!clone_trace_callback_)
+    return;
+  TracingSession::CloneTraceCallbackArgs callback_arg{};
+  callback_arg.success = args.success;
+  callback_arg.error = std::move(args.error);
+  callback_arg.uuid_msb = args.uuid.msb();
+  callback_arg.uuid_lsb = args.uuid.lsb();
+  muxer_->task_runner_->PostTask(
+      std::bind(std::move(clone_trace_callback_), std::move(callback_arg)));
+  clone_trace_callback_ = nullptr;
 }
 
 void TracingMuxerImpl::ConsumerImpl::OnTraceStats(
@@ -688,6 +701,15 @@
       [muxer, session_id] { muxer->StartTracingSession(session_id); });
 }
 
+void TracingMuxerImpl::TracingSessionImpl::CloneTrace(CloneTraceArgs args,
+                                                      CloneTraceCallback cb) {
+  auto* muxer = muxer_;
+  auto session_id = session_id_;
+  muxer->task_runner_->PostTask([muxer, session_id, args, cb] {
+    muxer->CloneTracingSession(session_id, args, std::move(cb));
+  });
+}
+
 // Can be called from any thread.
 void TracingMuxerImpl::TracingSessionImpl::ChangeTraceConfig(
     const TraceConfig& cfg) {
@@ -1927,6 +1949,32 @@
   // TODO implement support for the deferred-start + fast-triggering case.
 }
 
+void TracingMuxerImpl::CloneTracingSession(
+    TracingSessionGlobalID session_id,
+    TracingSession::CloneTraceArgs args,
+    TracingSession::CloneTraceCallback callback) {
+  PERFETTO_DCHECK_THREAD(thread_checker_);
+  auto* consumer = FindConsumer(session_id);
+  if (!consumer) {
+    TracingSession::CloneTraceCallbackArgs callback_arg{};
+    callback_arg.success = false;
+    callback_arg.error = "Tracing session not found";
+    callback(callback_arg);
+    return;
+  }
+  // Multiple concurrent cloning isn't supported.
+  PERFETTO_DCHECK(!consumer->clone_trace_callback_);
+  consumer->clone_trace_callback_ = std::move(callback);
+  ConsumerEndpoint::CloneSessionArgs consumer_args{};
+  consumer_args.unique_session_name = args.unique_session_name;
+  if (!consumer->connected_) {
+    consumer->session_to_clone_ = std::move(consumer_args);
+    return;
+  }
+  consumer->session_to_clone_ = std::nullopt;
+  consumer->service_->CloneSession(consumer_args);
+}
+
 void TracingMuxerImpl::ChangeTracingSessionConfig(
     TracingSessionGlobalID session_id,
     const TraceConfig& trace_config) {
diff --git a/src/tracing/internal/tracing_muxer_impl.h b/src/tracing/internal/tracing_muxer_impl.h
index 2ab59c4..82e23a6 100644
--- a/src/tracing/internal/tracing_muxer_impl.h
+++ b/src/tracing/internal/tracing_muxer_impl.h
@@ -37,6 +37,7 @@
 #include "perfetto/ext/tracing/core/basic_types.h"
 #include "perfetto/ext/tracing/core/consumer.h"
 #include "perfetto/ext/tracing/core/producer.h"
+#include "perfetto/ext/tracing/core/tracing_service.h"
 #include "perfetto/tracing/backend_type.h"
 #include "perfetto/tracing/core/data_source_descriptor.h"
 #include "perfetto/tracing/core/forward_decls.h"
@@ -169,6 +170,9 @@
                            const std::shared_ptr<TraceConfig>&,
                            base::ScopedFile trace_fd = base::ScopedFile());
   void StartTracingSession(TracingSessionGlobalID);
+  void CloneTracingSession(TracingSessionGlobalID,
+                           TracingSession::CloneTraceArgs,
+                           TracingSession::CloneTraceCallback);
   void ChangeTracingSessionConfig(TracingSessionGlobalID, const TraceConfig&);
   void StopTracingSession(TracingSessionGlobalID);
   void DestroyTracingSession(TracingSessionGlobalID);
@@ -334,6 +338,10 @@
     // consumer wasn't connected yet.
     bool get_trace_stats_pending_ = false;
 
+    // Similarly we need to buffer a session cloning args if the session is
+    // cloning another sesison before the consumer was connected.
+    std::optional<ConsumerEndpoint::CloneSessionArgs> session_to_clone_;
+
     // Whether this session was already stopped. This will happen in response to
     // Stop{,Blocking}, but also if the service stops the session for us
     // automatically (e.g., when there are no data sources).
@@ -362,6 +370,9 @@
     // An internal callback used to implement StopBlocking().
     std::function<void()> blocking_stop_complete_callback_;
 
+    // Callback for a pending call to CloneTrace().
+    TracingSession::CloneTraceCallback clone_trace_callback_;
+
     // Callback passed to ReadTrace().
     std::function<void(TracingSession::ReadTraceCallbackArgs)>
         read_trace_callback_;
@@ -390,6 +401,7 @@
     void Setup(const TraceConfig&, int fd) override;
     void Start() override;
     void StartBlocking() override;
+    void CloneTrace(CloneTraceArgs args, CloneTraceCallback) override;
     void SetOnStartCallback(std::function<void()>) override;
     void SetOnErrorCallback(std::function<void(TracingError)>) override;
     void Stop() override;
diff --git a/src/tracing/internal/track_event_internal.cc b/src/tracing/internal/track_event_internal.cc
index 4ad7f44..6658512 100644
--- a/src/tracing/internal/track_event_internal.cc
+++ b/src/tracing/internal/track_event_internal.cc
@@ -30,6 +30,9 @@
 #include "protos/perfetto/trace/trace_packet_defaults.pbzero.h"
 #include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
 #include "protos/perfetto/trace/track_event/track_descriptor.pbzero.h"
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_MAC)
+#include <os/signpost.h>
+#endif
 
 using perfetto::protos::pbzero::ClockSnapshot;
 
@@ -420,6 +423,18 @@
           thread_time_counter_track.uuid);
     }
 
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_MAC)
+    // Emit a MacOS point-of-interest signpost to synchonize Mac profiler time
+    // with boot time.
+    // TODO(leszeks): Consider allowing synchronization against other clocks
+    // than boot time.
+    static os_log_t log_handle = os_log_create(
+        "dev.perfetto.clock_sync", OS_LOG_CATEGORY_POINTS_OF_INTEREST);
+    os_signpost_event_emit(
+        log_handle, OS_SIGNPOST_ID_EXCLUSIVE, "boottime", "%" PRId64,
+        static_cast<uint64_t>(perfetto::base::GetBootTimeNs().count()));
+#endif
+
     if (tls_state.default_clock != static_cast<uint32_t>(GetClockId())) {
       ClockSnapshot* clocks = packet->set_clock_snapshot();
       // Trace clock.
diff --git a/src/tracing/ipc/consumer/consumer_ipc_client_impl.cc b/src/tracing/ipc/consumer/consumer_ipc_client_impl.cc
index 9aa7cd6..5ab5aed 100644
--- a/src/tracing/ipc/consumer/consumer_ipc_client_impl.cc
+++ b/src/tracing/ipc/consumer/consumer_ipc_client_impl.cc
@@ -464,15 +464,19 @@
   consumer_port_.SaveTraceForBugreport(req, std::move(async_response));
 }
 
-void ConsumerIPCClientImpl::CloneSession(TracingSessionID tsid,
-                                         CloneSessionArgs args) {
+void ConsumerIPCClientImpl::CloneSession(CloneSessionArgs args) {
   if (!connected_) {
     PERFETTO_DLOG("Cannot CloneSession(), not connected to tracing service");
     return;
   }
 
   protos::gen::CloneSessionRequest req;
-  req.set_session_id(tsid);
+  if (args.tsid) {
+    req.set_session_id(args.tsid);
+  }
+  if (!args.unique_session_name.empty()) {
+    req.set_unique_session_name(args.unique_session_name);
+  }
   req.set_skip_trace_filter(args.skip_trace_filter);
   req.set_for_bugreport(args.for_bugreport);
   ipc::Deferred<protos::gen::CloneSessionResponse> async_response;
diff --git a/src/tracing/ipc/consumer/consumer_ipc_client_impl.h b/src/tracing/ipc/consumer/consumer_ipc_client_impl.h
index 985de64..fd2d6e5 100644
--- a/src/tracing/ipc/consumer/consumer_ipc_client_impl.h
+++ b/src/tracing/ipc/consumer/consumer_ipc_client_impl.h
@@ -75,7 +75,7 @@
                          QueryServiceStateCallback) override;
   void QueryCapabilities(QueryCapabilitiesCallback) override;
   void SaveTraceForBugreport(SaveTraceForBugreportCallback) override;
-  void CloneSession(TracingSessionID, CloneSessionArgs) override;
+  void CloneSession(CloneSessionArgs) override;
 
   // ipc::ServiceProxy::EventListener implementation.
   // These methods are invoked by the IPC layer, which knows nothing about
diff --git a/src/tracing/ipc/posix_shared_memory.h b/src/tracing/ipc/posix_shared_memory.h
index d620991..d7b9e20 100644
--- a/src/tracing/ipc/posix_shared_memory.h
+++ b/src/tracing/ipc/posix_shared_memory.h
@@ -61,7 +61,8 @@
   int fd() const { return fd_.get(); }
 
   // SharedMemory implementation.
-  void* start() const override { return start_; }
+  using SharedMemory::start;  // Equal priority to const and non-const versions
+  const void* start() const override { return start_; }
   size_t size() const override { return size_; }
 
  private:
diff --git a/src/tracing/ipc/service/consumer_ipc_service.cc b/src/tracing/ipc/service/consumer_ipc_service.cc
index 8837dde..4773d90 100644
--- a/src/tracing/ipc/service/consumer_ipc_service.cc
+++ b/src/tracing/ipc/service/consumer_ipc_service.cc
@@ -333,8 +333,13 @@
   ConsumerEndpoint::CloneSessionArgs args;
   args.skip_trace_filter = req.skip_trace_filter();
   args.for_bugreport = req.for_bugreport();
-  remote_consumer->service_endpoint->CloneSession(req.session_id(),
-                                                  std::move(args));
+  if (req.has_session_id()) {
+    args.tsid = req.session_id();
+  }
+  if (req.has_unique_session_name()) {
+    args.unique_session_name = req.unique_session_name();
+  }
+  remote_consumer->service_endpoint->CloneSession(std::move(args));
 }
 
 // Called by the service in response to
diff --git a/src/tracing/ipc/service/relay_ipc_service.cc b/src/tracing/ipc/service/relay_ipc_service.cc
index c305f3b..9c2b328 100644
--- a/src/tracing/ipc/service/relay_ipc_service.cc
+++ b/src/tracing/ipc/service/relay_ipc_service.cc
@@ -20,9 +20,9 @@
 #include <utility>
 
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/clock_snapshots.h"
 #include "perfetto/ext/ipc/service.h"
 #include "perfetto/ext/tracing/core/tracing_service.h"
-#include "perfetto/tracing/core/clock_snapshots.h"
 #include "perfetto/tracing/core/forward_decls.h"
 
 namespace perfetto {
@@ -52,13 +52,13 @@
 
 void RelayIPCService::SyncClock(const protos::gen::SyncClockRequest& req,
                                 DeferredSyncClockResponse resp) {
-  auto host_clock_snapshots = CaptureClockSnapshots();
+  auto host_clock_snapshots = base::CaptureClockSnapshots();
 
   // Send the response to client to reduce RTT.
   auto async_resp = ipc::AsyncResult<protos::gen::SyncClockResponse>::Create();
   resp.Resolve(std::move(async_resp));
 
-  ClockSnapshotVector client_clock_snapshots;
+  base::ClockSnapshotVector client_clock_snapshots;
   for (size_t i = 0; i < req.clocks().size(); i++) {
     auto& client_clock = req.clocks()[i];
     client_clock_snapshots.emplace_back(client_clock.clock_id(),
diff --git a/src/tracing/ipc/service/service_ipc_host_impl.cc b/src/tracing/ipc/service/service_ipc_host_impl.cc
index 51cbb63..aa52934 100644
--- a/src/tracing/ipc/service/service_ipc_host_impl.cc
+++ b/src/tracing/ipc/service/service_ipc_host_impl.cc
@@ -104,7 +104,11 @@
   svc_ = TracingService::CreateInstance(std::move(shm_factory), task_runner_,
                                         init_opts_);
 
-  if (producer_ipc_ports_.empty() || !consumer_ipc_port_) {
+  if (producer_ipc_ports_.empty() || !consumer_ipc_port_ ||
+      std::any_of(producer_ipc_ports_.begin(), producer_ipc_ports_.end(),
+                  [](const std::unique_ptr<ipc::Host>& port) {
+                    return port == nullptr;
+                  })) {
     Shutdown();
     return false;
   }
diff --git a/src/tracing/ipc/shared_memory_windows.h b/src/tracing/ipc/shared_memory_windows.h
index ca6443b..9a4aeee 100644
--- a/src/tracing/ipc/shared_memory_windows.h
+++ b/src/tracing/ipc/shared_memory_windows.h
@@ -52,7 +52,8 @@
   const base::ScopedPlatformHandle& handle() const { return handle_; }
 
   // SharedMemory implementation.
-  void* start() const override { return start_; }
+  using SharedMemory::start;  // Equal priority to const and non-const versions
+  const void* start() const override { return start_; }
   size_t size() const override { return size_; }
 
  private:
diff --git a/src/tracing/service/BUILD.gn b/src/tracing/service/BUILD.gn
index 984c692..24d894d 100644
--- a/src/tracing/service/BUILD.gn
+++ b/src/tracing/service/BUILD.gn
@@ -31,17 +31,23 @@
     "../../../protos/perfetto/trace/perfetto:zero",  # For MetatraceWriter.
     "../../android_stats",
     "../../base",
+    "../../base:clock_snapshots",
     "../../base:version",
     "../../protozero/filtering:message_filter",
     "../../protozero/filtering:string_filter",
     "../core",
   ]
   sources = [
+    "clock.cc",
+    "clock.h",
+    "dependencies.h",
     "histogram.h",
     "metatrace_writer.cc",
     "metatrace_writer.h",
     "packet_stream_validator.cc",
     "packet_stream_validator.h",
+    "random.cc",
+    "random.h",
     "trace_buffer.cc",
     "trace_buffer.h",
     "tracing_service_impl.cc",
diff --git a/src/tracing/service/clock.cc b/src/tracing/service/clock.cc
new file mode 100644
index 0000000..350115a
--- /dev/null
+++ b/src/tracing/service/clock.cc
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/tracing/service/clock.h"
+
+namespace perfetto::tracing_service {
+
+Clock::~Clock() = default;
+
+ClockImpl::~ClockImpl() = default;
+
+base::TimeNanos ClockImpl::GetBootTimeNs() {
+  return base::GetBootTimeNs();
+}
+
+base::TimeNanos ClockImpl::GetWallTimeNs() {
+  return base::GetWallTimeNs();
+}
+
+}  // namespace perfetto::tracing_service
diff --git a/src/tracing/service/clock.h b/src/tracing/service/clock.h
new file mode 100644
index 0000000..9fc9ef4
--- /dev/null
+++ b/src/tracing/service/clock.h
@@ -0,0 +1,54 @@
+/*
+ * 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_TRACING_SERVICE_CLOCK_H_
+#define SRC_TRACING_SERVICE_CLOCK_H_
+
+#include "perfetto/base/time.h"
+
+namespace perfetto::tracing_service {
+
+class Clock {
+ public:
+  virtual ~Clock();
+  virtual base::TimeNanos GetBootTimeNs() = 0;
+  virtual base::TimeNanos GetWallTimeNs() = 0;
+
+  base::TimeMillis GetBootTimeMs() {
+    return std::chrono::duration_cast<base::TimeMillis>(GetBootTimeNs());
+  }
+  base::TimeMillis GetWallTimeMs() {
+    return std::chrono::duration_cast<base::TimeMillis>(GetWallTimeNs());
+  }
+
+  base::TimeSeconds GetBootTimeS() {
+    return std::chrono::duration_cast<base::TimeSeconds>(GetBootTimeNs());
+  }
+  base::TimeSeconds GetWallTimeS() {
+    return std::chrono::duration_cast<base::TimeSeconds>(GetWallTimeNs());
+  }
+};
+
+class ClockImpl : public Clock {
+ public:
+  ~ClockImpl() override;
+  base::TimeNanos GetBootTimeNs() override;
+  base::TimeNanos GetWallTimeNs() override;
+};
+
+}  // namespace perfetto::tracing_service
+
+#endif  // SRC_TRACING_SERVICE_CLOCK_H_
diff --git a/src/tracing/service/dependencies.h b/src/tracing/service/dependencies.h
new file mode 100644
index 0000000..e4da51b
--- /dev/null
+++ b/src/tracing/service/dependencies.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACING_SERVICE_DEPENDENCIES_H_
+#define SRC_TRACING_SERVICE_DEPENDENCIES_H_
+
+#include <memory>
+
+#include "src/tracing/service/clock.h"
+#include "src/tracing/service/random.h"
+
+namespace perfetto::tracing_service {
+
+// Dependencies of TracingServiceImpl. Can point to real implementations or to
+// mocks in tests.
+struct Dependencies {
+  std::unique_ptr<Clock> clock;
+  std::unique_ptr<Random> random;
+};
+
+}  // namespace perfetto::tracing_service
+
+#endif  // SRC_TRACING_SERVICE_DEPENDENCIES_H_
diff --git a/src/tracing/service/random.cc b/src/tracing/service/random.cc
new file mode 100644
index 0000000..d1c4cb0
--- /dev/null
+++ b/src/tracing/service/random.cc
@@ -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.
+ */
+
+#include "src/tracing/service/random.h"
+
+namespace perfetto::tracing_service {
+
+Random::~Random() = default;
+
+RandomImpl::RandomImpl(uint32_t seed) : prng_(seed) {}
+RandomImpl::~RandomImpl() = default;
+
+double RandomImpl::GetValue() {
+  return dist_(prng_);
+}
+
+}  // namespace perfetto::tracing_service
diff --git a/src/tracing/service/random.h b/src/tracing/service/random.h
new file mode 100644
index 0000000..26a4391
--- /dev/null
+++ b/src/tracing/service/random.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACING_SERVICE_RANDOM_H_
+#define SRC_TRACING_SERVICE_RANDOM_H_
+
+#include <random>
+
+namespace perfetto::tracing_service {
+
+class Random {
+ public:
+  virtual ~Random();
+  virtual double GetValue() = 0;
+};
+
+class RandomImpl : public Random {
+ public:
+  explicit RandomImpl(uint32_t seed);
+  ~RandomImpl() override;
+  double GetValue() override;
+
+ private:
+  std::minstd_rand prng_;
+  std::uniform_real_distribution<double> dist_;
+};
+
+}  // namespace perfetto::tracing_service
+
+#endif  // SRC_TRACING_SERVICE_RANDOM_H_
diff --git a/src/tracing/service/tracing_service_impl.cc b/src/tracing/service/tracing_service_impl.cc
index 37dfc35..68b5ba6 100644
--- a/src/tracing/service/tracing_service_impl.cc
+++ b/src/tracing/service/tracing_service_impl.cc
@@ -19,16 +19,13 @@
 #include <limits.h>
 #include <string.h>
 
+#include <algorithm>
 #include <cinttypes>
 #include <cstdint>
 #include <limits>
 #include <optional>
-#include <regex>
 #include <string>
 #include <unordered_set>
-#include "perfetto/base/time.h"
-#include "perfetto/ext/tracing/core/client_identity.h"
-#include "perfetto/tracing/core/clock_snapshots.h"
 
 #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) && \
     !PERFETTO_BUILDFLAG(PERFETTO_OS_NACL)
@@ -50,12 +47,11 @@
 #include <sys/stat.h>
 #endif
 
-#include <algorithm>
-
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/status.h"
 #include "perfetto/base/task_runner.h"
 #include "perfetto/ext/base/android_utils.h"
+#include "perfetto/ext/base/clock_snapshots.h"
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/metatrace.h"
 #include "perfetto/ext/base/string_utils.h"
@@ -66,6 +62,7 @@
 #include "perfetto/ext/base/version.h"
 #include "perfetto/ext/base/watchdog.h"
 #include "perfetto/ext/tracing/core/basic_types.h"
+#include "perfetto/ext/tracing/core/client_identity.h"
 #include "perfetto/ext/tracing/core/consumer.h"
 #include "perfetto/ext/tracing/core/observable_events.h"
 #include "perfetto/ext/tracing/core/producer.h"
@@ -112,6 +109,8 @@
 constexpr int kMaxBuffersPerConsumer = 128;
 constexpr uint32_t kDefaultSnapshotsIntervalMs = 10 * 1000;
 constexpr int kDefaultWriteIntoFilePeriodMs = 5000;
+constexpr int kMinWriteIntoFilePeriodMs = 100;
+constexpr uint32_t kAllDataSourceStartedTimeout = 20000;
 constexpr int kMaxConcurrentTracingSessions = 15;
 constexpr int kMaxConcurrentTracingSessionsPerUid = 5;
 constexpr int kMaxConcurrentTracingSessionsForStatsdUid = 10;
@@ -125,6 +124,8 @@
 constexpr uint32_t kGuardrailsMaxTracingBufferSizeKb = 128 * 1024;
 constexpr uint32_t kGuardrailsMaxTracingDurationMillis = 24 * kMillisPerHour;
 
+constexpr size_t kMaxLifecycleEventsListedDataSources = 32;
+
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) || PERFETTO_BUILDFLAG(PERFETTO_OS_NACL)
 struct iovec {
   void* iov_base;  // Address
@@ -332,21 +333,26 @@
     std::unique_ptr<SharedMemory::Factory> shm_factory,
     base::TaskRunner* task_runner,
     InitOpts init_opts) {
-  return std::unique_ptr<TracingService>(
-      new TracingServiceImpl(std::move(shm_factory), task_runner, init_opts));
+  tracing_service::Dependencies deps;
+  deps.clock = std::make_unique<tracing_service::ClockImpl>();
+  uint32_t seed = static_cast<uint32_t>(deps.clock->GetWallTimeMs().count());
+  deps.random = std::make_unique<tracing_service::RandomImpl>(seed);
+  return std::unique_ptr<TracingService>(new TracingServiceImpl(
+      std::move(shm_factory), task_runner, std::move(deps), init_opts));
 }
 
 TracingServiceImpl::TracingServiceImpl(
     std::unique_ptr<SharedMemory::Factory> shm_factory,
     base::TaskRunner* task_runner,
+    tracing_service::Dependencies deps,
     InitOpts init_opts)
     : task_runner_(task_runner),
+      clock_(std::move(deps.clock)),
+      random_(std::move(deps.random)),
       init_opts_(init_opts),
       shm_factory_(std::move(shm_factory)),
       uid_(base::GetCurrentUserId()),
       buffer_ids_(kMaxTraceBufferID),
-      trigger_probability_rand_(
-          static_cast<uint32_t>(base::GetWallTimeNs().count())),
       weak_ptr_factory_(this) {
   PERFETTO_DCHECK(task_runner_);
 }
@@ -831,7 +837,7 @@
   if (cfg.enable_extra_guardrails()) {
     // unique_session_name can be empty
     const std::string& name = cfg.unique_session_name();
-    int64_t now_s = base::GetBootTimeS().count();
+    int64_t now_s = clock_->GetBootTimeS().count();
 
     // Remove any entries where the time limit has passed so this map doesn't
     // grow indefinitely:
@@ -972,8 +978,8 @@
     uint32_t write_period_ms = cfg.file_write_period_ms();
     if (write_period_ms == 0)
       write_period_ms = kDefaultWriteIntoFilePeriodMs;
-    if (write_period_ms < min_write_period_ms_)
-      write_period_ms = min_write_period_ms_;
+    if (write_period_ms < kMinWriteIntoFilePeriodMs)
+      write_period_ms = kMinWriteIntoFilePeriodMs;
     tracing_session->write_period_ms = write_period_ms;
     tracing_session->max_file_size_bytes = cfg.max_file_size_bytes();
     tracing_session->bytes_written_into_file = 0;
@@ -1129,7 +1135,7 @@
   // TraceConfig.trigger_config(). If both are specified which ever one occurs
   // first will initiate the trace.
   if (!cfg.deferred_start() && !has_start_trigger)
-    return StartTracing(tsid);
+    StartTracing(tsid);
 
   return base::OkStatus();
 }
@@ -1250,14 +1256,22 @@
   }
 }
 
-base::Status TracingServiceImpl::StartTracing(TracingSessionID tsid) {
+uint32_t TracingServiceImpl::DelayToNextWritePeriodMs(
+    const TracingSession& session) {
+  PERFETTO_DCHECK(session.write_period_ms > 0);
+  return session.write_period_ms -
+         static_cast<uint32_t>(clock_->GetWallTimeMs().count() %
+                               session.write_period_ms);
+}
+
+void TracingServiceImpl::StartTracing(TracingSessionID tsid) {
   PERFETTO_DCHECK_THREAD(thread_checker_);
 
   auto weak_this = weak_ptr_factory_.GetWeakPtr();
   TracingSession* tracing_session = GetTracingSession(tsid);
   if (!tracing_session) {
-    return PERFETTO_SVC_ERR(
-        "StartTracing() failed, invalid session ID %" PRIu64, tsid);
+    PERFETTO_ELOG("StartTracing() failed, invalid session ID %" PRIu64, tsid);
+    return;
   }
 
   MaybeLogUploadEvent(tracing_session->config, tracing_session->trace_uuid,
@@ -1267,8 +1281,9 @@
     MaybeLogUploadEvent(
         tracing_session->config, tracing_session->trace_uuid,
         PerfettoStatsdAtom::kTracedStartTracingInvalidSessionState);
-    return PERFETTO_SVC_ERR("StartTracing() failed, invalid session state: %d",
-                            tracing_session->state);
+    PERFETTO_ELOG("StartTracing() failed, invalid session state: %d",
+                  tracing_session->state);
+    return;
   }
 
   tracing_session->state = TracingSession::STARTED;
@@ -1338,7 +1353,7 @@
           if (weak_this)
             weak_this->ReadBuffersIntoFile(tsid);
         },
-        tracing_session->delay_to_next_write_period_ms());
+        DelayToNextWritePeriodMs(*tracing_session));
   }
 
   // Start the periodic flush tasks if the config specified a flush period.
@@ -1361,7 +1376,17 @@
   }
 
   MaybeNotifyAllDataSourcesStarted(tracing_session);
-  return base::OkStatus();
+
+  // `did_notify_all_data_source_started` is only set if a consumer is
+  // connected.
+  if (tracing_session->consumer_maybe_null) {
+    task_runner_->PostDelayedTask(
+        [weak_this, tsid] {
+          if (weak_this)
+            weak_this->OnAllDataSourceStartedTimeout(tsid);
+        },
+        kAllDataSourceStartedTimeout);
+  }
 }
 
 // static
@@ -1433,7 +1458,6 @@
       return;
 
     case TracingSession::CLONED_READ_ONLY:
-      PERFETTO_DLOG("DisableTracing() cannot be called on a cloned session");
       return;
 
     // This is either:
@@ -1534,6 +1558,54 @@
   }  // for (tracing_session)
 }
 
+void TracingServiceImpl::OnAllDataSourceStartedTimeout(TracingSessionID tsid) {
+  PERFETTO_DCHECK_THREAD(thread_checker_);
+  TracingSession* tracing_session = GetTracingSession(tsid);
+  // It would be possible to check for `AllDataSourceInstancesStarted()` here,
+  // but it doesn't make much sense, because a data source can be registered
+  // after the session has started. Therefore this is tied to
+  // `did_notify_all_data_source_started`: if that notification happened, do not
+  // record slow data sources.
+  if (!tracing_session || !tracing_session->consumer_maybe_null ||
+      tracing_session->did_notify_all_data_source_started) {
+    return;
+  }
+
+  int64_t timestamp = clock_->GetBootTimeNs().count();
+
+  protozero::HeapBuffered<protos::pbzero::TracePacket> packet;
+  packet->set_timestamp(static_cast<uint64_t>(timestamp));
+  packet->set_trusted_uid(static_cast<int32_t>(uid_));
+  packet->set_trusted_packet_sequence_id(kServicePacketSequenceID);
+
+  size_t i = 0;
+  protos::pbzero::TracingServiceEvent::DataSources* slow_data_sources =
+      packet->set_service_event()->set_slow_starting_data_sources();
+  for (const auto& [producer_id, ds_instance] :
+       tracing_session->data_source_instances) {
+    if (ds_instance.state == DataSourceInstance::STARTED) {
+      continue;
+    }
+    ProducerEndpointImpl* producer = GetProducer(producer_id);
+    if (!producer) {
+      continue;
+    }
+    if (++i > kMaxLifecycleEventsListedDataSources) {
+      break;
+    }
+    auto* ds = slow_data_sources->add_data_source();
+    ds->set_producer_name(producer->name_);
+    ds->set_data_source_name(ds_instance.data_source_name);
+    PERFETTO_LOG(
+        "Data source failed to start within 20s data_source=\"%s\", "
+        "producer=\"%s\", tsid=%" PRIu64,
+        ds_instance.data_source_name.c_str(), producer->name_.c_str(), tsid);
+  }
+
+  tracing_session->slow_start_event = TracingSession::ArbitraryLifecycleEvent{
+      timestamp, packet.SerializeAsArray()};
+}
+
 void TracingServiceImpl::MaybeNotifyAllDataSourcesStarted(
     TracingSession* tracing_session) {
   if (!tracing_session->consumer_maybe_null)
@@ -1609,10 +1681,13 @@
   auto* producer = GetProducer(producer_id);
   PERFETTO_DCHECK(producer);
 
-  int64_t now_ns = base::GetBootTimeNs().count();
+  int64_t now_ns = clock_->GetBootTimeNs().count();
   for (const auto& trigger_name : triggers) {
     PERFETTO_DLOG("Received ActivateTriggers request for \"%s\"",
                   trigger_name.c_str());
+    android_stats::MaybeLogTriggerEvent(PerfettoTriggerAtom::kTracedTrigger,
+                                        trigger_name);
+
     base::Hasher hash;
     hash.Update(trigger_name.c_str(), trigger_name.size());
     std::string triggered_session_name;
@@ -1652,10 +1727,7 @@
 
       // Use a random number between 0 and 1 to check if we should allow this
       // trigger through or not.
-      double trigger_rnd =
-          trigger_rnd_override_for_testing_ > 0
-              ? trigger_rnd_override_for_testing_
-              : trigger_probability_dist_(trigger_probability_rand_);
+      double trigger_rnd = random_->GetValue();
       PERFETTO_DCHECK(trigger_rnd >= 0 && trigger_rnd < 1);
       if (trigger_rnd < iter->skip_probability()) {
         MaybeLogTriggerEvent(tracing_session.config,
@@ -1839,6 +1911,11 @@
     return;
   }
 
+  SnapshotLifecyleEvent(
+      tracing_session,
+      protos::pbzero::TracingServiceEvent::kFlushStartedFieldNumber,
+      false /* snapshot_clocks */);
+
   std::map<ProducerID, std::vector<DataSourceInstanceID>> data_source_instances;
   for (const auto& [producer_id, ds_inst] :
        tracing_session->data_source_instances) {
@@ -1874,6 +1951,8 @@
     return;
   }
 
+  tracing_session->last_flush_events.clear();
+
   ++tracing_session->flushes_requested;
   FlushRequestID flush_request_id = ++last_flush_request_id_;
   PendingFlush& pending_flush =
@@ -1899,9 +1978,9 @@
 
   auto weak_this = weak_ptr_factory_.GetWeakPtr();
   task_runner_->PostDelayedTask(
-      [weak_this, tsid = tracing_session->id, flush_request_id] {
+      [weak_this, tsid = tracing_session->id, flush_request_id, flush_flags] {
         if (weak_this)
-          weak_this->OnFlushTimeout(tsid, flush_request_id);
+          weak_this->OnFlushTimeout(tsid, flush_request_id, flush_flags);
       },
       timeout_ms);
 }
@@ -1935,7 +2014,8 @@
 }
 
 void TracingServiceImpl::OnFlushTimeout(TracingSessionID tsid,
-                                        FlushRequestID flush_request_id) {
+                                        FlushRequestID flush_request_id,
+                                        FlushFlags flush_flags) {
   TracingSession* tracing_session = GetTracingSession(tsid);
   if (!tracing_session)
     return;
@@ -1943,9 +2023,51 @@
   if (it == tracing_session->pending_flushes.end())
     return;  // Nominal case: flush was completed and acked on time.
 
+  PendingFlush& pending_flush = it->second;
+
   // If there were no producers to flush, consider it a success.
-  bool success = it->second.producers.empty();
-  auto callback = std::move(it->second.callback);
+  bool success = pending_flush.producers.empty();
+  auto callback = std::move(pending_flush.callback);
+  // If flush failed and this is a "final" flush, log which data sources were
+  // slow.
+  if ((flush_flags.reason() == FlushFlags::Reason::kTraceClone ||
+       flush_flags.reason() == FlushFlags::Reason::kTraceStop) &&
+      !success) {
+    int64_t timestamp = clock_->GetBootTimeNs().count();
+
+    protozero::HeapBuffered<protos::pbzero::TracePacket> packet;
+    packet->set_timestamp(static_cast<uint64_t>(timestamp));
+    packet->set_trusted_uid(static_cast<int32_t>(uid_));
+    packet->set_trusted_packet_sequence_id(kServicePacketSequenceID);
+
+    size_t i = 0;
+    protos::pbzero::TracingServiceEvent::DataSources* event =
+        packet->set_service_event()->set_last_flush_slow_data_sources();
+    for (const auto& producer_id : pending_flush.producers) {
+      ProducerEndpointImpl* producer = GetProducer(producer_id);
+      if (!producer) {
+        continue;
+      }
+      if (++i > kMaxLifecycleEventsListedDataSources) {
+        break;
+      }
+
+      auto ds_id_range =
+          tracing_session->data_source_instances.equal_range(producer_id);
+      for (auto ds_it = ds_id_range.first; ds_it != ds_id_range.second;
+           ds_it++) {
+        auto* ds = event->add_data_source();
+        ds->set_producer_name(producer->name_);
+        ds->set_data_source_name(ds_it->second.data_source_name);
+        if (++i > kMaxLifecycleEventsListedDataSources) {
+          break;
+        }
+      }
+    }
+
+    tracing_session->last_flush_events.push_back(
+        {timestamp, packet.SerializeAsArray()});
+  }
   tracing_session->pending_flushes.erase(it);
   CompleteFlush(tsid, std::move(callback), success);
 }
@@ -2140,7 +2262,7 @@
         if (weak_this)
           weak_this->PeriodicFlushTask(tsid, /*post_next_only=*/false);
       },
-      flush_period_ms - static_cast<uint32_t>(base::GetWallTimeMs().count() %
+      flush_period_ms - static_cast<uint32_t>(clock_->GetWallTimeMs().count() %
                                               flush_period_ms));
 
   if (post_next_only)
@@ -2174,7 +2296,7 @@
           weak_this->PeriodicClearIncrementalStateTask(
               tsid, /*post_next_only=*/false);
       },
-      clear_period_ms - static_cast<uint32_t>(base::GetWallTimeMs().count() %
+      clear_period_ms - static_cast<uint32_t>(clock_->GetWallTimeMs().count() %
                                               clear_period_ms));
 
   if (post_next_only)
@@ -2310,7 +2432,7 @@
         if (weak_this)
           weak_this->ReadBuffersIntoFile(tsid);
       },
-      tracing_session->delay_to_next_write_period_ms());
+      DelayToNextWritePeriodMs(*tracing_session));
   return true;
 }
 
@@ -2538,7 +2660,7 @@
   // by the earlier call to SetFilterRoot() in EnableTracing().
   PERFETTO_DCHECK(trace_filter.config().root_msg_index() != 0);
   std::vector<protozero::MessageFilter::InputSlice> filter_input;
-  auto start = base::GetWallTimeNs();
+  auto start = clock_->GetWallTimeNs();
   for (TracePacket& packet : *packets) {
     const auto& packet_slices = packet.slices();
     const size_t input_packet_size = packet.size();
@@ -2578,7 +2700,7 @@
                               filtered_packet.size, kMaxTracePacketSliceSize,
                               &packet);
   }
-  auto end = base::GetWallTimeNs();
+  auto end = clock_->GetWallTimeNs();
   tracing_session->filter_time_taken_ns +=
       static_cast<uint64_t>((end - start).count());
 }
@@ -3204,6 +3326,25 @@
 }
 
 TracingServiceImpl::TracingSession*
+TracingServiceImpl::GetTracingSessionByUniqueName(
+    const std::string& unique_session_name) {
+  PERFETTO_DCHECK_THREAD(thread_checker_);
+  if (unique_session_name.empty()) {
+    return nullptr;
+  }
+  for (auto& session_id_and_session : tracing_sessions_) {
+    TracingSession& session = session_id_and_session.second;
+    if (session.state == TracingSession::CLONED_READ_ONLY) {
+      continue;
+    }
+    if (session.config.unique_session_name() == unique_session_name) {
+      return &session;
+    }
+  }
+  return nullptr;
+}
+
+TracingServiceImpl::TracingSession*
 TracingServiceImpl::FindTracingSessionWithMaxBugreportScore() {
   TracingSession* max_session = nullptr;
   for (auto& session_id_and_session : tracing_sessions_) {
@@ -3337,7 +3478,7 @@
     event->timestamps.erase_front(1 + event->timestamps.size() -
                                   event->max_size);
   }
-  event->timestamps.emplace_back(base::GetBootTimeNs().count());
+  event->timestamps.emplace_back(clock_->GetBootTimeNs().count());
 }
 
 void TracingServiceImpl::MaybeSnapshotClocksIntoRingBuffer(
@@ -3380,7 +3521,8 @@
   // been emitted into the trace yet (see comment below).
   static constexpr int64_t kSignificantDriftNs = 10 * 1000 * 1000;  // 10 ms
 
-  TracingSession::ClockSnapshotData new_snapshot_data = CaptureClockSnapshots();
+  TracingSession::ClockSnapshotData new_snapshot_data =
+      base::CaptureClockSnapshots();
   // If we're about to update a session's latest clock snapshot that hasn't been
   // emitted into the trace yet, check whether the clocks have drifted enough to
   // warrant overriding the current snapshot values. The older snapshot would be
@@ -3613,6 +3755,14 @@
     PERFETTO_ELOG("Unable to read ro.build.fingerprint");
   }
 
+  std::string device_manufacturer_value =
+      base::GetAndroidProp("ro.product.manufacturer");
+  if (!device_manufacturer_value.empty()) {
+    info->set_android_device_manufacturer(device_manufacturer_value);
+  } else {
+    PERFETTO_ELOG("Unable to read ro.product.manufacturer");
+  }
+
   std::string sdk_str_value = base::GetAndroidProp("ro.build.version.sdk");
   std::optional<uint64_t> sdk_value = base::StringToUInt64(sdk_str_value);
   if (sdk_value.has_value()) {
@@ -3628,6 +3778,13 @@
     PERFETTO_ELOG("Unable to read ro.soc.model");
   }
 
+  // guest_soc model is not always present
+  std::string guest_soc_model_value =
+      base::GetAndroidProp("ro.boot.guest_soc.model");
+  if (!guest_soc_model_value.empty()) {
+    info->set_android_guest_soc_model(guest_soc_model_value);
+  }
+
   std::string hw_rev_value = base::GetAndroidProp("ro.boot.hardware.revision");
   if (!hw_rev_value.empty()) {
     info->set_android_hardware_revision(hw_rev_value);
@@ -3635,6 +3792,20 @@
     PERFETTO_ELOG("Unable to read ro.boot.hardware.revision");
   }
 
+  std::string hw_ufs_value = base::GetAndroidProp("ro.boot.hardware.ufs");
+  if (!hw_ufs_value.empty()) {
+    info->set_android_storage_model(hw_ufs_value);
+  } else {
+    PERFETTO_ELOG("Unable to read ro.boot.hardware.ufs");
+  }
+
+  std::string hw_ddr_value = base::GetAndroidProp("ro.boot.hardware.ddr");
+  if (!hw_ddr_value.empty()) {
+    info->set_android_ram_model(hw_ddr_value);
+  } else {
+    PERFETTO_ELOG("Unable to read ro.boot.hardware.ddr");
+  }
+
 #endif  // PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
   packet->set_trusted_uid(static_cast<int32_t>(uid_));
   packet->set_trusted_packet_sequence_id(kServicePacketSequenceID);
@@ -3662,6 +3833,18 @@
     event.timestamps.clear();
   }
 
+  if (tracing_session->slow_start_event.has_value()) {
+    const TracingSession::ArbitraryLifecycleEvent& event =
+        *tracing_session->slow_start_event;
+    timestamped_packets.emplace_back(event.timestamp, std::move(event.data));
+  }
+  tracing_session->slow_start_event.reset();
+
+  for (auto& event : tracing_session->last_flush_events) {
+    timestamped_packets.emplace_back(event.timestamp, std::move(event.data));
+  }
+  tracing_session->last_flush_events.clear();
+
   // We sort by timestamp here to ensure that the "sequence" of lifecycle
   // packets has monotonic timestamps like other sequences in the trace.
   // Note that these events could still be out of order with respect to other
@@ -3771,12 +3954,13 @@
 size_t TracingServiceImpl::PurgeExpiredAndCountTriggerInWindow(
     int64_t now_ns,
     uint64_t trigger_name_hash) {
+  constexpr int64_t kOneDayInNs = 24ll * 60 * 60 * 1000 * 1000 * 1000;
   PERFETTO_DCHECK(
       std::is_sorted(trigger_history_.begin(), trigger_history_.end()));
   size_t remove_count = 0;
   size_t trigger_count = 0;
   for (const TriggerHistory& h : trigger_history_) {
-    if (h.timestamp_ns < now_ns - trigger_window_ns_) {
+    if (h.timestamp_ns < now_ns - kOneDayInNs) {
       remove_count++;
     } else if (h.name_hash == trigger_name_hash) {
       trigger_count++;
@@ -3788,33 +3972,37 @@
 
 base::Status TracingServiceImpl::FlushAndCloneSession(
     ConsumerEndpointImpl* consumer,
-    TracingSessionID tsid,
-    bool skip_trace_filter,
-    bool for_bugreport) {
+    ConsumerEndpoint::CloneSessionArgs args) {
   PERFETTO_DCHECK_THREAD(thread_checker_);
   auto clone_target = FlushFlags::CloneTarget::kUnknown;
 
-  if (tsid == kBugreportSessionId) {
-    // This branch is only here to support the legacy protocol where we could
-    // clone only a single session using the magic ID kBugreportSessionId.
-    // The newer perfetto --clone-all-for-bugreport first queries the existing
-    // sessions and then issues individual clone requests specifying real
-    // session IDs, setting args.{for_bugreport,skip_trace_filter}=true.
-    PERFETTO_LOG("Looking for sessions for bugreport");
-    TracingSession* session = FindTracingSessionWithMaxBugreportScore();
-    if (!session) {
-      return base::ErrStatus(
-          "No tracing sessions eligible for bugreport found");
-    }
-    tsid = session->id;
-    clone_target = FlushFlags::CloneTarget::kBugreport;
-    skip_trace_filter = true;
-    for_bugreport = true;
-  } else if (for_bugreport) {
+  TracingSession* session = nullptr;
+  if (args.for_bugreport) {
     clone_target = FlushFlags::CloneTarget::kBugreport;
   }
+  if (args.tsid != 0) {
+    if (args.tsid == kBugreportSessionId) {
+      // This branch is only here to support the legacy protocol where we could
+      // clone only a single session using the magic ID kBugreportSessionId.
+      // The newer perfetto --clone-all-for-bugreport first queries the existing
+      // sessions and then issues individual clone requests specifying real
+      // session IDs, setting args.{for_bugreport,skip_trace_filter}=true.
+      PERFETTO_LOG("Looking for sessions for bugreport");
+      session = FindTracingSessionWithMaxBugreportScore();
+      if (!session) {
+        return base::ErrStatus(
+            "No tracing sessions eligible for bugreport found");
+      }
+      args.tsid = session->id;
+      clone_target = FlushFlags::CloneTarget::kBugreport;
+      args.skip_trace_filter = true;
+    } else {
+      session = GetTracingSession(args.tsid);
+    }
+  } else if (!args.unique_session_name.empty()) {
+    session = GetTracingSessionByUniqueName(args.unique_session_name);
+  }
 
-  TracingSession* session = GetTracingSession(tsid);
   if (!session) {
     return base::ErrStatus("Tracing session not found");
   }
@@ -3870,7 +4058,7 @@
   clone_op.buffers =
       std::vector<std::unique_ptr<TraceBuffer>>(session->buffers_index.size());
   clone_op.weak_consumer = weak_consumer;
-  clone_op.skip_trace_filter = skip_trace_filter;
+  clone_op.skip_trace_filter = args.skip_trace_filter;
 
   // Issue separate flush requests for separate buffer groups. The buffer marked
   // as transfer_on_clone will be flushed and cloned separately: even if they're
@@ -3888,12 +4076,15 @@
     }
   }
 
+  SnapshotLifecyleEvent(
+      session, protos::pbzero::TracingServiceEvent::kFlushStartedFieldNumber,
+      false /* snapshot_clocks */);
   clone_op.pending_flush_cnt = bufs_groups.size();
   for (const std::set<BufferID>& buf_group : bufs_groups) {
     FlushDataSourceInstances(
         session, 0,
         GetFlushableDataSourceInstancesForBuffers(session, buf_group),
-        [tsid, clone_id, buf_group, weak_this](bool final_flush) {
+        [tsid = session->id, clone_id, buf_group, weak_this](bool final_flush) {
           if (!weak_this)
             return;
           weak_this->OnFlushDoneForClone(tsid, clone_id, buf_group,
@@ -4096,6 +4287,8 @@
   src->num_triggers_emitted_into_trace = 0;
   cloned_session->lifecycle_events =
       std::vector<TracingSession::LifecycleEvent>(src->lifecycle_events);
+  cloned_session->slow_start_event = src->slow_start_event;
+  cloned_session->last_flush_events = src->last_flush_events;
   cloned_session->initial_clock_snapshot = src->initial_clock_snapshot;
   cloned_session->clock_snapshot_ring_buffer = src->clock_snapshot_ring_buffer;
   cloned_session->invalid_packets = src->invalid_packets;
@@ -4477,12 +4670,10 @@
 }
 
 void TracingServiceImpl::ConsumerEndpointImpl::CloneSession(
-    TracingSessionID tsid,
     CloneSessionArgs args) {
   PERFETTO_DCHECK_THREAD(thread_checker_);
   // FlushAndCloneSession will call OnSessionCloned after the async flush.
-  base::Status result = service_->FlushAndCloneSession(
-      this, tsid, args.skip_trace_filter, args.for_bugreport);
+  base::Status result = service_->FlushAndCloneSession(this, std::move(args));
 
   if (!result.ok()) {
     consumer_->OnSessionCloned({false, result.message(), {}});
@@ -4828,12 +5019,15 @@
       config(new_config),
       snapshot_periodic_task(task_runner),
       timed_stop_task(task_runner) {
-  // all_data_sources_flushed is special because we store up to 64 events of
-  // this type. Other events will go through the default case in
+  // all_data_sources_flushed (and flush_started) is special because we store up
+  // to 64 events of this type. Other events will go through the default case in
   // SnapshotLifecycleEvent() where they will be given a max history of 1.
   lifecycle_events.emplace_back(
       protos::pbzero::TracingServiceEvent::kAllDataSourcesFlushedFieldNumber,
       64 /* max_size */);
+  lifecycle_events.emplace_back(
+      protos::pbzero::TracingServiceEvent::kFlushStartedFieldNumber,
+      64 /* max_size */);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -4847,8 +5041,8 @@
 
 void TracingServiceImpl::RelayEndpointImpl::SyncClocks(
     SyncMode sync_mode,
-    ClockSnapshotVector client_clocks,
-    ClockSnapshotVector host_clocks) {
+    base::ClockSnapshotVector client_clocks,
+    base::ClockSnapshotVector host_clocks) {
   // We keep only the most recent 5 clock sync snapshots.
   static constexpr size_t kNumSyncClocks = 5;
   if (synced_clocks_.size() >= kNumSyncClocks)
diff --git a/src/tracing/service/tracing_service_impl.h b/src/tracing/service/tracing_service_impl.h
index 64b1f50..22f527b 100644
--- a/src/tracing/service/tracing_service_impl.h
+++ b/src/tracing/service/tracing_service_impl.h
@@ -22,7 +22,6 @@
 #include <map>
 #include <memory>
 #include <optional>
-#include <random>
 #include <set>
 #include <utility>
 #include <vector>
@@ -31,8 +30,8 @@
 #include "perfetto/base/status.h"
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/circular_queue.h"
+#include "perfetto/ext/base/clock_snapshots.h"
 #include "perfetto/ext/base/periodic_task.h"
-#include "perfetto/ext/base/string_view.h"
 #include "perfetto/ext/base/uuid.h"
 #include "perfetto/ext/base/weak_ptr.h"
 #include "perfetto/ext/tracing/core/basic_types.h"
@@ -48,6 +47,9 @@
 #include "perfetto/tracing/core/trace_config.h"
 #include "src/android_stats/perfetto_atoms.h"
 #include "src/tracing/core/id_allocator.h"
+#include "src/tracing/service/clock.h"
+#include "src/tracing/service/dependencies.h"
+#include "src/tracing/service/random.h"
 
 namespace protozero {
 class MessageFilter;
@@ -159,8 +161,6 @@
 
    private:
     friend class TracingServiceImpl;
-    friend class TracingServiceImplTest;
-    friend class TracingIntegrationTest;
     ProducerEndpointImpl(const ProducerEndpointImpl&) = delete;
     ProducerEndpointImpl& operator=(const ProducerEndpointImpl&) = delete;
 
@@ -230,7 +230,7 @@
                            QueryServiceStateCallback) override;
     void QueryCapabilities(QueryCapabilitiesCallback) override;
     void SaveTraceForBugreport(SaveTraceForBugreportCallback) override;
-    void CloneSession(TracingSessionID, CloneSessionArgs) override;
+    void CloneSession(CloneSessionArgs) override;
 
     // Will queue a task to notify the consumer about the state change.
     void OnDataSourceInstanceStateChange(const ProducerEndpointImpl&,
@@ -274,22 +274,22 @@
 
     struct SyncedClockSnapshots {
       SyncedClockSnapshots(SyncMode _sync_mode,
-                           ClockSnapshotVector _client_clocks,
-                           ClockSnapshotVector _host_clocks)
+                           base::ClockSnapshotVector _client_clocks,
+                           base::ClockSnapshotVector _host_clocks)
           : sync_mode(_sync_mode),
             client_clocks(std::move(_client_clocks)),
             host_clocks(std::move(_host_clocks)) {}
       SyncMode sync_mode;
-      ClockSnapshotVector client_clocks;
-      ClockSnapshotVector host_clocks;
+      base::ClockSnapshotVector client_clocks;
+      base::ClockSnapshotVector host_clocks;
     };
 
     explicit RelayEndpointImpl(RelayClientID relay_client_id,
                                TracingServiceImpl* service);
     ~RelayEndpointImpl() override;
     void SyncClocks(SyncMode sync_mode,
-                    ClockSnapshotVector client_clocks,
-                    ClockSnapshotVector host_clocks) override;
+                    base::ClockSnapshotVector client_clocks,
+                    base::ClockSnapshotVector host_clocks) override;
     void Disconnect() override;
 
     MachineID machine_id() const { return relay_client_id_.first; }
@@ -311,6 +311,7 @@
 
   explicit TracingServiceImpl(std::unique_ptr<SharedMemory::Factory>,
                               base::TaskRunner*,
+                              tracing_service::Dependencies,
                               InitOpts = {});
   ~TracingServiceImpl() override;
 
@@ -345,7 +346,7 @@
                              base::ScopedFile);
   void ChangeTraceConfig(ConsumerEndpointImpl*, const TraceConfig&);
 
-  base::Status StartTracing(TracingSessionID);
+  void StartTracing(TracingSessionID);
   void DisableTracing(TracingSessionID, bool disable_immediately = false);
   void Flush(TracingSessionID tsid,
              uint32_t timeout_ms,
@@ -353,9 +354,7 @@
              FlushFlags);
   void FlushAndDisableTracing(TracingSessionID);
   base::Status FlushAndCloneSession(ConsumerEndpointImpl*,
-                                    TracingSessionID,
-                                    bool skip_filter,
-                                    bool for_bugreport);
+                                    ConsumerEndpoint::CloneSessionArgs);
 
   // Starts reading the internal tracing buffers from the tracing session `tsid`
   // and sends them to `*consumer` (which must be != nullptr).
@@ -415,11 +414,6 @@
   ProducerEndpointImpl* GetProducer(ProducerID) const;
 
  private:
-  friend class TracingServiceImplTest;
-  friend class TracingIntegrationTest;
-
-  static constexpr int64_t kOneDayInNs = 24ll * 60 * 60 * 1000 * 1000 * 1000;
-
   struct TriggerHistory {
     int64_t timestamp_ns;
     uint64_t name_hash;
@@ -510,13 +504,6 @@
 
     size_t num_buffers() const { return buffers_index.size(); }
 
-    uint32_t delay_to_next_write_period_ms() const {
-      PERFETTO_DCHECK(write_period_ms > 0);
-      return write_period_ms -
-             static_cast<uint32_t>(base::GetWallTimeMs().count() %
-                                   write_period_ms);
-    }
-
     uint32_t flush_timeout_ms() {
       uint32_t timeout_ms = config.flush_timeout_ms();
       return timeout_ms ? timeout_ms : kDefaultFlushTimeoutMs;
@@ -663,8 +650,8 @@
     // Set to true on the first call to MaybeNotifyAllDataSourcesStarted().
     bool did_notify_all_data_source_started = false;
 
-    // Stores all lifecycle events of a particular type (i.e. associated with a
-    // single field id in the TracingServiceEvent proto).
+    // Stores simple lifecycle events of a particular type (i.e. associated with
+    // a single field id in the TracingServiceEvent proto).
     struct LifecycleEvent {
       LifecycleEvent(uint32_t f_id, uint32_t m_size = 1)
           : field_id(f_id), max_size(m_size), timestamps(m_size) {}
@@ -684,7 +671,18 @@
     };
     std::vector<LifecycleEvent> lifecycle_events;
 
-    using ClockSnapshotData = ClockSnapshotVector;
+    // Stores arbitrary lifecycle events that don't fit in lifecycle_events as
+    // serialized TracePacket protos.
+    struct ArbitraryLifecycleEvent {
+      int64_t timestamp;
+      std::vector<uint8_t> data;
+    };
+
+    std::optional<ArbitraryLifecycleEvent> slow_start_event;
+
+    std::vector<ArbitraryLifecycleEvent> last_flush_events;
+
+    using ClockSnapshotData = base::ClockSnapshotVector;
 
     // Initial clock snapshot, captured at trace start time (when state goes to
     // TracingSession::STARTED). Emitted into the trace when the consumer first
@@ -759,6 +757,12 @@
   // session doesn't exists.
   TracingSession* GetTracingSession(TracingSessionID);
 
+  // Returns a pointer to the |tracing_sessions_| entry with
+  // |unique_session_name| in the config (or nullptr if the
+  // session doesn't exists). CLONED_READ_ONLY sessions are ignored.
+  TracingSession* GetTracingSessionByUniqueName(
+      const std::string& unique_session_name);
+
   // Returns a pointer to the tracing session that has the highest
   // TraceConfig.bugreport_score, if any, or nullptr.
   TracingSession* FindTracingSessionWithMaxBugreportScore();
@@ -771,6 +775,7 @@
   // shared memory and trace buffers.
   void UpdateMemoryGuardrail();
 
+  uint32_t DelayToNextWritePeriodMs(const TracingSession&);
   void StartDataSourceInstance(ProducerEndpointImpl*,
                                TracingSession*,
                                DataSourceInstance*);
@@ -797,8 +802,9 @@
   void MaybeEmitReceivedTriggers(TracingSession*, std::vector<TracePacket>*);
   void MaybeEmitRemoteClockSync(TracingSession*, std::vector<TracePacket>*);
   void MaybeNotifyAllDataSourcesStarted(TracingSession*);
-  void OnFlushTimeout(TracingSessionID, FlushRequestID);
+  void OnFlushTimeout(TracingSessionID, FlushRequestID, FlushFlags);
   void OnDisableTracingTimeout(TracingSessionID);
+  void OnAllDataSourceStartedTimeout(TracingSessionID);
   void DisableTracingNotifyConsumerAndFlushFile(TracingSession*);
   void PeriodicFlushTask(TracingSessionID, bool post_next_only);
   void CompleteFlush(TracingSessionID tsid,
@@ -874,6 +880,8 @@
                                      TracingSessionID);
 
   base::TaskRunner* const task_runner_;
+  std::unique_ptr<tracing_service::Clock> clock_;
+  std::unique_ptr<tracing_service::Random> random_;
   const InitOpts init_opts_;
   std::unique_ptr<SharedMemory::Factory> shm_factory_;
   ProducerID last_producer_id_ = 0;
@@ -895,18 +903,12 @@
   std::map<std::string, int64_t> session_to_last_trace_s_;
 
   // Contains timestamps of triggers.
-  // The queue is sorted by timestamp and invocations older than
-  // |trigger_window_ns_| are purged when a trigger happens.
+  // The queue is sorted by timestamp and invocations older than 24 hours are
+  // purged when a trigger happens.
   base::CircularQueue<TriggerHistory> trigger_history_;
 
   bool smb_scraping_enabled_ = false;
   bool lockdown_mode_ = false;
-  uint32_t min_write_period_ms_ = 100;       // Overridable for testing.
-  int64_t trigger_window_ns_ = kOneDayInNs;  // Overridable for testing.
-
-  std::minstd_rand trigger_probability_rand_;
-  std::uniform_real_distribution<> trigger_probability_dist_;
-  double trigger_rnd_override_for_testing_ = 0;  // Overridable for testing.
 
   uint8_t sync_marker_packet_[32];  // Lazily initialized.
   size_t sync_marker_packet_size_ = 0;
diff --git a/src/tracing/service/tracing_service_impl_unittest.cc b/src/tracing/service/tracing_service_impl_unittest.cc
index cbe81a6..0a6438a 100644
--- a/src/tracing/service/tracing_service_impl_unittest.cc
+++ b/src/tracing/service/tracing_service_impl_unittest.cc
@@ -35,6 +35,7 @@
 #include "perfetto/base/proc_utils.h"
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/pipe.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/sys_types.h"
 #include "perfetto/ext/base/temp_file.h"
@@ -63,6 +64,7 @@
 #include "src/tracing/core/trace_writer_impl.h"
 #include "src/tracing/test/mock_consumer.h"
 #include "src/tracing/test/mock_producer.h"
+#include "src/tracing/test/proxy_producer_endpoint.h"
 #include "src/tracing/test/test_shared_memory.h"
 #include "test/gtest_and_gmock.h"
 
@@ -100,6 +102,7 @@
 using ::testing::IsEmpty;
 using ::testing::Mock;
 using ::testing::Ne;
+using ::testing::NiceMock;
 using ::testing::Not;
 using ::testing::Pointee;
 using ::testing::Property;
@@ -193,23 +196,61 @@
 }
 #endif  // PERFETTO_BUILDFLAG(PERFETTO_ZLIB)
 
-}  // namespace
+std::vector<std::string> GetReceivedTriggers(
+    const std::vector<protos::gen::TracePacket>& trace) {
+  std::vector<std::string> triggers;
+  for (const protos::gen::TracePacket& packet : trace) {
+    if (packet.has_trigger()) {
+      triggers.push_back(packet.trigger().trigger_name());
+    }
+  }
+  return triggers;
+}
+
+class MockClock : public tracing_service::Clock {
+ public:
+  ~MockClock() override = default;
+  MOCK_METHOD(base::TimeNanos, GetBootTimeNs, (), (override));
+  MOCK_METHOD(base::TimeNanos, GetWallTimeNs, (), (override));
+};
+
+class MockRandom : public tracing_service::Random {
+ public:
+  ~MockRandom() override = default;
+  MOCK_METHOD(double, GetValue, (), (override));
+};
 
 class TracingServiceImplTest : public testing::Test {
  public:
-  using DataSourceInstanceState =
-      TracingServiceImpl::DataSourceInstance::DataSourceInstanceState;
-
   TracingServiceImplTest() { InitializeSvcWithOpts({}); }
 
   void InitializeSvcWithOpts(TracingService::InitOpts init_opts) {
     auto shm_factory =
         std::unique_ptr<SharedMemory::Factory>(new TestSharedMemory::Factory());
-    svc.reset(static_cast<TracingServiceImpl*>(
-        TracingService::CreateInstance(std::move(shm_factory), &task_runner,
-                                       init_opts)
-            .release()));
-    svc->min_write_period_ms_ = 1;
+
+    tracing_service::Dependencies deps;
+
+    auto mock_clock = std::make_unique<NiceMock<MockClock>>();
+    mock_clock_ = mock_clock.get();
+    deps.clock = std::move(mock_clock);
+    ON_CALL(*mock_clock_, GetBootTimeNs).WillByDefault(Invoke([&] {
+      return real_clock_.GetBootTimeNs() + mock_clock_displacement_;
+    }));
+    ON_CALL(*mock_clock_, GetWallTimeNs).WillByDefault(Invoke([&] {
+      return real_clock_.GetWallTimeNs() + mock_clock_displacement_;
+    }));
+
+    auto mock_random = std::make_unique<NiceMock<MockRandom>>();
+    mock_random_ = mock_random.get();
+    deps.random = std::move(mock_random);
+    real_random_ = std::make_unique<tracing_service::RandomImpl>(
+        real_clock_.GetWallTimeMs().count());
+    ON_CALL(*mock_random_, GetValue).WillByDefault(Invoke([&] {
+      return real_random_->GetValue();
+    }));
+
+    svc = std::make_unique<TracingServiceImpl>(
+        std::move(shm_factory), &task_runner, std::move(deps), init_opts);
   }
 
   std::unique_ptr<MockProducer> CreateMockProducer() {
@@ -222,95 +263,31 @@
         new StrictMock<MockConsumer>(&task_runner));
   }
 
-  ProducerID* last_producer_id() { return &svc->last_producer_id_; }
-
-  uid_t GetProducerUid(ProducerID producer_id) {
-    return svc->GetProducer(producer_id)->uid();
-  }
-
-  TracingServiceImpl::TracingSession* GetTracingSession(TracingSessionID tsid) {
-    auto* session = svc->GetTracingSession(tsid);
-    EXPECT_NE(nullptr, session);
-    return session;
-  }
-
-  TracingServiceImpl::TracingSession* tracing_session() {
-    return GetTracingSession(GetTracingSessionID());
-  }
-
-  TracingSessionID GetTracingSessionID() {
-    return svc->last_tracing_session_id_;
-  }
-
-  const std::set<BufferID>& GetAllowedTargetBuffers(ProducerID producer_id) {
-    return svc->GetProducer(producer_id)->allowed_target_buffers_;
-  }
-
-  const std::map<WriterID, BufferID>& GetWriters(ProducerID producer_id) {
-    return svc->GetProducer(producer_id)->writers_;
-  }
-
-  SharedMemoryArbiterImpl* GetShmemArbiterForProducer(ProducerID producer_id) {
-    return svc->GetProducer(producer_id)->inproc_shmem_arbiter_.get();
-  }
-
-  std::unique_ptr<SharedMemoryArbiterImpl> StealShmemArbiterForProducer(
-      ProducerID producer_id) {
-    return std::move(svc->GetProducer(producer_id)->inproc_shmem_arbiter_);
-  }
-
-  size_t GetNumPendingFlushes() {
-    return tracing_session()->pending_flushes.size();
-  }
-
-  void WaitForNextSyncMarker() {
-    tracing_session()->should_emit_sync_marker = true;
-    static int attempt = 0;
-    while (tracing_session()->should_emit_sync_marker) {
-      auto checkpoint_name = "wait_snapshot_" + std::to_string(attempt++);
-      auto timer_expired = task_runner.CreateCheckpoint(checkpoint_name);
-      task_runner.PostDelayedTask([timer_expired] { timer_expired(); }, 1);
-      task_runner.RunUntilCheckpoint(checkpoint_name);
-    }
-  }
-
-  void WaitForTraceWritersChanged(ProducerID producer_id) {
-    static int i = 0;
-    auto checkpoint_name = "writers_changed_" + std::to_string(producer_id) +
-                           "_" + std::to_string(i++);
-    auto writers_changed = task_runner.CreateCheckpoint(checkpoint_name);
-    auto writers = GetWriters(producer_id);
-    std::function<void()> task;
-    task = [&task, writers, writers_changed, producer_id, this]() {
-      if (writers != GetWriters(producer_id)) {
-        writers_changed();
-        return;
+  TracingSessionID GetLastTracingSessionId(MockConsumer* consumer) {
+    TracingSessionID ret = 0;
+    TracingServiceState svc_state = consumer->QueryServiceState();
+    for (const auto& session : svc_state.tracing_sessions()) {
+      TracingSessionID id = session.id();
+      if (id > ret) {
+        ret = id;
       }
-      task_runner.PostDelayedTask(task, 1);
-    };
-    task_runner.PostDelayedTask(task, 1);
-    task_runner.RunUntilCheckpoint(checkpoint_name);
-  }
-
-  DataSourceInstanceState GetDataSourceInstanceState(const std::string& name) {
-    for (const auto& kv : tracing_session()->data_source_instances) {
-      if (kv.second.data_source_name == name)
-        return kv.second.state;
     }
-    PERFETTO_FATAL("Can't find data source instance with name %s",
-                   name.c_str());
+    return ret;
   }
 
-  void SetTriggerWindowNs(int64_t window_ns) {
-    svc->trigger_window_ns_ = window_ns;
+  void AdvanceTimeAndRunUntilIdle(uint32_t ms) {
+    mock_clock_displacement_ += base::TimeMillis(ms);
+    task_runner.AdvanceTimeAndRunUntilIdle(ms);
   }
 
-  void OverrideNextTriggerRandomNumber(double number) {
-    svc->trigger_rnd_override_for_testing_ = number;
-  }
+  base::TimeNanos mock_clock_displacement_{0};
+  tracing_service::ClockImpl real_clock_;
+  MockClock* mock_clock_;  // Owned by svc;
+  std::unique_ptr<tracing_service::RandomImpl> real_random_;
+  MockRandom* mock_random_;  // Owned by svc;
 
   base::TestTaskRunner task_runner;
-  std::unique_ptr<TracingServiceImpl> svc;
+  std::unique_ptr<TracingService> svc;
 };
 
 TEST_F(TracingServiceImplTest, AtMostOneConfig) {
@@ -382,11 +359,15 @@
   mock_producer_1->Connect(svc.get(), "mock_producer_1", 123u /* uid */);
   mock_producer_2->Connect(svc.get(), "mock_producer_2", 456u /* uid */);
 
-  ASSERT_EQ(2u, svc->num_producers());
-  ASSERT_EQ(mock_producer_1->endpoint(), svc->GetProducer(1));
-  ASSERT_EQ(mock_producer_2->endpoint(), svc->GetProducer(2));
-  ASSERT_EQ(123u, GetProducerUid(1));
-  ASSERT_EQ(456u, GetProducerUid(2));
+  std::unique_ptr<MockConsumer> consumer = CreateMockConsumer();
+  consumer->Connect(svc.get());
+
+  TracingServiceState svc_state = consumer->QueryServiceState();
+  ASSERT_EQ(svc_state.producers_size(), 2);
+  EXPECT_EQ(svc_state.producers().at(0).id(), 1);
+  EXPECT_EQ(svc_state.producers().at(0).uid(), 123);
+  EXPECT_EQ(svc_state.producers().at(1).id(), 2);
+  EXPECT_EQ(svc_state.producers().at(1).uid(), 456);
 
   mock_producer_1->RegisterDataSource("foo");
   mock_producer_2->RegisterDataSource("bar");
@@ -395,13 +376,15 @@
   mock_producer_2->UnregisterDataSource("bar");
 
   mock_producer_1.reset();
-  ASSERT_EQ(1u, svc->num_producers());
-  ASSERT_EQ(nullptr, svc->GetProducer(1));
+
+  svc_state = consumer->QueryServiceState();
+  ASSERT_EQ(svc_state.producers_size(), 1);
+  EXPECT_EQ(svc_state.producers().at(0).id(), 2);
 
   mock_producer_2.reset();
-  ASSERT_EQ(nullptr, svc->GetProducer(2));
 
-  ASSERT_EQ(0u, svc->num_producers());
+  svc_state = consumer->QueryServiceState();
+  ASSERT_EQ(svc_state.producers_size(), 0);
 }
 
 TEST_F(TracingServiceImplTest, EnableAndDisableTracing) {
@@ -479,13 +462,11 @@
   producer->WaitForDataSourceStop("ds_1");
   consumer->WaitForTracingDisabled();
 
-  ASSERT_EQ(1u, tracing_session()->received_triggers.size());
-  EXPECT_EQ("trigger_name",
-            tracing_session()->received_triggers[0].trigger_name);
-
+  std::vector<protos::gen::TracePacket> trace = consumer->ReadBuffers();
   EXPECT_THAT(
-      consumer->ReadBuffers(),
+      trace,
       HasTriggerMode(protos::gen::TraceConfig::TriggerConfig::START_TRACING));
+  EXPECT_THAT(GetReceivedTriggers(trace), ElementsAre("trigger_name"));
 }
 
 // Creates a tracing session with a START_TRACING trigger and checks that the
@@ -772,8 +753,6 @@
 
   producer->WaitForDataSourceSetup("ds_1");
 
-  auto tracing_session_1_id = GetTracingSessionID();
-
   (*trace_config.mutable_data_sources())[0].mutable_config()->set_name("ds_2");
   trigger = trace_config.mutable_trigger_config()->add_triggers();
   trigger->set_name("trigger_name_2");
@@ -783,9 +762,6 @@
 
   producer->WaitForDataSourceSetup("ds_2");
 
-  auto tracing_session_2_id = GetTracingSessionID();
-  EXPECT_NE(tracing_session_1_id, tracing_session_2_id);
-
   const DataSourceInstanceID id1 = producer->GetDataSourceInstanceId("ds_1");
   const DataSourceInstanceID id2 = producer->GetDataSourceInstanceId("ds_2");
 
@@ -800,24 +776,6 @@
   producer->WaitForDataSourceStart("ds_1");
   producer->WaitForDataSourceStart("ds_2");
 
-  // Now that they've started we can check the triggers they've seen.
-  auto* tracing_session_1 = GetTracingSession(tracing_session_1_id);
-  ASSERT_EQ(1u, tracing_session_1->received_triggers.size());
-  EXPECT_EQ("trigger_name",
-            tracing_session_1->received_triggers[0].trigger_name);
-
-  // This is actually dependent on the order in which the triggers were received
-  // but there isn't really a better way than iteration order so probably not to
-  // brittle of a test. And this caught a real bug in implementation.
-  auto* tracing_session_2 = GetTracingSession(tracing_session_2_id);
-  ASSERT_EQ(2u, tracing_session_2->received_triggers.size());
-
-  EXPECT_EQ("trigger_name",
-            tracing_session_2->received_triggers[0].trigger_name);
-
-  EXPECT_EQ("trigger_name_2",
-            tracing_session_2->received_triggers[1].trigger_name);
-
   auto writer1 = producer->CreateTraceWriter("ds_1");
   auto writer2 = producer->CreateTraceWriter("ds_2");
 
@@ -869,12 +827,18 @@
 
   EXPECT_TRUE(flushed_writer_1);
   EXPECT_TRUE(flushed_writer_2);
+
+  std::vector<protos::gen::TracePacket> trace1 = consumer_1->ReadBuffers();
   EXPECT_THAT(
-      consumer_1->ReadBuffers(),
+      trace1,
       HasTriggerMode(protos::gen::TraceConfig::TriggerConfig::START_TRACING));
+  EXPECT_THAT(GetReceivedTriggers(trace1), ElementsAre("trigger_name"));
+  std::vector<protos::gen::TracePacket> trace2 = consumer_2->ReadBuffers();
   EXPECT_THAT(
-      consumer_2->ReadBuffers(),
+      trace2,
       HasTriggerMode(protos::gen::TraceConfig::TriggerConfig::START_TRACING));
+  EXPECT_THAT(GetReceivedTriggers(trace2),
+              UnorderedElementsAre("trigger_name", "trigger_name_2"));
 }
 
 // Creates a tracing session with a START_TRACING trigger and checks that the
@@ -918,37 +882,14 @@
   producer->WaitForDataSourceStop("ds_1");
   consumer->WaitForTracingDisabled();
 
-  ASSERT_EQ(1u, tracing_session()->received_triggers.size());
-  EXPECT_EQ("trigger_name",
-            tracing_session()->received_triggers[0].trigger_name);
-
   auto packets = consumer->ReadBuffers();
   EXPECT_THAT(
       packets,
-      Contains(Property(
-          &protos::gen::TracePacket::trace_config,
-          Property(
-              &protos::gen::TraceConfig::trigger_config,
-              Property(&protos::gen::TraceConfig::TriggerConfig::trigger_mode,
-                       Eq(protos::gen::TraceConfig::TriggerConfig::
-                              START_TRACING))))));
-  auto expect_received_trigger = [&](const std::string& name) {
-    return Contains(AllOf(
-        Property(&protos::gen::TracePacket::trigger,
-                 AllOf(Property(&protos::gen::Trigger::trigger_name, Eq(name)),
-                       Property(&protos::gen::Trigger::trusted_producer_uid,
-                                Eq(123)),
-                       Property(&protos::gen::Trigger::producer_name,
-                                Eq("mock_producer")))),
-        Property(&protos::gen::TracePacket::trusted_packet_sequence_id,
-                 Eq(kServicePacketSequenceID))));
-  };
-  EXPECT_THAT(packets, expect_received_trigger("trigger_name"));
-  EXPECT_THAT(packets, Not(expect_received_trigger("trigger_name_2")));
-  EXPECT_THAT(packets, Not(expect_received_trigger("trigger_name_3")));
+      HasTriggerMode(protos::gen::TraceConfig::TriggerConfig::START_TRACING));
+  EXPECT_THAT(GetReceivedTriggers(packets), ElementsAre("trigger_name"));
 }
 
-// Creates a tracing session with a START_TRACING trigger and checks that the
+// Creates a tracing session with a STOP_TRACING trigger and checks that the
 // received_triggers are emitted as packets.
 TEST_F(TracingServiceImplTest, EmitTriggersWithStopTracingTrigger) {
   std::unique_ptr<MockConsumer> consumer = CreateMockConsumer();
@@ -991,40 +932,15 @@
   producer->WaitForDataSourceStop("ds_1");
   consumer->WaitForTracingDisabled();
 
-  ASSERT_EQ(2u, tracing_session()->received_triggers.size());
-  EXPECT_EQ("trigger_name",
-            tracing_session()->received_triggers[0].trigger_name);
-  EXPECT_EQ("trigger_name_3",
-            tracing_session()->received_triggers[1].trigger_name);
-
   auto packets = consumer->ReadBuffers();
   EXPECT_THAT(
       packets,
-      Contains(Property(
-          &protos::gen::TracePacket::trace_config,
-          Property(
-              &protos::gen::TraceConfig::trigger_config,
-              Property(&protos::gen::TraceConfig::TriggerConfig::trigger_mode,
-                       Eq(protos::gen::TraceConfig::TriggerConfig::
-                              STOP_TRACING))))));
-
-  auto expect_received_trigger = [&](const std::string& name) {
-    return Contains(AllOf(
-        Property(&protos::gen::TracePacket::trigger,
-                 AllOf(Property(&protos::gen::Trigger::trigger_name, Eq(name)),
-                       Property(&protos::gen::Trigger::trusted_producer_uid,
-                                Eq(321)),
-                       Property(&protos::gen::Trigger::producer_name,
-                                Eq("mock_producer")))),
-        Property(&protos::gen::TracePacket::trusted_packet_sequence_id,
-                 Eq(kServicePacketSequenceID))));
-  };
-  EXPECT_THAT(packets, expect_received_trigger("trigger_name"));
-  EXPECT_THAT(packets, Not(expect_received_trigger("trigger_name_2")));
-  EXPECT_THAT(packets, expect_received_trigger("trigger_name_3"));
+      HasTriggerMode(protos::gen::TraceConfig::TriggerConfig::STOP_TRACING));
+  EXPECT_THAT(GetReceivedTriggers(packets),
+              UnorderedElementsAre("trigger_name", "trigger_name_3"));
 }
 
-// Creates a tracing session with a START_TRACING trigger and checks that the
+// Creates a tracing session with a STOP_TRACING trigger and checks that the
 // received_triggers are emitted as packets even ones after the initial
 // ReadBuffers() call.
 TEST_F(TracingServiceImplTest, EmitTriggersRepeatedly) {
@@ -1052,16 +968,6 @@
 
   trigger_config->set_trigger_timeout_ms(30000);
 
-  auto expect_received_trigger = [&](const std::string& name) {
-    return Contains(AllOf(
-        Property(&protos::gen::TracePacket::trigger,
-                 AllOf(Property(&protos::gen::Trigger::trigger_name, Eq(name)),
-                       Property(&protos::gen::Trigger::producer_name,
-                                Eq("mock_producer")))),
-        Property(&protos::gen::TracePacket::trusted_packet_sequence_id,
-                 Eq(kServicePacketSequenceID))));
-  };
-
   consumer->EnableTracing(trace_config);
   producer->WaitForTracingSetup();
   producer->WaitForDataSourceSetup("ds_1");
@@ -1074,15 +980,8 @@
   auto packets = consumer->ReadBuffers();
   EXPECT_THAT(
       packets,
-      Contains(Property(
-          &protos::gen::TracePacket::trace_config,
-          Property(
-              &protos::gen::TraceConfig::trigger_config,
-              Property(&protos::gen::TraceConfig::TriggerConfig::trigger_mode,
-                       Eq(protos::gen::TraceConfig::TriggerConfig::
-                              STOP_TRACING))))));
-  EXPECT_THAT(packets, expect_received_trigger("trigger_name"));
-  EXPECT_THAT(packets, Not(expect_received_trigger("trigger_name_2")));
+      HasTriggerMode(protos::gen::TraceConfig::TriggerConfig::STOP_TRACING));
+  EXPECT_THAT(GetReceivedTriggers(packets), ElementsAre("trigger_name"));
 
   // Send a new trigger.
   producer->endpoint()->ActivateTriggers({"trigger_name_2"});
@@ -1092,16 +991,9 @@
   producer->WaitForDataSourceStop("ds_1");
   consumer->WaitForTracingDisabled();
 
-  ASSERT_EQ(2u, tracing_session()->received_triggers.size());
-  EXPECT_EQ("trigger_name",
-            tracing_session()->received_triggers[0].trigger_name);
-  EXPECT_EQ("trigger_name_2",
-            tracing_session()->received_triggers[1].trigger_name);
-
   packets = consumer->ReadBuffers();
   // We don't rewrite the old trigger.
-  EXPECT_THAT(packets, Not(expect_received_trigger("trigger_name")));
-  EXPECT_THAT(packets, expect_received_trigger("trigger_name_2"));
+  EXPECT_THAT(GetReceivedTriggers(packets), ElementsAre("trigger_name_2"));
 }
 
 // Creates a tracing session with a STOP_TRACING trigger and checks that the
@@ -1125,8 +1017,6 @@
   // The trace won't return data because there has been no trigger
   EXPECT_THAT(consumer->ReadBuffers(), IsEmpty());
 
-  ASSERT_EQ(0u, tracing_session()->received_triggers.size());
-
   consumer->WaitForTracingDisabled();
 
   // The trace won't return data because there has been no trigger
@@ -1197,14 +1087,11 @@
   }
   producer->ExpectFlush(writer.get());
 
-  ASSERT_EQ(1u, tracing_session()->received_triggers.size());
-  EXPECT_EQ("trigger_name",
-            tracing_session()->received_triggers[0].trigger_name);
-
   producer->WaitForDataSourceStop("ds_1");
   consumer->WaitForTracingDisabled();
 
   auto packets = consumer->ReadBuffers();
+  EXPECT_THAT(GetReceivedTriggers(packets), ElementsAre("trigger_name"));
   EXPECT_LT(kNumTestPackets, packets.size());
   // We expect for the TraceConfig preamble packet to be there correctly and
   // then we expect each payload to be there, but not the |large_payload|
@@ -1274,17 +1161,14 @@
   auto writer = producer->CreateTraceWriter("ds_1");
   producer->ExpectFlush(writer.get());
 
-  ASSERT_EQ(2u, tracing_session()->received_triggers.size());
-  EXPECT_EQ("trigger_name",
-            tracing_session()->received_triggers[0].trigger_name);
-  EXPECT_EQ("trigger_name_2",
-            tracing_session()->received_triggers[1].trigger_name);
-
   producer->WaitForDataSourceStop("ds_1");
   consumer->WaitForTracingDisabled();
+  std::vector<protos::gen::TracePacket> packets = consumer->ReadBuffers();
   EXPECT_THAT(
-      consumer->ReadBuffers(),
+      packets,
       HasTriggerMode(protos::gen::TraceConfig::TriggerConfig::STOP_TRACING));
+  EXPECT_THAT(GetReceivedTriggers(packets),
+              UnorderedElementsAre("trigger_name", "trigger_name_2"));
 }
 
 TEST_F(TracingServiceImplTest, SecondTriggerHitsLimit) {
@@ -1322,20 +1206,20 @@
     req.push_back("trigger_name");
     producer->endpoint()->ActivateTriggers(req);
 
-    ASSERT_EQ(1u, tracing_session()->received_triggers.size());
-    EXPECT_EQ("trigger_name",
-              tracing_session()->received_triggers[0].trigger_name);
-
     auto writer = producer->CreateTraceWriter("data_source_a");
     producer->ExpectFlush(writer.get());
 
     producer->WaitForDataSourceStop("data_source_a");
     consumer->WaitForTracingDisabled();
+    std::vector<protos::gen::TracePacket> packets = consumer->ReadBuffers();
     EXPECT_THAT(
-        consumer->ReadBuffers(),
+        packets,
         HasTriggerMode(protos::gen::TraceConfig::TriggerConfig::STOP_TRACING));
+    EXPECT_THAT(GetReceivedTriggers(packets), ElementsAre("trigger_name"));
   }
 
+  AdvanceTimeAndRunUntilIdle(23 * 60 * 60 * 1000);  // 23h
+
   // Second session.
   {
     std::unique_ptr<MockProducer> producer = CreateMockProducer();
@@ -1356,13 +1240,14 @@
     req.push_back("trigger_name");
     producer->endpoint()->ActivateTriggers(req);
 
-    ASSERT_EQ(0u, tracing_session()->received_triggers.size());
-
     consumer->DisableTracing();
-    consumer->FreeBuffers();
 
     producer->WaitForDataSourceStop("data_source_b");
     consumer->WaitForTracingDisabled();
+    // When triggers are not hit, the tracing session doesn't return any data.
+    EXPECT_THAT(consumer->ReadBuffers(), IsEmpty());
+
+    consumer->FreeBuffers();
   }
 }
 
@@ -1381,10 +1266,6 @@
 
   auto* ds = trace_config.add_data_sources()->mutable_config();
 
-  // Set the trigger window size to something really small so the second
-  // session is still allowed through.
-  SetTriggerWindowNs(1);
-
   // First session.
   {
     std::unique_ptr<MockProducer> producer = CreateMockProducer();
@@ -1405,22 +1286,19 @@
     req.push_back("trigger_name");
     producer->endpoint()->ActivateTriggers(req);
 
-    ASSERT_EQ(1u, tracing_session()->received_triggers.size());
-    EXPECT_EQ("trigger_name",
-              tracing_session()->received_triggers[0].trigger_name);
-
     auto writer = producer->CreateTraceWriter("data_source_a");
     producer->ExpectFlush(writer.get());
 
     producer->WaitForDataSourceStop("data_source_a");
     consumer->WaitForTracingDisabled();
+    std::vector<protos::gen::TracePacket> packets = consumer->ReadBuffers();
     EXPECT_THAT(
-        consumer->ReadBuffers(),
+        packets,
         HasTriggerMode(protos::gen::TraceConfig::TriggerConfig::STOP_TRACING));
+    EXPECT_THAT(GetReceivedTriggers(packets), ElementsAre("trigger_name"));
   }
 
-  // Sleep 1 micro so that we're sure that the window time would have elapsed.
-  base::SleepMicroseconds(1);
+  AdvanceTimeAndRunUntilIdle(24 * 60 * 60 * 1000);  // 24h
 
   // Second session.
   {
@@ -1442,18 +1320,16 @@
     req.push_back("trigger_name");
     producer->endpoint()->ActivateTriggers(req);
 
-    ASSERT_EQ(1u, tracing_session()->received_triggers.size());
-    EXPECT_EQ("trigger_name",
-              tracing_session()->received_triggers[0].trigger_name);
-
     auto writer = producer->CreateTraceWriter("data_source_b");
     producer->ExpectFlush(writer.get());
 
     producer->WaitForDataSourceStop("data_source_b");
     consumer->WaitForTracingDisabled();
+    std::vector<protos::gen::TracePacket> packets = consumer->ReadBuffers();
     EXPECT_THAT(
-        consumer->ReadBuffers(),
+        packets,
         HasTriggerMode(protos::gen::TraceConfig::TriggerConfig::STOP_TRACING));
+    EXPECT_THAT(GetReceivedTriggers(packets), ElementsAre("trigger_name"));
   }
 }
 
@@ -1488,27 +1364,26 @@
   req.push_back("trigger_name");
 
   // This is below the probability of 0.15 so should be skipped.
-  OverrideNextTriggerRandomNumber(0.14);
+  EXPECT_CALL(*mock_random_, GetValue).WillOnce(Return(0.14));
   producer->endpoint()->ActivateTriggers(req);
 
-  ASSERT_EQ(0u, tracing_session()->received_triggers.size());
+  // When triggers are not hit, the tracing session doesn't return any data.
+  EXPECT_THAT(consumer->ReadBuffers(), IsEmpty());
 
-  // This is above the probaility of 0.15 so should be allowed.
-  OverrideNextTriggerRandomNumber(0.16);
+  // This is above the probability of 0.15 so should be allowed.
+  EXPECT_CALL(*mock_random_, GetValue).WillOnce(Return(0.16));
   producer->endpoint()->ActivateTriggers(req);
 
   auto writer = producer->CreateTraceWriter("data_source");
   producer->ExpectFlush(writer.get());
 
-  ASSERT_EQ(1u, tracing_session()->received_triggers.size());
-  EXPECT_EQ("trigger_name",
-            tracing_session()->received_triggers[0].trigger_name);
-
   producer->WaitForDataSourceStop("data_source");
   consumer->WaitForTracingDisabled();
+  std::vector<protos::gen::TracePacket> packets = consumer->ReadBuffers();
   EXPECT_THAT(
-      consumer->ReadBuffers(),
+      packets,
       HasTriggerMode(protos::gen::TraceConfig::TriggerConfig::STOP_TRACING));
+  EXPECT_THAT(GetReceivedTriggers(packets), ElementsAre("trigger_name"));
 }
 
 // Creates a tracing session with a CLONE_SNAPSHOT trigger and checks that
@@ -1545,7 +1420,7 @@
 
   auto writer = producer->CreateTraceWriter("ds_1");
 
-  TracingSessionID orig_tsid = GetTracingSessionID();
+  std::optional<TracingSessionID> orig_tsid;
 
   // Iterate over a sequence of trigger + CloneSession, to emulate a long trace
   // receiving different triggers and being cloned several times.
@@ -1553,10 +1428,6 @@
     std::string trigger_name = "trigger_" + std::to_string(iter);
     producer->endpoint()->ActivateTriggers({trigger_name});
 
-    auto* orig_session = GetTracingSession(orig_tsid);
-    ASSERT_EQ(orig_session->received_triggers.size(), 1u);
-    EXPECT_EQ(trigger_name, orig_session->received_triggers[0].trigger_name);
-
     // Reading the original trace session should always return nothing. Only the
     // cloned sessions should return data.
     EXPECT_THAT(consumer->ReadBuffers(), IsEmpty());
@@ -1564,12 +1435,15 @@
     // Now clone the session and check that the cloned session has the triggers.
     std::unique_ptr<MockConsumer> clone_cons = CreateMockConsumer();
     clone_cons->Connect(svc.get());
+    if (!orig_tsid) {
+      orig_tsid = GetLastTracingSessionId(clone_cons.get());
+    }
 
     std::string checkpoint_name = "clone_done_" + std::to_string(iter);
     auto clone_done = task_runner.CreateCheckpoint(checkpoint_name);
     EXPECT_CALL(*clone_cons, OnSessionCloned(_))
         .WillOnce(InvokeWithoutArgs(clone_done));
-    clone_cons->CloneSession(orig_tsid);
+    clone_cons->CloneSession(*orig_tsid);
     // CloneSession() will implicitly issue a flush. Linearize with that.
     producer->ExpectFlush(writer.get());
     task_runner.RunUntilCheckpoint(checkpoint_name);
@@ -1827,33 +1701,6 @@
   producer->WaitForDataSourceStart("data_source");
 }
 
-TEST_F(TracingServiceImplTest, ProducerIDWrapping) {
-  std::vector<std::unique_ptr<MockProducer>> producers;
-  producers.push_back(nullptr);
-
-  auto connect_producer_and_get_id = [&producers,
-                                      this](const std::string& name) {
-    producers.emplace_back(CreateMockProducer());
-    producers.back()->Connect(svc.get(), "mock_producer_" + name);
-    return *last_producer_id();
-  };
-
-  // Connect producers 1-4.
-  for (ProducerID i = 1; i <= 4; i++)
-    ASSERT_EQ(i, connect_producer_and_get_id(std::to_string(i)));
-
-  // Disconnect producers 1,3.
-  producers[1].reset();
-  producers[3].reset();
-
-  *last_producer_id() = kMaxProducerID - 1;
-  ASSERT_EQ(kMaxProducerID, connect_producer_and_get_id("maxid"));
-  ASSERT_EQ(1u, connect_producer_and_get_id("1_again"));
-  ASSERT_EQ(3u, connect_producer_and_get_id("3_again"));
-  ASSERT_EQ(5u, connect_producer_and_get_id("5"));
-  ASSERT_EQ(6u, connect_producer_and_get_id("6"));
-}
-
 TEST_F(TracingServiceImplTest, CompressionConfiguredButUnsupported) {
   // Initialize the service without support for compression.
   TracingService::InitOpts init_opts;
@@ -2530,7 +2377,6 @@
   auto flush_req_4 = consumer->Flush(/*timeout_ms=*/10);
 
   task_runner.RunUntilCheckpoint("all_flushes_received");
-  ASSERT_EQ(4u, GetNumPendingFlushes());
 
   writer->Flush();
   // Reply only to flush 3. Do not reply to 1,2 and 4.
@@ -2756,13 +2602,6 @@
 
   producer->WaitForTracingSetup();
 
-  EXPECT_EQ(GetDataSourceInstanceState("ds_will_ack_1"),
-            DataSourceInstanceState::CONFIGURED);
-  EXPECT_EQ(GetDataSourceInstanceState("ds_wont_ack"),
-            DataSourceInstanceState::CONFIGURED);
-  EXPECT_EQ(GetDataSourceInstanceState("ds_will_ack_2"),
-            DataSourceInstanceState::CONFIGURED);
-
   producer->WaitForDataSourceSetup("ds_will_ack_1");
   producer->WaitForDataSourceSetup("ds_wont_ack");
   producer->WaitForDataSourceSetup("ds_will_ack_2");
@@ -2776,18 +2615,8 @@
   producer->WaitForDataSourceStart("ds_wont_ack");
   producer->WaitForDataSourceStart("ds_will_ack_2");
 
-  EXPECT_EQ(GetDataSourceInstanceState("ds_will_ack_1"),
-            DataSourceInstanceState::STARTING);
-  EXPECT_EQ(GetDataSourceInstanceState("ds_wont_ack"),
-            DataSourceInstanceState::STARTED);
-  EXPECT_EQ(GetDataSourceInstanceState("ds_will_ack_2"),
-            DataSourceInstanceState::STARTED);
-
   producer->endpoint()->NotifyDataSourceStarted(id1);
 
-  EXPECT_EQ(GetDataSourceInstanceState("ds_will_ack_1"),
-            DataSourceInstanceState::STARTED);
-
   std::unique_ptr<TraceWriter> writer =
       producer->CreateTraceWriter("ds_wont_ack");
   producer->ExpectFlush(writer.get());
@@ -2796,21 +2625,9 @@
   producer->WaitForDataSourceStop("ds_wont_ack");
   producer->WaitForDataSourceStop("ds_will_ack_2");
 
-  EXPECT_EQ(GetDataSourceInstanceState("ds_will_ack_1"),
-            DataSourceInstanceState::STOPPING);
-  EXPECT_EQ(GetDataSourceInstanceState("ds_wont_ack"),
-            DataSourceInstanceState::STOPPED);
-  EXPECT_EQ(GetDataSourceInstanceState("ds_will_ack_2"),
-            DataSourceInstanceState::STOPPING);
-
   producer->endpoint()->NotifyDataSourceStopped(id1);
   producer->endpoint()->NotifyDataSourceStopped(id2);
 
-  EXPECT_EQ(GetDataSourceInstanceState("ds_will_ack_1"),
-            DataSourceInstanceState::STOPPED);
-  EXPECT_EQ(GetDataSourceInstanceState("ds_will_ack_2"),
-            DataSourceInstanceState::STOPPED);
-
   // Wait for at most half of the service timeout, so that this test fails if
   // the service falls back on calling the OnTracingDisabled() because some of
   // the expected acks weren't received.
@@ -2953,7 +2770,8 @@
   auto* ds_config = trace_config.add_data_sources()->mutable_config();
   ds_config->set_name("data_source");
   trace_config.set_write_into_file(true);
-  trace_config.set_file_write_period_ms(1);
+  trace_config.set_file_write_period_ms(100);
+  trace_config.mutable_builtin_data_sources()->set_snapshot_interval_ms(100);
   base::TempFile tmp_file = base::TempFile::Create();
   consumer->EnableTracing(trace_config, base::ScopedFile(dup(tmp_file.fd())));
   producer->WaitForTracingSetup();
@@ -2970,7 +2788,8 @@
     writer->NewTracePacket()->set_for_testing()->set_str(payload.c_str());
     if (i % (100 / kNumMarkers) == 0) {
       writer->Flush();
-      WaitForNextSyncMarker();
+      // The snapshot will happen every 100ms
+      AdvanceTimeAndRunUntilIdle(100);
     }
   }
   writer->Flush();
@@ -3158,82 +2977,6 @@
                    Eq(4u)))));
 }
 
-TEST_F(TracingServiceImplTest, AllowedBuffers) {
-  std::unique_ptr<MockConsumer> consumer = CreateMockConsumer();
-  consumer->Connect(svc.get());
-
-  std::unique_ptr<MockProducer> producer1 = CreateMockProducer();
-  producer1->Connect(svc.get(), "mock_producer1");
-  ProducerID producer1_id = *last_producer_id();
-  producer1->RegisterDataSource("data_source1");
-  std::unique_ptr<MockProducer> producer2 = CreateMockProducer();
-  producer2->Connect(svc.get(), "mock_producer2");
-  ProducerID producer2_id = *last_producer_id();
-  producer2->RegisterDataSource("data_source2.1");
-  producer2->RegisterDataSource("data_source2.2");
-  producer2->RegisterDataSource("data_source2.3");
-
-  EXPECT_EQ(std::set<BufferID>(), GetAllowedTargetBuffers(producer1_id));
-  EXPECT_EQ(std::set<BufferID>(), GetAllowedTargetBuffers(producer2_id));
-
-  TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(128);
-  trace_config.add_buffers()->set_size_kb(128);
-  trace_config.add_buffers()->set_size_kb(128);
-  auto* ds_config1 = trace_config.add_data_sources()->mutable_config();
-  ds_config1->set_name("data_source1");
-  ds_config1->set_target_buffer(0);
-  auto* ds_config21 = trace_config.add_data_sources()->mutable_config();
-  ds_config21->set_name("data_source2.1");
-  ds_config21->set_target_buffer(1);
-  auto* ds_config22 = trace_config.add_data_sources()->mutable_config();
-  ds_config22->set_name("data_source2.2");
-  ds_config22->set_target_buffer(2);
-  auto* ds_config23 = trace_config.add_data_sources()->mutable_config();
-  ds_config23->set_name("data_source2.3");
-  ds_config23->set_target_buffer(2);  // same buffer as data_source2.2.
-  consumer->EnableTracing(trace_config);
-
-  producer1->WaitForTracingSetup();
-  producer1->WaitForDataSourceSetup("data_source1");
-
-  producer2->WaitForTracingSetup();
-  producer2->WaitForDataSourceSetup("data_source2.1");
-  producer2->WaitForDataSourceSetup("data_source2.2");
-  producer2->WaitForDataSourceSetup("data_source2.3");
-
-  ASSERT_EQ(3u, tracing_session()->num_buffers());
-  std::set<BufferID> expected_buffers_producer1 = {
-      tracing_session()->buffers_index[0]};
-  std::set<BufferID> expected_buffers_producer2 = {
-      tracing_session()->buffers_index[1], tracing_session()->buffers_index[2]};
-  EXPECT_EQ(expected_buffers_producer1, GetAllowedTargetBuffers(producer1_id));
-  EXPECT_EQ(expected_buffers_producer2, GetAllowedTargetBuffers(producer2_id));
-
-  producer1->WaitForDataSourceStart("data_source1");
-  producer2->WaitForDataSourceStart("data_source2.1");
-  producer2->WaitForDataSourceStart("data_source2.2");
-  producer2->WaitForDataSourceStart("data_source2.3");
-
-  producer2->UnregisterDataSource("data_source2.3");
-  producer2->WaitForDataSourceStop("data_source2.3");
-
-  // Should still be allowed to write to buffers 1 (data_source2.1) and 2
-  // (data_source2.2).
-  EXPECT_EQ(expected_buffers_producer2, GetAllowedTargetBuffers(producer2_id));
-
-  consumer->DisableTracing();
-  producer1->WaitForDataSourceStop("data_source1");
-  producer2->WaitForDataSourceStop("data_source2.1");
-  producer2->WaitForDataSourceStop("data_source2.2");
-  consumer->WaitForTracingDisabled();
-
-  consumer->FreeBuffers();
-  task_runner.RunUntilIdle();
-  EXPECT_EQ(std::set<BufferID>(), GetAllowedTargetBuffers(producer1_id));
-  EXPECT_EQ(std::set<BufferID>(), GetAllowedTargetBuffers(producer2_id));
-}
-
 #if !PERFETTO_DCHECK_IS_ON()
 TEST_F(TracingServiceImplTest, CommitToForbiddenBufferIsDiscarded) {
   std::unique_ptr<MockConsumer> consumer = CreateMockConsumer();
@@ -3241,10 +2984,11 @@
 
   std::unique_ptr<MockProducer> producer = CreateMockProducer();
   producer->Connect(svc.get(), "mock_producer");
-  ProducerID producer_id = *last_producer_id();
   producer->RegisterDataSource("data_source");
 
-  EXPECT_EQ(std::set<BufferID>(), GetAllowedTargetBuffers(producer_id));
+  std::unique_ptr<MockProducer> producer2 = CreateMockProducer();
+  producer2->Connect(svc.get(), "mock_producer_2");
+  producer2->RegisterDataSource("data_source_2");
 
   TraceConfig trace_config;
   trace_config.add_buffers()->set_size_kb(128);
@@ -3252,42 +2996,74 @@
   auto* ds_config = trace_config.add_data_sources()->mutable_config();
   ds_config->set_name("data_source");
   ds_config->set_target_buffer(0);
+  ds_config = trace_config.add_data_sources()->mutable_config();
+  ds_config->set_name("data_source_2");
+  ds_config->set_target_buffer(1);
   consumer->EnableTracing(trace_config);
 
-  ASSERT_EQ(2u, tracing_session()->num_buffers());
-  std::set<BufferID> expected_buffers = {tracing_session()->buffers_index[0]};
-  EXPECT_EQ(expected_buffers, GetAllowedTargetBuffers(producer_id));
-
   producer->WaitForTracingSetup();
   producer->WaitForDataSourceSetup("data_source");
+
+  producer2->WaitForTracingSetup();
+  producer2->WaitForDataSourceSetup("data_source_2");
+
   producer->WaitForDataSourceStart("data_source");
+  producer2->WaitForDataSourceStart("data_source_2");
+
+  const auto* ds1 = producer->GetDataSourceInstance("data_source");
+  ASSERT_NE(ds1, nullptr);
+  const auto* ds2 = producer2->GetDataSourceInstance("data_source_2");
+  ASSERT_NE(ds2, nullptr);
+  BufferID buf0 = ds1->target_buffer;
+  BufferID buf1 = ds2->target_buffer;
 
   // Try to write to the correct buffer.
-  std::unique_ptr<TraceWriter> writer = producer->endpoint()->CreateTraceWriter(
-      tracing_session()->buffers_index[0]);
+  std::unique_ptr<TraceWriter> writer =
+      producer->endpoint()->CreateTraceWriter(buf0);
   {
     auto tp = writer->NewTracePacket();
     tp->set_for_testing()->set_str("good_payload");
   }
 
   auto flush_request = consumer->Flush();
-  producer->ExpectFlush(writer.get());
+  EXPECT_CALL(*producer, Flush)
+      .WillOnce(Invoke([&](FlushRequestID flush_req_id,
+                           const DataSourceInstanceID*, size_t, FlushFlags) {
+        writer->Flush();
+        producer->endpoint()->NotifyFlushComplete(flush_req_id);
+      }));
+  EXPECT_CALL(*producer2, Flush)
+      .WillOnce(Invoke([&](FlushRequestID flush_req_id,
+                           const DataSourceInstanceID*, size_t, FlushFlags) {
+        producer2->endpoint()->NotifyFlushComplete(flush_req_id);
+      }));
   ASSERT_TRUE(flush_request.WaitForReply());
 
   // Try to write to the wrong buffer.
-  writer = producer->endpoint()->CreateTraceWriter(
-      tracing_session()->buffers_index[1]);
+  writer = producer->endpoint()->CreateTraceWriter(buf1);
   {
     auto tp = writer->NewTracePacket();
     tp->set_for_testing()->set_str("bad_payload");
   }
 
   flush_request = consumer->Flush();
-  producer->ExpectFlush(writer.get());
+  EXPECT_CALL(*producer, Flush)
+      .WillOnce(Invoke([&](FlushRequestID flush_req_id,
+                           const DataSourceInstanceID*, size_t, FlushFlags) {
+        writer->Flush();
+        producer->endpoint()->NotifyFlushComplete(flush_req_id);
+      }));
+  EXPECT_CALL(*producer2, Flush)
+      .WillOnce(Invoke([&](FlushRequestID flush_req_id,
+                           const DataSourceInstanceID*, size_t, FlushFlags) {
+        producer2->endpoint()->NotifyFlushComplete(flush_req_id);
+      }));
+
   ASSERT_TRUE(flush_request.WaitForReply());
 
   consumer->DisableTracing();
   producer->WaitForDataSourceStop("data_source");
+  producer2->WaitForDataSourceStop("data_source_2");
   consumer->WaitForTracingDisabled();
 
   auto packets = consumer->ReadBuffers();
@@ -3300,67 +3076,9 @@
                   Property(&protos::gen::TestEvent::str, Eq("bad_payload"))))));
 
   consumer->FreeBuffers();
-  EXPECT_EQ(std::set<BufferID>(), GetAllowedTargetBuffers(producer_id));
 }
 #endif  // !PERFETTO_DCHECK_IS_ON()
 
-TEST_F(TracingServiceImplTest, RegisterAndUnregisterTraceWriter) {
-  std::unique_ptr<MockConsumer> consumer = CreateMockConsumer();
-  consumer->Connect(svc.get());
-
-  std::unique_ptr<MockProducer> producer = CreateMockProducer();
-  producer->Connect(svc.get(), "mock_producer");
-  ProducerID producer_id = *last_producer_id();
-  producer->RegisterDataSource("data_source");
-
-  EXPECT_TRUE(GetWriters(producer_id).empty());
-
-  TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(128);
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("data_source");
-  ds_config->set_target_buffer(0);
-  consumer->EnableTracing(trace_config);
-
-  producer->WaitForTracingSetup();
-  producer->WaitForDataSourceSetup("data_source");
-  producer->WaitForDataSourceStart("data_source");
-
-  // Creating the trace writer should register it with the service.
-  std::unique_ptr<TraceWriter> writer = producer->endpoint()->CreateTraceWriter(
-      tracing_session()->buffers_index[0]);
-
-  WaitForTraceWritersChanged(producer_id);
-
-  std::map<WriterID, BufferID> expected_writers;
-  expected_writers[writer->writer_id()] = tracing_session()->buffers_index[0];
-  EXPECT_EQ(expected_writers, GetWriters(producer_id));
-
-  // Verify writing works.
-  {
-    auto tp = writer->NewTracePacket();
-    tp->set_for_testing()->set_str("payload");
-  }
-
-  auto flush_request = consumer->Flush();
-  producer->ExpectFlush(writer.get());
-  ASSERT_TRUE(flush_request.WaitForReply());
-
-  // Destroying the writer should unregister it.
-  writer.reset();
-  WaitForTraceWritersChanged(producer_id);
-  EXPECT_TRUE(GetWriters(producer_id).empty());
-
-  consumer->DisableTracing();
-  producer->WaitForDataSourceStop("data_source");
-  consumer->WaitForTracingDisabled();
-
-  auto packets = consumer->ReadBuffers();
-  EXPECT_THAT(packets, Contains(Property(&protos::gen::TracePacket::for_testing,
-                                         Property(&protos::gen::TestEvent::str,
-                                                  Eq("payload")))));
-}
-
 TEST_F(TracingServiceImplTest, ScrapeBuffersOnFlush) {
   svc->SetSMBScrapingEnabled(true);
 
@@ -3369,7 +3087,6 @@
 
   std::unique_ptr<MockProducer> producer = CreateMockProducer();
   producer->Connect(svc.get(), "mock_producer");
-  ProducerID producer_id = *last_producer_id();
   producer->RegisterDataSource("data_source");
 
   TraceConfig trace_config;
@@ -3383,9 +3100,10 @@
   producer->WaitForDataSourceSetup("data_source");
   producer->WaitForDataSourceStart("data_source");
 
-  std::unique_ptr<TraceWriter> writer = producer->endpoint()->CreateTraceWriter(
-      tracing_session()->buffers_index[0]);
-  WaitForTraceWritersChanged(producer_id);
+  std::unique_ptr<TraceWriter> writer =
+      producer->CreateTraceWriter("data_source");
+  // Wait for the writer to be registered.
+  task_runner.RunUntilIdle();
 
   // Write a few trace packets.
   writer->NewTracePacket()->set_for_testing()->set_str("payload1");
@@ -3456,7 +3174,6 @@
 
   std::unique_ptr<MockProducer> producer = CreateMockProducer();
   producer->Connect(svc.get(), "mock_producer");
-  ProducerID producer_id = *last_producer_id();
   producer->RegisterDataSource("data_source");
 
   TraceConfig trace_config;
@@ -3470,9 +3187,10 @@
   producer->WaitForDataSourceSetup("data_source");
   producer->WaitForDataSourceStart("data_source");
 
-  std::unique_ptr<TraceWriter> writer = producer->endpoint()->CreateTraceWriter(
-      tracing_session()->buffers_index[0], BufferExhaustedPolicy::kDrop);
-  WaitForTraceWritersChanged(producer_id);
+  std::unique_ptr<TraceWriter> writer =
+      producer->CreateTraceWriter("data_source", BufferExhaustedPolicy::kDrop);
+  // Wait for the writer to be registered.
+  task_runner.RunUntilIdle();
 
   std::atomic<bool> packets_written = false;
   std::atomic<bool> quit = false;
@@ -3513,8 +3231,19 @@
   consumer->Connect(svc.get());
 
   std::unique_ptr<MockProducer> producer = CreateMockProducer();
-  producer->Connect(svc.get(), "mock_producer");
-  ProducerID producer_id = *last_producer_id();
+
+  static constexpr size_t kShmSizeBytes = 1024 * 1024;
+  static constexpr size_t kShmPageSizeBytes = 4 * 1024;
+
+  TestSharedMemory::Factory factory;
+  auto shm = factory.CreateSharedMemory(kShmSizeBytes);
+
+  // Service should adopt the SMB provided by the producer.
+  producer->Connect(svc.get(), "mock_producer", /*uid=*/42, /*pid=*/1025,
+                    /*shared_memory_size_hint_bytes=*/0, kShmPageSizeBytes,
+                    TestRefSharedMemory::Create(shm.get()),
+                    /*in_process=*/false);
+
   producer->RegisterDataSource("data_source");
 
   TraceConfig trace_config;
@@ -3528,9 +3257,20 @@
   producer->WaitForDataSourceSetup("data_source");
   producer->WaitForDataSourceStart("data_source");
 
-  std::unique_ptr<TraceWriter> writer = producer->endpoint()->CreateTraceWriter(
-      tracing_session()->buffers_index[0]);
-  WaitForTraceWritersChanged(producer_id);
+  auto client_producer_endpoint = std::make_unique<ProxyProducerEndpoint>();
+  client_producer_endpoint->set_backend(producer->endpoint());
+
+  auto shmem_arbiter = std::make_unique<SharedMemoryArbiterImpl>(
+      shm->start(), shm->size(), SharedMemoryABI::ShmemMode::kDefault,
+      kShmPageSizeBytes, client_producer_endpoint.get(), &task_runner);
+  shmem_arbiter->SetDirectSMBPatchingSupportedByService();
+
+  const auto* ds_inst = producer->GetDataSourceInstance("data_source");
+  ASSERT_NE(nullptr, ds_inst);
+  std::unique_ptr<TraceWriter> writer =
+      shmem_arbiter->CreateTraceWriter(ds_inst->target_buffer);
+  // Wait for the TraceWriter to be registered.
+  task_runner.RunUntilIdle();
 
   // Write a few trace packets.
   writer->NewTracePacket()->set_for_testing()->set_str("payload1");
@@ -3538,9 +3278,8 @@
   writer->NewTracePacket()->set_for_testing()->set_str("payload3");
 
   // Disconnect the producer without committing the chunk. This should cause a
-  // scrape of the SMB. Avoid destroying the ShmemArbiter until writer is
-  // destroyed.
-  auto shmem_arbiter = StealShmemArbiterForProducer(producer_id);
+  // scrape of the SMB.
+  client_producer_endpoint->set_backend(nullptr);
   producer.reset();
 
   // Chunk with the packets should have been scraped.
@@ -3555,9 +3294,6 @@
                                          Property(&protos::gen::TestEvent::str,
                                                   Eq("payload3")))));
 
-  // Cleanup writer without causing a crash because the producer already went
-  // away.
-  static_cast<TraceWriterImpl*>(writer.get())->ResetChunkForTesting();
   writer.reset();
   shmem_arbiter.reset();
 
@@ -3573,7 +3309,6 @@
 
   std::unique_ptr<MockProducer> producer = CreateMockProducer();
   producer->Connect(svc.get(), "mock_producer");
-  ProducerID producer_id = *last_producer_id();
   producer->RegisterDataSource("data_source");
 
   TraceConfig trace_config;
@@ -3587,9 +3322,10 @@
   producer->WaitForDataSourceSetup("data_source");
   producer->WaitForDataSourceStart("data_source");
 
-  std::unique_ptr<TraceWriter> writer = producer->endpoint()->CreateTraceWriter(
-      tracing_session()->buffers_index[0]);
-  WaitForTraceWritersChanged(producer_id);
+  std::unique_ptr<TraceWriter> writer =
+      producer->CreateTraceWriter("data_source");
+  // Wait for the TraceWriter to be registered.
+  task_runner.RunUntilIdle();
 
   // Write a few trace packets.
   writer->NewTracePacket()->set_for_testing()->set_str("payload1");
@@ -3624,8 +3360,19 @@
     consumer_ = CreateMockConsumer();
     consumer_->Connect(svc.get());
     producer_ = CreateMockProducer();
-    producer_->Connect(svc.get(), "mock_producer");
-    ProducerID producer_id = *last_producer_id();
+
+    static constexpr size_t kShmSizeBytes = 1024 * 1024;
+    static constexpr size_t kShmPageSizeBytes = 4 * 1024;
+
+    TestSharedMemory::Factory factory;
+    shm_ = factory.CreateSharedMemory(kShmSizeBytes);
+
+    // Service should adopt the SMB provided by the producer.
+    producer_->Connect(svc.get(), "mock_producer", /*uid=*/42, /*pid=*/1025,
+                       /*shared_memory_size_hint_bytes=*/0, kShmPageSizeBytes,
+                       TestRefSharedMemory::Create(shm_.get()),
+                       /*in_process=*/false);
+
     producer_->RegisterDataSource("data_source");
 
     TraceConfig trace_config;
@@ -3639,11 +3386,19 @@
     producer_->WaitForDataSourceSetup("data_source");
     producer_->WaitForDataSourceStart("data_source");
 
-    writer_ = producer_->endpoint()->CreateTraceWriter(
-        tracing_session()->buffers_index[0]);
-    WaitForTraceWritersChanged(producer_id);
+    arbiter_ = std::make_unique<SharedMemoryArbiterImpl>(
+        shm_->start(), shm_->size(), SharedMemoryABI::ShmemMode::kDefault,
+        kShmPageSizeBytes, producer_->endpoint(), &task_runner);
+    arbiter_->SetDirectSMBPatchingSupportedByService();
 
-    arbiter_ = GetShmemArbiterForProducer(producer_id);
+    const auto* ds = producer_->GetDataSourceInstance("data_source");
+    ASSERT_NE(ds, nullptr);
+
+    target_buffer_ = ds->target_buffer;
+
+    writer_ = arbiter_->CreateTraceWriter(target_buffer_);
+    // Wait for the writer to be registered.
+    task_runner.RunUntilIdle();
   }
 
   void TearDown() override {
@@ -3658,17 +3413,23 @@
   std::optional<std::vector<protos::gen::TracePacket>> FlushAndRead() {
     // Scrape: ask the service to flush but don't flush the chunk.
     auto flush_request = consumer_->Flush();
-    producer_->ExpectFlush(nullptr, /*reply=*/true);
+
+    EXPECT_CALL(*producer_, Flush)
+        .WillOnce(Invoke([&](FlushRequestID flush_req_id,
+                             const DataSourceInstanceID*, size_t, FlushFlags) {
+          arbiter_->NotifyFlushComplete(flush_req_id);
+        }));
     if (flush_request.WaitForReply()) {
       return consumer_->ReadBuffers();
     }
     return std::nullopt;
   }
   std::unique_ptr<MockConsumer> consumer_;
+  std::unique_ptr<SharedMemory> shm_;
+  std::unique_ptr<SharedMemoryArbiterImpl> arbiter_;
   std::unique_ptr<MockProducer> producer_;
   std::unique_ptr<TraceWriter> writer_;
-  // Owned by `svc`.
-  SharedMemoryArbiterImpl* arbiter_;
+  BufferID target_buffer_{};
 
   struct : public protozero::ScatteredStreamWriter::Delegate {
     protozero::ContiguousMemoryRange GetNewBuffer() override {
@@ -3739,8 +3500,7 @@
                   &protos::gen::TracePacket::for_testing,
                   Property(&protos::gen::TestEvent::str, Eq("payload1"))))));
 
-  arbiter_->ReturnCompletedChunk(std::move(chunk),
-                                 tracing_session()->buffers_index[0],
+  arbiter_->ReturnCompletedChunk(std::move(chunk), target_buffer_,
                                  &empty_patch_list_);
 
   packets = FlushAndRead();
@@ -3795,8 +3555,7 @@
   uint8_t zero_size = 0;
   stream_writer.WriteBytesUnsafe(&zero_size, sizeof zero_size);
 
-  arbiter_->ReturnCompletedChunk(std::move(chunk),
-                                 tracing_session()->buffers_index[0],
+  arbiter_->ReturnCompletedChunk(std::move(chunk), target_buffer_,
                                  &empty_patch_list_);
 
   packets = FlushAndRead();
@@ -4440,11 +4199,14 @@
   ASSERT_TRUE(flush_request.WaitForReply());
 
   auto packets = consumer->ReadBuffers();
-  uint32_t count = 0;
+  uint32_t flush_started_count = 0;
+  uint32_t flush_done_count = 0;
   for (const auto& packet : packets) {
-    count += packet.service_event().all_data_sources_flushed();
+    flush_started_count += packet.service_event().flush_started();
+    flush_done_count += packet.service_event().all_data_sources_flushed();
   }
-  ASSERT_EQ(count, 2u);
+  EXPECT_EQ(flush_started_count, 2u);
+  EXPECT_EQ(flush_done_count, 2u);
 
   consumer->DisableTracing();
   producer->WaitForDataSourceStop("data_source");
@@ -5482,6 +5244,117 @@
   consumer->WaitForTracingDisabled();
 }
 
+TEST_F(TracingServiceImplTest, CloneSessionByName) {
+  // The consumer the creates the initial tracing session.
+  std::unique_ptr<MockConsumer> consumer = CreateMockConsumer();
+  consumer->Connect(svc.get());
+
+  // The consumer that clones it and reads back the data.
+  std::unique_ptr<MockConsumer> consumer2 = CreateMockConsumer();
+  consumer2->Connect(svc.get());
+
+  std::unique_ptr<MockProducer> producer = CreateMockProducer();
+  producer->Connect(svc.get(), "mock_producer");
+
+  producer->RegisterDataSource("ds_1");
+
+  TraceConfig trace_config;
+  trace_config.add_buffers()->set_size_kb(32);
+  trace_config.set_unique_session_name("my_unique_session_name");
+  auto* ds_cfg = trace_config.add_data_sources()->mutable_config();
+  ds_cfg->set_name("ds_1");
+  ds_cfg->set_target_buffer(0);
+
+  consumer->EnableTracing(trace_config);
+  producer->WaitForTracingSetup();
+  producer->WaitForDataSourceSetup("ds_1");
+  producer->WaitForDataSourceStart("ds_1");
+
+  std::unique_ptr<TraceWriter> writer = producer->CreateTraceWriter("ds_1");
+
+  static constexpr size_t kNumTestPackets = 20;
+  for (size_t i = 0; i < kNumTestPackets; i++) {
+    auto tp = writer->NewTracePacket();
+    std::string payload("payload" + std::to_string(i));
+    tp->set_for_testing()->set_str(payload.c_str(), payload.size());
+    tp->set_timestamp(static_cast<uint64_t>(i));
+  }
+
+  {
+    auto clone_done = task_runner.CreateCheckpoint("clone_done");
+    EXPECT_CALL(*consumer2, OnSessionCloned(_))
+        .WillOnce(
+            Invoke([clone_done](const Consumer::OnSessionClonedArgs& args) {
+              ASSERT_TRUE(args.success);
+              ASSERT_TRUE(args.error.empty());
+              clone_done();
+            }));
+    ConsumerEndpoint::CloneSessionArgs args;
+    args.unique_session_name = "my_unique_session_name";
+    consumer2->endpoint()->CloneSession(args);
+    // CloneSession() will implicitly issue a flush. Linearize with that.
+    producer->ExpectFlush(writer.get());
+    task_runner.RunUntilCheckpoint("clone_done");
+  }
+
+  // Disable the initial tracing session.
+  consumer->DisableTracing();
+  producer->WaitForDataSourceStop("ds_1");
+  consumer->WaitForTracingDisabled();
+
+  // Read back the cloned trace and the original trace.
+  auto packets = consumer->ReadBuffers();
+  auto cloned_packets = consumer2->ReadBuffers();
+  for (size_t i = 0; i < kNumTestPackets; i++) {
+    std::string payload = "payload" + std::to_string(i);
+    EXPECT_THAT(packets,
+                Contains(Property(
+                    &protos::gen::TracePacket::for_testing,
+                    Property(&protos::gen::TestEvent::str, Eq(payload)))));
+    EXPECT_THAT(cloned_packets,
+                Contains(Property(
+                    &protos::gen::TracePacket::for_testing,
+                    Property(&protos::gen::TestEvent::str, Eq(payload)))));
+  }
+
+  // Delete the original tracing session.
+  consumer->FreeBuffers();
+
+  {
+    std::unique_ptr<MockConsumer> consumer3 = CreateMockConsumer();
+    consumer3->Connect(svc.get());
+
+    // The original session is gone. The cloned session is still there. It
+    // should not be possible to clone that by name.
+
+    auto clone_failed = task_runner.CreateCheckpoint("clone_failed");
+    EXPECT_CALL(*consumer3, OnSessionCloned(_))
+        .WillOnce(
+            Invoke([clone_failed](const Consumer::OnSessionClonedArgs& args) {
+              EXPECT_FALSE(args.success);
+              EXPECT_THAT(args.error, HasSubstr("Tracing session not found"));
+              clone_failed();
+            }));
+    ConsumerEndpoint::CloneSessionArgs args_f;
+    args_f.unique_session_name = "my_unique_session_name";
+    consumer3->endpoint()->CloneSession(args_f);
+    task_runner.RunUntilCheckpoint("clone_failed");
+
+    // But it should be possible to clone that by id.
+    auto clone_success = task_runner.CreateCheckpoint("clone_success");
+    EXPECT_CALL(*consumer3, OnSessionCloned(_))
+        .WillOnce(
+            Invoke([clone_success](const Consumer::OnSessionClonedArgs& args) {
+              EXPECT_TRUE(args.success);
+              clone_success();
+            }));
+    ConsumerEndpoint::CloneSessionArgs args_s;
+    args_s.tsid = GetLastTracingSessionId(consumer3.get());
+    consumer3->endpoint()->CloneSession(args_s);
+    task_runner.RunUntilCheckpoint("clone_success");
+  }
+}
+
 TEST_F(TracingServiceImplTest, InvalidBufferSizes) {
   std::unique_ptr<MockConsumer> consumer = CreateMockConsumer();
   consumer->Connect(svc.get());
@@ -6036,4 +5909,189 @@
                                                   Eq("payload-2")))));
 }
 
+TEST_F(TracingServiceImplTest, DetachDurationTimeoutFreeBuffers) {
+  std::unique_ptr<MockConsumer> consumer = CreateMockConsumer();
+  consumer->Connect(svc.get());
+
+  TraceConfig trace_config;
+  trace_config.add_buffers()->set_size_kb(128);
+  auto* ds_config = trace_config.add_data_sources()->mutable_config();
+  ds_config->set_name("data_source");
+  trace_config.set_duration_ms(1);
+  trace_config.set_write_into_file(true);
+  trace_config.set_file_write_period_ms(100000);
+  auto pipe_pair = base::Pipe::Create();
+  consumer->EnableTracing(trace_config, std::move(pipe_pair.wr));
+
+  std::string on_detach_name = "on_detach";
+  auto on_detach = task_runner.CreateCheckpoint(on_detach_name);
+  EXPECT_CALL(*consumer, OnDetach(Eq(true))).WillOnce(Invoke(on_detach));
+
+  consumer->Detach("mykey");
+
+  task_runner.RunUntilCheckpoint(on_detach_name);
+
+  std::string file_closed_name = "file_closed";
+  auto file_closed = task_runner.CreateCheckpoint(file_closed_name);
+  task_runner.AddFileDescriptorWatch(*pipe_pair.rd, [&] {
+    char buf[1024];
+    if (base::Read(*pipe_pair.rd, buf, sizeof(buf)) <= 0) {
+      file_closed();
+    }
+  });
+  task_runner.RunUntilCheckpoint(file_closed_name);
+
+  // Disabled and detached tracing sessions are automatically deleted:
+  // reattaching fails.
+  std::string on_attach_name = "on_attach";
+  auto on_attach = task_runner.CreateCheckpoint(on_attach_name);
+  EXPECT_CALL(*consumer, OnAttach(Eq(false), _))
+      .WillOnce(InvokeWithoutArgs(on_attach));
+  consumer->Attach("mykey");
+  task_runner.RunUntilCheckpoint(on_attach_name);
+}
+
+TEST_F(TracingServiceImplTest, SlowStartingDataSources) {
+  std::unique_ptr<MockConsumer> consumer = CreateMockConsumer();
+  consumer->Connect(svc.get());
+
+  std::unique_ptr<MockProducer> producer = CreateMockProducer();
+  producer->Connect(svc.get(), "mock_producer");
+  producer->RegisterDataSource("data_source1", /*ack_stop=*/false,
+                               /*ack_start=*/true);
+  producer->RegisterDataSource("data_source2", /*ack_stop=*/false,
+                               /*ack_start=*/true);
+  producer->RegisterDataSource("data_source3", /*ack_stop=*/false,
+                               /*ack_start=*/true);
+
+  TraceConfig trace_config;
+  trace_config.add_buffers()->set_size_kb(128);
+  trace_config.add_data_sources()->mutable_config()->set_name("data_source1");
+  trace_config.add_data_sources()->mutable_config()->set_name("data_source2");
+  trace_config.add_data_sources()->mutable_config()->set_name("data_source3");
+  consumer->EnableTracing(trace_config);
+
+  producer->WaitForTracingSetup();
+  producer->WaitForDataSourceSetup("data_source1");
+  producer->WaitForDataSourceSetup("data_source2");
+  producer->WaitForDataSourceSetup("data_source3");
+
+  producer->WaitForDataSourceStart("data_source1");
+  producer->WaitForDataSourceStart("data_source2");
+  producer->WaitForDataSourceStart("data_source3");
+
+  DataSourceInstanceID id1 = producer->GetDataSourceInstanceId("data_source1");
+  DataSourceInstanceID id3 = producer->GetDataSourceInstanceId("data_source3");
+
+  producer->endpoint()->NotifyDataSourceStarted(id1);
+  producer->endpoint()->NotifyDataSourceStarted(id3);
+
+  // This matches kAllDataSourceStartedTimeout.
+  AdvanceTimeAndRunUntilIdle(20000);
+
+  consumer->DisableTracing();
+  producer->WaitForDataSourceStop("data_source1");
+  producer->WaitForDataSourceStop("data_source2");
+  producer->WaitForDataSourceStop("data_source3");
+  consumer->WaitForTracingDisabled();
+
+  std::vector<protos::gen::TracePacket> packets = consumer->ReadBuffers();
+  EXPECT_THAT(
+      packets,
+      Contains(Property(
+          &protos::gen::TracePacket::service_event,
+          Property(
+              &protos::gen::TracingServiceEvent::slow_starting_data_sources,
+              Property(
+                  &protos::gen::TracingServiceEvent::DataSources::data_source,
+                  ElementsAre(
+                      Property(&protos::gen::TracingServiceEvent::DataSources::
+                                   DataSource::data_source_name,
+                               "data_source2")))))));
+}
+
+TEST_F(TracingServiceImplTest, FlushTimeoutEventsEmitted) {
+  std::unique_ptr<MockConsumer> consumer = CreateMockConsumer();
+  consumer->Connect(svc.get());
+
+  std::unique_ptr<MockProducer> producer = CreateMockProducer();
+  producer->Connect(svc.get(), "mock_producer1");
+  producer->RegisterDataSource("ds_1");
+
+  TraceConfig trace_config;
+  trace_config.add_buffers()->set_size_kb(1024);  // Buf 0.
+  auto* ds_cfg = trace_config.add_data_sources()->mutable_config();
+  ds_cfg->set_name("ds_1");
+  ds_cfg->set_target_buffer(0);
+
+  consumer->EnableTracing(trace_config);
+  producer->WaitForTracingSetup();
+  producer->WaitForDataSourceSetup("ds_1");
+  producer->WaitForDataSourceStart("ds_1");
+
+  std::unique_ptr<TraceWriter> writer1 = producer->CreateTraceWriter("ds_1");
+
+  // Do not reply to Flush.
+  std::string producer_flush1_checkpoint_name = "producer_flush1_requested";
+  auto flush1_requested =
+      task_runner.CreateCheckpoint(producer_flush1_checkpoint_name);
+  EXPECT_CALL(*producer, Flush).WillOnce(Invoke(flush1_requested));
+  consumer->Flush(5000, FlushFlags(FlushFlags::Initiator::kTraced,
+                                   FlushFlags::Reason::kTraceStop));
+
+  task_runner.RunUntilCheckpoint(producer_flush1_checkpoint_name);
+
+  AdvanceTimeAndRunUntilIdle(5000);
+
+  // ReadBuffers returns a last_flush_slow_data_source event.
+  std::vector<protos::gen::TracePacket> packets = consumer->ReadBuffers();
+  EXPECT_THAT(
+      packets,
+      Contains(Property(
+          &protos::gen::TracePacket::service_event,
+          Property(
+              &protos::gen::TracingServiceEvent::last_flush_slow_data_sources,
+              Property(
+                  &protos::gen::TracingServiceEvent::DataSources::data_source,
+                  ElementsAre(
+                      Property(&protos::gen::TracingServiceEvent::DataSources::
+                                   DataSource::data_source_name,
+                               "ds_1")))))));
+
+  // Reply to Flush.
+  std::string producer_flush2_checkpoint_name = "producer_flush2_requested";
+  auto flush2_requested =
+      task_runner.CreateCheckpoint(producer_flush2_checkpoint_name);
+  FlushRequestID flush2_req_id;
+  EXPECT_CALL(*producer, Flush(_, _, _, _))
+      .WillOnce([&](FlushRequestID req_id, const DataSourceInstanceID*, size_t,
+                    FlushFlags) {
+        flush2_req_id = req_id;
+        flush2_requested();
+      });
+  consumer->Flush(5000, FlushFlags(FlushFlags::Initiator::kTraced,
+                                   FlushFlags::Reason::kTraceStop));
+
+  task_runner.RunUntilCheckpoint(producer_flush2_checkpoint_name);
+
+  producer->endpoint()->NotifyFlushComplete(flush2_req_id);
+
+  AdvanceTimeAndRunUntilIdle(5000);
+
+  // ReadBuffers returns a last_flush_slow_data_source event.
+  packets = consumer->ReadBuffers();
+  EXPECT_THAT(
+      packets,
+      Not(Contains(Property(&protos::gen::TracePacket::service_event,
+                            Property(&protos::gen::TracingServiceEvent::
+                                         has_last_flush_slow_data_sources,
+                                     Eq(true))))));
+
+  consumer->DisableTracing();
+  producer->WaitForDataSourceStop("ds_1");
+  consumer->WaitForTracingDisabled();
+}
+
+}  // namespace
+
 }  // namespace perfetto
diff --git a/src/tracing/test/BUILD.gn b/src/tracing/test/BUILD.gn
index 7fff140..3acedef 100644
--- a/src/tracing/test/BUILD.gn
+++ b/src/tracing/test/BUILD.gn
@@ -41,6 +41,7 @@
     "aligned_buffer_test.h",
     "fake_packet.cc",
     "fake_packet.h",
+    "test_shared_memory.cc",
     "test_shared_memory.h",
     "traced_value_test_support.cc",
   ]
@@ -54,6 +55,8 @@
       "mock_producer.cc",
       "mock_producer.h",
       "mock_producer_endpoint.h",
+      "proxy_producer_endpoint.cc",
+      "proxy_producer_endpoint.h",
     ]
   }
 }
@@ -113,11 +116,11 @@
       "../../../protos/perfetto/trace/interned_data:zero",
       "../../../protos/perfetto/trace/profiling:cpp",
       "../../../protos/perfetto/trace/track_event:cpp",
+      "../../../test:integrationtest_initializer",
       "../../base",
     ]
     sources = [
       "api_integrationtest.cc",
-      "api_integrationtest_main.cc",
       "tracing_module.cc",
       "tracing_module.h",
       "tracing_module2.cc",
diff --git a/src/tracing/test/aligned_buffer_test.h b/src/tracing/test/aligned_buffer_test.h
index 18ee4ba..d604f4e 100644
--- a/src/tracing/test/aligned_buffer_test.h
+++ b/src/tracing/test/aligned_buffer_test.h
@@ -20,6 +20,7 @@
 #include <stdlib.h>
 
 #include <memory>
+#include <utility>
 
 #include "perfetto/ext/base/utils.h"
 #include "src/tracing/test/test_shared_memory.h"
@@ -34,7 +35,10 @@
   void SetUp() override;
   void TearDown() override;
 
-  uint8_t* buf() const { return reinterpret_cast<uint8_t*>(buf_->start()); }
+  uint8_t* buf() { return const_cast<uint8_t*>(std::as_const(*this).buf()); }
+  const uint8_t* buf() const {
+    return reinterpret_cast<const uint8_t*>(buf_->start());
+  }
   size_t buf_size() const { return buf_->size(); }
   size_t page_size() const { return page_size_; }
 
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index 5072c3d..7fc2f70 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -33,6 +33,7 @@
 
 #include "perfetto/tracing.h"
 #include "test/gtest_and_gmock.h"
+#include "test/integrationtest_initializer.h"
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
 #include <Windows.h>  // For CreateFile().
@@ -2044,6 +2045,72 @@
   perfetto::TrackEvent::EraseTrackDescriptor(track);
 }
 
+TEST_P(PerfettoApiTest, TrackEventCustomNamedTrack) {
+  // Create a new trace session.
+  auto* tracing_session = NewTraceWithCategories({"bar"});
+  tracing_session->get()->StartBlocking();
+
+  // Declare a custom track and give it a name.
+  uint64_t async_id = 123;
+
+  // Start events on one thread and end them on another.
+  TRACE_EVENT_BEGIN("bar", "AsyncEvent",
+                    perfetto::NamedTrack("MyCustomTrack", async_id),
+                    "debug_arg", 123);
+
+  TRACE_EVENT_BEGIN("bar", "SubEvent",
+                    perfetto::NamedTrack("MyCustomTrack", async_id),
+                    [](perfetto::EventContext) {});
+  const auto main_thread_track = perfetto::NamedTrack(
+      "MyCustomTrack", async_id, perfetto::ThreadTrack::Current());
+  std::thread thread([&] {
+    TRACE_EVENT_END("bar", perfetto::NamedTrack("MyCustomTrack", async_id));
+    TRACE_EVENT_END("bar", perfetto::NamedTrack("MyCustomTrack", async_id),
+                    "arg1", false, "arg2", true);
+    const auto thread_track = perfetto::NamedTrack(
+        "MyCustomTrack", async_id, perfetto::ThreadTrack::Current());
+    // Thread-scoped tracks will have different uuids on different thread even
+    // if the id matches.
+    ASSERT_NE(main_thread_track.uuid, thread_track.uuid);
+  });
+  thread.join();
+
+  auto trace = StopSessionAndReturnParsedTrace(tracing_session);
+
+  // Check that the track uuids match on the begin and end events.
+  const auto track = perfetto::NamedTrack("MyCustomTrack", async_id);
+  uint32_t main_thread_sequence = GetMainThreadPacketSequenceId(trace);
+  int event_count = 0;
+  bool found_descriptor = false;
+  for (const auto& packet : trace.packet()) {
+    if (packet.has_track_descriptor() &&
+        !packet.track_descriptor().has_process() &&
+        !packet.track_descriptor().has_thread()) {
+      auto td = packet.track_descriptor();
+      EXPECT_EQ("MyCustomTrack", td.static_name());
+      EXPECT_EQ(track.uuid, td.uuid());
+      EXPECT_EQ(perfetto::ProcessTrack::Current().uuid, td.parent_uuid());
+      found_descriptor = true;
+      continue;
+    }
+
+    if (!packet.has_track_event())
+      continue;
+    auto track_event = packet.track_event();
+    if (track_event.type() ==
+        perfetto::protos::gen::TrackEvent::TYPE_SLICE_BEGIN) {
+      EXPECT_EQ(main_thread_sequence, packet.trusted_packet_sequence_id());
+      EXPECT_EQ(track.uuid, track_event.track_uuid());
+    } else {
+      EXPECT_NE(main_thread_sequence, packet.trusted_packet_sequence_id());
+      EXPECT_EQ(track.uuid, track_event.track_uuid());
+    }
+    event_count++;
+  }
+  EXPECT_TRUE(found_descriptor);
+  EXPECT_EQ(4, event_count);
+}
+
 TEST_P(PerfettoApiTest, TrackEventCustomTimestampClock) {
   // Create a new trace session.
   auto* tracing_session = NewTraceWithCategories({"foo"});
@@ -6305,6 +6372,41 @@
   data_source->handle_stop_asynchronously = false;
 }
 
+TEST_P(PerfettoApiTest, CloneSession) {
+  perfetto::TraceConfig cfg;
+  cfg.set_unique_session_name("test_session");
+  auto* tracing_session = NewTraceWithCategories({"test"}, {}, cfg);
+  tracing_session->get()->StartBlocking();
+
+  TRACE_EVENT_BEGIN("test", "TestEvent");
+  TRACE_EVENT_END("test");
+
+  sessions_.emplace_back();
+  TestTracingSessionHandle* other_tracing_session = &sessions_.back();
+  other_tracing_session->session =
+      perfetto::Tracing::NewTrace(/*backend_type=*/GetParam());
+
+  WaitableTestEvent session_cloned;
+  other_tracing_session->get()->CloneTrace(
+      {"test_session"}, [&](perfetto::TracingSession::CloneTraceCallbackArgs) {
+        session_cloned.Notify();
+      });
+  session_cloned.Wait();
+
+  {
+    std::vector<char> raw_trace =
+        other_tracing_session->get()->ReadTraceBlocking();
+    std::string trace(raw_trace.data(), raw_trace.size());
+    EXPECT_THAT(trace, HasSubstr("TestEvent"));
+  }
+
+  {
+    std::vector<char> raw_trace = StopSessionAndReturnBytes(tracing_session);
+    std::string trace(raw_trace.data(), raw_trace.size());
+    EXPECT_THAT(trace, HasSubstr("TestEvent"));
+  }
+}
+
 class PerfettoStartupTracingApiTest : public PerfettoApiTest {
  public:
   using SetupStartupTracingOpts = perfetto::Tracing::SetupStartupTracingOpts;
@@ -7224,6 +7326,28 @@
                          ::testing::Values(perfetto::kSystemBackend),
                          BackendTypeAsString());
 
+class PerfettoApiEnvironment : public ::testing::Environment {
+ public:
+  void TearDown() override {
+    // Test shutting down Perfetto only when all other tests have been run and
+    // no more tracing code will be executed.
+    PERFETTO_CHECK(!perfetto::Tracing::IsInitialized());
+    perfetto::TracingInitArgs args;
+    args.backends = perfetto::kInProcessBackend;
+    perfetto::Tracing::Initialize(args);
+    perfetto::Tracing::Shutdown();
+    PERFETTO_CHECK(!perfetto::Tracing::IsInitialized());
+    // Shutting down again is a no-op.
+    perfetto::Tracing::Shutdown();
+    PERFETTO_CHECK(!perfetto::Tracing::IsInitialized());
+  }
+};
+
+int PERFETTO_UNUSED initializer =
+    perfetto::integration_tests::RegisterApiIntegrationTestInitializer([] {
+      ::testing::AddGlobalTestEnvironment(new PerfettoApiEnvironment);
+    });
+
 }  // namespace
 
 PERFETTO_DECLARE_DATA_SOURCE_STATIC_MEMBERS(CustomDataSource);
diff --git a/src/tracing/test/api_integrationtest_main.cc b/src/tracing/test/api_integrationtest_main.cc
deleted file mode 100644
index 5073244..0000000
--- a/src/tracing/test/api_integrationtest_main.cc
+++ /dev/null
@@ -1,46 +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.
- */
-
-#include "test/gtest_and_gmock.h"
-
-#include "perfetto/tracing.h"
-
-namespace {
-
-class PerfettoApiEnvironment : public ::testing::Environment {
- public:
-  void TearDown() override {
-    // Test shutting down Perfetto only when all other tests have been run and
-    // no more tracing code will be executed.
-    PERFETTO_CHECK(!perfetto::Tracing::IsInitialized());
-    perfetto::TracingInitArgs args;
-    args.backends = perfetto::kInProcessBackend;
-    perfetto::Tracing::Initialize(args);
-    perfetto::Tracing::Shutdown();
-    PERFETTO_CHECK(!perfetto::Tracing::IsInitialized());
-    // Shutting down again is a no-op.
-    perfetto::Tracing::Shutdown();
-    PERFETTO_CHECK(!perfetto::Tracing::IsInitialized());
-  }
-};
-
-}  // namespace
-
-int main(int argc, char** argv) {
-  ::testing::AddGlobalTestEnvironment(new PerfettoApiEnvironment);
-  ::testing::InitGoogleTest(&argc, argv);
-  return RUN_ALL_TESTS();
-}
diff --git a/src/tracing/test/mock_consumer.cc b/src/tracing/test/mock_consumer.cc
index 0171bea..09c1f24 100644
--- a/src/tracing/test/mock_consumer.cc
+++ b/src/tracing/test/mock_consumer.cc
@@ -79,7 +79,9 @@
 }
 
 void MockConsumer::CloneSession(TracingSessionID tsid) {
-  service_endpoint_->CloneSession(tsid, {});
+  ConsumerEndpoint::CloneSessionArgs args;
+  args.tsid = tsid;
+  service_endpoint_->CloneSession(args);
 }
 
 void MockConsumer::WaitForTracingDisabledWithError(
diff --git a/src/tracing/test/mock_producer.cc b/src/tracing/test/mock_producer.cc
index 501afc3..ca36075 100644
--- a/src/tracing/test/mock_producer.cc
+++ b/src/tracing/test/mock_producer.cc
@@ -73,13 +73,15 @@
                            pid_t pid,
                            size_t shared_memory_size_hint_bytes,
                            size_t shared_memory_page_size_hint_bytes,
-                           std::unique_ptr<SharedMemory> shm) {
+                           std::unique_ptr<SharedMemory> shm,
+                           bool in_process) {
   producer_name_ = producer_name;
-  service_endpoint_ = svc->ConnectProducer(
-      this, ClientIdentity(uid, pid), producer_name,
-      shared_memory_size_hint_bytes,
-      /*in_process=*/true, TracingService::ProducerSMBScrapingMode::kDefault,
-      shared_memory_page_size_hint_bytes, std::move(shm));
+  service_endpoint_ =
+      svc->ConnectProducer(this, ClientIdentity(uid, pid), producer_name,
+                           shared_memory_size_hint_bytes,
+                           /*in_process=*/in_process,
+                           TracingService::ProducerSMBScrapingMode::kDefault,
+                           shared_memory_page_size_hint_bytes, std::move(shm));
   auto checkpoint_name = "on_producer_connect_" + producer_name;
   auto on_connect = task_runner_->CreateCheckpoint(checkpoint_name);
   EXPECT_CALL(*this, OnConnect()).WillOnce(Invoke(on_connect));
@@ -190,10 +192,11 @@
 }
 
 std::unique_ptr<TraceWriter> MockProducer::CreateTraceWriter(
-    const std::string& data_source_name) {
+    const std::string& data_source_name,
+    BufferExhaustedPolicy buffer_exhausted_policy) {
   PERFETTO_DCHECK(data_source_instances_.count(data_source_name));
   BufferID buf_id = data_source_instances_[data_source_name].target_buffer;
-  return service_endpoint_->CreateTraceWriter(buf_id);
+  return service_endpoint_->CreateTraceWriter(buf_id, buffer_exhausted_policy);
 }
 
 void MockProducer::ExpectFlush(TraceWriter* writer_to_flush,
diff --git a/src/tracing/test/mock_producer.h b/src/tracing/test/mock_producer.h
index f3fab40..fcd678a 100644
--- a/src/tracing/test/mock_producer.h
+++ b/src/tracing/test/mock_producer.h
@@ -51,7 +51,8 @@
                pid_t pid = 1025,
                size_t shared_memory_size_hint_bytes = 0,
                size_t shared_memory_page_size_hint_bytes = 0,
-               std::unique_ptr<SharedMemory> shm = nullptr);
+               std::unique_ptr<SharedMemory> shm = nullptr,
+               bool in_process = true);
   void RegisterDataSource(const std::string& name,
                           bool ack_stop = false,
                           bool ack_start = false,
@@ -73,7 +74,9 @@
   DataSourceInstanceID GetDataSourceInstanceId(const std::string& name);
   const EnabledDataSource* GetDataSourceInstance(const std::string& name);
   std::unique_ptr<TraceWriter> CreateTraceWriter(
-      const std::string& data_source_name);
+      const std::string& data_source_name,
+      BufferExhaustedPolicy buffer_exhausted_policy =
+          BufferExhaustedPolicy::kDefault);
 
   // Expect a flush. Flushes |writer_to_flush| if non-null. If |reply| is true,
   // replies to the flush request, otherwise ignores it and doesn't reply.
diff --git a/src/tracing/test/proxy_producer_endpoint.cc b/src/tracing/test/proxy_producer_endpoint.cc
new file mode 100644
index 0000000..31dca1e
--- /dev/null
+++ b/src/tracing/test/proxy_producer_endpoint.cc
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/tracing/test/proxy_producer_endpoint.h"
+
+#include "perfetto/ext/tracing/core/trace_writer.h"
+
+namespace perfetto {
+
+ProxyProducerEndpoint::~ProxyProducerEndpoint() = default;
+
+void ProxyProducerEndpoint::Disconnect() {
+  if (!backend_) {
+    return;
+  }
+  backend_->Disconnect();
+}
+void ProxyProducerEndpoint::RegisterDataSource(
+    const DataSourceDescriptor& dsd) {
+  if (!backend_) {
+    return;
+  }
+  backend_->RegisterDataSource(dsd);
+}
+void ProxyProducerEndpoint::UpdateDataSource(const DataSourceDescriptor& dsd) {
+  if (!backend_) {
+    return;
+  }
+  backend_->UpdateDataSource(dsd);
+}
+void ProxyProducerEndpoint::UnregisterDataSource(const std::string& name) {
+  if (!backend_) {
+    return;
+  }
+  backend_->UnregisterDataSource(name);
+}
+void ProxyProducerEndpoint::RegisterTraceWriter(uint32_t writer_id,
+                                                uint32_t target_buffer) {
+  if (!backend_) {
+    return;
+  }
+  backend_->RegisterTraceWriter(writer_id, target_buffer);
+}
+void ProxyProducerEndpoint::UnregisterTraceWriter(uint32_t writer_id) {
+  if (!backend_) {
+    return;
+  }
+  backend_->UnregisterTraceWriter(writer_id);
+}
+void ProxyProducerEndpoint::CommitData(const CommitDataRequest& req,
+                                       CommitDataCallback callback) {
+  if (!backend_) {
+    return;
+  }
+  backend_->CommitData(req, callback);
+}
+SharedMemory* ProxyProducerEndpoint::shared_memory() const {
+  if (!backend_) {
+    return nullptr;
+  }
+  return backend_->shared_memory();
+}
+size_t ProxyProducerEndpoint::shared_buffer_page_size_kb() const {
+  if (!backend_) {
+    return 0;
+  }
+  return backend_->shared_buffer_page_size_kb();
+}
+std::unique_ptr<TraceWriter> ProxyProducerEndpoint::CreateTraceWriter(
+    BufferID target_buffer,
+    BufferExhaustedPolicy buffer_exhausted_policy) {
+  if (!backend_) {
+    return nullptr;
+  }
+  return backend_->CreateTraceWriter(target_buffer, buffer_exhausted_policy);
+}
+SharedMemoryArbiter* ProxyProducerEndpoint::MaybeSharedMemoryArbiter() {
+  if (!backend_) {
+    return nullptr;
+  }
+  return backend_->MaybeSharedMemoryArbiter();
+}
+bool ProxyProducerEndpoint::IsShmemProvidedByProducer() const {
+  if (!backend_) {
+    return false;
+  }
+  return backend_->IsShmemProvidedByProducer();
+}
+void ProxyProducerEndpoint::NotifyFlushComplete(FlushRequestID id) {
+  if (!backend_) {
+    return;
+  }
+  backend_->NotifyFlushComplete(id);
+}
+void ProxyProducerEndpoint::NotifyDataSourceStarted(DataSourceInstanceID id) {
+  if (!backend_) {
+    return;
+  }
+  backend_->NotifyDataSourceStarted(id);
+}
+void ProxyProducerEndpoint::NotifyDataSourceStopped(DataSourceInstanceID id) {
+  if (!backend_) {
+    return;
+  }
+  backend_->NotifyDataSourceStopped(id);
+}
+void ProxyProducerEndpoint::ActivateTriggers(
+    const std::vector<std::string>& triggers) {
+  if (!backend_) {
+    return;
+  }
+  backend_->ActivateTriggers(triggers);
+}
+void ProxyProducerEndpoint::Sync(std::function<void()> callback) {
+  if (!backend_) {
+    return;
+  }
+  backend_->Sync(callback);
+}
+
+}  // namespace perfetto
diff --git a/src/tracing/test/proxy_producer_endpoint.h b/src/tracing/test/proxy_producer_endpoint.h
new file mode 100644
index 0000000..708151f
--- /dev/null
+++ b/src/tracing/test/proxy_producer_endpoint.h
@@ -0,0 +1,66 @@
+/*
+ * 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_TRACING_TEST_PROXY_PRODUCER_ENDPOINT_H_
+#define SRC_TRACING_TEST_PROXY_PRODUCER_ENDPOINT_H_
+
+#include "perfetto/ext/tracing/core/tracing_service.h"
+
+namespace perfetto {
+
+// A "proxy" ProducerEndpoint that forwards all the requests to a real
+// (`backend_`) ProducerEndpoint endpoint or drops them if (`backend_`) is
+// nullptr.
+class ProxyProducerEndpoint : public ProducerEndpoint {
+ public:
+  ~ProxyProducerEndpoint() override;
+
+  // `backend` is not owned.
+  void set_backend(ProducerEndpoint* backend) { backend_ = backend; }
+
+  ProducerEndpoint* backend() const { return backend_; }
+
+  // Begin ProducerEndpoint implementation
+  void Disconnect() override;
+  void RegisterDataSource(const DataSourceDescriptor&) override;
+  void UpdateDataSource(const DataSourceDescriptor&) override;
+  void UnregisterDataSource(const std::string& name) override;
+  void RegisterTraceWriter(uint32_t writer_id, uint32_t target_buffer) override;
+  void UnregisterTraceWriter(uint32_t writer_id) override;
+  void CommitData(const CommitDataRequest&,
+                  CommitDataCallback callback = {}) override;
+  SharedMemory* shared_memory() const override;
+  size_t shared_buffer_page_size_kb() const override;
+  std::unique_ptr<TraceWriter> CreateTraceWriter(
+      BufferID target_buffer,
+      BufferExhaustedPolicy buffer_exhausted_policy =
+          BufferExhaustedPolicy::kDefault) override;
+  SharedMemoryArbiter* MaybeSharedMemoryArbiter() override;
+  bool IsShmemProvidedByProducer() const override;
+  void NotifyFlushComplete(FlushRequestID) override;
+  void NotifyDataSourceStarted(DataSourceInstanceID) override;
+  void NotifyDataSourceStopped(DataSourceInstanceID) override;
+  void ActivateTriggers(const std::vector<std::string>&) override;
+  void Sync(std::function<void()> callback) override;
+  // End ProducerEndpoint implementation
+
+ private:
+  ProducerEndpoint* backend_ = nullptr;
+};
+
+}  // namespace perfetto
+
+#endif  // SRC_TRACING_TEST_PROXY_PRODUCER_ENDPOINT_H_
diff --git a/src/tracing/test/test_shared_memory.cc b/src/tracing/test/test_shared_memory.cc
new file mode 100644
index 0000000..509c8e0
--- /dev/null
+++ b/src/tracing/test/test_shared_memory.cc
@@ -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.
+ */
+
+#include "src/tracing/test/test_shared_memory.h"
+
+namespace perfetto {
+
+TestRefSharedMemory::~TestRefSharedMemory() = default;
+
+const void* TestRefSharedMemory::start() const {
+  return start_;
+}
+size_t TestRefSharedMemory::size() const {
+  return size_;
+}
+
+}  // namespace perfetto
diff --git a/src/tracing/test/test_shared_memory.h b/src/tracing/test/test_shared_memory.h
index e932ad6..2c62b9b 100644
--- a/src/tracing/test/test_shared_memory.h
+++ b/src/tracing/test/test_shared_memory.h
@@ -21,7 +21,6 @@
 
 #include <memory>
 
-#include "perfetto/ext/base/paged_memory.h"
 #include "perfetto/ext/tracing/core/shared_memory.h"
 #include "src/tracing/core/in_process_shared_memory.h"
 
@@ -31,6 +30,33 @@
 // (just a wrapper around malloc() that fits the SharedMemory API).
 using TestSharedMemory = InProcessSharedMemory;
 
+// An implementation of the SharedMemory that doesn't own any memory, but just
+// points to memory owned by another SharedMemory.
+//
+// This is useful to test two components that own separate SharedMemory that
+// really point to the same memory underneath without setting up real posix
+// shared memory.
+class TestRefSharedMemory : public SharedMemory {
+ public:
+  // N.B. `*mem` must outlive `*this`.
+  explicit TestRefSharedMemory(SharedMemory* mem)
+      : start_(mem->start()), size_(mem->size()) {}
+  ~TestRefSharedMemory() override;
+
+  static std::unique_ptr<TestRefSharedMemory> Create(SharedMemory* mem) {
+    return std::make_unique<TestRefSharedMemory>(mem);
+  }
+
+  // SharedMemory implementation.
+  using SharedMemory::start;  // Equal priority to const and non-const versions
+  const void* start() const override;
+  size_t size() const override;
+
+ private:
+  void* start_;
+  size_t size_;
+};
+
 }  // namespace perfetto
 
 #endif  // SRC_TRACING_TEST_TEST_SHARED_MEMORY_H_
diff --git a/src/tracing/test/tracing_integration_test.cc b/src/tracing/test/tracing_integration_test.cc
index 45def13..b9c2bac 100644
--- a/src/tracing/test/tracing_integration_test.cc
+++ b/src/tracing/test/tracing_integration_test.cc
@@ -200,35 +200,6 @@
     return TracingService::ProducerSMBScrapingMode::kDefault;
   }
 
-  void WaitForTraceWritersChanged(ProducerID producer_id) {
-    static int i = 0;
-    auto checkpoint_name = "writers_changed_" + std::to_string(producer_id) +
-                           "_" + std::to_string(i++);
-    auto writers_changed = task_runner_->CreateCheckpoint(checkpoint_name);
-    auto writers = GetWriters(producer_id);
-    std::function<void()> task;
-    task = [&task, writers, writers_changed, producer_id, this]() {
-      if (writers != GetWriters(producer_id)) {
-        writers_changed();
-        return;
-      }
-      task_runner_->PostDelayedTask(task, 1);
-    };
-    task_runner_->PostDelayedTask(task, 1);
-    task_runner_->RunUntilCheckpoint(checkpoint_name);
-  }
-
-  const std::map<WriterID, BufferID>& GetWriters(ProducerID producer_id) {
-    return reinterpret_cast<TracingServiceImpl*>(svc_->service())
-        ->GetProducer(producer_id)
-        ->writers_;
-  }
-
-  ProducerID* last_producer_id() {
-    return &reinterpret_cast<TracingServiceImpl*>(svc_->service())
-                ->last_producer_id_;
-  }
-
   std::unique_ptr<base::TestTaskRunner> task_runner_;
   std::unique_ptr<ServiceIPCHost> svc_;
   std::unique_ptr<TracingService::ProducerEndpoint> producer_endpoint_;
@@ -515,7 +486,7 @@
   ASSERT_TRUE(writer);
 
   // Wait for the writer to be registered.
-  WaitForTraceWritersChanged(*last_producer_id());
+  task_runner_->RunUntilIdle();
 
   // Write a few trace packets.
   writer->NewTracePacket()->set_for_testing()->set_str("payload1");
diff --git a/src/tracing/tracing.cc b/src/tracing/tracing.cc
index a0ab7b2..10e0992 100644
--- a/src/tracing/tracing.cc
+++ b/src/tracing/tracing.cc
@@ -126,6 +126,8 @@
 
 TracingSession::~TracingSession() = default;
 
+void TracingSession::CloneTrace(CloneTraceArgs, CloneTraceCallback) {}
+
 // Can be called from any thread.
 bool TracingSession::FlushBlocking(uint32_t timeout_ms) {
   std::atomic<bool> flush_result;
diff --git a/src/tracing/track.cc b/src/tracing/track.cc
index dc02609..152c39e 100644
--- a/src/tracing/track.cc
+++ b/src/tracing/track.cc
@@ -116,6 +116,21 @@
   desc->AppendRawProtoBytes(bytes.data(), bytes.size());
 }
 
+protos::gen::TrackDescriptor NamedTrack::Serialize() const {
+  auto desc = Track::Serialize();
+  if (static_name_) {
+    desc.set_static_name(static_name_.value);
+  } else {
+    desc.set_name(dynamic_name_.value);
+  }
+  return desc;
+}
+
+void NamedTrack::Serialize(protos::pbzero::TrackDescriptor* desc) const {
+  auto bytes = Serialize().SerializeAsString();
+  desc->AppendRawProtoBytes(bytes.data(), bytes.size());
+}
+
 protos::gen::TrackDescriptor CounterTrack::Serialize() const {
   auto desc = Track::Serialize();
   auto* counter = desc.mutable_counter();
diff --git a/test/.gitignore b/test/.gitignore
index 5a1a164..7d2e22e 100644
--- a/test/.gitignore
+++ b/test/.gitignore
@@ -6,8 +6,8 @@
 !/data/OWNERS
 
 !/data/ui-screenshots
-/data/ui-screenshots/*
-!/data/ui-screenshots/*.sha256
+/data/ui-screenshots/*.png
+/data/ui-screenshots/**/*.png
 
 !/data/chrome
 /data/chrome/*
@@ -29,4 +29,4 @@
 
 !/data/zip
 /data/zip/*
-!/data/zip/*.sha256
\ No newline at end of file
+!/data/zip/*.sha256
diff --git a/test/BUILD.gn b/test/BUILD.gn
index 2a0a4bc..406eca4 100644
--- a/test/BUILD.gn
+++ b/test/BUILD.gn
@@ -159,6 +159,24 @@
   }
 }
 
+if (enable_perfetto_integration_tests) {
+  source_set("integrationtest_initializer") {
+    testonly = true
+    deps = [ "../gn:default_deps" ]
+    sources = [ "integrationtest_initializer.h" ]
+  }
+  source_set("integrationtest_main") {
+    testonly = true
+    deps = [
+      ":integrationtest_initializer",
+      "../src/base",
+      "../gn:default_deps",
+      "../gn:gtest_and_gmock",
+    ]
+    sources = [ "integrationtest_main.cc" ]
+  }
+}
+
 if (enable_perfetto_benchmarks) {
   source_set("end_to_end_benchmarks") {
     testonly = true
diff --git a/test/ci/common.sh b/test/ci/common.sh
index c28dff6..bdabb51 100644
--- a/test/ci/common.sh
+++ b/test/ci/common.sh
@@ -37,7 +37,7 @@
 tools/check_sql_modules.py
 tools/check_sql_metrics.py
 
-if !(git diff --name-only HEAD^1 HEAD | egrep -qv '^(ui|docs|infra)/'); then
+if !(git diff --name-only HEAD^1 HEAD | egrep -qv '^(ui|docs|infra|test/data/ui-screenshots)/'); then
 export UI_DOCS_INFRA_ONLY_CHANGE=1
 else
 export UI_DOCS_INFRA_ONLY_CHANGE=0
diff --git a/test/ci/ui_tests.sh b/test/ci/ui_tests.sh
index 50b0d7f..00567d4 100755
--- a/test/ci/ui_tests.sh
+++ b/test/ci/ui_tests.sh
@@ -15,6 +15,8 @@
 
 source $(dirname ${BASH_SOURCE[0]})/common.sh
 
+export CI=1
+
 infra/perfetto.dev/build
 
 ui/build --out ${OUT_PATH}
@@ -24,11 +26,30 @@
 ui/run-unittests --out ${OUT_PATH} --no-build
 
 set +e
+
+# Install chrome
+(
+  mkdir /ci/ramdisk/chrome
+  cd /ci/ramdisk/chrome
+  CHROME_VERSION=128.0.6613.137
+  curl -Ls -o chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}-1_amd64.deb
+  dpkg-deb -x chrome.deb  .
+)
 ui/run-integrationtests --out ${OUT_PATH} --no-build
 RES=$?
 
+set +x
+
 # Copy the output of screenshots diff testing.
 if [ -d ${OUT_PATH}/ui-test-artifacts ]; then
   cp -a ${OUT_PATH}/ui-test-artifacts /ci/artifacts/ui-test-artifacts
+  echo "UI integration test report with screnshots:"
+  echo "https://storage.googleapis.com/perfetto-ci-artifacts/$PERFETTO_TEST_JOB/ui-test-artifacts/index.html"
+  echo ""
+  echo "To download locally the changed screenshots run:"
+  echo "tools/download_changed_screenshots.py $PERFETTO_TEST_JOB"
+  echo ""
+  echo "Perfetto UI build for this CL"
+  echo "https://storage.googleapis.com/perfetto-ci-artifacts/$PERFETTO_TEST_JOB/ui/index.html"
   exit $RES
 fi
diff --git a/test/cmdline_integrationtest.cc b/test/cmdline_integrationtest.cc
index 6fd2b75..c4a6fc7 100644
--- a/test/cmdline_integrationtest.cc
+++ b/test/cmdline_integrationtest.cc
@@ -47,6 +47,7 @@
 
 using ::testing::ContainsRegex;
 using ::testing::Each;
+using ::testing::ElementsAre;
 using ::testing::ElementsAreArray;
 using ::testing::Eq;
 using ::testing::HasSubstr;
@@ -99,6 +100,52 @@
   return trace_config;
 }
 
+// For the regular tests.
+TraceConfig CreateTraceConfigForTest(uint32_t test_msg_count = 11,
+                                     uint32_t test_msg_size = 32) {
+  TraceConfig trace_config;
+  trace_config.add_buffers()->set_size_kb(1024);
+  auto* ds_config = trace_config.add_data_sources()->mutable_config();
+  ds_config->set_name("android.perfetto.FakeProducer");
+  ds_config->mutable_for_testing()->set_message_count(test_msg_count);
+  ds_config->mutable_for_testing()->set_message_size(test_msg_size);
+  return trace_config;
+}
+
+void ExpectTraceContainsTestMessages(const protos::gen::Trace& trace,
+                                     uint32_t count) {
+  ssize_t actual_test_packets_count = std::count_if(
+      trace.packet().begin(), trace.packet().end(),
+      [](const protos::gen::TracePacket& tp) { return tp.has_for_testing(); });
+  EXPECT_EQ(count, static_cast<uint32_t>(actual_test_packets_count));
+}
+
+void ExpectTraceContainsTestMessagesWithSize(const protos::gen::Trace& trace,
+                                             uint32_t message_size) {
+  for (const auto& packet : trace.packet()) {
+    if (packet.has_for_testing()) {
+      EXPECT_EQ(message_size, packet.for_testing().str().size());
+    }
+  }
+}
+
+void ExpectTraceContainsConfigWithTriggerMode(
+    const protos::gen::Trace& trace,
+    protos::gen::TraceConfig::TriggerConfig::TriggerMode trigger_mode) {
+  // GTest three level nested Property matcher is hard to read, so we use
+  // 'find_if' with lambda to ensure the trace config properly includes the
+  // trigger mode we set.
+  auto found =
+      std::find_if(trace.packet().begin(), trace.packet().end(),
+                   [trigger_mode](const protos::gen::TracePacket& tp) {
+                     return tp.has_trace_config() &&
+                            tp.trace_config().trigger_config().trigger_mode() ==
+                                trigger_mode;
+                   });
+  EXPECT_NE(found, trace.packet().end())
+      << "Trace config doesn't include expected trigger mode.";
+}
+
 class ScopedFileRemove {
  public:
   explicit ScopedFileRemove(const std::string& path) : path_(path) {}
@@ -106,6 +153,27 @@
   std::string path_;
 };
 
+bool ParseNotEmptyTraceFromFile(const std::string& trace_path,
+                                protos::gen::Trace& out) {
+  std::string trace_str;
+  if (!base::ReadFile(trace_path, &trace_str))
+    return false;
+  if (trace_str.empty())
+    return false;
+  return out.ParseFromString(trace_str);
+}
+
+std::vector<std::string> GetReceivedTriggerNames(
+    const protos::gen::Trace& trace) {
+  std::vector<std::string> triggers;
+  for (const protos::gen::TracePacket& packet : trace.packet()) {
+    if (packet.has_trigger()) {
+      triggers.push_back(packet.trigger().trigger_name());
+    }
+  }
+  return triggers;
+}
+
 class PerfettoCmdlineTest : public ::testing::Test {
  public:
   void StartServiceIfRequiredNoNewExecsAfterThis() {
@@ -190,11 +258,8 @@
       // Read the trace written in the fixed location
       // (/data/misc/perfetto-traces/ on Android, /tmp/ on Linux/Mac) and make
       // sure it has the right contents.
-      std::string trace_str;
-      base::ReadFile(trace_path, &trace_str);
-      ASSERT_FALSE(trace_str.empty());
       protos::gen::Trace trace;
-      ASSERT_TRUE(trace.ParseFromString(trace_str));
+      ASSERT_TRUE(ParseNotEmptyTraceFromFile(trace_path, trace));
       uint32_t test_packets = 0;
       for (const auto& p : trace.packet())
         test_packets += p.has_for_testing() ? 1 : 0;
@@ -212,6 +277,11 @@
   std::string stderr_;
   base::TestTaskRunner task_runner_;
 
+  // We use these two constants to set test data payload parameters and assert
+  // it was correctly written to the trace.
+  static constexpr size_t kTestMessageCount = 11;
+  static constexpr size_t kTestMessageSize = 32;
+
  private:
   bool exec_allowed_ = true;
   TestHelper test_helper_{&task_runner_};
@@ -350,15 +420,8 @@
 }
 
 TEST_F(PerfettoCmdlineTest, StartTracingTrigger) {
-  // See |message_count| and |message_size| in the TraceConfig above.
-  constexpr size_t kMessageCount = 11;
-  constexpr size_t kMessageSize = 32;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   auto* trigger_cfg = trace_config.mutable_trigger_config();
   trigger_cfg->set_trigger_mode(
       protos::gen::TraceConfig::TriggerConfig::START_TRACING);
@@ -404,53 +467,25 @@
   test_helper().WaitForProducerSetup();
   EXPECT_EQ(0, trigger_proc.Run(&stderr_));
 
-  // Wait for the producer to start, and then write out 11 packets.
+  // Wait for the producer to start, and then write out some test packets.
   test_helper().WaitForProducerEnabled();
   auto on_data_written = task_runner_.CreateCheckpoint("data_written");
   fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
   task_runner_.RunUntilCheckpoint("data_written");
   background_trace.join();
 
-  std::string trace_str;
-  base::ReadFile(path, &trace_str);
   protos::gen::Trace trace;
-  ASSERT_TRUE(trace.ParseFromString(trace_str));
-  size_t for_testing_packets = 0;
-  size_t trigger_packets = 0;
-  size_t trace_config_packets = 0;
-  for (const auto& packet : trace.packet()) {
-    if (packet.has_trace_config()) {
-      // Ensure the trace config properly includes the trigger mode we set.
-      auto kStartTrig = protos::gen::TraceConfig::TriggerConfig::START_TRACING;
-      EXPECT_EQ(kStartTrig,
-                packet.trace_config().trigger_config().trigger_mode());
-      ++trace_config_packets;
-    } else if (packet.has_trigger()) {
-      // validate that the triggers are properly added to the trace.
-      EXPECT_EQ("trigger_name", packet.trigger().trigger_name());
-      ++trigger_packets;
-    } else if (packet.has_for_testing()) {
-      // Make sure that the data size is correctly set based on what we
-      // requested.
-      EXPECT_EQ(kMessageSize, packet.for_testing().str().size());
-      ++for_testing_packets;
-    }
-  }
-  EXPECT_EQ(trace_config_packets, 1u);
-  EXPECT_EQ(trigger_packets, 1u);
-  EXPECT_EQ(for_testing_packets, kMessageCount);
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+  ExpectTraceContainsConfigWithTriggerMode(
+      trace, protos::gen::TraceConfig::TriggerConfig::START_TRACING);
+  EXPECT_THAT(GetReceivedTriggerNames(trace), ElementsAre("trigger_name"));
+  ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
 }
 
 TEST_F(PerfettoCmdlineTest, StopTracingTrigger) {
-  // See |message_count| and |message_size| in the TraceConfig above.
-  constexpr size_t kMessageCount = 11;
-  constexpr size_t kMessageSize = 32;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   auto* trigger_cfg = trace_config.mutable_trigger_config();
   trigger_cfg->set_trigger_mode(
       protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -497,8 +532,8 @@
   });
 
   test_helper().WaitForProducerEnabled();
-  // Wait for the producer to start, and then write out 11 packets, before the
-  // trace actually starts (the trigger is seen).
+  // Wait for the producer to start, and then write out some test packets,
+  // before the trace actually starts (the trigger is seen).
   auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
   fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
   task_runner_.RunUntilCheckpoint("data_written_1");
@@ -507,57 +542,23 @@
 
   background_trace.join();
 
-  std::string trace_str;
-  base::ReadFile(path, &trace_str);
   protos::gen::Trace trace;
-  ASSERT_TRUE(trace.ParseFromString(trace_str));
-  bool seen_first_trigger = false;
-  size_t for_testing_packets = 0;
-  size_t trigger_packets = 0;
-  size_t trace_config_packets = 0;
-  for (const auto& packet : trace.packet()) {
-    if (packet.has_trace_config()) {
-      // Ensure the trace config properly includes the trigger mode we set.
-      auto kStopTrig = protos::gen::TraceConfig::TriggerConfig::STOP_TRACING;
-      EXPECT_EQ(kStopTrig,
-                packet.trace_config().trigger_config().trigger_mode());
-      ++trace_config_packets;
-    } else if (packet.has_trigger()) {
-      // validate that the triggers are properly added to the trace.
-      if (!seen_first_trigger) {
-        EXPECT_EQ("trigger_name", packet.trigger().trigger_name());
-        seen_first_trigger = true;
-      } else {
-        EXPECT_EQ("trigger_name_3", packet.trigger().trigger_name());
-      }
-      ++trigger_packets;
-    } else if (packet.has_for_testing()) {
-      // Make sure that the data size is correctly set based on what we
-      // requested.
-      EXPECT_EQ(kMessageSize, packet.for_testing().str().size());
-      ++for_testing_packets;
-    }
-  }
-  EXPECT_EQ(trace_config_packets, 1u);
-  EXPECT_EQ(trigger_packets, 2u);
-  EXPECT_EQ(for_testing_packets, kMessageCount);
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+  ExpectTraceContainsConfigWithTriggerMode(
+      trace, protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
+  EXPECT_THAT(GetReceivedTriggerNames(trace),
+              ElementsAre("trigger_name", "trigger_name_3"));
+  ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
 }
 
 // Dropbox on the commandline client only works on android builds. So disable
 // this test on all other builds.
 TEST_F(PerfettoCmdlineTest, AndroidOnly(NoDataNoFileWithoutTrigger)) {
-  // See |message_count| and |message_size| in the TraceConfig above.
-  constexpr size_t kMessageCount = 11;
-  constexpr size_t kMessageSize = 32;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
-  trace_config.set_allow_user_build_tracing(true);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   auto* incident_config = trace_config.mutable_incident_report_config();
   incident_config->set_destination_package("foo.bar.baz");
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
   auto* trigger_cfg = trace_config.mutable_trigger_config();
   trigger_cfg->set_trigger_mode(
       protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -602,15 +603,8 @@
 }
 
 TEST_F(PerfettoCmdlineTest, StopTracingTriggerFromConfig) {
-  // See |message_count| and |message_size| in the TraceConfig above.
-  constexpr size_t kMessageCount = 11;
-  constexpr size_t kMessageSize = 32;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   auto* trigger_cfg = trace_config.mutable_trigger_config();
   trigger_cfg->set_trigger_mode(
       protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -667,8 +661,8 @@
   });
 
   test_helper().WaitForProducerEnabled();
-  // Wait for the producer to start, and then write out 11 packets, before the
-  // trace actually starts (the trigger is seen).
+  // Wait for the producer to start, and then write out some test packets,
+  // before the trace actually starts (the trigger is seen).
   auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
   fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
   task_runner_.RunUntilCheckpoint("data_written_1");
@@ -677,44 +671,20 @@
 
   background_trace.join();
 
-  std::string trace_str;
-  base::ReadFile(path, &trace_str);
   protos::gen::Trace trace;
-  ASSERT_TRUE(trace.ParseFromString(trace_str));
-  EXPECT_LT(static_cast<int>(kMessageCount), trace.packet_size());
-  bool seen_first_trigger = false;
-  for (const auto& packet : trace.packet()) {
-    if (packet.has_trace_config()) {
-      // Ensure the trace config properly includes the trigger mode we set.
-      auto kStopTrig = protos::gen::TraceConfig::TriggerConfig::STOP_TRACING;
-      EXPECT_EQ(kStopTrig,
-                packet.trace_config().trigger_config().trigger_mode());
-    } else if (packet.has_trigger()) {
-      // validate that the triggers are properly added to the trace.
-      if (!seen_first_trigger) {
-        EXPECT_EQ("trigger_name", packet.trigger().trigger_name());
-        seen_first_trigger = true;
-      } else {
-        EXPECT_EQ("trigger_name_3", packet.trigger().trigger_name());
-      }
-    } else if (packet.has_for_testing()) {
-      // Make sure that the data size is correctly set based on what we
-      // requested.
-      EXPECT_EQ(kMessageSize, packet.for_testing().str().size());
-    }
-  }
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+  EXPECT_LT(static_cast<int>(kTestMessageCount), trace.packet_size());
+  ExpectTraceContainsConfigWithTriggerMode(
+      trace, protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
+  EXPECT_THAT(GetReceivedTriggerNames(trace),
+              ElementsAre("trigger_name", "trigger_name_3"));
+  ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
 }
 
 TEST_F(PerfettoCmdlineTest, TriggerFromConfigStopsFileOpening) {
-  // See |message_count| and |message_size| in the TraceConfig above.
-  constexpr size_t kMessageCount = 11;
-  constexpr size_t kMessageSize = 32;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   auto* trigger_cfg = trace_config.mutable_trigger_config();
   trigger_cfg->set_trigger_mode(
       protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -773,15 +743,8 @@
 }
 
 TEST_F(PerfettoCmdlineTest, AndroidOnly(CmdTriggerWithUploadFlag)) {
-  // See |message_count| and |message_size| in the TraceConfig above.
-  constexpr size_t kMessageCount = 2;
-  constexpr size_t kMessageSize = 2;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   auto* trigger_cfg = trace_config.mutable_trigger_config();
   trigger_cfg->set_trigger_mode(
       protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -832,8 +795,8 @@
   });
 
   test_helper().WaitForProducerEnabled();
-  // Wait for the producer to start, and then write out 11 packets, before the
-  // trace actually starts (the trigger is seen).
+  // Wait for the producer to start, and then write out some test packets,
+  // before the trace actually starts (the trigger is seen).
   auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
   fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
   task_runner_.RunUntilCheckpoint("data_written_1");
@@ -842,11 +805,11 @@
 
   background_trace.join();
 
-  std::string trace_str;
-  base::ReadFile(path, &trace_str);
   protos::gen::Trace trace;
-  ASSERT_TRUE(trace.ParseFromString(trace_str));
-  EXPECT_LT(static_cast<int>(kMessageCount), trace.packet_size());
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+  ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
+  EXPECT_LT(static_cast<int>(kTestMessageCount), trace.packet_size());
   EXPECT_THAT(trace.packet(),
               Contains(Property(&protos::gen::TracePacket::trigger,
                                 Property(&protos::gen::Trigger::trigger_name,
@@ -854,14 +817,8 @@
 }
 
 TEST_F(PerfettoCmdlineTest, TriggerCloneSnapshot) {
-  constexpr size_t kMessageCount = 2;
-  constexpr size_t kMessageSize = 2;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   auto* trigger_cfg = trace_config.mutable_trigger_config();
   trigger_cfg->set_trigger_mode(
       protos::gen::TraceConfig::TriggerConfig::CLONE_SNAPSHOT);
@@ -911,8 +868,8 @@
   });
 
   test_helper().WaitForProducerEnabled();
-  // Wait for the producer to start, and then write out 11 packets, before the
-  // trace actually starts (the trigger is seen).
+  // Wait for the producer to start, and then write out some test packets,
+  // before the trace actually starts (the trigger is seen).
   auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
   fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
   task_runner_.RunUntilCheckpoint("data_written_1");
@@ -932,11 +889,11 @@
   perfetto_proc.SendSigterm();
   background_trace.join();
 
-  std::string trace_str;
-  base::ReadFile(snapshot_path, &trace_str);
   protos::gen::Trace trace;
-  ASSERT_TRUE(trace.ParseFromString(trace_str));
-  EXPECT_LT(static_cast<int>(kMessageCount), trace.packet_size());
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(snapshot_path, trace));
+  ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
+  EXPECT_LT(static_cast<int>(kTestMessageCount), trace.packet_size());
   EXPECT_THAT(trace.packet(),
               Contains(Property(&protos::gen::TracePacket::trigger,
                                 Property(&protos::gen::Trigger::trigger_name,
@@ -961,6 +918,81 @@
                    /*use_explicit_clone=*/true);
 }
 
+TEST_F(PerfettoCmdlineTest, CloneByName) {
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
+  trace_config.set_unique_session_name("my_unique_session_name");
+
+  // We have to construct all the processes we want to fork before we start the
+  // service with |StartServiceIfRequired()|. this is because it is unsafe
+  // (could deadlock) to fork after we've spawned some threads which might
+  // printf (and thus hold locks).
+  const std::string path = RandomTraceFileName();
+  ScopedFileRemove remove_on_test_exit(path);
+  auto perfetto_proc = ExecPerfetto(
+      {
+          "-o",
+          path,
+          "-c",
+          "-",
+      },
+      trace_config.SerializeAsString());
+
+  const std::string path_cloned = RandomTraceFileName();
+  ScopedFileRemove path_cloned_remove(path_cloned);
+  auto perfetto_proc_clone = ExecPerfetto({
+      "-o",
+      path_cloned,
+      "--clone-by-name",
+      "my_unique_session_name",
+  });
+
+  const std::string path_cloned_2 = RandomTraceFileName();
+  ScopedFileRemove path_cloned_2_remove(path_cloned_2);
+  auto perfetto_proc_clone_2 = ExecPerfetto({
+      "-o",
+      path_cloned_2,
+      "--clone-by-name",
+      "non_existing_session_name",
+  });
+
+  // Start the service and connect a simple fake producer.
+  StartServiceIfRequiredNoNewExecsAfterThis();
+  auto* fake_producer = test_helper().ConnectFakeProducer();
+  EXPECT_TRUE(fake_producer);
+
+  std::thread background_trace([&perfetto_proc]() {
+    std::string stderr_str;
+    EXPECT_EQ(0, perfetto_proc.Run(&stderr_str)) << stderr_str;
+  });
+
+  test_helper().WaitForProducerEnabled();
+
+  auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
+  fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
+  task_runner_.RunUntilCheckpoint("data_written_1");
+
+  EXPECT_EQ(0, perfetto_proc_clone.Run(&stderr_)) << "stderr: " << stderr_;
+  EXPECT_TRUE(base::FileExists(path_cloned));
+
+  // The command still returns 0, but doesn't create a file.
+  EXPECT_EQ(0, perfetto_proc_clone_2.Run(&stderr_)) << "stderr: " << stderr_;
+  EXPECT_FALSE(base::FileExists(path_cloned_2));
+
+  protos::gen::Trace cloned_trace;
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(path_cloned, cloned_trace));
+  ExpectTraceContainsTestMessages(cloned_trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(cloned_trace, kTestMessageSize);
+
+  perfetto_proc.SendSigterm();
+  background_trace.join();
+
+  protos::gen::Trace trace;
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+  ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
+}
+
 // Regression test for b/279753347: --save-for-bugreport would create an empty
 // file if no session with bugreport_score was active.
 TEST_F(PerfettoCmdlineTest, UnavailableBugreportLeavesNoEmptyFiles) {
@@ -1098,10 +1130,8 @@
   auto check_trace = [&](std::string fname, int expected_score) {
     std::string fpath = GetBugreportTraceDir() + "/" + fname;
     ASSERT_TRUE(base::FileExists(fpath)) << fpath;
-    std::string trace_str;
-    base::ReadFile(fpath, &trace_str);
     protos::gen::Trace trace;
-    ASSERT_TRUE(trace.ParseFromString(trace_str)) << fpath;
+    ASSERT_TRUE(ParseNotEmptyTraceFromFile(fpath, trace)) << fpath;
     EXPECT_THAT(
         trace.packet(),
         Contains(Property(&protos::gen::TracePacket::trace_config,
@@ -1124,8 +1154,14 @@
   auto remove_on_exit = base::OnScopeExit(remove_br_files);
 
   const uint32_t kMsgCount = 10000;
+  const uint32_t kMsgSize = 1024;
   TraceConfig cfg = CreateTraceConfigForBugreportTest(
-      /*score=*/1, /*add_filter=*/false, kMsgCount, /*msg_size=*/1024);
+      /*score=*/1, /*add_filter=*/false, kMsgCount, kMsgSize);
+
+  auto session_name = "bugreport_test_" +
+                      std::to_string(base::GetWallTimeNs().count() % 1000000);
+
+  cfg.set_unique_session_name(session_name);
   std::string cfg_str = cfg.SerializeAsString();
   Exec trace_proc = ExecPerfetto({"-o", base::kDevNull, "-c", "-"}, cfg_str);
   Exec perfetto_br_proc = ExecPerfetto({"--save-all-for-bugreport"});
@@ -1144,7 +1180,15 @@
   // Wait that the tracing session is started.
   test_helper().ConnectConsumer();
   test_helper().WaitForConsumerConnect();
-  while (test_helper().QueryServiceStateAndWait().num_sessions_started() == 0) {
+  for (;;) {
+    auto state = test_helper().QueryServiceStateAndWait();
+    const auto& sessions = state.tracing_sessions();
+    if (std::count_if(sessions.begin(), sessions.end(),
+                      [&](const TracingServiceState::TracingSession& s) {
+                        return s.unique_session_name() == session_name;
+                      }) >= 1) {
+      break;
+    }
     base::SleepMicroseconds(100 * 1000);
   }
   test_helper().SyncAndWaitProducer();
@@ -1163,14 +1207,10 @@
 
   std::string fpath = GetBugreportTraceDir() + "/systrace.pftrace";
   ASSERT_TRUE(base::FileExists(fpath)) << fpath;
-  std::string trace_str;
-  base::ReadFile(fpath, &trace_str);
   protos::gen::Trace trace;
-  ASSERT_TRUE(trace.ParseFromString(trace_str)) << fpath;
-  ssize_t num_test_packets = std::count_if(
-      trace.packet().begin(), trace.packet().end(),
-      [](const protos::gen::TracePacket& tp) { return tp.has_for_testing(); });
-  EXPECT_EQ(num_test_packets, static_cast<ssize_t>(kMsgCount));
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(fpath, trace)) << fpath;
+  ExpectTraceContainsTestMessages(trace, kMsgCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kMsgSize);
 }
 
 }  // namespace perfetto
diff --git a/test/cts/heapprofd_java_test_cts.cc b/test/cts/heapprofd_java_test_cts.cc
index ff0e759..1a492b7 100644
--- a/test/cts/heapprofd_java_test_cts.cc
+++ b/test/cts/heapprofd_java_test_cts.cc
@@ -72,7 +72,7 @@
     StopApp(app_name, "old.app.stopped", &task_runner);
     task_runner.RunUntilCheckpoint("old.app.stopped", 10000 /*ms*/);
   }
-  StartAppActivity(app_name, "MainActivity", "target.app.running", &task_runner,
+  StartAppActivity(app_name, "NoopActivity", "target.app.running", &task_runner,
                    /*delay_ms=*/100);
   task_runner.RunUntilCheckpoint("target.app.running", 10000 /*ms*/);
   // If we try to dump too early in app initialization, we sometimes deadlock.
@@ -219,7 +219,7 @@
     StopApp(app_name, "old.app.stopped", &task_runner);
     task_runner.RunUntilCheckpoint("old.app.stopped", 10000 /*ms*/);
   }
-  StartAppActivity(app_name, "MainActivity", "target.app.running", &task_runner,
+  StartAppActivity(app_name, "NoopActivity", "target.app.running", &task_runner,
                    /*delay_ms=*/100);
   task_runner.RunUntilCheckpoint("target.app.running", 10000 /*ms*/);
   // If we try to dump too early in app initialization, we sometimes deadlock.
diff --git a/test/cts/reporter/reporter_test_cts.cc b/test/cts/reporter/reporter_test_cts.cc
index 8745dcf..c2a2e7b 100644
--- a/test/cts/reporter/reporter_test_cts.cc
+++ b/test/cts/reporter/reporter_test_cts.cc
@@ -41,7 +41,6 @@
   TraceConfig trace_config;
   trace_config.add_buffers()->set_size_kb(1024);
   trace_config.set_duration_ms(200);
-  trace_config.set_allow_user_build_tracing(true);
   trace_config.set_unique_session_name("TestEndToEndReport");
 
   // Make the trace as small as possible (see b/282508742).
diff --git a/test/cts/test_apps/AndroidManifest_debuggable.xml b/test/cts/test_apps/AndroidManifest_debuggable.xml
index 79993f8..e2fe258 100755
--- a/test/cts/test_apps/AndroidManifest_debuggable.xml
+++ b/test/cts/test_apps/AndroidManifest_debuggable.xml
@@ -18,7 +18,20 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.perfetto.cts.app.debuggable">
 
-    <application android:debuggable="true">
+    <!-- vmSafeMode="true" disables the JIT.
+
+      HeapprofdJavaCtsTest cover Java heap dumps.
+
+      Java heap dumps are not 100% reliable because they fork the app process,
+      which is multithreaded. If another thread is holding a lock, the forked
+      process can get stuck. This is a known limitation of java heap dumps.
+
+      debuggable apps are not AOT-compiled, so there's a high chance that the
+      JIT is in use. The JIT runs on another thread and can hold locks. To
+      reduce the chance of running into the fork deadlock described earlier,
+      we simply disable the JIT for debuggable apps in tests.
+    -->
+    <application android:debuggable="true" android:vmSafeMode="true">
         <activity
           android:name="android.perfetto.cts.app.MainActivity"
           android:exported="true">
@@ -71,6 +84,19 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+        <activity
+          android:name="android.perfetto.cts.app.NoopActivity"
+          android:exported="true">
+        </activity>
+        <activity-alias
+          android:name="android.perfetto.cts.app.debuggable.NoopActivity"
+          android:targetActivity="android.perfetto.cts.app.NoopActivity"
+          android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity-alias>
         <provider
           android:name="android.perfetto.cts.app.FileContentProvider"
           android:authorities="android.perfetto.cts.app.debuggable"
diff --git a/test/cts/test_apps/AndroidManifest_nonprofileable.xml b/test/cts/test_apps/AndroidManifest_nonprofileable.xml
index 8322daf..da3210d 100755
--- a/test/cts/test_apps/AndroidManifest_nonprofileable.xml
+++ b/test/cts/test_apps/AndroidManifest_nonprofileable.xml
@@ -59,6 +59,19 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+        <activity
+          android:name="android.perfetto.cts.app.NoopActivity"
+          android:exported="true">
+        </activity>
+        <activity-alias
+          android:name="android.perfetto.cts.app.nonprofileable.NoopActivity"
+          android:targetActivity="android.perfetto.cts.app.NoopActivity"
+          android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity-alias>
         <provider
           android:name="android.perfetto.cts.app.FileContentProvider"
           android:authorities="android.perfetto.cts.app.nonprofileable"
diff --git a/test/cts/test_apps/AndroidManifest_profileable.xml b/test/cts/test_apps/AndroidManifest_profileable.xml
index cd434d4..aebb810 100755
--- a/test/cts/test_apps/AndroidManifest_profileable.xml
+++ b/test/cts/test_apps/AndroidManifest_profileable.xml
@@ -72,6 +72,19 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+        <activity
+          android:name="android.perfetto.cts.app.NoopActivity"
+          android:exported="true">
+        </activity>
+        <activity-alias
+          android:name="android.perfetto.cts.app.profileable.NoopActivity"
+          android:targetActivity="android.perfetto.cts.app.NoopActivity"
+          android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity-alias>
         <provider
           android:name="android.perfetto.cts.app.FileContentProvider"
           android:authorities="android.perfetto.cts.app.profileable"
diff --git a/test/cts/test_apps/AndroidManifest_release.xml b/test/cts/test_apps/AndroidManifest_release.xml
index 1795a59..8a87e23 100755
--- a/test/cts/test_apps/AndroidManifest_release.xml
+++ b/test/cts/test_apps/AndroidManifest_release.xml
@@ -71,6 +71,19 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+        <activity
+          android:name="android.perfetto.cts.app.NoopActivity"
+          android:exported="true">
+        </activity>
+        <activity-alias
+          android:name="android.perfetto.cts.app.release.NoopActivity"
+          android:targetActivity="android.perfetto.cts.app.NoopActivity"
+          android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity-alias>
         <provider
           android:name="android.perfetto.cts.app.FileContentProvider"
           android:authorities="android.perfetto.cts.app.release"
diff --git a/test/cts/test_apps/src/android/perfetto/cts/app/NoopActivity.java b/test/cts/test_apps/src/android/perfetto/cts/app/NoopActivity.java
new file mode 100644
index 0000000..296e692
--- /dev/null
+++ b/test/cts/test_apps/src/android/perfetto/cts/app/NoopActivity.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 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
+ *
+ *      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.
+ */
+
+package android.perfetto.cts.app;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class NoopActivity extends Activity {
+    @Override
+    public void onCreate(Bundle state) {
+        super.onCreate(state);
+    }
+}
diff --git a/test/data/android_desktop_mode/multiple_window_only_update.pb.sha256 b/test/data/android_desktop_mode/multiple_window_only_update.pb.sha256
new file mode 100644
index 0000000..939c986
--- /dev/null
+++ b/test/data/android_desktop_mode/multiple_window_only_update.pb.sha256
@@ -0,0 +1 @@
+de2c99d57ebec6ab843bc56047eb12dbedc3d08a8a1d989d9b3302ed30057286
\ No newline at end of file
diff --git a/test/data/android_desktop_mode/multiple_windows_add_update_remove.pb.sha256 b/test/data/android_desktop_mode/multiple_windows_add_update_remove.pb.sha256
new file mode 100644
index 0000000..5866d07
--- /dev/null
+++ b/test/data/android_desktop_mode/multiple_windows_add_update_remove.pb.sha256
@@ -0,0 +1 @@
+0967fdf494167fe47e5ddf4c540fcc7cc0b22e36ddafa4b4c1172fe3c171b3d6
\ No newline at end of file
diff --git a/test/data/android_desktop_mode/session_with_same_instance_id.pb.sha256 b/test/data/android_desktop_mode/session_with_same_instance_id.pb.sha256
new file mode 100644
index 0000000..a24473b
--- /dev/null
+++ b/test/data/android_desktop_mode/session_with_same_instance_id.pb.sha256
@@ -0,0 +1 @@
+bc81316b9dbfefa7a998f94750594c1eb7b07e070006f335b80815349f09ad96
\ No newline at end of file
diff --git a/test/data/android_desktop_mode/single_window_add_update_no_remove.pb.sha256 b/test/data/android_desktop_mode/single_window_add_update_no_remove.pb.sha256
new file mode 100644
index 0000000..f4656e4
--- /dev/null
+++ b/test/data/android_desktop_mode/single_window_add_update_no_remove.pb.sha256
@@ -0,0 +1 @@
+0bb09b4c68a125a09b6856879af92cd47dd980c2dcaa1513c370b9b6a7b575d1
\ No newline at end of file
diff --git a/test/data/android_desktop_mode/single_window_add_update_remove.pb.sha256 b/test/data/android_desktop_mode/single_window_add_update_remove.pb.sha256
new file mode 100644
index 0000000..3496183
--- /dev/null
+++ b/test/data/android_desktop_mode/single_window_add_update_remove.pb.sha256
@@ -0,0 +1 @@
+bab0c50523ac903872cd11f3fdd4a9ecda98e937573d17011eb877636e82c3c3
\ No newline at end of file
diff --git a/test/data/android_desktop_mode/single_window_no_add_update_remove.pb.sha256 b/test/data/android_desktop_mode/single_window_no_add_update_remove.pb.sha256
new file mode 100644
index 0000000..3c01fec
--- /dev/null
+++ b/test/data/android_desktop_mode/single_window_no_add_update_remove.pb.sha256
@@ -0,0 +1 @@
+7f63f17b65a0fca4bc5ccd75c9a7eb854779585b20cb91fcf371e7892642327e
\ No newline at end of file
diff --git a/test/data/android_desktop_mode/single_window_only_update.pb.sha256 b/test/data/android_desktop_mode/single_window_only_update.pb.sha256
new file mode 100644
index 0000000..20f7be3
--- /dev/null
+++ b/test/data/android_desktop_mode/single_window_only_update.pb.sha256
@@ -0,0 +1 @@
+ee1172b8ad0eaf856e2776575b861f44663bb98f1d40d7d4e66ee0f532220568
\ No newline at end of file
diff --git a/test/data/android_job_scheduler.perfetto-trace.sha256 b/test/data/android_job_scheduler.perfetto-trace.sha256
new file mode 100644
index 0000000..f3779ec
--- /dev/null
+++ b/test/data/android_job_scheduler.perfetto-trace.sha256
@@ -0,0 +1 @@
+9941708f26c9dddfb250f8fdbef94a160302e3020c677f86287a3c061a430738
\ No newline at end of file
diff --git a/test/data/art-method-tracing-streaming.trace.sha256 b/test/data/art-method-tracing-streaming.trace.sha256
new file mode 100644
index 0000000..11425d6
--- /dev/null
+++ b/test/data/art-method-tracing-streaming.trace.sha256
@@ -0,0 +1 @@
+8444001ac344e3f2da2def058e4c6622db424b377a456237d94e47dc85321d7f
\ No newline at end of file
diff --git a/test/data/art-method-tracing.trace.sha256 b/test/data/art-method-tracing.trace.sha256
new file mode 100644
index 0000000..38910a6
--- /dev/null
+++ b/test/data/art-method-tracing.trace.sha256
@@ -0,0 +1 @@
+ebd46d41eaa4656ad06535dacc1d3c6f6018a180f89c546515fed4f7e1df2337
\ No newline at end of file
diff --git a/test/data/chrome/scroll_m131.pftrace.sha256 b/test/data/chrome/scroll_m131.pftrace.sha256
new file mode 100644
index 0000000..38b6159
--- /dev/null
+++ b/test/data/chrome/scroll_m131.pftrace.sha256
@@ -0,0 +1 @@
+14171c9e502a65a454f39fe14fce8b313c7012a2c14394bed496fc93b1644b0d
\ No newline at end of file
diff --git a/test/data/instruments_trace.xml.sha256 b/test/data/instruments_trace.xml.sha256
new file mode 100644
index 0000000..f524f24
--- /dev/null
+++ b/test/data/instruments_trace.xml.sha256
@@ -0,0 +1 @@
+1f87b2e3f5617f947c3c22fe2282e3341ed4d3b4f37ad2e6752c6dd54836db8a
\ No newline at end of file
diff --git a/test/data/instruments_trace_symbols.pb.sha256 b/test/data/instruments_trace_symbols.pb.sha256
new file mode 100644
index 0000000..8e33c19
--- /dev/null
+++ b/test/data/instruments_trace_symbols.pb.sha256
@@ -0,0 +1 @@
+1f5096a97bd9b7176b1ec52863d02e3e85e19eedbfbbe51cbbecedce8a0248cb
\ No newline at end of file
diff --git a/test/data/instruments_trace_with_symbols.zip.sha256 b/test/data/instruments_trace_with_symbols.zip.sha256
new file mode 100644
index 0000000..adbf7aa
--- /dev/null
+++ b/test/data/instruments_trace_with_symbols.zip.sha256
@@ -0,0 +1 @@
+70733124cf53b8065512204d9a72c7818ebd04afb10cf3c69cc926a2aa5ee07e
\ No newline at end of file
diff --git a/test/data/perf_track_sym.tar.gz.sha256 b/test/data/perf_track_sym.tar.gz.sha256
new file mode 100644
index 0000000..edccf35
--- /dev/null
+++ b/test/data/perf_track_sym.tar.gz.sha256
@@ -0,0 +1 @@
+358db7f9628a9bb79d2dffa274c73c2894c3c7108b67bfb9f513bf4bac34acd6
\ No newline at end of file
diff --git a/test/data/sfgate-gzip-multi-stream.json.gz.sha256 b/test/data/sfgate-gzip-multi-stream.json.gz.sha256
new file mode 100644
index 0000000..eca4350
--- /dev/null
+++ b/test/data/sfgate-gzip-multi-stream.json.gz.sha256
@@ -0,0 +1 @@
+c46a162875fd826893daec6f1271f8d98710d7ae9a096c108c4a0635b06f14ac
\ No newline at end of file
diff --git a/test/data/simpleperf/etm.perf.data.zip.sha256 b/test/data/simpleperf/etm.perf.data.zip.sha256
new file mode 100644
index 0000000..7f1353f
--- /dev/null
+++ b/test/data/simpleperf/etm.perf.data.zip.sha256
@@ -0,0 +1 @@
+d199330364a2df97bc1a5aa9bd14d27070943ce2b11980d19b9d903625efb15f
\ No newline at end of file
diff --git a/test/data/simpleperf/spe.trace.zip.sha256 b/test/data/simpleperf/spe.trace.zip.sha256
new file mode 100644
index 0000000..62d7c6d
--- /dev/null
+++ b/test/data/simpleperf/spe.trace.zip.sha256
@@ -0,0 +1 @@
+199e74a411f20e4670c9330e891b371f638bd8745a82dc22bb5f83c0ca8ba5ac
\ No newline at end of file
diff --git a/test/data/simpleperf_as_gecko.json.sha256 b/test/data/simpleperf_as_gecko.json.sha256
new file mode 100644
index 0000000..5162d27
--- /dev/null
+++ b/test/data/simpleperf_as_gecko.json.sha256
@@ -0,0 +1 @@
+225e86a03ada87e586c4cf053a7369516f8f124318495e2109022e72389d0b68
\ No newline at end of file
diff --git a/test/data/simpleperf_as_text.txt.sha256 b/test/data/simpleperf_as_text.txt.sha256
new file mode 100644
index 0000000..65c8bc3
--- /dev/null
+++ b/test/data/simpleperf_as_text.txt.sha256
@@ -0,0 +1 @@
+40d817e31b5dac6bd745cf2eddb1bc7dc4c78aa3a8c398e718dcb3b650975388
\ No newline at end of file
diff --git a/test/data/trace_processor_perf_as_gecko.json.sha256 b/test/data/trace_processor_perf_as_gecko.json.sha256
new file mode 100644
index 0000000..0341371
--- /dev/null
+++ b/test/data/trace_processor_perf_as_gecko.json.sha256
@@ -0,0 +1 @@
+8e2b7d825d190f12bd87fee90220ec2b91110c056ca9f55c5e00dd0d1f3455c5
\ No newline at end of file
diff --git a/test/data/trace_processor_perf_as_text.txt.sha256 b/test/data/trace_processor_perf_as_text.txt.sha256
new file mode 100644
index 0000000..2d5a681
--- /dev/null
+++ b/test/data/trace_processor_perf_as_text.txt.sha256
@@ -0,0 +1 @@
+a39b72bf2a6c9355c4dec142814874c7977b071fc4cc1f37bd78167efee12e14
\ No newline at end of file
diff --git a/test/data/track_event_ordered.pb.sha256 b/test/data/track_event_ordered.pb.sha256
new file mode 100644
index 0000000..645416e
--- /dev/null
+++ b/test/data/track_event_ordered.pb.sha256
@@ -0,0 +1 @@
+1bab9bdb32c88b22a0e8f09ddad84338c5c320e416429f92e6d7ff5a671eb06e
\ No newline at end of file
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
new file mode 100644
index 0000000..85d36de
--- /dev/null
+++ b/test/data/ui-screenshots/aggregation.test.ts/frametimeline/frame-timeline-aggregation.png.sha256
@@ -0,0 +1 @@
+9cf722b3752271f0e82ac12afb3bf682a9bdb665110df684a0b9879076415c6a
\ 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
new file mode 100644
index 0000000..b7aa6eb
--- /dev/null
+++ b/test/data/ui-screenshots/aggregation.test.ts/gpu-counter/gpu-counter-aggregation.png.sha256
@@ -0,0 +1 @@
+2a1be50f914dd081829704878691425954b63a60e3c33ca10c23b24c6e62af53
\ 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
new file mode 100644
index 0000000..b1c1f3e
--- /dev/null
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-process.png.sha256
@@ -0,0 +1 @@
+cdcb5c93e489619244b12f52003cfb57f705054180bcbd3729945b11010090ab
\ 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
new file mode 100644
index 0000000..287e4cc
--- /dev/null
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-thread.png.sha256
@@ -0,0 +1 @@
+312cf370818fc7d5b9359d0bebb9cde36f3897331f31933c09986f1f6f8d139b
\ 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
new file mode 100644
index 0000000..6bcd9f2
--- /dev/null
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-occurrences.png.sha256
@@ -0,0 +1 @@
+5c88453331ee4fd6af586ca1d170ea2ba2124a7b3a12662395d6360e94f064d8
\ 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
new file mode 100644
index 0000000..45ce9a3
--- /dev/null
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration-desc.png.sha256
@@ -0,0 +1 @@
+b462dfa84705f813ea4bee02097fd6444420c2dac7624d6f60c211fff251c85b
\ 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
new file mode 100644
index 0000000..fda23e2
--- /dev/null
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration.png.sha256
@@ -0,0 +1 @@
+cee7ff84c65e1363a61e7ecb38ff882392ee64e07965913ca8cf4cca779d876e
\ 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
new file mode 100644
index 0000000..8b08f2a
--- /dev/null
+++ b/test/data/ui-screenshots/aggregation.test.ts/slices/slice-aggregation.png.sha256
@@ -0,0 +1 @@
+d7ff2899d9756375f5b3c5d3c1a58f943742d613be96bef181be07bd10f61333
\ 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
new file mode 100644
index 0000000..b2f1c8a
--- /dev/null
+++ b/test/data/ui-screenshots/chrome_missing_track_names.test.ts/expand-all-tracks/all-tracks-expanded.png.sha256
@@ -0,0 +1 @@
+664dd536449ade2ba5667c670d43c13a0807394221503f1e19ec375f49c9c02e
\ 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
new file mode 100644
index 0000000..f33f150
--- /dev/null
+++ b/test/data/ui-screenshots/chrome_missing_track_names.test.ts/trace-loaded/trace-loaded.png.sha256
@@ -0,0 +1 @@
+b584a88e648c34d8a154d84aa7e9c7a829eb93df98d006435493d47b35520c0d
\ 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
new file mode 100644
index 0000000..5462947
--- /dev/null
+++ b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/expand-browser/browser-expanded.png.sha256
@@ -0,0 +1 @@
+fdbe33426328cca5b441bd252e35b7364418bcf40f68f1f653f706fd7b23a547
\ 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
new file mode 100644
index 0000000..5e2a9ef
--- /dev/null
+++ b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/load-trace/loaded.png.sha256
@@ -0,0 +1 @@
+2bda98865dbd14472a07d68093d75efcb2c1d240d96ecf809614315f18b91767
\ 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
new file mode 100644
index 0000000..02b96b1
--- /dev/null
+++ b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/slice-with-flows/slice-with-flows.png.sha256
@@ -0,0 +1 @@
+fb7aea9a2f5fa70103eb9bbad432dd7bff64cb6604c19b9ab199ca507f8bb336
\ 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
new file mode 100644
index 0000000..8eb007d
--- /dev/null
+++ b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks-pivot/debug-track-pivot.png.sha256
@@ -0,0 +1 @@
+d06cd9a16a7aefdd0c9254f56444b80056920d0e6ea0b14b1919deab6103eb3f
\ 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
new file mode 100644
index 0000000..f18a143
--- /dev/null
+++ b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-slice-clicked.png.sha256
@@ -0,0 +1 @@
+f89e12d7bc8ba6c45d35be4c157bff221e9659465e2106dc22c9960cfd6b6de8
\ 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
new file mode 100644
index 0000000..86980b4
--- /dev/null
+++ b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-added.png.sha256
@@ -0,0 +1 @@
+882aff2a3750f56e47cda9adacb2ca71be837f23ccd54ae2cf32eefdd65b2618
\ 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
new file mode 100644
index 0000000..e5bab38
--- /dev/null
+++ b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-removed.png.sha256
@@ -0,0 +1 @@
+fda4f777d5a613c3c7622f5825031610441bf924af8c7920bf63840526a87819
\ 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
new file mode 100644
index 0000000..83fd6cc
--- /dev/null
+++ b/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tab/ftrace-tab.png.sha256
@@ -0,0 +1 @@
+bb7fb0c07b33c4c08039436d33519f9dee5c4baf98f1067e6926a9f9875edeaf
\ 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
new file mode 100644
index 0000000..b97e598
--- /dev/null
+++ b/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256
@@ -0,0 +1 @@
+ef239fe1e10e3b830f7adf9b5b8f96a52c69a50e6d879cfb5a873cd26caab769
\ 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
new file mode 100644
index 0000000..98255ae
--- /dev/null
+++ b/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip-expanded.png.sha256
@@ -0,0 +1 @@
+526c3bce9a8622bc5ac983a88bec6fb41194c7f84dd7b740468bd275be13ee8e
\ 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
new file mode 100644
index 0000000..eff6f6b
--- /dev/null
+++ b/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip.png.sha256
@@ -0,0 +1 @@
+67240fa5e871e2e8cb9369c06a27dad3a3754bc4628d8d6060ffd6711ab3a012
\ No newline at end of file
diff --git a/test/data/ui-screenshots/independent_features.test.ts/trace-error-notification/error-icon.png.sha256 b/test/data/ui-screenshots/independent_features.test.ts/trace-error-notification/error-icon.png.sha256
new file mode 100644
index 0000000..7a64aaf
--- /dev/null
+++ b/test/data/ui-screenshots/independent_features.test.ts/trace-error-notification/error-icon.png.sha256
@@ -0,0 +1 @@
+d11a2d89b1d96ede01644accc98cee5ac6fadb15a13d40baa78977fd5c212670
\ 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
new file mode 100644
index 0000000..732451a
--- /dev/null
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/info-and-stats/back-to-timeline.png.sha256
@@ -0,0 +1 @@
+6114bce1d944eb9308105215a845a6b0e01830fc3bf1b0cd0029eb0b6bf7a586
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/info-and-stats/into-and-stats.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/info-and-stats/into-and-stats.png.sha256
new file mode 100644
index 0000000..928eb48
--- /dev/null
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/info-and-stats/into-and-stats.png.sha256
@@ -0,0 +1 @@
+821c18e11f1a721dc5a72c1cf8060526dd4aeff95fe3ec6b5becb601beec00cd
\ 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
new file mode 100644
index 0000000..732451a
--- /dev/null
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/load-trace/loaded.png.sha256
@@ -0,0 +1 @@
+6114bce1d944eb9308105215a845a6b0e01830fc3bf1b0cd0029eb0b6bf7a586
\ 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
new file mode 100644
index 0000000..91ad70c
--- /dev/null
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-0.png.sha256
@@ -0,0 +1 @@
+45ae6941d53babfaf49915bc0cf9f46900385fd4fd7466e0fb48be8848fb9332
\ 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
new file mode 100644
index 0000000..7264b55
--- /dev/null
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-1.png.sha256
@@ -0,0 +1 @@
+ed546d5013d0f9d536248df2ac7df9d4a02c318265a3f120d7d0de0960615bf9
\ 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
new file mode 100644
index 0000000..47f9a48
--- /dev/null
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-2.png.sha256
@@ -0,0 +1 @@
+f0a2d7936b06f27ed78c7c3cbd0323de50e3d666d52f11f161c60cfefb397b1a
\ 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
new file mode 100644
index 0000000..058e1df
--- /dev/null
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-3.png.sha256
@@ -0,0 +1 @@
+2a38de005da72784917a8a1fbeb60d7237fc71cd62615afc57642234a63f6165
\ 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
new file mode 100644
index 0000000..7990d70
--- /dev/null
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/process-details.png.sha256
@@ -0,0 +1 @@
+f974f09cd7010de66ea75aae2b0deb6e69fcf5ff1f6a6747572aaac2898e8fb9
\ 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
new file mode 100644
index 0000000..0012208
--- /dev/null
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/search-slice.png.sha256
@@ -0,0 +1 @@
+3ba015fed84e47ef9ba11967d3745036a5e3a80f76d47d2e3a8a064b136e31ef
\ 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
new file mode 100644
index 0000000..b683281
--- /dev/null
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/one-track-pinned.png.sha256
@@ -0,0 +1 @@
+df9c6cea86b96bed3d607a1c1253628fbb999913e962443ac04aaed0e004eb14
\ 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
new file mode 100644
index 0000000..958fc22
--- /dev/null
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/two-tracks-pinned.png.sha256
@@ -0,0 +1 @@
+1f8e548dbb67ab743c2d7a0812e2b9019096043f596c83d6c0e936072c98c9a0
\ 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
new file mode 100644
index 0000000..d2c55f0
--- /dev/null
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-compressed.png.sha256
@@ -0,0 +1 @@
+007dfdd647fb12be40704f8aef1cc2e6188fdd16912dd6b2a8bc5c23f2cd9c8f
\ 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
new file mode 100644
index 0000000..d2c55f0
--- /dev/null
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-expanded.png.sha256
@@ -0,0 +1 @@
+007dfdd647fb12be40704f8aef1cc2e6188fdd16912dd6b2a8bc5c23f2cd9c8f
\ 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
new file mode 100644
index 0000000..5ae135f
--- /dev/null
+++ b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/back-to-trace-1.png.sha256
@@ -0,0 +1 @@
+6e04f747447d8c57a54c7317d2028141a8d4f5651c75d348e4e943ca6a3263c8
\ 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
new file mode 100644
index 0000000..7b10b88
--- /dev/null
+++ b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/confirmation-dialog.png.sha256
@@ -0,0 +1 @@
+d00d65fb1e0f4bb661a71e7e3cb7ae07ceeb1bc5925d49632fa932dd33fe9813
\ 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
new file mode 100644
index 0000000..79e1c86
--- /dev/null
+++ b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-1.png.sha256
@@ -0,0 +1 @@
+e9c26966c58a1bc50e5f6fd4ab8b906499b6b1e799a3ccf66d27afae5805ee75
\ 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
new file mode 100644
index 0000000..9ae5378
--- /dev/null
+++ b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-2.png.sha256
@@ -0,0 +1 @@
+9b37991687fb75c5aa52896fde047b45298c2ae2c5e179906d1647f1091772c2
\ 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
new file mode 100644
index 0000000..20bcf2f
--- /dev/null
+++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/omnibox-cleared.png.sha256
@@ -0,0 +1 @@
+21cd6ce57f0a7bbb3e1c683b6e3ef965b70eef223c83675a1908c78610d8e1b4
\ 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
new file mode 100644
index 0000000..dec1614
--- /dev/null
+++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png.sha256
@@ -0,0 +1 @@
+5cde26dc18d32e38af60db7b82608bf97b64a8772f74d8e2d6a8bad374f58630
\ 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
new file mode 100644
index 0000000..59bc15d
--- /dev/null
+++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-1-clicked.png.sha256
@@ -0,0 +1 @@
+cc3eda4a58d455908eaa865ebe64128f5fe61d7c4f88cfb803ec92db24dcc55a
\ 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
new file mode 100644
index 0000000..3dbf2fa
--- /dev/null
+++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-2-clicked.png.sha256
@@ -0,0 +1 @@
+e4bb9566ee1ff007ce17bdbe29f4d3ca946fcecf65eb60fddf206664a1d48bb6
\ No newline at end of file
diff --git a/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-1.png.sha256 b/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-1.png.sha256
new file mode 100644
index 0000000..76a97ba
--- /dev/null
+++ b/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-1.png.sha256
@@ -0,0 +1 @@
+aaf361593ca634a7dc9beaa578844ca8118482cb2553ae4308a36a045add60b0
\ No newline at end of file
diff --git a/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-2.png.sha256 b/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-2.png.sha256
new file mode 100644
index 0000000..5090443
--- /dev/null
+++ b/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-2.png.sha256
@@ -0,0 +1 @@
+8344e56ced8e7d23ec5f17fa0ee17a429fd29377ae953c374eb5afdeb02ba704
\ No newline at end of file
diff --git a/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-3.png.sha256 b/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-3.png.sha256
new file mode 100644
index 0000000..184c548
--- /dev/null
+++ b/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-3.png.sha256
@@ -0,0 +1 @@
+9667eb3714b75569d5055237e302f6053468e52242ac89d9fd01dbc2157b7bf8
\ 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
new file mode 100644
index 0000000..5326122
--- /dev/null
+++ b/test/data/ui-screenshots/sql_table_tab.test.ts/ShowTable-command/slices-table.png.sha256
@@ -0,0 +1 @@
+2e8ea1113b2bfa09408c06c34cabec429ee7ca7d83fa2db8d2d913ef34292077
\ 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
new file mode 100644
index 0000000..e3b1a1a
--- /dev/null
+++ b/test/data/ui-screenshots/sql_table_tab.test.ts/slices-with-same-name/slices-with-same-name.png.sha256
@@ -0,0 +1 @@
+ef48a99b241e3bf2c56764c43d345cbf3e5a95e16cdc2bed89ee8605ed97b769
\ 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
new file mode 100644
index 0000000..aaa8d26
--- /dev/null
+++ b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/chronological-order/chronological.png.sha256
@@ -0,0 +1 @@
+11a59e122d629a1e21852d46bba18fd97ae5753d4ae4249c3559ff164fcd355c
\ 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
new file mode 100644
index 0000000..6b03833
--- /dev/null
+++ b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/explicit-order/explicit.png.sha256
@@ -0,0 +1 @@
+2eed4857125ffea15828a71637c42f279f80b662f357c92c353d98e1a668fbe7
\ 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
new file mode 100644
index 0000000..5f58f66
--- /dev/null
+++ b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/lexicographic-tracks/lexicographic.png.sha256
@@ -0,0 +1 @@
+788e540b171775317df23c1def6d7adac15adff8acbc1072d29b4e649e25af97
\ 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
new file mode 100644
index 0000000..03c2394
--- /dev/null
+++ b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/load-trace/loaded.png.sha256
@@ -0,0 +1 @@
+e8b5caa9b53e81ec5c5c9ab892c2326172ba56b91d4c1421a17e971341704255
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
deleted file mode 100644
index 3493f58..0000000
--- a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-be3adb9c7cdf3d54fff83980689419a75e0ac3a6fa7e172cc9195daad5072f8f
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
deleted file mode 100644
index 9175c32..0000000
--- a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-f7ef3a2bb325027af248129dda8a50363859c788084998807d871999929b175d
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_search.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_search.png.sha256
deleted file mode 100644
index fae0e8f..0000000
--- a/test/data/ui-screenshots/ui-android_trace_30s_search.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-540bf6e6be871ea65a77e6823e5a2f1c33b190fa86220749a487a3e96e08c21a
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256 b/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256
deleted file mode 100644
index 5000b5a..0000000
--- a/test/data/ui-screenshots/ui-chrome_missing_track_names_load.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-88e38270bc539e55cb7530ce6ff850536b18a91843544aa8711f2714477e4116
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_area_selection.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_area_selection.png.sha256
deleted file mode 100644
index 6b5d5d7..0000000
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_area_selection.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-8713b7474180ec39428930c6f11c156aaff4540da6725b37fc2466e8bbf91493
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
deleted file mode 100644
index dae4ffc..0000000
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-20b88ef93e7aac1f06f782afab95927f3bbc9812251f173efd4e3555c0aaa3b8
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
deleted file mode 100644
index 31dc264..0000000
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_load.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-a2e7b0c84668728f88924c15161ac64197cc35ba3017c8464774751c326e05d7
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
deleted file mode 100644
index 5e02d8e..0000000
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_select_slice_with_flows.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-9e7e068fe412696696c601d610dd15ca31d23d213223595b87951ee566479416
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-features_track_debuggable_chip.png.sha256 b/test/data/ui-screenshots/ui-features_track_debuggable_chip.png.sha256
deleted file mode 100644
index d23f8c0..0000000
--- a/test/data/ui-screenshots/ui-features_track_debuggable_chip.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-5a6907c20c525bf626a4c89bad070dd7f1bb22e90d16183d012ef3bd9cd01384
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256
deleted file mode 100644
index 1fc1da5..0000000
--- a/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-39356631a4f52af86bd9b10e46836bec70679b2ab67f230261ba3dde6144adaf
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256
deleted file mode 100644
index 1981d8f..0000000
--- a/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-f7486efa6610c3ea280b1e097e5fafbc5f4cc72b1a53a2eec872555e133c38a0
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
deleted file mode 100644
index 4a7ccd0..0000000
--- a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-07b0221b35ae6ea9e911040f853e72d19339eec92fd2c439515743ce84dfe985
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
deleted file mode 100644
index 1677d33..0000000
--- a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-1d86da6fc4d65abe02d0abb376161b7d97c63b0bf7e383353667b84c9f6fcf49
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256
deleted file mode 100644
index 3065302..0000000
--- a/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-1a80c1925b89e34c13ac276dbb112fcce892243c0e8351790ce481bb8f024e1b
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256 b/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256
deleted file mode 100644
index f07a6d2..0000000
--- a/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-97e8c64b23fa8458e6e420b1de47263789c16eec774f76eadf5466718ebc3c56
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
deleted file mode 100644
index 71bff80..0000000
--- a/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-39eb008fcb8f59b2d56abbc0ce255acad3e9fd3bcd3ba7ee6ba521ae21384465
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256 b/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256
deleted file mode 100644
index 2fc2741..0000000
--- a/test/data/ui-screenshots/ui-routing_open_invalid_trace_from_blank_page.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-bd2829967465c023c8238753af48fc2b686f6c028692f8da224838e3a914caee
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256 b/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256
deleted file mode 100644
index f78c98b..0000000
--- a/test/data/ui-screenshots/ui-routing_open_trace_and_go_back_to_landing_page.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-dc9d2ff90d480e7b92434d8f45351f238ff21b5ee4b697f2593a863187487b7f
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256
deleted file mode 100644
index 67a54ac..0000000
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_access_subpage_then_go_back.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-51c88fda41a2241872a4698586d43f82fb71deef9bcd51a08f17113948d8659f
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
deleted file mode 100644
index 71bff80..0000000
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-39eb008fcb8f59b2d56abbc0ce255acad3e9fd3bcd3ba7ee6ba521ae21384465
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256
deleted file mode 100644
index 67a54ac..0000000
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_second_trace_from_url.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-51c88fda41a2241872a4698586d43f82fb71deef9bcd51a08f17113948d8659f
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_trace_from_url.png.sha256
deleted file mode 100644
index c2bdfba..0000000
--- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_trace_from_url.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-1394a4ff204fada30758050221f4c201857e201178131e0f7741a62492d48814
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256
deleted file mode 100644
index 67a54ac..0000000
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_back_to_first_trace.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-51c88fda41a2241872a4698586d43f82fb71deef9bcd51a08f17113948d8659f
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_to_page_with_no_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_to_page_with_no_trace.png.sha256
deleted file mode 100644
index b925464..0000000
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_go_to_page_with_no_trace.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-52a03a10b8a051ae72877ce9c2abab155382a71ca3e406773e212851e3ff1867
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256
deleted file mode 100644
index 6491e68..0000000
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_invalid_trace.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-08722fcfa246e0cba21e8d20a962aa67b5b038d6beb48eefe53d6a8a37decfa7
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
deleted file mode 100644
index 71bff80..0000000
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-39eb008fcb8f59b2d56abbc0ce255acad3e9fd3bcd3ba7ee6ba521ae21384465
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256
deleted file mode 100644
index 67a54ac..0000000
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_trace_.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-51c88fda41a2241872a4698586d43f82fb71deef9bcd51a08f17113948d8659f
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256
deleted file mode 100644
index 67a54ac..0000000
--- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_refresh.png.sha256
+++ /dev/null
@@ -1 +0,0 @@
-51c88fda41a2241872a4698586d43f82fb71deef9bcd51a08f17113948d8659f
\ 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
new file mode 100644
index 0000000..86f06ee
--- /dev/null
+++ b/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-process.png.sha256
@@ -0,0 +1 @@
+aef36afce71de9eaa3fb92d36c2dc37dbfdfc82d9b25717f0b80c11db35d7caa
\ 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
new file mode 100644
index 0000000..d51ed7f
--- /dev/null
+++ b/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-thread.png.sha256
@@ -0,0 +1 @@
+e0ec62b98f6c2a4d1781ab4b0913f9d11c24b1c5d44b03010fcd178aeda06c3d
\ 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
new file mode 100644
index 0000000..8121185
--- /dev/null
+++ b/test/data/ui-screenshots/wattson.test.ts/wattson-aggregations/wattson-estimate-aggr.png.sha256
@@ -0,0 +1 @@
+1cacd75a7bc38896b5609ef77666345242ecd1efb692760f891061bd8ae0d86b
\ No newline at end of file
diff --git a/test/data/v8-samples.json.sha256 b/test/data/v8-samples.json.sha256
new file mode 100644
index 0000000..58baadc
--- /dev/null
+++ b/test/data/v8-samples.json.sha256
@@ -0,0 +1 @@
+b0714096a2112ed161bda0aa4bbd6ab80f0ac195c72ff2be1a1a57272fd14735
\ No newline at end of file
diff --git a/test/data/v8-samples.pftrace.sha256 b/test/data/v8-samples.pftrace.sha256
new file mode 100644
index 0000000..c9a7d2f
--- /dev/null
+++ b/test/data/v8-samples.pftrace.sha256
@@ -0,0 +1 @@
+564159912db8d8252562f143e6206ae9964759e16193ebd3addd04823d02a6ae
\ No newline at end of file
diff --git a/test/data/wattson_syscore_suspend.pb.sha256 b/test/data/wattson_syscore_suspend.pb.sha256
new file mode 100644
index 0000000..6a6d34e
--- /dev/null
+++ b/test/data/wattson_syscore_suspend.pb.sha256
@@ -0,0 +1 @@
+611b1824b4c5a26c7cf9e0bf06c4af32a5e30149ecd07ca2cd10160a0872108d
\ No newline at end of file
diff --git a/test/data/wattson_tk4_pcmark.pb.sha256 b/test/data/wattson_tk4_pcmark.pb.sha256
new file mode 100644
index 0000000..c39092d
--- /dev/null
+++ b/test/data/wattson_tk4_pcmark.pb.sha256
@@ -0,0 +1 @@
+9f807b520842034eda96b81ef6e6be6d2b395e83d08a3162ad0df2956ca9f947
\ No newline at end of file
diff --git a/test/data/wattson_w_packages_Imarkers.pb.sha256 b/test/data/wattson_w_packages_Imarkers.pb.sha256
new file mode 100644
index 0000000..50a52a5
--- /dev/null
+++ b/test/data/wattson_w_packages_Imarkers.pb.sha256
@@ -0,0 +1 @@
+f3168e8c024ef26103a361e26f9cacadabd6e64047ab26e8042d119f93031628
\ No newline at end of file
diff --git a/test/integrationtest_initializer.h b/test/integrationtest_initializer.h
new file mode 100644
index 0000000..5aea19c
--- /dev/null
+++ b/test/integrationtest_initializer.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef TEST_INTEGRATIONTEST_INITIALIZER_H_
+#define TEST_INTEGRATIONTEST_INITIALIZER_H_
+
+namespace perfetto::integration_tests {
+
+// Simple mechanism to execute code at the beginning of the integrationtest
+// main() before the gtest tests are run.
+//
+// Usage
+// ```
+// int PERFETTO_UNUSED initializer =
+//     integration_tests::Register...Initializer(
+//         &InitializerFunction);
+// ```
+//
+// This is probably more verbose than required to keep the implementation
+// straightforward and avoid as much as possible all the pitfalls of static
+// initialization order.
+
+// Implemented in integrationtest_main.cc
+
+int RegisterHeapprofdEndToEndTestInitializer(void (*fn)(void));
+int RegisterApiIntegrationTestInitializer(void (*fn)(void));
+
+}  // namespace perfetto::integration_tests
+
+#endif  // TEST_INTEGRATIONTEST_INITIALIZER_H_
diff --git a/test/integrationtest_main.cc b/test/integrationtest_main.cc
new file mode 100644
index 0000000..afe998d
--- /dev/null
+++ b/test/integrationtest_main.cc
@@ -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.
+ */
+
+#include "test/integrationtest_initializer.h"
+
+#include "perfetto/base/logging.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto::integration_tests {
+
+static void (*heapprofd_end_to_end_test_initializer)(void) = nullptr;
+int RegisterHeapprofdEndToEndTestInitializer(void (*fn)(void)) {
+  PERFETTO_CHECK(heapprofd_end_to_end_test_initializer == nullptr);
+  heapprofd_end_to_end_test_initializer = fn;
+  return 0;
+}
+
+static void (*api_integration_test_initializer)(void) = nullptr;
+int RegisterApiIntegrationTestInitializer(void (*fn)(void)) {
+  PERFETTO_CHECK(api_integration_test_initializer == nullptr);
+  api_integration_test_initializer = fn;
+  return 0;
+}
+
+}  // namespace perfetto::integration_tests
+
+int main(int argc, char** argv) {
+  if (perfetto::integration_tests::heapprofd_end_to_end_test_initializer) {
+    perfetto::integration_tests::heapprofd_end_to_end_test_initializer();
+  }
+  if (perfetto::integration_tests::api_integration_test_initializer) {
+    perfetto::integration_tests::api_integration_test_initializer();
+  }
+
+  ::testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}
diff --git a/test/synth_common.py b/test/synth_common.py
index a65af74..c0355c3 100644
--- a/test/synth_common.py
+++ b/test/synth_common.py
@@ -215,8 +215,8 @@
       thread.name = name
     self.proc_map[tid] = cmdline
 
-  def add_battery_counters(self, ts, charge_uah, cap_prct, curr_ua,
-                           curr_avg_ua):
+  def add_battery_counters(self, ts, charge_uah, cap_prct, curr_ua, curr_avg_ua,
+                           voltage_uv):
     self.packet = self.trace.packet.add()
     self.packet.timestamp = ts
     battery_count = self.packet.battery
@@ -224,6 +224,7 @@
     battery_count.capacity_percent = cap_prct
     battery_count.current_ua = curr_ua
     battery_count.current_avg_ua = curr_avg_ua
+    battery_count.voltage_uv = voltage_uv
 
   def add_binder_transaction(self, transaction_id, ts_start, ts_end, tid, pid,
                              reply_id, reply_ts_start, reply_ts_end, reply_tid,
@@ -255,13 +256,14 @@
     reply_binder_transaction_received.debug_id = reply_id
 
   def add_battery_counters_no_curr_ua(self, ts, charge_uah, cap_prct,
-                                      curr_avg_ua):
+                                      curr_avg_ua, voltage_uv):
     self.packet = self.trace.packet.add()
     self.packet.timestamp = ts
     battery_count = self.packet.battery
     battery_count.charge_counter_uah = charge_uah
     battery_count.capacity_percent = cap_prct
     battery_count.current_avg_ua = curr_avg_ua
+    battery_count.voltage_uv = voltage_uv
 
   def add_power_rails_desc(self, index_val, name):
     power_rails = self.packet.power_rails
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index 5840800..69c1bc7 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -60,6 +60,7 @@
 from diff_tests.parser.android.tests_surfaceflinger_transactions import SurfaceFlingerTransactions
 from diff_tests.parser.android.tests_viewcapture import ViewCapture
 from diff_tests.parser.android.tests_windowmanager import WindowManager
+from diff_tests.parser.art_method.tests import ArtMethodParser
 from diff_tests.parser.atrace.tests import Atrace
 from diff_tests.parser.atrace.tests_error_handling import AtraceErrorHandling
 from diff_tests.parser.chrome.tests import ChromeParser
@@ -68,10 +69,14 @@
 from diff_tests.parser.cros.tests import Cros
 from diff_tests.parser.fs.tests import Fs
 from diff_tests.parser.ftrace.ftrace_crop_tests import FtraceCrop
+from diff_tests.parser.ftrace.kprobes_tests import Kprobes
 from diff_tests.parser.fuchsia.tests import Fuchsia
+from diff_tests.parser.gecko.tests import GeckoParser
 from diff_tests.parser.graphics.tests import GraphicsParser
 from diff_tests.parser.graphics.tests_drm_related_ftrace_events import GraphicsDrmRelatedFtraceEvents
 from diff_tests.parser.graphics.tests_gpu_trace import GraphicsGpuTrace
+from diff_tests.parser.gzip.tests import Gzip
+from diff_tests.parser.instruments.tests import Instruments
 from diff_tests.parser.json.tests import JsonParser
 from diff_tests.parser.memory.tests import MemoryParser
 from diff_tests.parser.network.tests import NetworkParser
@@ -81,6 +86,7 @@
 from diff_tests.parser.parsing.tests_rss_stats import ParsingRssStats
 from diff_tests.parser.parsing.tests_sys_stats import ParsingSysStats
 from diff_tests.parser.parsing.tests_traced_stats import ParsingTracedStats
+from diff_tests.parser.perf_text.tests import PerfTextParser
 from diff_tests.parser.power.tests_energy_breakdown import PowerEnergyBreakdown
 from diff_tests.parser.power.tests_entity_state_residency import EntityStateResidency
 from diff_tests.parser.power.tests_linux_sysfs_power import LinuxSysfsPower
@@ -102,15 +108,15 @@
 from diff_tests.parser.ufs.tests import Ufs
 from diff_tests.parser.zip.tests import Zip
 from diff_tests.stdlib.android.cpu_cluster_tests import CpuClusters
+from diff_tests.stdlib.android.desktop_mode_tests import DesktopMode
 from diff_tests.stdlib.android.frames_tests import Frames
 from diff_tests.stdlib.android.gpu import AndroidGpu
 from diff_tests.stdlib.android.heap_graph_tests import HeapGraph
+from diff_tests.stdlib.android.heap_profile_tests import HeapProfile
 from diff_tests.stdlib.android.memory import AndroidMemory
 from diff_tests.stdlib.android.startups_tests import Startups
 from diff_tests.stdlib.android.tests import AndroidStdlib
 from diff_tests.stdlib.chrome.chrome_stdlib_testsuites import CHROME_STDLIB_TESTSUITES
-from diff_tests.stdlib.common.tests import StdlibCommon
-from diff_tests.stdlib.common.tests import StdlibCommon
 from diff_tests.stdlib.counters.tests import StdlibCounterIntervals
 from diff_tests.stdlib.dynamic_tables.tests import DynamicTables
 from diff_tests.stdlib.export.tests import ExportTests
@@ -124,8 +130,6 @@
 from diff_tests.stdlib.linux.cpu import LinuxCpu
 from diff_tests.stdlib.linux.memory import Memory
 from diff_tests.stdlib.linux.tests import LinuxTests
-from diff_tests.stdlib.metasql.column_list import ColumnListTests
-from diff_tests.stdlib.metasql.table_list import TableListTests
 from diff_tests.stdlib.pkvm.tests import Pkvm
 from diff_tests.stdlib.prelude.math_functions_tests import PreludeMathFunctions
 from diff_tests.stdlib.prelude.pprof_functions_tests import PreludePprofFunctions
@@ -139,6 +143,7 @@
 from diff_tests.stdlib.span_join.tests_smoke import SpanJoinSmoke
 from diff_tests.stdlib.tests import StdlibSmoke
 from diff_tests.stdlib.timestamps.tests import Timestamps
+from diff_tests.stdlib.viz.tests import Viz
 from diff_tests.stdlib.wattson.tests import WattsonStdlib
 from diff_tests.syntax.filtering_tests import PerfettoFiltering
 from diff_tests.syntax.function_tests import PerfettoFunction
@@ -233,11 +238,18 @@
       *ParsingMemoryCounters(index_path, 'parser/parsing',
                              'ParsingMemoryCounters').fetch(),
       *FtraceCrop(index_path, 'parser/ftrace', 'FtraceCrop').fetch(),
+      *Kprobes(index_path, 'parser/ftrace', 'Kprobes').fetch(),
       *ParsingTracedStats(index_path, 'parser/parsing',
                           'ParsingTracedStats').fetch(),
       *Zip(index_path, 'parser/zip', 'Zip').fetch(),
       *AndroidInputEvent(index_path, 'parser/android',
                          'AndroidInputEvent').fetch(),
+      *Instruments(index_path, 'parser/instruments', 'Instruments').fetch(),
+      *Gzip(index_path, 'parser/gzip', 'Gzip').fetch(),
+      *GeckoParser(index_path, 'parser/gecko', 'GeckoParser').fetch(),
+      *ArtMethodParser(index_path, 'parser/art_method',
+                       'ArtMethodParser').fetch(),
+      *PerfTextParser(index_path, 'parser/perf_text', 'PerfTextParser').fetch(),
   ]
 
   metrics_tests = [
@@ -284,6 +296,7 @@
       *AndroidGpu(index_path, 'stdlib/android', 'AndroidGpu').fetch(),
       *AndroidStdlib(index_path, 'stdlib/android', 'AndroidStdlib').fetch(),
       *CpuClusters(index_path, 'stdlib/android', 'CpuClusters').fetch(),
+      *DesktopMode(index_path, 'stdlib/android', 'DesktopMode').fetch(),
       *LinuxCpu(index_path, 'stdlib/linux/cpu', 'LinuxCpu').fetch(),
       *LinuxTests(index_path, 'stdlib/linux', 'LinuxTests').fetch(),
       *DominatorTree(index_path, 'stdlib/graphs', 'DominatorTree').fetch(),
@@ -299,10 +312,6 @@
                               'StdlibCounterIntervals').fetch(),
       *DynamicTables(index_path, 'stdlib/dynamic_tables',
                      'DynamicTables').fetch(),
-      *ColumnListTests(index_path, 'stdlib/column_list',
-                       'ColumnListTests').fetch(),
-      *TableListTests(index_path, 'stdlib/table_list',
-                      'TableListTests').fetch(),
       *Memory(index_path, 'stdlib/linux', 'Memory').fetch(),
       *PreludeMathFunctions(index_path, 'stdlib/prelude',
                             'PreludeMathFunctions').fetch(),
@@ -315,7 +324,6 @@
       *Pkvm(index_path, 'stdlib/pkvm', 'Pkvm').fetch(),
       *PreludeSlices(index_path, 'stdlib/prelude', 'PreludeSlices').fetch(),
       *StdlibSmoke(index_path, 'stdlib', 'StdlibSmoke').fetch(),
-      *StdlibCommon(index_path, 'stdlib/common', 'StdlibCommon').fetch(),
       *Slices(index_path, 'stdlib/slices', 'Slices').fetch(),
       *SpanJoinLeftJoin(index_path, 'stdlib/span_join',
                         'SpanJoinLeftJoin').fetch(),
@@ -324,14 +332,15 @@
       *SpanJoinRegression(index_path, 'stdlib/span_join',
                           'SpanJoinRegression').fetch(),
       *SpanJoinSmoke(index_path, 'stdlib/span_join', 'SpanJoinSmoke').fetch(),
-      *StdlibCommon(index_path, 'stdlib/common', 'StdlibCommon').fetch(),
       *StdlibIntervals(index_path, 'stdlib/intervals',
                        'StdlibIntervals').fetch(),
       *IntervalsIntersect(index_path, 'stdlib/intervals',
                           'StdlibIntervalsIntersect').fetch(),
       *Startups(index_path, 'stdlib/android', 'Startups').fetch(),
       *Timestamps(index_path, 'stdlib/timestamps', 'Timestamps').fetch(),
+      *Viz(index_path, 'stdlib/viz', 'Viz').fetch(),
       *WattsonStdlib(index_path, 'stdlib/wattson', 'WattsonStdlib').fetch(),
+      *HeapProfile(index_path, 'stdlib/android', 'HeapProfile').fetch(),
   ] + chrome_stdlib_tests
 
   syntax_tests = [
diff --git a/test/trace_processor/diff_tests/metrics/android/tests.py b/test/trace_processor/diff_tests/metrics/android/tests.py
index c9ebdab..5f24e2e 100644
--- a/test/trace_processor/diff_tests/metrics/android/tests.py
+++ b/test/trace_processor/diff_tests/metrics/android/tests.py
@@ -369,53 +369,67 @@
         query=Metric("android_broadcasts"),
         out=Path('android_broadcasts.out'))
 
-  def test_wattson_app_startup_output(self):
+  def test_wattson_app_startup_rails_output(self):
     return DiffTestBlueprint(
         trace=DataPath('android_calculator_startup.pb'),
-        query=Metric("wattson_app_startup"),
+        query=Metric("wattson_app_startup_rails"),
         out=Csv("""
-        wattson_app_startup {
-          metric_version: 2
+        wattson_app_startup_rails {
+          metric_version: 4
+          power_model_version: 1
           period_info {
             period_id: 1
-            period_dur: 384847394
+            period_dur: 384847255
             cpu_subsystem {
-              estimate_mw: 4567.958008
+              estimated_mw: 4568.1772
+              estimated_mws: 1758.050415
               policy0 {
-                estimate_mw: 578.353088
+                estimated_mw: 578.31256
+                estimated_mws: 222.561996
                 cpu0 {
-                  estimate_mw: 149.026062
+                  estimated_mw: 148.99423
+                  estimated_mws: 57.340019
                 }
                 cpu1 {
-                  estimate_mw: 130.140015
+                  estimated_mw: 130.13142
+                  estimated_mws: 50.080723
                 }
                 cpu2 {
-                  estimate_mw: 127.601807
+                  estimated_mw: 127.60357
+                  estimated_mws: 49.107883
                 }
                 cpu3 {
-                  estimate_mw: 171.585205
+                  estimated_mw: 171.58333
+                  estimated_mws: 66.033371
                 }
               }
               policy4 {
-                estimate_mw: 684.187256
+                estimated_mw: 684.18835
+                estimated_mws: 263.308014
                 cpu4 {
-                  estimate_mw: 344.394531
+                  estimated_mw: 344.39563
+                  estimated_mws: 132.539703
                 }
                 cpu5 {
-                  estimate_mw: 339.792725
+                  estimated_mw: 339.7927
+                  estimated_mws: 130.768295
                 }
               }
               policy6 {
-                estimate_mw: 2163.018066
+                estimated_mw: 2163.158
+                estimated_mws: 832.48541
                 cpu6 {
-                  estimate_mw: 1080.465820
+                  estimated_mw: 1080.6881
+                  estimated_mws: 415.89984
                 }
                 cpu7 {
-                  estimate_mw: 1082.552246
+                  estimated_mw: 1082.47
+                  estimated_mws: 416.585602
                 }
               }
               dsu_scu {
-                estimate_mw: 1142.399658
+                estimated_mw: 1142.5181
+                estimated_mws: 439.694946
               }
             }
           }
@@ -428,29 +442,37 @@
         query=Metric("wattson_trace_rails"),
         out=Csv("""
         wattson_trace_rails {
-          metric_version: 2
+          metric_version: 4
+          power_model_version: 1
           period_info {
             period_id: 1
-            period_dur: 61792614416
+            period_dur: 61792677852
             cpu_subsystem {
-              estimate_mw: 42.126297
+              estimated_mw: 42.123608
+              estimated_mws: 2602.930420
               policy0 {
-                estimate_mw: 34.721622
+                estimated_mw: 34.71892
+                estimated_mws: 2145.375244
                 cpu0 {
-                  estimate_mw: 10.706565
+                  estimated_mw: 10.705099
+                  estimated_mws: 661.496704
                 }
                 cpu1 {
-                  estimate_mw: 8.314949
+                  estimated_mw: 8.315703
+                  estimated_mws: 513.849548
                 }
                 cpu2 {
-                  estimate_mw: 7.7762628
+                  estimated_mw: 7.7776227
+                  estimated_mws: 480.600128
                 }
                 cpu3 {
-                  estimate_mw: 7.9238434
+                  estimated_mw: 7.9204974
+                  estimated_mws: 489.428741
                 }
               }
               dsu_scu {
-                estimate_mw: 7.404674
+                estimated_mw: 7.404684
+                estimated_mws: 457.555267
               }
             }
           }
@@ -468,3 +490,52 @@
         trace=DataPath('android_binder_metric_trace.atr'),
         query=Metric('android_anomaly'),
         out=Path('android_anomaly_metric.out'))
+
+  def test_wattson_markers_threads_output(self):
+    return DiffTestBlueprint(
+        trace=DataPath('wattson_w_packages_Imarkers.pb'),
+        query=Metric("wattson_markers_threads"),
+        out=Path('wattson_markers_threads.out'))
+
+  def test_wattson_markers_rails_output(self):
+    return DiffTestBlueprint(
+        trace=DataPath('wattson_w_packages_Imarkers.pb'),
+        query=Metric("wattson_markers_rails"),
+        out=Csv("""
+        wattson_markers_rails {
+          metric_version: 4
+          power_model_version: 1
+          period_info {
+            period_id: 1
+            period_dur: 2031871358
+            cpu_subsystem {
+              estimated_mw: 46.540943
+              estimated_mws: 94.565208
+              policy0 {
+                estimated_mw: 34.037483
+                estimated_mws: 69.159790
+                cpu0 {
+                  estimated_mw: 14.416655
+                  estimated_mws: 29.292788
+                }
+                cpu1 {
+                  estimated_mw: 6.641429
+                  estimated_mws: 13.494529
+                }
+                cpu2 {
+                  estimated_mw: 8.134797
+                  estimated_mws: 16.528862
+                }
+                cpu3 {
+                  estimated_mw: 4.8446035
+                  estimated_mws: 9.843612
+                }
+              }
+              dsu_scu {
+                estimated_mw: 12.503458
+                estimated_mws: 25.405418
+              }
+            }
+          }
+        }
+        """))
diff --git a/test/trace_processor/diff_tests/metrics/android/wattson_markers_threads.out b/test/trace_processor/diff_tests/metrics/android/wattson_markers_threads.out
new file mode 100644
index 0000000..4168034
--- /dev/null
+++ b/test/trace_processor/diff_tests/metrics/android/wattson_markers_threads.out
@@ -0,0 +1,722 @@
+wattson_markers_threads {
+  metric_version: 4
+  power_model_version: 1
+  period_info {
+    period_id: 1
+    task_info {
+      estimated_mws: 15.333553
+      estimated_mw: 7.546518
+      thread_name: "swapper"
+      thread_id: 0
+      process_id: 0
+    }
+    task_info {
+      estimated_mws: 11.805121
+      estimated_mw: 5.809974
+      idle_transitions_mws: 0.300579
+      thread_name: "RenderThread"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 3099
+      process_id: 2710
+    }
+    task_info {
+      estimated_mws: 9.112684
+      estimated_mw: 4.484872
+      idle_transitions_mws: 0.013483
+      thread_name: "binder:683_3"
+      process_name: "/vendor/bin/hw/vendor.qti.hardware.display.composer-service"
+      thread_id: 816
+      process_id: 683
+    }
+    task_info {
+      estimated_mws: 8.802570
+      estimated_mw: 4.332248
+      idle_transitions_mws: 0.223591
+      thread_name: "surfaceflinger"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 742
+      process_id: 742
+    }
+    task_info {
+      estimated_mws: 4.007993
+      estimated_mw: 1.972562
+      idle_transitions_mws: 0.449704
+      thread_name: ".wearable.sysui"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 2710
+      process_id: 2710
+    }
+    task_info {
+      estimated_mws: 1.779128
+      estimated_mw: 0.875610
+      idle_transitions_mws: 0.686579
+      thread_name: "crtc_commit:80"
+      process_name: "crtc_commit:80"
+      thread_id: 300
+      process_id: 300
+    }
+    task_info {
+      estimated_mws: 1.436499
+      estimated_mw: 0.706983
+      idle_transitions_mws: 0.113163
+      thread_name: "binder:2710_E"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 6515
+      process_id: 2710
+    }
+    task_info {
+      estimated_mws: 1.262685
+      estimated_mw: 0.621440
+      idle_transitions_mws: 0.337691
+      thread_name: "TimerDispatch"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 819
+      process_id: 742
+    }
+    task_info {
+      estimated_mws: 1.242906
+      estimated_mw: 0.611705
+      idle_transitions_mws: 0.318923
+      thread_name: "kworker/u8:4"
+      process_name: "kworker/u8:4"
+      thread_id: 11407
+      process_id: 11407
+    }
+    task_info {
+      estimated_mws: 1.231494
+      estimated_mw: 0.606089
+      idle_transitions_mws: 0.311600
+      thread_name: "BckgrndExec HP"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 837
+      process_id: 742
+    }
+    task_info {
+      estimated_mws: 1.194067
+      estimated_mw: 0.587669
+      idle_transitions_mws: 0.427458
+      thread_name: "kworker/u8:5"
+      process_name: "kworker/u8:5"
+      thread_id: 10610
+      process_id: 10610
+    }
+    task_info {
+      estimated_mws: 1.132809
+      estimated_mw: 0.557520
+      idle_transitions_mws: 0.095612
+      thread_name: "binder:742_2"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 791
+      process_id: 742
+    }
+    task_info {
+      estimated_mws: 1.065350
+      estimated_mw: 0.524320
+      idle_transitions_mws: 1.154817
+      thread_name: "rcu_preempt"
+      process_name: "rcu_preempt"
+      thread_id: 14
+      process_id: 14
+    }
+    task_info {
+      estimated_mws: 0.872391
+      estimated_mw: 0.429353
+      idle_transitions_mws: 0.072071
+      thread_name: "binder:2710_7"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 5691
+      process_id: 2710
+    }
+    task_info {
+      estimated_mws: 0.865098
+      estimated_mw: 0.425764
+      idle_transitions_mws: 0.010689
+      thread_name: "traced_probes"
+      process_name: "/system/bin/traced_probes"
+      thread_id: 916
+      process_id: 916
+    }
+    task_info {
+      estimated_mws: 0.844506
+      estimated_mw: 0.415630
+      idle_transitions_mws: 0.000684
+      thread_name: "sleep"
+      process_name: "sleep"
+      thread_id: 11474
+      process_id: 11474
+    }
+    task_info {
+      estimated_mws: 0.795564
+      estimated_mw: 0.391542
+      idle_transitions_mws: 0.252256
+      thread_name: "kgsl_dispatcher"
+      process_name: "kgsl_dispatcher"
+      thread_id: 122
+      process_id: 122
+    }
+    task_info {
+      estimated_mws: 0.715993
+      estimated_mw: 0.352381
+      idle_transitions_mws: 0.426966
+      thread_name: "irq/33-4520300."
+      process_name: "irq/33-4520300."
+      thread_id: 307
+      process_id: 307
+    }
+    task_info {
+      estimated_mws: 0.715212
+      estimated_mw: 0.351997
+      idle_transitions_mws: 0.062089
+      thread_name: "binder:742_1"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 786
+      process_id: 742
+    }
+    task_info {
+      estimated_mws: 0.659174
+      estimated_mw: 0.324417
+      idle_transitions_mws: 0.153809
+      thread_name: "surfaceflinger"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 788
+      process_id: 742
+    }
+    task_info {
+      estimated_mws: 0.653970
+      estimated_mw: 0.321856
+      idle_transitions_mws: 0.111764
+      thread_name: "app"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 820
+      process_id: 742
+    }
+    task_info {
+      estimated_mws: 0.463974
+      estimated_mw: 0.228348
+      idle_transitions_mws: 0.175385
+      thread_name: "rcuog/0"
+      process_name: "rcuog/0"
+      thread_id: 15
+      process_id: 15
+    }
+    task_info {
+      estimated_mws: 0.434532
+      estimated_mw: 0.213858
+      idle_transitions_mws: 0.095329
+      thread_name: "Primes-Jank"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 5094
+      process_id: 2710
+    }
+    task_info {
+      estimated_mws: 0.356684
+      estimated_mw: 0.175544
+      idle_transitions_mws: 0.276803
+      thread_name: "crtc_event:80"
+      process_name: "crtc_event:80"
+      thread_id: 301
+      process_id: 301
+    }
+    task_info {
+      estimated_mws: 0.271999
+      estimated_mw: 0.133866
+      idle_transitions_mws: 0.151016
+      thread_name: "rcuog/2"
+      process_name: "rcuog/2"
+      thread_id: 40
+      process_id: 40
+    }
+    task_info {
+      estimated_mws: 0.204649
+      estimated_mw: 0.100719
+      idle_transitions_mws: 0.067710
+      thread_name: "binder:2710_2"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 3100
+      process_id: 2710
+    }
+    task_info {
+      estimated_mws: 0.197450
+      estimated_mw: 0.097176
+      thread_name: "FileWatcherThre"
+      process_name: "/vendor/bin/hw/android.hardware.thermal-service.pixel"
+      thread_id: 1544
+      process_id: 1529
+    }
+    task_info {
+      estimated_mws: 0.165350
+      estimated_mw: 0.081378
+      idle_transitions_mws: 0.030527
+      thread_name: "rcuop/0"
+      process_name: "rcuop/0"
+      thread_id: 16
+      process_id: 16
+    }
+    task_info {
+      estimated_mws: 0.123135
+      estimated_mw: 0.060602
+      thread_name: "msm_irqbalance"
+      process_name: "/vendor/bin/msm_irqbalance"
+      thread_id: 3230
+      process_id: 3230
+    }
+    task_info {
+      estimated_mws: 0.122703
+      estimated_mw: 0.060389
+      idle_transitions_mws: 0.026041
+      thread_name: "kgsl-events"
+      process_name: "kgsl-events"
+      thread_id: 120
+      process_id: 120
+    }
+    task_info {
+      estimated_mws: 0.106642
+      estimated_mw: 0.052485
+      thread_name: "traced"
+      process_name: "/system/bin/traced"
+      thread_id: 919
+      process_id: 919
+    }
+    task_info {
+      estimated_mws: 0.104195
+      estimated_mw: 0.051280
+      idle_transitions_mws: 0.301336
+      thread_name: "kworker/2:0"
+      process_name: "kworker/2:0"
+      thread_id: 11444
+      process_id: 11444
+    }
+    task_info {
+      estimated_mws: 0.095284
+      estimated_mw: 0.046894
+      idle_transitions_mws: 0.039497
+      thread_name: "rcuop/2"
+      process_name: "rcuop/2"
+      thread_id: 41
+      process_id: 41
+    }
+    task_info {
+      estimated_mws: 0.084534
+      estimated_mw: 0.041604
+      idle_transitions_mws: 0.025241
+      thread_name: "RegSampIdle"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 826
+      process_id: 742
+    }
+    task_info {
+      estimated_mws: 0.076505
+      estimated_mw: 0.037652
+      idle_transitions_mws: 0.075963
+      thread_name: "rcuop/1"
+      process_name: "rcuop/1"
+      thread_id: 32
+      process_id: 32
+    }
+    task_info {
+      estimated_mws: 0.067736
+      estimated_mw: 0.033337
+      thread_name: "sh"
+      process_name: "/system/bin/sh"
+      thread_id: 11472
+      process_id: 11472
+    }
+    task_info {
+      estimated_mws: 0.065940
+      estimated_mw: 0.032453
+      idle_transitions_mws: 0.002859
+      thread_name: "BG"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 3524
+      process_id: 2710
+    }
+    task_info {
+      estimated_mws: 0.053141
+      estimated_mw: 0.026154
+      idle_transitions_mws: 0.000744
+      thread_name: "StateService"
+      process_name: "com.google.android.apps.scone"
+      thread_id: 3621
+      process_id: 3505
+    }
+    task_info {
+      estimated_mws: 0.052200
+      estimated_mw: 0.025691
+      idle_transitions_mws: 0.000544
+      thread_name: "Blocking Thread"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 11310
+      process_id: 11279
+    }
+    task_info {
+      estimated_mws: 0.040767
+      estimated_mw: 0.020064
+      idle_transitions_mws: 0.003269
+      thread_name: "kworker/0:1"
+      process_name: "kworker/0:1"
+      thread_id: 11436
+      process_id: 11436
+    }
+    task_info {
+      estimated_mws: 0.040587
+      estimated_mw: 0.019975
+      thread_name: "binder:1629_7"
+      process_name: "system_server"
+      thread_id: 2635
+      process_id: 1629
+    }
+    task_info {
+      estimated_mws: 0.040484
+      estimated_mw: 0.019924
+      idle_transitions_mws: 0.020651
+      thread_name: "rcuop/3"
+      process_name: "rcuop/3"
+      thread_id: 49
+      process_id: 49
+    }
+    task_info {
+      estimated_mws: 0.038016
+      estimated_mw: 0.018710
+      idle_transitions_mws: 0.002906
+      thread_name: "atchdog.monitor"
+      process_name: "system_server"
+      thread_id: 1669
+      process_id: 1629
+    }
+    task_info {
+      estimated_mws: 0.036888
+      estimated_mw: 0.018155
+      thread_name: "logd.writer"
+      process_name: "/system/bin/logd"
+      thread_id: 228
+      process_id: 213
+    }
+    task_info {
+      estimated_mws: 0.032972
+      estimated_mw: 0.016227
+      idle_transitions_mws: 0.025051
+      thread_name: "surfaceflinger"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 828
+      process_id: 742
+    }
+    task_info {
+      estimated_mws: 0.032239
+      estimated_mw: 0.015867
+      idle_transitions_mws: 0.001840
+      thread_name: "it.FitbitMobile"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 11279
+      process_id: 11279
+    }
+    task_info {
+      estimated_mws: 0.031160
+      estimated_mw: 0.015336
+      idle_transitions_mws: 0.000559
+      thread_name: "binder:11279_4"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 11426
+      process_id: 11279
+    }
+    task_info {
+      estimated_mws: 0.028389
+      estimated_mw: 0.013972
+      thread_name: "irq/207-dwc3"
+      process_name: "irq/207-dwc3"
+      thread_id: 9733
+      process_id: 9733
+    }
+    task_info {
+      estimated_mws: 0.027208
+      estimated_mw: 0.013391
+      idle_transitions_mws: 0.000951
+      thread_name: "UsbFfs-worker"
+      process_name: "/apex/com.android.adbd/bin/adbd"
+      thread_id: 9734
+      process_id: 5154
+    }
+    task_info {
+      estimated_mws: 0.024832
+      estimated_mw: 0.012221
+      thread_name: "logcat"
+      process_name: "logcat"
+      thread_id: 1199
+      process_id: 1199
+    }
+    task_info {
+      estimated_mws: 0.023707
+      estimated_mw: 0.011668
+      idle_transitions_mws: 0.000854
+      thread_name: "logd.reader.per"
+      process_name: "/system/bin/logd"
+      thread_id: 1227
+      process_id: 213
+    }
+    task_info {
+      estimated_mws: 0.022160
+      estimated_mw: 0.010906
+      idle_transitions_mws: 0.006160
+      thread_name: "kworker/u8:2"
+      process_name: "kworker/u8:2"
+      thread_id: 11458
+      process_id: 11458
+    }
+    task_info {
+      estimated_mws: 0.019052
+      estimated_mw: 0.009376
+      idle_transitions_mws: 0.008121
+      thread_name: "Scheduled BG"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 3575
+      process_id: 2710
+    }
+    task_info {
+      estimated_mws: 0.018414
+      estimated_mw: 0.009063
+      idle_transitions_mws: 0.040061
+      thread_name: "RegionSampling"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 825
+      process_id: 742
+    }
+    task_info {
+      estimated_mws: 0.016701
+      estimated_mw: 0.008220
+      idle_transitions_mws: 0.006448
+      thread_name: "halt_drain_rqs"
+      process_name: "halt_drain_rqs"
+      thread_id: 108
+      process_id: 108
+    }
+    task_info {
+      estimated_mws: 0.011023
+      estimated_mw: 0.005425
+      idle_transitions_mws: 0.001553
+      thread_name: "irq/26-4744000."
+      process_name: "irq/26-4744000."
+      thread_id: 112
+      process_id: 112
+    }
+    task_info {
+      estimated_mws: 0.010004
+      estimated_mw: 0.004924
+      thread_name: "migration/2"
+      process_name: "migration/2"
+      thread_id: 35
+      process_id: 35
+    }
+    task_info {
+      estimated_mws: 0.008819
+      estimated_mw: 0.004341
+      thread_name: "ksoftirqd/0"
+      process_name: "ksoftirqd/0"
+      thread_id: 13
+      process_id: 13
+    }
+    task_info {
+      estimated_mws: 0.007911
+      estimated_mw: 0.003894
+      idle_transitions_mws: 0.001205
+      thread_name: "watchdog"
+      process_name: "system_server"
+      thread_id: 1676
+      process_id: 1629
+    }
+    task_info {
+      estimated_mws: 0.007796
+      estimated_mw: 0.003837
+      idle_transitions_mws: 0.000627
+      thread_name: "pool-283-thread"
+      process_name: "system_server"
+      thread_id: 4427
+      process_id: 1629
+    }
+    task_info {
+      estimated_mws: 0.007628
+      estimated_mw: 0.003754
+      idle_transitions_mws: 0.001280
+      thread_name: "adbd"
+      process_name: "/apex/com.android.adbd/bin/adbd"
+      thread_id: 5154
+      process_id: 5154
+    }
+    task_info {
+      estimated_mws: 0.006796
+      estimated_mw: 0.003344
+      idle_transitions_mws: 0.001484
+      thread_name: "pool-1-thread-1"
+      process_name: "system_server"
+      thread_id: 2655
+      process_id: 1629
+    }
+    task_info {
+      estimated_mws: 0.005691
+      estimated_mw: 0.002801
+      idle_transitions_mws: 0.001832
+      thread_name: "pool-1-thread-1"
+      process_name: "com.google.android.apps.scone"
+      thread_id: 3625
+      process_id: 3505
+    }
+    task_info {
+      estimated_mws: 0.005476
+      estimated_mw: 0.002695
+      thread_name: "binder:237_2"
+      process_name: "/system/bin/vold"
+      thread_id: 237
+      process_id: 237
+    }
+    task_info {
+      estimated_mws: 0.004537
+      estimated_mw: 0.002233
+      thread_name: "shell svc 11472"
+      process_name: "/apex/com.android.adbd/bin/adbd"
+      thread_id: 11473
+      process_id: 5154
+    }
+    task_info {
+      estimated_mws: 0.003924
+      estimated_mw: 0.001931
+      thread_name: "ksoftirqd/1"
+      process_name: "ksoftirqd/1"
+      thread_id: 29
+      process_id: 29
+    }
+    task_info {
+      estimated_mws: 0.002908
+      estimated_mw: 0.001431
+      thread_name: "BG"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 5230
+      process_id: 2710
+    }
+    task_info {
+      estimated_mws: 0.002492
+      estimated_mw: 0.001226
+      idle_transitions_mws: 0.028896
+      thread_name: "kworker/3:2"
+      process_name: "kworker/3:2"
+      thread_id: 9832
+      process_id: 9832
+    }
+    task_info {
+      estimated_mws: 0.002333
+      estimated_mw: 0.001148
+      idle_transitions_mws: 0.000780
+      thread_name: "Scheduled BG"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 3577
+      process_id: 2710
+    }
+    task_info {
+      estimated_mws: 0.002293
+      estimated_mw: 0.001128
+      idle_transitions_mws: 0.000781
+      thread_name: "InputReader"
+      process_name: "system_server"
+      thread_id: 2560
+      process_id: 1629
+    }
+    task_info {
+      estimated_mws: 0.002261
+      estimated_mw: 0.001113
+      thread_name: "DefaultDispatch"
+      process_name: "com.google.android.wearable.media.sessions"
+      thread_id: 3618
+      process_id: 3553
+    }
+    task_info {
+      estimated_mws: 0.002226
+      estimated_mw: 0.001095
+      thread_name: "migration/3"
+      process_name: "migration/3"
+      thread_id: 44
+      process_id: 44
+    }
+    task_info {
+      estimated_mws: 0.002100
+      estimated_mw: 0.001034
+      idle_transitions_mws: 0.000657
+      thread_name: "InputDispatcher"
+      process_name: "system_server"
+      thread_id: 2559
+      process_id: 1629
+    }
+    task_info {
+      estimated_mws: 0.001973
+      estimated_mw: 0.000971
+      idle_transitions_mws: 0.000764
+      thread_name: "kworker/1:0"
+      process_name: "kworker/1:0"
+      thread_id: 10984
+      process_id: 10984
+    }
+    task_info {
+      estimated_mws: 0.001966
+      estimated_mw: 0.000967
+      thread_name: "irq/193-wdog-ba"
+      process_name: "irq/193-wdog-ba"
+      thread_id: 344
+      process_id: 344
+    }
+    task_info {
+      estimated_mws: 0.001867
+      estimated_mw: 0.000919
+      thread_name: "DefaultDispatch"
+      process_name: "com.google.android.wearable.media.sessions"
+      thread_id: 3615
+      process_id: 3553
+    }
+    task_info {
+      estimated_mws: 0.001867
+      estimated_mw: 0.000919
+      idle_transitions_mws: 0.000705
+      thread_name: "irq/25-mmc0"
+      process_name: "irq/25-mmc0"
+      thread_id: 115
+      process_id: 115
+    }
+    task_info {
+      estimated_mws: 0.001728
+      estimated_mw: 0.000851
+      thread_name: "iou-wrk-214"
+      process_name: "/system/bin/lmkd"
+      thread_id: 11440
+      process_id: 214
+    }
+    task_info {
+      estimated_mws: 0.001600
+      estimated_mw: 0.000787
+      idle_transitions_mws: 0.001911
+      thread_name: "DefaultDispatch"
+      process_name: "com.google.android.wearable.media.sessions"
+      thread_id: 3616
+      process_id: 3553
+    }
+    task_info {
+      estimated_mws: 0.001393
+      estimated_mw: 0.000686
+      thread_name: "kworker/u8:1"
+      process_name: "kworker/u8:1"
+      thread_id: 11185
+      process_id: 11185
+    }
+    task_info {
+      estimated_mws: 0.001373
+      estimated_mw: 0.000676
+      idle_transitions_mws: 0.007386
+      thread_name: "Scheduled BG"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 3576
+      process_id: 2710
+    }
+    task_info {
+      estimated_mws: 0.000811
+      estimated_mw: 0.000399
+      thread_name: "Scheduled BG"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 3622
+      process_id: 2710
+    }
+  }
+}
diff --git a/test/trace_processor/diff_tests/metrics/android/wattson_trace_threads.out b/test/trace_processor/diff_tests/metrics/android/wattson_trace_threads.out
index 11d67a5..bcc7daf 100644
--- a/test/trace_processor/diff_tests/metrics/android/wattson_trace_threads.out
+++ b/test/trace_processor/diff_tests/metrics/android/wattson_trace_threads.out
@@ -1,3838 +1,4050 @@
 wattson_trace_threads {
-  metric_version: 1
-  task_info {
-    estimate_mws: 34.338558
-    estimate_mw: 3.970208
-    thread_name: "swapper"
-    thread_id: 0
-    process_id: 0
-  }
-  task_info {
-    estimate_mws: 19.853722
-    estimate_mw: 2.295478
-    thread_name: "RenderThread"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 1986
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 17.530441
-    estimate_mw: 2.026861
-    thread_name: "Jit thread pool"
-    process_name: "system_server"
-    thread_id: 1344
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 16.980276
-    estimate_mw: 1.963251
-    thread_name: "surfaceflinger"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 755
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 14.908094
-    estimate_mw: 1.723667
-    thread_name: ".wearable.sysui"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 1926
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 13.373357
-    estimate_mw: 1.546221
-    thread_name: "binder:685_3"
-    process_name: "/vendor/bin/hw/vendor.qti.hardware.display.composer-service"
-    thread_id: 804
-    process_id: 685
-  }
-  task_info {
-    estimate_mws: 6.747261
-    estimate_mw: 0.780115
-    thread_name: "binder:1302_7"
-    process_name: "system_server"
-    thread_id: 1671
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 6.504194
-    estimate_mw: 0.752012
-    thread_name: "binder:1302_A"
-    process_name: "system_server"
-    thread_id: 2015
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 4.858775
-    estimate_mw: 0.561769
-    thread_name: "android.anim"
-    process_name: "system_server"
-    thread_id: 1419
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 4.769787
-    estimate_mw: 0.551480
-    thread_name: "RenderEngine"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 788
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 4.672233
-    estimate_mw: 0.540201
-    thread_name: "kswapd0"
-    process_name: "kswapd0"
-    thread_id: 63
-    process_id: 63
-  }
-  task_info {
-    estimate_mws: 4.314495
-    estimate_mw: 0.498840
-    thread_name: "lowpool[2]"
-    process_name: "com.google.android.gms"
-    thread_id: 3525
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 4.117814
-    estimate_mw: 0.476100
-    thread_name: "logd.writer"
-    process_name: "/system/bin/logd"
-    thread_id: 221
-    process_id: 211
-  }
-  task_info {
-    estimate_mws: 4.108276
-    estimate_mw: 0.474997
-    thread_name: "binder:1302_17"
-    process_name: "system_server"
-    thread_id: 5202
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 3.723955
-    estimate_mw: 0.430562
-    thread_name: "binder:1302_6"
-    process_name: "system_server"
-    thread_id: 1662
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 3.666289
-    estimate_mw: 0.423895
-    thread_name: "e.watchface.rwf"
-    process_name: "com.google.android.wearable.watchface.rwf"
-    thread_id: 1999
-    process_id: 1999
-  }
-  task_info {
-    estimate_mws: 3.524869
-    estimate_mw: 0.407544
-    thread_name: "killall"
-    process_name: "/system/bin/sh"
-    thread_id: 5620
-    process_id: 5620
-  }
-  task_info {
-    estimate_mws: 3.495762
-    estimate_mw: 0.404178
-    thread_name: "CachedAppOptimi"
-    process_name: "system_server"
-    thread_id: 1773
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 3.459922
-    estimate_mw: 0.400034
-    thread_name: "logcat"
-    process_name: "logcat"
-    thread_id: 1230
-    process_id: 1230
-  }
-  task_info {
-    estimate_mws: 3.429554
-    estimate_mw: 0.396523
-    thread_name: "system_server"
-    process_name: "system_server"
-    thread_id: 1302
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 3.300661
-    estimate_mw: 0.381621
-    thread_name: "crtc_commit:80"
-    process_name: "crtc_commit:80"
-    thread_id: 244
-    process_id: 244
-  }
-  task_info {
-    estimate_mws: 3.194881
-    estimate_mw: 0.369391
-    thread_name: "InputDispatcher"
-    process_name: "system_server"
-    thread_id: 1783
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 3.011913
-    estimate_mw: 0.348236
-    thread_name: "binder:755_1"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 782
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 3.006022
-    estimate_mw: 0.347555
-    thread_name: "android.display"
-    process_name: "system_server"
-    thread_id: 1418
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 2.856301
-    estimate_mw: 0.330244
-    thread_name: "binder:524_2"
-    process_name: "/vendor/bin/mcu_mgmtd"
-    thread_id: 524
-    process_id: 524
-  }
-  task_info {
-    estimate_mws: 2.712443
-    estimate_mw: 0.313611
-    thread_name: "traced_probes"
-    process_name: "/system/bin/traced_probes"
-    thread_id: 904
-    process_id: 904
-  }
-  task_info {
-    estimate_mws: 2.550922
-    estimate_mw: 0.294936
-    thread_name: "kworker/u8:0"
-    process_name: "kworker/u8:0"
-    thread_id: 8
-    process_id: 8
-  }
-  task_info {
-    estimate_mws: 2.487099
-    estimate_mw: 0.287557
-    thread_name: "surfaceflinger"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 883
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 2.386123
-    estimate_mw: 0.275882
-    thread_name: "binder:1302_15"
-    process_name: "system_server"
-    thread_id: 3754
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 2.258779
-    estimate_mw: 0.261159
-    thread_name: "logd.reader.per"
-    process_name: "/system/bin/logd"
-    thread_id: 1274
-    process_id: 211
-  }
-  task_info {
-    estimate_mws: 2.171289
-    estimate_mw: 0.251043
-    thread_name: "RenderThread"
-    process_name: "com.google.android.wearable.watchface.rwf"
-    thread_id: 2301
-    process_id: 1999
-  }
-  task_info {
-    estimate_mws: 2.143151
-    estimate_mw: 0.247790
-    thread_name: "InputReader"
-    process_name: "system_server"
-    thread_id: 1784
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 2.091428
-    estimate_mw: 0.241810
-    thread_name: "rcu_preempt"
-    process_name: "rcu_preempt"
-    thread_id: 14
-    process_id: 14
-  }
-  task_info {
-    estimate_mws: 2.048920
-    estimate_mw: 0.236895
-    thread_name: "binder:1926_4"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 2262
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 1.914560
-    estimate_mw: 0.221360
-    thread_name: "arable.systemui"
-    process_name: "com.google.android.apps.wearable.systemui"
-    thread_id: 2171
-    process_id: 2171
-  }
-  task_info {
-    estimate_mws: 1.854433
-    estimate_mw: 0.214409
-    thread_name: "android.ui"
-    process_name: "system_server"
-    thread_id: 1416
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 1.777087
-    estimate_mw: 0.205466
-    thread_name: "kworker/u8:4"
-    process_name: "kworker/u8:4"
-    thread_id: 431
-    process_id: 431
-  }
-  task_info {
-    estimate_mws: 1.773777
-    estimate_mw: 0.205083
-    thread_name: "TimerDispatch"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 865
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 1.760400
-    estimate_mw: 0.203537
-    thread_name: "ActivityManager"
-    process_name: "system_server"
-    thread_id: 1431
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 1.733169
-    estimate_mw: 0.200388
-    thread_name: "PowerManagerSer"
-    process_name: "system_server"
-    thread_id: 1506
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 1.639501
-    estimate_mw: 0.189558
-    thread_name: "WifiHandlerThre"
-    process_name: "system_server"
-    thread_id: 1818
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 1.631037
-    estimate_mw: 0.188580
-    thread_name: "binder:755_5"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 1987
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 1.605931
-    estimate_mw: 0.185677
-    thread_name: "kgsl_dispatcher"
-    process_name: "kgsl_dispatcher"
-    thread_id: 111
-    process_id: 111
-  }
-  task_info {
-    estimate_mws: 1.564964
-    estimate_mw: 0.180940
-    thread_name: "binder:1302_8"
-    process_name: "system_server"
-    thread_id: 1679
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 1.476619
-    estimate_mw: 0.170726
-    thread_name: "lowpool[5]"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 3489
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 1.470155
-    estimate_mw: 0.169979
-    thread_name: "-Executor] idle"
-    process_name: "com.google.android.gms"
-    thread_id: 5591
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 1.469958
-    estimate_mw: 0.169956
-    thread_name: "binder:1302_B"
-    process_name: "system_server"
-    thread_id: 2033
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 1.390635
-    estimate_mw: 0.160785
-    thread_name: "binder:755_4"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 1125
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 1.327049
-    estimate_mw: 0.153433
-    thread_name: "batterystats-ha"
-    process_name: "system_server"
-    thread_id: 1484
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 1.312721
-    estimate_mw: 0.151776
-    thread_name: "statsd.writer"
-    process_name: "/apex/com.android.os.statsd/bin/statsd"
-    thread_id: 980
-    process_id: 545
-  }
-  task_info {
-    estimate_mws: 1.252738
-    estimate_mw: 0.144841
-    thread_name: "kworker/u8:2"
-    process_name: "kworker/u8:2"
-    thread_id: 62
-    process_id: 62
-  }
-  task_info {
-    estimate_mws: 1.251944
-    estimate_mw: 0.144749
-    thread_name: "app"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 867
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 1.233674
-    estimate_mw: 0.142637
-    thread_name: "system_server"
-    process_name: "system_server"
-    thread_id: 1343
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 1.228813
-    estimate_mw: 0.142075
-    thread_name: "irq/33-4520300."
-    process_name: "irq/33-4520300.qcom,bwmon-ddr"
-    thread_id: 95
-    process_id: 95
-  }
-  task_info {
-    estimate_mws: 1.068197
-    estimate_mw: 0.123504
-    thread_name: "logd.klogd"
-    process_name: "/system/bin/logd"
-    thread_id: 234
-    process_id: 211
-  }
-  task_info {
-    estimate_mws: 1.007066
-    estimate_mw: 0.116436
-    thread_name: "android.fg"
-    process_name: "system_server"
-    thread_id: 1415
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.969983
-    estimate_mw: 0.112149
-    thread_name: "rcuog/0"
-    process_name: "rcuog/0"
-    thread_id: 15
-    process_id: 15
-  }
-  task_info {
-    estimate_mws: 0.952084
-    estimate_mw: 0.110079
-    thread_name: "binder:1926_3"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 1940
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.946746
-    estimate_mw: 0.109462
-    thread_name: "gle.android.gms"
-    process_name: "com.google.android.gms"
-    thread_id: 2856
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.930774
-    estimate_mw: 0.107616
-    thread_name: "crtc_event:80"
-    process_name: "crtc_event:80"
-    thread_id: 245
-    process_id: 245
-  }
-  task_info {
-    estimate_mws: 0.907425
-    estimate_mw: 0.104916
-    thread_name: "binder:755_3"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 1124
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 0.897620
-    estimate_mw: 0.103782
-    thread_name: "init"
-    process_name: "/system/bin/init"
-    thread_id: 143
-    process_id: 1
-  }
-  task_info {
-    estimate_mws: 0.880853
-    estimate_mw: 0.101844
-    thread_name: "wmshell.main"
-    process_name: "com.google.android.apps.wearable.systemui"
-    thread_id: 2260
-    process_id: 2171
-  }
-  task_info {
-    estimate_mws: 0.870598
-    estimate_mw: 0.100658
-    thread_name: "Primes-1"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 1944
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.847641
-    estimate_mw: 0.098004
-    thread_name: "init"
-    process_name: "/system/bin/init"
-    thread_id: 1
-    process_id: 1
-  }
-  task_info {
-    estimate_mws: 0.846054
-    estimate_mw: 0.097820
-    thread_name: "binder:1302_D"
-    process_name: "system_server"
-    thread_id: 2043
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.844958
-    estimate_mw: 0.097694
-    thread_name: "surfaceflinger"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 786
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 0.833920
-    estimate_mw: 0.096417
-    thread_name: "kworker/u8:5"
-    process_name: "kworker/u8:5"
-    thread_id: 5304
-    process_id: 5304
-  }
-  task_info {
-    estimate_mws: 0.780835
-    estimate_mw: 0.090280
-    thread_name: "kworker/2:4"
-    process_name: "kworker/2:4"
-    thread_id: 4995
-    process_id: 4995
-  }
-  task_info {
-    estimate_mws: 0.747755
-    estimate_mw: 0.086455
-    thread_name: "binder:2171_4"
-    process_name: "com.google.android.apps.wearable.systemui"
-    thread_id: 2374
-    process_id: 2171
-  }
-  task_info {
-    estimate_mws: 0.746488
-    estimate_mw: 0.086309
-    thread_name: "binder:1999_5"
-    process_name: "com.google.android.wearable.watchface.rwf"
-    thread_id: 3678
-    process_id: 1999
-  }
-  task_info {
-    estimate_mws: 0.744167
-    estimate_mw: 0.086040
-    thread_name: "servicemanager"
-    process_name: "/system/bin/servicemanager"
-    thread_id: 213
-    process_id: 213
-  }
-  task_info {
-    estimate_mws: 0.717520
-    estimate_mw: 0.082959
-    thread_name: "wmshell.anim"
-    process_name: "com.google.android.apps.wearable.systemui"
-    thread_id: 2269
-    process_id: 2171
-  }
-  task_info {
-    estimate_mws: 0.699681
-    estimate_mw: 0.080897
-    thread_name: "GoogleApiHandle"
-    process_name: "com.google.android.gms"
-    thread_id: 3208
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.675997
-    estimate_mw: 0.078158
-    thread_name: "binder:1302_4"
-    process_name: "system_server"
-    thread_id: 1592
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.647999
-    estimate_mw: 0.074921
-    thread_name: "batterystats-wo"
-    process_name: "system_server"
-    thread_id: 1487
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.640576
-    estimate_mw: 0.074063
-    thread_name: ".gms.persistent"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 1949
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.631830
-    estimate_mw: 0.073052
-    thread_name: "binder:1926_6"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 5211
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.627672
-    estimate_mw: 0.072571
-    thread_name: "DisplayOffloadB"
-    process_name: "system_server"
-    thread_id: 1512
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.627487
-    estimate_mw: 0.072550
-    thread_name: "binder:682_2"
-    process_name: "/vendor/bin/hw/vendor.qti.hardware.display.allocator-service"
-    thread_id: 682
-    process_id: 682
-  }
-  task_info {
-    estimate_mws: 0.624294
-    estimate_mw: 0.072181
-    thread_name: "rcuog/2"
-    process_name: "rcuog/2"
-    thread_id: 37
-    process_id: 37
-  }
-  task_info {
-    estimate_mws: 0.623909
-    estimate_mw: 0.072136
-    thread_name: "kworker/0:6"
-    process_name: "kworker/0:6"
-    thread_id: 586
-    process_id: 586
-  }
-  task_info {
-    estimate_mws: 0.597177
-    estimate_mw: 0.069045
-    thread_name: "diag-router"
-    process_name: "/vendor/bin/diag-router"
-    thread_id: 634
-    process_id: 634
-  }
-  task_info {
-    estimate_mws: 0.582498
-    estimate_mw: 0.067348
-    thread_name: "HeapTaskDaemon"
-    process_name: "com.google.android.gms"
-    thread_id: 2882
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.579675
-    estimate_mw: 0.067022
-    thread_name: "FileWatcherThre"
-    process_name: "/vendor/bin/hw/android.hardware.thermal-service.pixel"
-    thread_id: 1411
-    process_id: 1404
-  }
-  task_info {
-    estimate_mws: 0.568415
-    estimate_mw: 0.065720
-    thread_name: "TaskSnapshotPer"
-    process_name: "system_server"
-    thread_id: 1913
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.565566
-    estimate_mw: 0.065390
-    thread_name: "lmkd"
-    process_name: "/system/bin/lmkd"
-    thread_id: 212
-    process_id: 212
-  }
-  task_info {
-    estimate_mws: 0.554734
-    estimate_mw: 0.064138
-    thread_name: "binder:1949_8"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 3269
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.517529
-    estimate_mw: 0.059836
-    thread_name: "appSf"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 868
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 0.514221
-    estimate_mw: 0.059454
-    thread_name: "kworker/1:1"
-    process_name: "kworker/1:1"
-    thread_id: 47
-    process_id: 47
-  }
-  task_info {
-    estimate_mws: 0.507581
-    estimate_mw: 0.058686
-    thread_name: "android.hardwar"
-    process_name: "/vendor/bin/hw/android.hardware.usb-service.qti"
-    thread_id: 1861
-    process_id: 665
-  }
-  task_info {
-    estimate_mws: 0.504068
-    estimate_mw: 0.058280
-    thread_name: "Primes-Jank"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 2389
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.493578
-    estimate_mw: 0.057067
-    thread_name: "binder:2171_3"
-    process_name: "com.google.android.apps.wearable.systemui"
-    thread_id: 2235
-    process_id: 2171
-  }
-  task_info {
-    estimate_mws: 0.490345
-    estimate_mw: 0.056693
-    thread_name: "traced"
-    process_name: "/system/bin/traced"
-    thread_id: 905
-    process_id: 905
-  }
-  task_info {
-    estimate_mws: 0.468415
-    estimate_mw: 0.054158
-    thread_name: "eduling.default"
-    process_name: "system_server"
-    thread_id: 1761
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.462913
-    estimate_mw: 0.053522
-    thread_name: "binder:545_2"
-    process_name: "/apex/com.android.os.statsd/bin/statsd"
-    thread_id: 553
-    process_id: 545
-  }
-  task_info {
-    estimate_mws: 0.462537
-    estimate_mw: 0.053478
-    thread_name: "User"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 2234
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.454063
-    estimate_mw: 0.052498
-    thread_name: "putmethod.latin"
-    process_name: "com.google.android.inputmethod.latin"
-    thread_id: 4997
-    process_id: 4997
-  }
-  task_info {
-    estimate_mws: 0.450612
-    estimate_mw: 0.052100
-    thread_name: "ueventd"
-    process_name: "/system/bin/ueventd"
-    thread_id: 145
-    process_id: 145
-  }
-  task_info {
-    estimate_mws: 0.448044
-    estimate_mw: 0.051803
-    thread_name: "wpa_supplicant"
-    process_name: "/vendor/bin/hw/wpa_supplicant"
-    thread_id: 5214
-    process_id: 5214
-  }
-  task_info {
-    estimate_mws: 0.431304
-    estimate_mw: 0.049867
-    thread_name: "rcuop/0"
-    process_name: "rcuop/0"
-    thread_id: 16
-    process_id: 16
-  }
-  task_info {
-    estimate_mws: 0.416635
-    estimate_mw: 0.048171
-    thread_name: "Jit thread pool"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 1933
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.404592
-    estimate_mw: 0.046779
-    thread_name: "pixelstats-vend"
-    process_name: "/vendor/bin/pixelstats-vendor"
-    thread_id: 267
-    process_id: 255
-  }
-  task_info {
-    estimate_mws: 0.396838
-    estimate_mw: 0.045882
-    thread_name: "irq/236-NVT-ts"
-    process_name: "irq/236-NVT-ts"
-    thread_id: 505
-    process_id: 505
-  }
-  task_info {
-    estimate_mws: 0.393249
-    estimate_mw: 0.045467
-    thread_name: "nanohub"
-    process_name: "nanohub"
-    thread_id: 297
-    process_id: 297
-  }
-  task_info {
-    estimate_mws: 0.376686
-    estimate_mw: 0.043552
-    thread_name: "android.bg"
-    process_name: "system_server"
-    thread_id: 1430
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.375869
-    estimate_mw: 0.043458
-    thread_name: "chre"
-    process_name: "/vendor/bin/chre"
-    thread_id: 1041
-    process_id: 1041
-  }
-  task_info {
-    estimate_mws: 0.373520
-    estimate_mw: 0.043186
-    thread_name: "lowpool[1]"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 2279
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.366001
-    estimate_mw: 0.042317
-    thread_name: "TracingMuxer"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 783
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 0.359494
-    estimate_mw: 0.041565
-    thread_name: "kgsl-events"
-    process_name: "kgsl-events"
-    thread_id: 109
-    process_id: 109
-  }
-  task_info {
-    estimate_mws: 0.359130
-    estimate_mw: 0.041522
-    thread_name: "IpClient.wlan0"
-    process_name: "com.android.networkstack.process"
-    thread_id: 5216
-    process_id: 2049
-  }
-  task_info {
-    estimate_mws: 0.346517
-    estimate_mw: 0.040064
-    thread_name: "binder:257_5"
-    process_name: "/system/bin/hw/android.system.suspend-service"
-    thread_id: 1491
-    process_id: 257
-  }
-  task_info {
-    estimate_mws: 0.341200
-    estimate_mw: 0.039449
-    thread_name: "binder:1901_3"
-    process_name: "/vendor/bin/hw/android.hardware.wifi-service-lazy"
-    thread_id: 1905
-    process_id: 1901
-  }
-  task_info {
-    estimate_mws: 0.335534
-    estimate_mw: 0.038794
-    thread_name: "binder:740_1"
-    process_name: "/system/bin/audioserver"
-    thread_id: 821
-    process_id: 740
-  }
-  task_info {
-    estimate_mws: 0.331405
-    estimate_mw: 0.038317
-    thread_name: "BG"
-    process_name: "com.google.wear.services"
-    thread_id: 2023
-    process_id: 1948
-  }
-  task_info {
-    estimate_mws: 0.326344
-    estimate_mw: 0.037732
-    thread_name: "kworker/0:5H"
-    process_name: "kworker/0:5H"
-    thread_id: 1337
-    process_id: 1337
-  }
-  task_info {
-    estimate_mws: 0.322384
-    estimate_mw: 0.037274
-    thread_name: "binder:755_2"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 784
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 0.319511
-    estimate_mw: 0.036942
-    thread_name: "audioserver"
-    process_name: "/system/bin/audioserver"
-    thread_id: 740
-    process_id: 740
-  }
-  task_info {
-    estimate_mws: 0.310996
-    estimate_mw: 0.035957
-    thread_name: "binder:1949_2"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 1978
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.302115
-    estimate_mw: 0.034930
-    thread_name: "-Executor] idle"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 5602
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.301578
-    estimate_mw: 0.034868
-    thread_name: "pool-11-thread-"
-    process_name: "com.google.android.wearable.healthservices"
-    thread_id: 3329
-    process_id: 3028
-  }
-  task_info {
-    estimate_mws: 0.299169
-    estimate_mw: 0.034590
-    thread_name: "android.io"
-    process_name: "system_server"
-    thread_id: 1417
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.296825
-    estimate_mw: 0.034319
-    thread_name: "binder:1901_3"
-    process_name: "/vendor/bin/hw/android.hardware.wifi-service-lazy"
-    thread_id: 5205
-    process_id: 1901
-  }
-  task_info {
-    estimate_mws: 0.294242
-    estimate_mw: 0.034020
-    thread_name: "rcuop/1"
-    process_name: "rcuop/1"
-    thread_id: 30
-    process_id: 30
-  }
-  task_info {
-    estimate_mws: 0.286642
-    estimate_mw: 0.033141
-    thread_name: "binder:1948_6"
-    process_name: "com.google.wear.services"
-    thread_id: 5315
-    process_id: 1948
-  }
-  task_info {
-    estimate_mws: 0.285983
-    estimate_mw: 0.033065
-    thread_name: "AssistantHandle"
-    process_name: "com.google.android.wearable.assistant"
-    thread_id: 4081
-    process_id: 4038
-  }
-  task_info {
-    estimate_mws: 0.283378
-    estimate_mw: 0.032764
-    thread_name: "binder:1999_1"
-    process_name: "com.google.android.wearable.watchface.rwf"
-    thread_id: 2016
-    process_id: 1999
-  }
-  task_info {
-    estimate_mws: 0.279959
-    estimate_mw: 0.032369
-    thread_name: "binder:2182_7"
-    process_name: "com.android.phone"
-    thread_id: 2694
-    process_id: 2182
-  }
-  task_info {
-    estimate_mws: 0.279816
-    estimate_mw: 0.032352
-    thread_name: "kworker/3:2H"
-    process_name: "kworker/3:2H"
-    thread_id: 226
-    process_id: 226
-  }
-  task_info {
-    estimate_mws: 0.277230
-    estimate_mw: 0.032053
-    thread_name: "BG"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 3005
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.274735
-    estimate_mw: 0.031765
-    thread_name: "lowpool[3]"
-    process_name: "com.google.android.gms"
-    thread_id: 3527
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.267749
-    estimate_mw: 0.030957
-    thread_name: "hvdcp_opti"
-    process_name: "/vendor/bin/hvdcp_opti"
-    thread_id: 1276
-    process_id: 1270
-  }
-  task_info {
-    estimate_mws: 0.262081
-    estimate_mw: 0.030302
-    thread_name: "binder:1926_3"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 2022
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.259248
-    estimate_mw: 0.029974
-    thread_name: "kworker/3:5"
-    process_name: "kworker/3:5"
-    thread_id: 104
-    process_id: 104
-  }
-  task_info {
-    estimate_mws: 0.256714
-    estimate_mw: 0.029681
-    thread_name: "binder:257_2"
-    process_name: "/system/bin/hw/android.system.suspend-service"
-    thread_id: 264
-    process_id: 257
-  }
-  task_info {
-    estimate_mws: 0.247046
-    estimate_mw: 0.028563
-    thread_name: "SDM_EventThread"
-    process_name: "/vendor/bin/hw/vendor.qti.hardware.display.composer-service"
-    thread_id: 727
-    process_id: 685
-  }
-  task_info {
-    estimate_mws: 0.244112
-    estimate_mw: 0.028224
-    thread_name: "POSIX timer 2"
-    process_name: "/vendor/bin/hw/android.hardware.sensors-service.multihal"
-    thread_id: 1600
-    process_id: 664
-  }
-  task_info {
-    estimate_mws: 0.242754
-    estimate_mw: 0.028067
-    thread_name: "binder:2856_4"
-    process_name: "com.google.android.gms"
-    thread_id: 3679
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.241348
-    estimate_mw: 0.027905
-    thread_name: "pool-2-thread-1"
-    process_name: "com.android.networkstack.process"
-    thread_id: 2416
-    process_id: 2049
-  }
-  task_info {
-    estimate_mws: 0.231145
-    estimate_mw: 0.026725
-    thread_name: "rcuop/3"
-    process_name: "rcuop/3"
-    thread_id: 45
-    process_id: 45
-  }
-  task_info {
-    estimate_mws: 0.230341
-    estimate_mw: 0.026632
-    thread_name: "f2fs_ckpt-254:4"
-    process_name: "f2fs_ckpt-254:43"
-    thread_id: 347
-    process_id: 347
-  }
-  task_info {
-    estimate_mws: 0.229722
-    estimate_mw: 0.026560
-    thread_name: "OomAdjuster"
-    process_name: "system_server"
-    thread_id: 1482
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.226417
-    estimate_mw: 0.026178
-    thread_name: "binder:740_6"
-    process_name: "/system/bin/audioserver"
-    thread_id: 2639
-    process_id: 740
-  }
-  task_info {
-    estimate_mws: 0.226007
-    estimate_mw: 0.026131
-    thread_name: "rcuop/2"
-    process_name: "rcuop/2"
-    thread_id: 38
-    process_id: 38
-  }
-  task_info {
-    estimate_mws: 0.225133
-    estimate_mw: 0.026030
-    thread_name: "kworker/0:7"
-    process_name: "kworker/0:7"
-    thread_id: 598
-    process_id: 598
-  }
-  task_info {
-    estimate_mws: 0.212788
-    estimate_mw: 0.024602
-    thread_name: "queued-work-loo"
-    process_name: "system_server"
-    thread_id: 1886
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.202562
-    estimate_mw: 0.023420
-    thread_name: "pool-13-thread-"
-    process_name: "com.google.android.wearable.healthservices"
-    thread_id: 3327
-    process_id: 3028
-  }
-  task_info {
-    estimate_mws: 0.201687
-    estimate_mw: 0.023319
-    thread_name: "WearSdkThread"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 2207
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.201119
-    estimate_mw: 0.023253
-    thread_name: "qrtr_ns"
-    process_name: "qrtr_ns"
-    thread_id: 88
-    process_id: 88
-  }
-  task_info {
-    estimate_mws: 0.200639
-    estimate_mw: 0.023198
-    thread_name: "binder:740_7"
-    process_name: "/system/bin/audioserver"
-    thread_id: 5206
-    process_id: 740
-  }
-  task_info {
-    estimate_mws: 0.196587
-    estimate_mw: 0.022729
-    thread_name: "binder:4997_4"
-    process_name: "com.google.android.inputmethod.latin"
-    thread_id: 5122
-    process_id: 4997
-  }
-  task_info {
-    estimate_mws: 0.194754
-    estimate_mw: 0.022517
-    thread_name: "m.android.phone"
-    process_name: "com.android.phone"
-    thread_id: 2182
-    process_id: 2182
-  }
-  task_info {
-    estimate_mws: 0.192336
-    estimate_mw: 0.022238
-    thread_name: "HwcAsyncWorker"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 835
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 0.190522
-    estimate_mw: 0.022028
-    thread_name: "binder:636_2"
-    process_name: "/vendor/bin/hw/android.hardware.audio.service"
-    thread_id: 636
-    process_id: 636
-  }
-  task_info {
-    estimate_mws: 0.188908
-    estimate_mw: 0.021841
-    thread_name: "SettingsProvide"
-    process_name: "system_server"
-    thread_id: 1771
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.181172
-    estimate_mw: 0.020947
-    thread_name: "binder:1302_2"
-    process_name: "system_server"
-    thread_id: 1350
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.177579
-    estimate_mw: 0.020532
-    thread_name: "RegSampIdle"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 872
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 0.168738
-    estimate_mw: 0.019509
-    thread_name: "ice] processing"
-    process_name: "com.google.android.gms"
-    thread_id: 3238
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.162808
-    estimate_mw: 0.018824
-    thread_name: "binder:682_3"
-    process_name: "/vendor/bin/hw/vendor.qti.hardware.display.allocator-service"
-    thread_id: 2308
-    process_id: 682
-  }
-  task_info {
-    estimate_mws: 0.158857
-    estimate_mw: 0.018367
-    thread_name: "binder:678_3"
-    process_name: "/apex/com.google.wearable.wac.whshal/bin/hw/vendor.google.wearable.wac.whshal@2.0-service"
-    thread_id: 1884
-    process_id: 678
-  }
-  task_info {
-    estimate_mws: 0.158685
-    estimate_mw: 0.018347
-    thread_name: "binder:650_4"
-    process_name: "/vendor/bin/hw/android.hardware.gnss-aidl-service-qti"
-    thread_id: 5498
-    process_id: 650
-  }
-  task_info {
-    estimate_mws: 0.157956
-    estimate_mw: 0.018263
-    thread_name: "vndservicemanag"
-    process_name: "/vendor/bin/vndservicemanager"
-    thread_id: 215
-    process_id: 215
-  }
-  task_info {
-    estimate_mws: 0.153600
-    estimate_mw: 0.017759
-    thread_name: "GoogleLocationS"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 3355
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.153038
-    estimate_mw: 0.017694
-    thread_name: "TransportThread"
-    process_name: "/vendor/bin/chre"
-    thread_id: 1078
-    process_id: 1041
-  }
-  task_info {
-    estimate_mws: 0.152058
-    estimate_mw: 0.017581
-    thread_name: "kworker/2:1H"
-    process_name: "kworker/2:1H"
-    thread_id: 123
-    process_id: 123
-  }
-  task_info {
-    estimate_mws: 0.148559
-    estimate_mw: 0.017176
-    thread_name: "BG"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 2120
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.148352
-    estimate_mw: 0.017152
-    thread_name: "vendor.google.w"
-    process_name: "/apex/com.google.wearable.wac.whshal/bin/hw/vendor.google.wearable.wac.whshal@2.0-service"
-    thread_id: 1881
-    process_id: 678
-  }
-  task_info {
-    estimate_mws: 0.140864
-    estimate_mw: 0.016287
-    thread_name: "NetworkStats"
-    process_name: "system_server"
-    thread_id: 1814
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.138579
-    estimate_mw: 0.016022
-    thread_name: "binder:969_2"
-    process_name: "/system/vendor/bin/cnd"
-    thread_id: 1011
-    process_id: 969
-  }
-  task_info {
-    estimate_mws: 0.134607
-    estimate_mw: 0.015563
-    thread_name: "dmabuf-deferred"
-    process_name: "dmabuf-deferred-free-worker"
-    thread_id: 69
-    process_id: 69
-  }
-  task_info {
-    estimate_mws: 0.129449
-    estimate_mw: 0.014967
-    thread_name: "highpool[5]"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 3354
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.126973
-    estimate_mw: 0.014681
-    thread_name: "ice] processing"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 2363
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.126830
-    estimate_mw: 0.014664
-    thread_name: "ediator.Toggler"
-    process_name: "system_server"
-    thread_id: 1910
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.125742
-    estimate_mw: 0.014538
-    thread_name: "surfaceflinger"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 875
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 0.123833
-    estimate_mw: 0.014317
-    thread_name: "wificond"
-    process_name: "/system/bin/wificond"
-    thread_id: 964
-    process_id: 964
-  }
-  task_info {
-    estimate_mws: 0.123248
-    estimate_mw: 0.014250
-    thread_name: "MobileDataStats"
-    process_name: "system_server"
-    thread_id: 1912
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.119885
-    estimate_mw: 0.013861
-    thread_name: "GlobalScheduler"
-    process_name: "com.google.android.gms"
-    thread_id: 3156
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.119479
-    estimate_mw: 0.013814
-    thread_name: "RenderThread"
-    thread_id: 5599
-  }
-  task_info {
-    estimate_mws: 0.119243
-    estimate_mw: 0.013787
-    thread_name: "TouchTimer"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 866
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 0.114249
-    estimate_mw: 0.013209
-    thread_name: "binder:4038_1"
-    process_name: "com.google.android.wearable.assistant"
-    thread_id: 4050
-    process_id: 4038
-  }
-  task_info {
-    estimate_mws: 0.112705
-    estimate_mw: 0.013031
-    thread_name: "displayoffload@"
-    process_name: "/vendor/bin/hw/vendor.google_clockwork.displayoffload@2.0-service.1p"
-    thread_id: 937
-    process_id: 937
-  }
-  task_info {
-    estimate_mws: 0.111279
-    estimate_mw: 0.012866
-    thread_name: "adbd"
-    process_name: "/apex/com.android.adbd/bin/adbd"
-    thread_id: 5544
-    process_id: 5544
-  }
-  task_info {
-    estimate_mws: 0.108114
-    estimate_mw: 0.012500
-    thread_name: "kworker/1:2H"
-    process_name: "kworker/1:2H"
-    thread_id: 300
-    process_id: 300
-  }
-  task_info {
-    estimate_mws: 0.107442
-    estimate_mw: 0.012422
-    thread_name: "RenderThread"
-    thread_id: 5584
-  }
-  task_info {
-    estimate_mws: 0.105863
-    estimate_mw: 0.012240
-    thread_name: "Primes-2"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 1946
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.104306
-    estimate_mw: 0.012060
-    thread_name: "iptables-restor"
-    process_name: "/system/bin/iptables-restore"
-    thread_id: 558
-    process_id: 558
-  }
-  task_info {
-    estimate_mws: 0.104093
-    estimate_mw: 0.012035
-    thread_name: "RenderThread"
-    process_name: "system_server"
-    thread_id: 5223
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.102912
-    estimate_mw: 0.011899
-    thread_name: "irq/168-nanohub"
-    process_name: "irq/168-nanohub-irq1"
-    thread_id: 296
-    process_id: 296
-  }
-  task_info {
-    estimate_mws: 0.102167
-    estimate_mw: 0.011812
-    thread_name: "RenderThread"
-    thread_id: 5604
-  }
-  task_info {
-    estimate_mws: 0.101945
-    estimate_mw: 0.011787
-    thread_name: "ksoftirqd/2"
-    process_name: "ksoftirqd/2"
-    thread_id: 34
-    process_id: 34
-  }
-  task_info {
-    estimate_mws: 0.101282
-    estimate_mw: 0.011710
-    thread_name: "PhotonicModulat"
-    process_name: "system_server"
-    thread_id: 1899
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.100285
-    estimate_mw: 0.011595
-    thread_name: "ip6tables-resto"
-    process_name: "/system/bin/ip6tables-restore"
-    thread_id: 559
-    process_id: 559
-  }
-  task_info {
-    estimate_mws: 0.099432
-    estimate_mw: 0.011496
-    thread_name: "init"
-    process_name: "/system/bin/init"
-    thread_id: 144
-    process_id: 144
-  }
-  task_info {
-    estimate_mws: 0.096314
-    estimate_mw: 0.011136
-    thread_name: "FrameworkReceiv"
-    process_name: ".qtidataservices"
-    thread_id: 2793
-    process_id: 2118
-  }
-  task_info {
-    estimate_mws: 0.095442
-    estimate_mw: 0.011035
-    thread_name: "Jit thread pool"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 1969
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.094448
-    estimate_mw: 0.010920
-    thread_name: "pool-14-thread-"
-    process_name: "com.google.android.wearable.healthservices"
-    thread_id: 3314
-    process_id: 3028
-  }
-  task_info {
-    estimate_mws: 0.093937
-    estimate_mw: 0.010861
-    thread_name: "binder:1926_2"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 1939
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.093621
-    estimate_mw: 0.010824
-    thread_name: "ChreMsgHandler"
-    process_name: "/vendor/bin/chre"
-    thread_id: 1080
-    process_id: 1041
-  }
-  task_info {
-    estimate_mws: 0.091738
-    estimate_mw: 0.010607
-    thread_name: "DispatcherModul"
-    process_name: "/vendor/bin/hw/qcrilNrd"
-    thread_id: 1673
-    process_id: 1062
-  }
-  task_info {
-    estimate_mws: 0.091698
-    estimate_mw: 0.010602
-    thread_name: "irq/234-pixart_"
-    process_name: "irq/234-pixart_pat9126_irq"
-    thread_id: 500
-    process_id: 500
-  }
-  task_info {
-    estimate_mws: 0.090883
-    estimate_mw: 0.010508
-    thread_name: "scheduler_threa"
-    process_name: "scheduler_thread"
-    thread_id: 5198
-    process_id: 5198
-  }
-  task_info {
-    estimate_mws: 0.089001
-    estimate_mw: 0.010290
-    thread_name: "binder:2085_4"
-    process_name: "com.google.android.bluetooth"
-    thread_id: 2713
-    process_id: 2085
-  }
-  task_info {
-    estimate_mws: 0.086934
-    estimate_mw: 0.010051
-    thread_name: "binder:3028_5"
-    process_name: "com.google.android.wearable.healthservices"
-    thread_id: 5434
-    process_id: 3028
-  }
-  task_info {
-    estimate_mws: 0.085941
-    estimate_mw: 0.009936
-    thread_name: "binder:2670_6"
-    process_name: "com.android.nfc"
-    thread_id: 3159
-    process_id: 2670
-  }
-  task_info {
-    estimate_mws: 0.083859
-    estimate_mw: 0.009696
-    thread_name: "psimon"
-    process_name: "psimon"
-    thread_id: 1480
-    process_id: 1480
-  }
-  task_info {
-    estimate_mws: 0.083773
-    estimate_mw: 0.009686
-    thread_name: "binder:233_2"
-    process_name: "/system/bin/vold"
-    thread_id: 252
-    process_id: 233
-  }
-  task_info {
-    estimate_mws: 0.080959
-    estimate_mw: 0.009360
-    thread_name: "binder:2049_2"
-    process_name: "com.android.networkstack.process"
-    thread_id: 2068
-    process_id: 2049
-  }
-  task_info {
-    estimate_mws: 0.080298
-    estimate_mw: 0.009284
-    thread_name: "netd"
-    process_name: "/system/bin/netd"
-    thread_id: 568
-    process_id: 546
-  }
-  task_info {
-    estimate_mws: 0.080265
-    estimate_mw: 0.009280
-    thread_name: "UEventObserver"
-    process_name: "system_server"
-    thread_id: 1857
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.079337
-    estimate_mw: 0.009173
-    thread_name: "RenderThread"
-    thread_id: 5619
-  }
-  task_info {
-    estimate_mws: 0.078696
-    estimate_mw: 0.009099
-    thread_name: "pool-8-thread-1"
-    process_name: "com.google.android.gms"
-    thread_id: 3102
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.077362
-    estimate_mw: 0.008945
-    thread_name: "mcu_mgmtd"
-    process_name: "/vendor/bin/mcu_mgmtd"
-    thread_id: 594
-    process_id: 524
-  }
-  task_info {
-    estimate_mws: 0.074501
-    estimate_mw: 0.008614
-    thread_name: "spi0"
-    process_name: "spi0"
-    thread_id: 295
-    process_id: 295
-  }
-  task_info {
-    estimate_mws: 0.073914
-    estimate_mw: 0.008546
-    thread_name: "com.android.nfc"
-    process_name: "com.android.nfc"
-    thread_id: 2670
-    process_id: 2670
-  }
-  task_info {
-    estimate_mws: 0.072671
-    estimate_mw: 0.008402
-    thread_name: "rcu_exp_gp_kthr"
-    process_name: "rcu_exp_gp_kthread_worker"
-    thread_id: 19
-    process_id: 19
-  }
-  task_info {
-    estimate_mws: 0.070587
-    estimate_mw: 0.008161
-    thread_name: "adbd"
-    process_name: "/apex/com.android.adbd/bin/adbd"
-    thread_id: 5546
-    process_id: 5544
-  }
-  task_info {
-    estimate_mws: 0.070105
-    estimate_mw: 0.008105
-    thread_name: "servicemanager"
-    thread_id: 5598
-  }
-  task_info {
-    estimate_mws: 0.069214
-    estimate_mw: 0.008002
-    thread_name: "android.imms"
-    process_name: "system_server"
-    thread_id: 1791
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.068600
-    estimate_mw: 0.007931
-    thread_name: "lowpool[2]"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 2321
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.068467
-    estimate_mw: 0.007916
-    thread_name: "FileObserver"
-    process_name: "com.google.android.gms"
-    thread_id: 3035
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.068316
-    estimate_mw: 0.007899
-    thread_name: "binder:546_3"
-    process_name: "/system/bin/netd"
-    thread_id: 546
-    process_id: 546
-  }
-  task_info {
-    estimate_mws: 0.066410
-    estimate_mw: 0.007678
-    thread_name: "pool-51-thread-"
-    process_name: "com.google.android.gms"
-    thread_id: 4215
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.064761
-    estimate_mw: 0.007488
-    thread_name: "AudioService"
-    process_name: "system_server"
-    thread_id: 1844
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.064339
-    estimate_mw: 0.007439
-    thread_name: "adbd"
-    process_name: "/apex/com.android.adbd/bin/adbd"
-    thread_id: 5545
-    process_id: 5544
-  }
-  task_info {
-    estimate_mws: 0.063188
-    estimate_mw: 0.007306
-    thread_name: "droid.bluetooth"
-    process_name: "com.google.android.bluetooth"
-    thread_id: 2085
-    process_id: 2085
-  }
-  task_info {
-    estimate_mws: 0.061069
-    estimate_mw: 0.007061
-    thread_name: "msm_irqbalance"
-    process_name: "/vendor/bin/msm_irqbalance"
-    thread_id: 2466
-    process_id: 2466
-  }
-  task_info {
-    estimate_mws: 0.060920
-    estimate_mw: 0.007044
-    thread_name: "BackgroundInsta"
-    process_name: "system_server"
-    thread_id: 1875
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.059901
-    estimate_mw: 0.006926
-    thread_name: "ConnectivitySer"
-    process_name: "system_server"
-    thread_id: 1827
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.059295
-    estimate_mw: 0.006856
-    thread_name: "pool-1-thread-1"
-    process_name: "system_server"
-    thread_id: 1873
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.059196
-    estimate_mw: 0.006844
-    thread_name: "binder:3028_1"
-    process_name: "com.google.android.wearable.healthservices"
-    thread_id: 3049
-    process_id: 3028
-  }
-  task_info {
-    estimate_mws: 0.058807
-    estimate_mw: 0.006799
-    thread_name: "roid.apps.scone"
-    process_name: "com.google.android.apps.scone"
-    thread_id: 5245
-    process_id: 5245
-  }
-  task_info {
-    estimate_mws: 0.057400
-    estimate_mw: 0.006637
-    thread_name: "android.hardwar"
-    process_name: "/vendor/bin/hw/android.hardware.health-service.eos"
-    thread_id: 1271
-    process_id: 1271
-  }
-  task_info {
-    estimate_mws: 0.056947
-    estimate_mw: 0.006584
-    thread_name: "bgres-controlle"
-    process_name: "system_server"
-    thread_id: 1495
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.056879
-    estimate_mw: 0.006576
-    thread_name: "netd"
-    process_name: "/system/bin/netd"
-    thread_id: 569
-    process_id: 546
-  }
-  task_info {
-    estimate_mws: 0.055771
-    estimate_mw: 0.006448
-    thread_name: "UsbFfs-worker"
-    process_name: "/apex/com.android.adbd/bin/adbd"
-    thread_id: 5560
-    process_id: 5544
-  }
-  task_info {
-    estimate_mws: 0.055642
-    estimate_mw: 0.006433
-    thread_name: "system_server"
-    thread_id: 5590
-  }
-  task_info {
-    estimate_mws: 0.054764
-    estimate_mw: 0.006332
-    thread_name: "binder:2049_4"
-    process_name: "com.android.networkstack.process"
-    thread_id: 2083
-    process_id: 2049
-  }
-  task_info {
-    estimate_mws: 0.053127
-    estimate_mw: 0.006142
-    thread_name: "binder:1949_4"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 2302
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.052984
-    estimate_mw: 0.006126
-    thread_name: "oid.grilservice"
-    process_name: "com.google.android.grilservice"
-    thread_id: 2129
-    process_id: 2129
-  }
-  task_info {
-    estimate_mws: 0.052980
-    estimate_mw: 0.006126
-    thread_name: "binder:2856_9"
-    process_name: "com.google.android.gms"
-    thread_id: 5585
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.052715
-    estimate_mw: 0.006095
-    thread_name: "StateService"
-    process_name: "com.google.android.apps.scone"
-    thread_id: 5269
-    process_id: 5245
-  }
-  task_info {
-    estimate_mws: 0.052232
-    estimate_mw: 0.006039
-    thread_name: "vndservicemanag"
-    thread_id: 5597
-  }
-  task_info {
-    estimate_mws: 0.052027
-    estimate_mw: 0.006015
-    thread_name: "binder:1949_9"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 3359
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.051717
-    estimate_mw: 0.005979
-    thread_name: "Ipc-5004:1"
-    process_name: "/vendor/bin/hw/android.hardware.gnss-aidl-service-qti"
-    thread_id: 5483
-    process_id: 650
-  }
-  task_info {
-    estimate_mws: 0.049546
-    estimate_mw: 0.005729
-    thread_name: "PackageManager"
-    process_name: "system_server"
-    thread_id: 1530
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.048227
-    estimate_mw: 0.005576
-    thread_name: "android.hardwar"
-    process_name: "/vendor/bin/hw/android.hardware.power-service"
-    thread_id: 660
-    process_id: 660
-  }
-  task_info {
-    estimate_mws: 0.047454
-    estimate_mw: 0.005487
-    thread_name: "BluetoothScanMa"
-    process_name: "com.google.android.bluetooth"
-    thread_id: 2609
-    process_id: 2085
-  }
-  task_info {
-    estimate_mws: 0.046472
-    estimate_mw: 0.005373
-    thread_name: "subsystem_ramdu"
-    process_name: "/system/vendor/bin/subsystem_ramdump"
-    thread_id: 816
-    process_id: 799
-  }
-  task_info {
-    estimate_mws: 0.046295
-    estimate_mw: 0.005353
-    thread_name: "Ipc-5004:2"
-    process_name: "/vendor/bin/hw/android.hardware.gnss-aidl-service-qti"
-    thread_id: 5484
-    process_id: 650
-  }
-  task_info {
-    estimate_mws: 0.045805
-    estimate_mw: 0.005296
-    thread_name: "binder:975_2"
-    process_name: "/vendor/bin/imsdaemon"
-    thread_id: 1047
-    process_id: 975
-  }
-  task_info {
-    estimate_mws: 0.045282
-    estimate_mw: 0.005235
-    thread_name: ".healthservices"
-    process_name: "com.google.android.wearable.healthservices"
-    thread_id: 3028
-    process_id: 3028
-  }
-  task_info {
-    estimate_mws: 0.045087
-    estimate_mw: 0.005213
-    thread_name: "queued-work-loo"
-    process_name: "com.google.android.gms"
-    thread_id: 3236
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.043921
-    estimate_mw: 0.005078
-    thread_name: "powerstateservi"
-    process_name: "/vendor/bin/hw/vendor.qti.hardware.powerstateservice@1.0-service"
-    thread_id: 276
-    process_id: 269
-  }
-  task_info {
-    estimate_mws: 0.043653
-    estimate_mw: 0.005047
-    thread_name: "wlan_logging_th"
-    process_name: "wlan_logging_thread"
-    thread_id: 368
-    process_id: 368
-  }
-  task_info {
-    estimate_mws: 0.043574
-    estimate_mw: 0.005038
-    thread_name: "FlpThread"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 3279
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.043436
-    estimate_mw: 0.005022
-    thread_name: "Light-P0-2"
-    process_name: "com.google.android.inputmethod.latin"
-    thread_id: 5071
-    process_id: 4997
-  }
-  task_info {
-    estimate_mws: 0.042985
-    estimate_mw: 0.004970
-    thread_name: "binder:5245_4"
-    process_name: "com.google.android.apps.scone"
-    thread_id: 5270
-    process_id: 5245
-  }
-  task_info {
-    estimate_mws: 0.042946
-    estimate_mw: 0.004965
-    thread_name: "HWC_UeventThrea"
-    process_name: "/vendor/bin/hw/vendor.qti.hardware.display.composer-service"
-    thread_id: 717
-    process_id: 685
-  }
-  task_info {
-    estimate_mws: 0.042762
-    estimate_mw: 0.004944
-    thread_name: "pool-12-thread-"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 2371
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.042752
-    estimate_mw: 0.004943
-    thread_name: "android.hardwar"
-    process_name: "/vendor/bin/hw/android.hardware.contexthub-service.wac"
-    thread_id: 668
-    process_id: 644
-  }
-  task_info {
-    estimate_mws: 0.042468
-    estimate_mw: 0.004910
-    thread_name: "binder:1948_3"
-    process_name: "com.google.wear.services"
-    thread_id: 1976
-    process_id: 1948
-  }
-  task_info {
-    estimate_mws: 0.042220
-    estimate_mw: 0.004881
-    thread_name: "netlink socket"
-    process_name: "/system/vendor/bin/ipacm"
-    thread_id: 538
-    process_id: 523
-  }
-  task_info {
-    estimate_mws: 0.040507
-    estimate_mw: 0.004683
-    thread_name: "RegionSampling"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 871
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 0.040385
-    estimate_mw: 0.004669
-    thread_name: "TransportThread"
-    process_name: "/vendor/bin/hw/android.hardware.sensors-service.multihal"
-    thread_id: 794
-    process_id: 664
-  }
-  task_info {
-    estimate_mws: 0.040349
-    estimate_mw: 0.004665
-    thread_name: "binder:2856_1"
-    process_name: "com.google.android.gms"
-    thread_id: 2898
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.040180
-    estimate_mw: 0.004646
-    thread_name: "servicemanager"
-    thread_id: 5595
-  }
-  task_info {
-    estimate_mws: 0.039525
-    estimate_mw: 0.004570
-    thread_name: "pd-mapper"
-    process_name: "/vendor/bin/pd-mapper"
-    thread_id: 752
-    process_id: 725
-  }
-  task_info {
-    estimate_mws: 0.039196
-    estimate_mw: 0.004532
-    thread_name: "vndservicemanag"
-    thread_id: 5618
-  }
-  task_info {
-    estimate_mws: 0.039101
-    estimate_mw: 0.004521
-    thread_name: "vndservicemanag"
-    thread_id: 5605
-  }
-  task_info {
-    estimate_mws: 0.038960
-    estimate_mw: 0.004505
-    thread_name: "WifiScanningSer"
-    process_name: "system_server"
-    thread_id: 1823
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.038580
-    estimate_mw: 0.004461
-    thread_name: "cnss-daemon"
-    process_name: "/system/vendor/bin/cnss-daemon"
-    thread_id: 5204
-    process_id: 1009
-  }
-  task_info {
-    estimate_mws: 0.038053
-    estimate_mw: 0.004400
-    thread_name: "shell svc 5620"
-    process_name: "/apex/com.android.adbd/bin/adbd"
-    thread_id: 5622
-    process_id: 5544
-  }
-  task_info {
-    estimate_mws: 0.037116
-    estimate_mw: 0.004291
-    thread_name: "rmt_storage"
-    process_name: "/vendor/bin/rmt_storage"
-    thread_id: 758
-    process_id: 758
-  }
-  task_info {
-    estimate_mws: 0.036357
-    estimate_mw: 0.004204
-    thread_name: "halt_drain_rqs"
-    process_name: "halt_drain_rqs"
-    thread_id: 105
-    process_id: 105
-  }
-  task_info {
-    estimate_mws: 0.035907
-    estimate_mw: 0.004152
-    thread_name: "BG Thread #2"
-    process_name: "com.google.android.wearable.assistant"
-    thread_id: 4106
-    process_id: 4038
-  }
-  task_info {
-    estimate_mws: 0.035876
-    estimate_mw: 0.004148
-    thread_name: "-Executor] idle"
-    process_name: "com.google.android.gms"
-    thread_id: 5592
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.035444
-    estimate_mw: 0.004098
-    thread_name: "tftp_server"
-    process_name: "/vendor/bin/tftp_server"
-    thread_id: 759
-    process_id: 759
-  }
-  task_info {
-    estimate_mws: 0.035386
-    estimate_mw: 0.004091
-    thread_name: "FinalizerWatchd"
-    process_name: "com.google.android.gms"
-    thread_id: 2887
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.035171
-    estimate_mw: 0.004066
-    thread_name: "servicemanager"
-    thread_id: 5606
-  }
-  task_info {
-    estimate_mws: 0.035157
-    estimate_mw: 0.004065
-    thread_name: "Blocking Thread"
-    process_name: "com.google.android.wearable.assistant"
-    thread_id: 5587
-    process_id: 4038
-  }
-  task_info {
-    estimate_mws: 0.035034
-    estimate_mw: 0.004051
-    thread_name: "vndservicemanag"
-    thread_id: 5611
-  }
-  task_info {
-    estimate_mws: 0.034307
-    estimate_mw: 0.003967
-    thread_name: "vndservicemanag"
-    thread_id: 5593
-  }
-  task_info {
-    estimate_mws: 0.034030
-    estimate_mw: 0.003935
-    thread_name: "servicemanager"
-    thread_id: 5621
-  }
-  task_info {
-    estimate_mws: 0.032631
-    estimate_mw: 0.003773
-    thread_name: "binder:685_3"
-    thread_id: 5586
-  }
-  task_info {
-    estimate_mws: 0.031847
-    estimate_mw: 0.003682
-    thread_name: "radioext@1.0-se"
-    process_name: "/vendor/bin/hw/vendor.google.radioext@1.0-service"
-    thread_id: 676
-    process_id: 676
-  }
-  task_info {
-    estimate_mws: 0.031818
-    estimate_mw: 0.003679
-    thread_name: "BgBroadcastRegi"
-    process_name: "com.google.wear.services"
-    thread_id: 2017
-    process_id: 1948
-  }
-  task_info {
-    estimate_mws: 0.031204
-    estimate_mw: 0.003608
-    thread_name: "DefaultExecutor"
-    process_name: "com.google.android.wearable.watchface.rwf"
-    thread_id: 5600
-    process_id: 1999
-  }
-  task_info {
-    estimate_mws: 0.030138
-    estimate_mw: 0.003484
-    thread_name: "Light-P0-1"
-    process_name: "com.google.android.inputmethod.latin"
-    thread_id: 5064
-    process_id: 4997
-  }
-  task_info {
-    estimate_mws: 0.029778
-    estimate_mw: 0.003443
-    thread_name: "binder:978_2"
-    process_name: "/system/vendor/bin/nicmd"
-    thread_id: 1368
-    process_id: 978
-  }
-  task_info {
-    estimate_mws: 0.029627
-    estimate_mw: 0.003425
-    thread_name: "atchdog.monitor"
-    process_name: "system_server"
-    thread_id: 1414
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.029590
-    estimate_mw: 0.003421
-    thread_name: "UsfHalWorker"
-    process_name: "/vendor/bin/hw/android.hardware.sensors-service.multihal"
-    thread_id: 792
-    process_id: 664
-  }
-  task_info {
-    estimate_mws: 0.028316
-    estimate_mw: 0.003274
-    thread_name: "binder:1999_5"
-    process_name: "com.google.android.wearable.watchface.rwf"
-    thread_id: 4985
-    process_id: 1999
-  }
-  task_info {
-    estimate_mws: 0.028097
-    estimate_mw: 0.003249
-    thread_name: "SatelliteContro"
-    process_name: "com.android.phone"
-    thread_id: 2382
-    process_id: 2182
-  }
-  task_info {
-    estimate_mws: 0.027797
-    estimate_mw: 0.003214
-    thread_name: "pm-service"
-    process_name: "/vendor/bin/pm-service"
-    thread_id: 745
-    process_id: 730
-  }
-  task_info {
-    estimate_mws: 0.027301
-    estimate_mw: 0.003157
-    thread_name: "-Executor] idle"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 5603
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.027287
-    estimate_mw: 0.003155
-    thread_name: "perfetto"
-    process_name: "perfetto"
-    thread_id: 5581
-    process_id: 5581
-  }
-  task_info {
-    estimate_mws: 0.026942
-    estimate_mw: 0.003115
-    thread_name: "ExeSeq-P10-1"
-    process_name: "com.google.android.inputmethod.latin"
-    thread_id: 5074
-    process_id: 4997
-  }
-  task_info {
-    estimate_mws: 0.026776
-    estimate_mw: 0.003096
-    thread_name: "binder:978_2"
-    process_name: "/system/vendor/bin/nicmd"
-    thread_id: 1364
-    process_id: 978
-  }
-  task_info {
-    estimate_mws: 0.026328
-    estimate_mw: 0.003044
-    thread_name: "HwBinder:2129_1"
-    process_name: "com.google.android.grilservice"
-    thread_id: 3649
-    process_id: 2129
-  }
-  task_info {
-    estimate_mws: 0.026013
-    estimate_mw: 0.003008
-    thread_name: "DefaultExecutor"
-    process_name: "com.google.android.wearable.watchface.rwf"
-    thread_id: 5588
-    process_id: 1999
-  }
-  task_info {
-    estimate_mws: 0.025701
-    estimate_mw: 0.002972
-    thread_name: "rkstack.process"
-    process_name: "com.android.networkstack.process"
-    thread_id: 2049
-    process_id: 2049
-  }
-  task_info {
-    estimate_mws: 0.024843
-    estimate_mw: 0.002872
-    thread_name: "hwuiTask1"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 1997
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.024811
-    estimate_mw: 0.002869
-    thread_name: "pool-1-thread-1"
-    process_name: "com.google.android.apps.scone"
-    thread_id: 5271
-    process_id: 5245
-  }
-  task_info {
-    estimate_mws: 0.024633
-    estimate_mw: 0.002848
-    thread_name: "binder:978_2"
-    process_name: "/system/vendor/bin/nicmd"
-    thread_id: 1360
-    process_id: 978
-  }
-  task_info {
-    estimate_mws: 0.023564
-    estimate_mw: 0.002724
-    thread_name: "binder:978_2"
-    process_name: "/system/vendor/bin/nicmd"
-    thread_id: 1366
-    process_id: 978
-  }
-  task_info {
-    estimate_mws: 0.023405
-    estimate_mw: 0.002706
-    thread_name: "cnss-daemon"
-    process_name: "/system/vendor/bin/cnss-daemon"
-    thread_id: 5613
-    process_id: 1009
-  }
-  task_info {
-    estimate_mws: 0.023393
-    estimate_mw: 0.002705
-    thread_name: "servicemanager"
-    thread_id: 5608
-  }
-  task_info {
-    estimate_mws: 0.022813
-    estimate_mw: 0.002638
-    thread_name: "android.hardwar"
-    process_name: "/vendor/bin/hw/android.hardware.contexthub-service.wac"
-    thread_id: 644
-    process_id: 644
-  }
-  task_info {
-    estimate_mws: 0.022774
-    estimate_mw: 0.002633
-    thread_name: "binder:978_2"
-    process_name: "/system/vendor/bin/nicmd"
-    thread_id: 1362
-    process_id: 978
-  }
-  task_info {
-    estimate_mws: 0.022714
-    estimate_mw: 0.002626
-    thread_name: "binder:685_3"
-    thread_id: 5594
-  }
-  task_info {
-    estimate_mws: 0.022642
-    estimate_mw: 0.002618
-    thread_name: "binder:978_2"
-    process_name: "/system/vendor/bin/nicmd"
-    thread_id: 1358
-    process_id: 978
-  }
-  task_info {
-    estimate_mws: 0.022617
-    estimate_mw: 0.002615
-    thread_name: "vndservicemanag"
-    thread_id: 5607
-  }
-  task_info {
-    estimate_mws: 0.021814
-    estimate_mw: 0.002522
-    thread_name: "it.FitbitMobile"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5377
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.021313
-    estimate_mw: 0.002464
-    thread_name: "irq/199-dwc3"
-    process_name: "irq/199-dwc3"
-    thread_id: 5559
-    process_id: 5559
-  }
-  task_info {
-    estimate_mws: 0.021307
-    estimate_mw: 0.002463
-    thread_name: "SysUiBg"
-    process_name: "com.google.android.apps.wearable.systemui"
-    thread_id: 2294
-    process_id: 2171
-  }
-  task_info {
-    estimate_mws: 0.020941
-    estimate_mw: 0.002421
-    thread_name: "-Executor] idle"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 5623
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.020393
-    estimate_mw: 0.002358
-    thread_name: "vndservicemanag"
-    thread_id: 5582
-  }
-  task_info {
-    estimate_mws: 0.019946
-    estimate_mw: 0.002306
-    thread_name: "qcom,system-poo"
-    process_name: "qcom,system-pool-refill-thread"
-    thread_id: 81
-    process_id: 81
-  }
-  task_info {
-    estimate_mws: 0.019901
-    estimate_mw: 0.002301
-    thread_name: "binder:2129_9"
-    process_name: "com.google.android.grilservice"
-    thread_id: 5203
-    process_id: 2129
-  }
-  task_info {
-    estimate_mws: 0.019723
-    estimate_mw: 0.002280
-    thread_name: "servicemanager"
-    thread_id: 5610
-  }
-  task_info {
-    estimate_mws: 0.019537
-    estimate_mw: 0.002259
-    thread_name: "cnss-daemon"
-    process_name: "/system/vendor/bin/cnss-daemon"
-    thread_id: 1009
-    process_id: 1009
-  }
-  task_info {
-    estimate_mws: 0.019390
-    estimate_mw: 0.002242
-    thread_name: "pool-9-thread-1"
-    process_name: "com.google.android.wearable.watchface.rwf"
-    thread_id: 2073
-    process_id: 1999
-  }
-  task_info {
-    estimate_mws: 0.019219
-    estimate_mw: 0.002222
-    thread_name: "binder:1302_3"
-    process_name: "system_server"
-    thread_id: 1433
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.019059
-    estimate_mw: 0.002204
-    thread_name: "mcu_mgmtd"
-    process_name: "/vendor/bin/mcu_mgmtd"
-    thread_id: 587
-    process_id: 524
-  }
-  task_info {
-    estimate_mws: 0.018619
-    estimate_mw: 0.002153
-    thread_name: "WCMTelemetryLog"
-    process_name: "system_server"
-    thread_id: 1906
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.018508
-    estimate_mw: 0.002140
-    thread_name: "droid.tethering"
-    process_name: "com.android.networkstack.process"
-    thread_id: 2158
-    process_id: 2049
-  }
-  task_info {
-    estimate_mws: 0.018055
-    estimate_mw: 0.002087
-    thread_name: "DefaultWallpape"
-    process_name: "com.google.android.wearable.watchface.rwf"
-    thread_id: 2082
-    process_id: 1999
-  }
-  task_info {
-    estimate_mws: 0.017725
-    estimate_mw: 0.002049
-    thread_name: "HeapTaskDaemon"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5386
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.017125
-    estimate_mw: 0.001980
-    thread_name: "WearConnectionT"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 2172
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.016887
-    estimate_mw: 0.001952
-    thread_name: "wear-services-w"
-    process_name: "com.google.wear.services"
-    thread_id: 2029
-    process_id: 1948
-  }
-  task_info {
-    estimate_mws: 0.016252
-    estimate_mw: 0.001879
-    thread_name: "BG Thread #1"
-    process_name: "com.google.android.wearable.assistant"
-    thread_id: 4080
-    process_id: 4038
-  }
-  task_info {
-    estimate_mws: 0.016215
-    estimate_mw: 0.001875
-    thread_name: "GlobalScheduler"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 2276
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.016141
-    estimate_mw: 0.001866
-    thread_name: "pool-31-thread-"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 5617
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.016069
-    estimate_mw: 0.001858
-    thread_name: "servicemanager"
-    thread_id: 5583
-  }
-  task_info {
-    estimate_mws: 0.015376
-    estimate_mw: 0.001778
-    thread_name: "RenderEngine"
-    process_name: "/system/bin/surfaceflinger"
-    thread_id: 5601
-    process_id: 755
-  }
-  task_info {
-    estimate_mws: 0.015374
-    estimate_mw: 0.001777
-    thread_name: "pool-11-thread-"
-    process_name: "com.google.android.wearable.healthservices"
-    thread_id: 5596
-    process_id: 3028
-  }
-  task_info {
-    estimate_mws: 0.015097
-    estimate_mw: 0.001745
-    thread_name: "dsi_err_workq"
-    process_name: "dsi_err_workq"
-    thread_id: 5589
-    process_id: 5589
-  }
-  task_info {
-    estimate_mws: 0.015062
-    estimate_mw: 0.001741
-    thread_name: "InteractionJank"
-    process_name: "com.google.android.apps.wearable.systemui"
-    thread_id: 2300
-    process_id: 2171
-  }
-  task_info {
-    estimate_mws: 0.015034
-    estimate_mw: 0.001738
-    thread_name: "vndservicemanag"
-    thread_id: 5609
-  }
-  task_info {
-    estimate_mws: 0.014592
-    estimate_mw: 0.001687
-    thread_name: "hwuiTask0"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 1996
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.014520
-    estimate_mw: 0.001679
-    thread_name: "tworkPolicy.uid"
-    process_name: "system_server"
-    thread_id: 1817
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.014278
-    estimate_mw: 0.001651
-    thread_name: "highpool[10]"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 3417
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.014123
-    estimate_mw: 0.001633
-    thread_name: "LowMemThread"
-    process_name: "system_server"
-    thread_id: 1481
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.014026
-    estimate_mw: 0.001622
-    thread_name: "pixelstats-vend"
-    process_name: "/vendor/bin/pixelstats-vendor"
-    thread_id: 266
-    process_id: 255
-  }
-  task_info {
-    estimate_mws: 0.013579
-    estimate_mw: 0.001570
-    thread_name: "kworker/u9:0"
-    process_name: "kworker/u9:0"
-    thread_id: 64
-    process_id: 64
-  }
-  task_info {
-    estimate_mws: 0.013322
-    estimate_mw: 0.001540
-    thread_name: "migration/1"
-    process_name: "migration/1"
-    thread_id: 25
-    process_id: 25
-  }
-  task_info {
-    estimate_mws: 0.013321
-    estimate_mw: 0.001540
-    thread_name: "GlobalDispatchi"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 2290
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.013127
-    estimate_mw: 0.001518
-    thread_name: "main"
-    process_name: "/vendor/bin/hw/qcrilNrd"
-    thread_id: 1568
-    process_id: 1062
-  }
-  task_info {
-    estimate_mws: 0.012863
-    estimate_mw: 0.001487
-    thread_name: "servicemanager"
-    thread_id: 5612
-  }
-  task_info {
-    estimate_mws: 0.012835
-    estimate_mw: 0.001484
-    thread_name: "qtidataservices"
-    process_name: ".qtidataservices"
-    thread_id: 2846
-    process_id: 2118
-  }
-  task_info {
-    estimate_mws: 0.012734
-    estimate_mw: 0.001472
-    thread_name: "shortcut"
-    process_name: "system_server"
-    thread_id: 1874
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.012433
-    estimate_mw: 0.001438
-    thread_name: "irq/25-mmc0"
-    process_name: "irq/25-mmc0"
-    thread_id: 120
-    process_id: 120
-  }
-  task_info {
-    estimate_mws: 0.012244
-    estimate_mw: 0.001416
-    thread_name: "GlobalDispatchi"
-    process_name: "com.google.android.gms"
-    thread_id: 3155
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.012130
-    estimate_mw: 0.001402
-    thread_name: "ConnectivityThr"
-    process_name: "com.google.android.gms"
-    thread_id: 4172
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.011932
-    estimate_mw: 0.001380
-    thread_name: "RenderThread"
-    thread_id: 5616
-  }
-  task_info {
-    estimate_mws: 0.011783
-    estimate_mw: 0.001362
-    thread_name: "AGMIPC@1.0-serv"
-    process_name: "/vendor/bin/hw/vendor.qti.hardware.AGMIPC@1.0-service"
-    thread_id: 1454
-    process_id: 1446
-  }
-  task_info {
-    estimate_mws: 0.011706
-    estimate_mw: 0.001353
-    thread_name: "binder:2171_2"
-    process_name: "com.google.android.apps.wearable.systemui"
-    thread_id: 2209
-    process_id: 2171
-  }
-  task_info {
-    estimate_mws: 0.011675
-    estimate_mw: 0.001350
-    thread_name: "radioext@1.0-se"
-    process_name: "/vendor/bin/hw/vendor.google.radioext@1.0-service"
-    thread_id: 714
-    process_id: 676
-  }
-  task_info {
-    estimate_mws: 0.011615
-    estimate_mw: 0.001343
-    thread_name: "servicemanager"
-    thread_id: 5615
-  }
-  task_info {
-    estimate_mws: 0.011467
-    estimate_mw: 0.001326
-    thread_name: "LocApiMsgTask"
-    process_name: "/vendor/bin/hw/android.hardware.gnss-aidl-service-qti"
-    thread_id: 694
-    process_id: 650
-  }
-  task_info {
-    estimate_mws: 0.011318
-    estimate_mw: 0.001309
-    thread_name: "LocApiMsgTask"
-    process_name: "xtra-daemon"
-    thread_id: 1090
-    process_id: 1031
-  }
-  task_info {
-    estimate_mws: 0.011273
-    estimate_mw: 0.001303
-    thread_name: "vndservicemanag"
-    thread_id: 5614
-  }
-  task_info {
-    estimate_mws: 0.011024
-    estimate_mw: 0.001275
-    thread_name: "TimerThread"
-    process_name: "/system/bin/audioserver"
-    thread_id: 1486
-    process_id: 740
-  }
-  task_info {
-    estimate_mws: 0.010869
-    estimate_mw: 0.001257
-    thread_name: "irq/26-4744000."
-    process_name: "irq/26-4744000.sdhci"
-    thread_id: 117
-    process_id: 117
-  }
-  task_info {
-    estimate_mws: 0.010764
-    estimate_mw: 0.001245
-    thread_name: "SurfaceSyncGrou"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 1994
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.010731
-    estimate_mw: 0.001241
-    thread_name: "migration/3"
-    process_name: "migration/3"
-    thread_id: 40
-    process_id: 40
-  }
-  task_info {
-    estimate_mws: 0.010156
-    estimate_mw: 0.001174
-    thread_name: "id.wearable.app"
-    process_name: "com.google.android.wearable.app"
-    thread_id: 3857
-    process_id: 3857
-  }
-  task_info {
-    estimate_mws: 0.009691
-    estimate_mw: 0.001121
-    thread_name: "ksoftirqd/0"
-    process_name: "ksoftirqd/0"
-    thread_id: 13
-    process_id: 13
-  }
-  task_info {
-    estimate_mws: 0.009668
-    estimate_mw: 0.001118
-    thread_name: "cnss-daemon"
-    process_name: "/system/vendor/bin/cnss-daemon"
-    thread_id: 1052
-    process_id: 1009
-  }
-  task_info {
-    estimate_mws: 0.009639
-    estimate_mw: 0.001114
-    thread_name: "kworker/u9:2"
-    process_name: "kworker/u9:2"
-    thread_id: 338
-    process_id: 338
-  }
-  task_info {
-    estimate_mws: 0.009404
-    estimate_mw: 0.001087
-    thread_name: "binder:2856_3"
-    process_name: "com.google.android.gms"
-    thread_id: 3157
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.009364
-    estimate_mw: 0.001083
-    thread_name: "binder:969_3"
-    process_name: "/system/vendor/bin/cnd"
-    thread_id: 1013
-    process_id: 969
-  }
-  task_info {
-    estimate_mws: 0.008837
-    estimate_mw: 0.001022
-    thread_name: "binder:2118_2"
-    process_name: ".qtidataservices"
-    thread_id: 2142
-    process_id: 2118
-  }
-  task_info {
-    estimate_mws: 0.008775
-    estimate_mw: 0.001015
-    thread_name: "thermal-engine-"
-    process_name: "/vendor/bin/thermal-engine-v2"
-    thread_id: 2520
-    process_id: 2493
-  }
-  task_info {
-    estimate_mws: 0.008724
-    estimate_mw: 0.001009
-    thread_name: "binder:2856_7"
-    process_name: "com.google.android.gms"
-    thread_id: 4825
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.008275
-    estimate_mw: 0.000957
-    thread_name: "binder:2856_6"
-    process_name: "com.google.android.gms"
-    thread_id: 4824
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.008136
-    estimate_mw: 0.000941
-    thread_name: "binder:3857_3"
-    process_name: "com.google.android.wearable.app"
-    thread_id: 3872
-    process_id: 3857
-  }
-  task_info {
-    estimate_mws: 0.007913
-    estimate_mw: 0.000915
-    thread_name: "binder:975_3"
-    process_name: "/vendor/bin/imsdaemon"
-    thread_id: 1630
-    process_id: 975
-  }
-  task_info {
-    estimate_mws: 0.007892
-    estimate_mw: 0.000913
-    thread_name: "time_daemon"
-    process_name: "/vendor/bin/time_daemon"
-    thread_id: 525
-    process_id: 522
-  }
-  task_info {
-    estimate_mws: 0.007750
-    estimate_mw: 0.000896
-    thread_name: "binder:978_2"
-    process_name: "/system/vendor/bin/nicmd"
-    thread_id: 1044
-    process_id: 978
-  }
-  task_info {
-    estimate_mws: 0.007591
-    estimate_mw: 0.000878
-    thread_name: "binder:2856_5"
-    process_name: "com.google.android.gms"
-    thread_id: 3681
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.007392
-    estimate_mw: 0.000855
-    thread_name: "binder:2182_1"
-    process_name: "com.android.phone"
-    thread_id: 2231
-    process_id: 2182
-  }
-  task_info {
-    estimate_mws: 0.007384
-    estimate_mw: 0.000854
-    thread_name: "binder:2171_5"
-    process_name: "com.google.android.apps.wearable.systemui"
-    thread_id: 2638
-    process_id: 2171
-  }
-  task_info {
-    estimate_mws: 0.007245
-    estimate_mw: 0.000838
-    thread_name: "qrtr_rx"
-    process_name: "qrtr_rx"
-    thread_id: 1556
-    process_id: 1556
-  }
-  task_info {
-    estimate_mws: 0.007086
-    estimate_mw: 0.000819
-    thread_name: "radioext@1.0-se"
-    process_name: "/vendor/bin/hw/vendor.google.radioext@1.0-service"
-    thread_id: 1605
-    process_id: 676
-  }
-  task_info {
-    estimate_mws: 0.006850
-    estimate_mw: 0.000792
-    thread_name: "hwservicemanage"
-    process_name: "/system/system_ext/bin/hwservicemanager"
-    thread_id: 214
-    process_id: 214
-  }
-  task_info {
-    estimate_mws: 0.006731
-    estimate_mw: 0.000778
-    thread_name: "rcub/0"
-    process_name: "rcub/0"
-    thread_id: 17
-    process_id: 17
-  }
-  task_info {
-    estimate_mws: 0.006663
-    estimate_mw: 0.000770
-    thread_name: "binder:2856_8"
-    process_name: "com.google.android.gms"
-    thread_id: 4826
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.006650
-    estimate_mw: 0.000769
-    thread_name: "kthreadd"
-    process_name: "kthreadd"
-    thread_id: 2
-    process_id: 2
-  }
-  task_info {
-    estimate_mws: 0.006574
-    estimate_mw: 0.000760
-    thread_name: "binder:233_2"
-    process_name: "/system/bin/vold"
-    thread_id: 233
-    process_id: 233
-  }
-  task_info {
-    estimate_mws: 0.006115
-    estimate_mw: 0.000707
-    thread_name: "cds_ol_rx_threa"
-    process_name: "cds_ol_rx_thread"
-    thread_id: 5199
-    process_id: 5199
-  }
-  task_info {
-    estimate_mws: 0.005829
-    estimate_mw: 0.000674
-    thread_name: "NsdService"
-    process_name: "system_server"
-    thread_id: 1831
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.005734
-    estimate_mw: 0.000663
-    thread_name: "binder:978_2"
-    process_name: "/system/vendor/bin/nicmd"
-    thread_id: 1367
-    process_id: 978
-  }
-  task_info {
-    estimate_mws: 0.005699
-    estimate_mw: 0.000659
-    thread_name: "Scheduled BG"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 2895
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.005530
-    estimate_mw: 0.000639
-    thread_name: "binder:2856_2"
-    process_name: "com.google.android.gms"
-    thread_id: 2903
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.005484
-    estimate_mw: 0.000634
-    thread_name: "binder:978_2"
-    process_name: "/system/vendor/bin/nicmd"
-    thread_id: 1361
-    process_id: 978
-  }
-  task_info {
-    estimate_mws: 0.005366
-    estimate_mw: 0.000620
-    thread_name: "binder:978_2"
-    process_name: "/system/vendor/bin/nicmd"
-    thread_id: 1357
-    process_id: 978
-  }
-  task_info {
-    estimate_mws: 0.005364
-    estimate_mw: 0.000620
-    thread_name: "FileObserver"
-    process_name: "system_server"
-    thread_id: 1498
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.005278
-    estimate_mw: 0.000610
-    thread_name: "binder:978_2"
-    process_name: "/system/vendor/bin/nicmd"
-    thread_id: 1359
-    process_id: 978
-  }
-  task_info {
-    estimate_mws: 0.005261
-    estimate_mw: 0.000608
-    thread_name: "Lite Thread #0"
-    process_name: "com.google.android.wearable.assistant"
-    thread_id: 4109
-    process_id: 4038
-  }
-  task_info {
-    estimate_mws: 0.005011
-    estimate_mw: 0.000579
-    thread_name: "kworker/3:1H"
-    process_name: "kworker/3:1H"
-    thread_id: 122
-    process_id: 122
-  }
-  task_info {
-    estimate_mws: 0.004996
-    estimate_mw: 0.000578
-    thread_name: "binder:978_2"
-    process_name: "/system/vendor/bin/nicmd"
-    thread_id: 1365
-    process_id: 978
-  }
-  task_info {
-    estimate_mws: 0.004986
-    estimate_mw: 0.000576
-    thread_name: "Scheduled BG"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 2890
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.004877
-    estimate_mw: 0.000564
-    thread_name: "perfetto_hprof_"
-    process_name: "com.google.android.gms"
-    thread_id: 2880
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.004761
-    estimate_mw: 0.000550
-    thread_name: "backup-0"
-    process_name: "system_server"
-    thread_id: 2660
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.004757
-    estimate_mw: 0.000550
-    thread_name: "tts-player-0"
-    process_name: "com.google.android.wearable.assistant"
-    thread_id: 4209
-    process_id: 4038
-  }
-  task_info {
-    estimate_mws: 0.004444
-    estimate_mw: 0.000514
-    thread_name: "Signal Catcher"
-    process_name: "com.google.android.gms"
-    thread_id: 2878
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.004331
-    estimate_mw: 0.000501
-    thread_name: "binder:978_2"
-    process_name: "/system/vendor/bin/nicmd"
-    thread_id: 1363
-    process_id: 978
-  }
-  task_info {
-    estimate_mws: 0.004292
-    estimate_mw: 0.000496
-    thread_name: "f2fs_discard-25"
-    process_name: "f2fs_discard-254:43"
-    thread_id: 349
-    process_id: 349
-  }
-  task_info {
-    estimate_mws: 0.004283
-    estimate_mw: 0.000495
-    thread_name: "irq/24-glink-na"
-    process_name: "irq/24-glink-native-rpm-glink"
-    thread_id: 86
-    process_id: 86
-  }
-  task_info {
-    estimate_mws: 0.004252
-    estimate_mw: 0.000492
-    thread_name: "pool-4-thread-1"
-    process_name: "system_server"
-    thread_id: 1774
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.004010
-    estimate_mw: 0.000464
-    thread_name: "PasspointProvis"
-    process_name: "system_server"
-    thread_id: 1821
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.003934
-    estimate_mw: 0.000455
-    thread_name: "binder:5377_5"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5573
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.003880
-    estimate_mw: 0.000449
-    thread_name: "BG Thread #3"
-    process_name: "com.google.android.wearable.assistant"
-    thread_id: 4107
-    process_id: 4038
-  }
-  task_info {
-    estimate_mws: 0.003872
-    estimate_mw: 0.000448
-    thread_name: "TransportThread"
-    process_name: "/vendor/bin/mcu_mgmtd"
-    thread_id: 3540
-    process_id: 524
-  }
-  task_info {
-    estimate_mws: 0.003835
-    estimate_mw: 0.000443
-    thread_name: "kworker/1:1H"
-    process_name: "kworker/1:1H"
-    thread_id: 127
-    process_id: 127
-  }
-  task_info {
-    estimate_mws: 0.003754
-    estimate_mw: 0.000434
-    thread_name: "watchdog"
-    process_name: "system_server"
-    thread_id: 1421
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.003628
-    estimate_mw: 0.000419
-    thread_name: "PackageInstalle"
-    process_name: "system_server"
-    thread_id: 1744
-    process_id: 1302
-  }
-  task_info {
-    estimate_mws: 0.003469
-    estimate_mw: 0.000401
-    thread_name: "Lite Thread #1"
-    process_name: "com.google.android.wearable.assistant"
-    thread_id: 4118
-    process_id: 4038
-  }
-  task_info {
-    estimate_mws: 0.003393
-    estimate_mw: 0.000392
-    thread_name: "FinalizerWatchd"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5389
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.003365
-    estimate_mw: 0.000389
-    thread_name: "DFacilitator-1"
-    process_name: "com.google.android.inputmethod.latin"
-    thread_id: 5128
-    process_id: 4997
-  }
-  task_info {
-    estimate_mws: 0.003243
-    estimate_mw: 0.000375
-    thread_name: "pool-8-thread-1"
-    process_name: "com.google.android.wearable.healthservices"
-    thread_id: 3308
-    process_id: 3028
-  }
-  task_info {
-    estimate_mws: 0.002873
-    estimate_mw: 0.000332
-    thread_name: "lowpool[1]"
-    process_name: "com.google.android.gms"
-    thread_id: 3503
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.002842
-    estimate_mw: 0.000329
-    thread_name: "ReferenceQueueD"
-    process_name: "com.google.android.gms"
-    thread_id: 2883
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.002778
-    estimate_mw: 0.000321
-    thread_name: "ipacm-diag"
-    process_name: "/system/vendor/bin/ipacm-diag"
-    thread_id: 976
-    process_id: 976
-  }
-  task_info {
-    estimate_mws: 0.002739
-    estimate_mw: 0.000317
-    thread_name: "migration/2"
-    process_name: "migration/2"
-    thread_id: 32
-    process_id: 32
-  }
-  task_info {
-    estimate_mws: 0.002654
-    estimate_mw: 0.000307
-    thread_name: "qrtr_rx"
-    process_name: "qrtr_rx"
-    thread_id: 564
-    process_id: 564
-  }
-  task_info {
-    estimate_mws: 0.002601
-    estimate_mw: 0.000301
-    thread_name: "card0-crtc0"
-    process_name: "card0-crtc0"
-    thread_id: 247
-    process_id: 247
-  }
-  task_info {
-    estimate_mws: 0.002574
-    estimate_mw: 0.000298
-    thread_name: "pool-7-thread-3"
-    process_name: "com.google.android.wearable.healthservices"
-    thread_id: 5435
-    process_id: 3028
-  }
-  task_info {
-    estimate_mws: 0.002458
-    estimate_mw: 0.000284
-    thread_name: "RenderThread"
-    process_name: "com.google.android.apps.wearable.systemui"
-    thread_id: 2319
-    process_id: 2171
-  }
-  task_info {
-    estimate_mws: 0.002443
-    estimate_mw: 0.000283
-    thread_name: "highpool[0]"
-    process_name: "com.google.android.gms"
-    thread_id: 3154
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.002437
-    estimate_mw: 0.000282
-    thread_name: "lowpool[0]"
-    process_name: "com.google.android.gms"
-    thread_id: 3478
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.002241
-    estimate_mw: 0.000259
-    thread_name: "queued-work-loo"
-    process_name: "com.google.android.gms.persistent"
-    thread_id: 3533
-    process_id: 1949
-  }
-  task_info {
-    estimate_mws: 0.002222
-    estimate_mw: 0.000257
-    thread_name: "arch_disk_io_2"
-    process_name: "com.google.android.gms"
-    thread_id: 4174
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.002160
-    estimate_mw: 0.000250
-    thread_name: "binder:523_2"
-    process_name: "/system/vendor/bin/ipacm"
-    thread_id: 537
-    process_id: 523
-  }
-  task_info {
-    estimate_mws: 0.002129
-    estimate_mw: 0.000246
-    thread_name: "Jit thread pool"
-    process_name: "com.google.android.gms"
-    thread_id: 2881
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.002082
-    estimate_mw: 0.000241
-    thread_name: "pool-48-thread-"
-    process_name: "com.google.android.gms"
-    thread_id: 4110
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.002071
-    estimate_mw: 0.000239
-    thread_name: "RenderThread"
-    process_name: "com.google.android.inputmethod.latin"
-    thread_id: 5120
-    process_id: 4997
-  }
-  task_info {
-    estimate_mws: 0.002020
-    estimate_mw: 0.000234
-    thread_name: "arch_disk_io_0"
-    process_name: "com.google.android.gms"
-    thread_id: 4031
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.001985
-    estimate_mw: 0.000229
-    thread_name: "arch_disk_io_1"
-    process_name: "com.google.android.gms"
-    thread_id: 4034
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.001856
-    estimate_mw: 0.000215
-    thread_name: "BG Thread #0"
-    process_name: "com.google.android.wearable.assistant"
-    thread_id: 4061
-    process_id: 4038
-  }
-  task_info {
-    estimate_mws: 0.001782
-    estimate_mw: 0.000206
-    thread_name: "binder:1926_1"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 1938
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.001776
-    estimate_mw: 0.000205
-    thread_name: "highpool[3]"
-    process_name: "com.google.android.gms"
-    thread_id: 3470
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.001776
-    estimate_mw: 0.000205
-    thread_name: "migration/0"
-    process_name: "migration/0"
-    thread_id: 21
-    process_id: 21
-  }
-  task_info {
-    estimate_mws: 0.001772
-    estimate_mw: 0.000205
-    thread_name: "AsyncTask #2"
-    process_name: "com.google.android.gms"
-    thread_id: 4164
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.001720
-    estimate_mw: 0.000199
-    thread_name: "arch_disk_io_3"
-    process_name: "com.google.android.gms"
-    thread_id: 4175
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.001562
-    estimate_mw: 0.000181
-    thread_name: "POSIX timer 0"
-    process_name: "/vendor/bin/hw/android.hardware.sensors-service.multihal"
-    thread_id: 850
-    process_id: 664
-  }
-  task_info {
-    estimate_mws: 0.001520
-    estimate_mw: 0.000176
-    thread_name: "ksoftirqd/3"
-    process_name: "ksoftirqd/3"
-    thread_id: 42
-    process_id: 42
-  }
-  task_info {
-    estimate_mws: 0.001401
-    estimate_mw: 0.000162
-    thread_name: "Primes-1"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5394
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.001320
-    estimate_mw: 0.000153
-    thread_name: "binder:5377_3"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5392
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.001316
-    estimate_mw: 0.000152
-    thread_name: "msm-watchdog"
-    process_name: "msm-watchdog"
-    thread_id: 76
-    process_id: 76
-  }
-  task_info {
-    estimate_mws: 0.001222
-    estimate_mw: 0.000141
-    thread_name: "Lite Thread #1"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5421
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.001220
-    estimate_mw: 0.000141
-    thread_name: "Signal Catcher"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5382
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.001179
-    estimate_mw: 0.000136
-    thread_name: "GoogleApiHandle"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5398
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.001127
-    estimate_mw: 0.000130
-    thread_name: "binder:2171_6"
-    process_name: "com.google.android.apps.wearable.systemui"
-    thread_id: 2678
-    process_id: 2171
-  }
-  task_info {
-    estimate_mws: 0.001103
-    estimate_mw: 0.000128
-    thread_name: "Blocking Thread"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5574
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.001055
-    estimate_mw: 0.000122
-    thread_name: "WM.task-3"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5430
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000990
-    estimate_mw: 0.000114
-    thread_name: "highpool[1]"
-    process_name: "com.google.android.gms"
-    thread_id: 3373
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.000984
-    estimate_mw: 0.000114
-    thread_name: "Primes-nativecr"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5397
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000961
-    estimate_mw: 0.000111
-    thread_name: "binder:740_4"
-    process_name: "/system/bin/audioserver"
-    thread_id: 2183
-    process_id: 740
-  }
-  task_info {
-    estimate_mws: 0.000954
-    estimate_mw: 0.000110
-    thread_name: "Lite Thread #0"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5404
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000927
-    estimate_mw: 0.000107
-    thread_name: "BG Thread #0"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5395
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000926
-    estimate_mw: 0.000107
-    thread_name: "FinalizerDaemon"
-    process_name: "com.google.android.gms"
-    thread_id: 2885
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.000887
-    estimate_mw: 0.000103
-    thread_name: "BG Thread #1"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5396
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000865
-    estimate_mw: 0.000100
-    thread_name: "Jit thread pool"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5385
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000858
-    estimate_mw: 0.000099
-    thread_name: "highpool[2]"
-    process_name: "com.google.android.gms"
-    thread_id: 3375
-    process_id: 2856
-  }
-  task_info {
-    estimate_mws: 0.000855
-    estimate_mw: 0.000099
-    thread_name: "ConnectivityThr"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5423
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000828
-    estimate_mw: 0.000096
-    thread_name: "Profile Saver"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5393
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000808
-    estimate_mw: 0.000093
-    thread_name: "ReferenceQueueD"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5387
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000803
-    estimate_mw: 0.000093
-    thread_name: "binder:5377_4"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5433
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000783
-    estimate_mw: 0.000091
-    thread_name: "ksoftirqd/1"
-    process_name: "ksoftirqd/1"
-    thread_id: 27
-    process_id: 27
-  }
-  task_info {
-    estimate_mws: 0.000782
-    estimate_mw: 0.000090
-    thread_name: "HsConnectionMan"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5422
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000769
-    estimate_mw: 0.000089
-    thread_name: "ADB-JDWP Connec"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5384
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000742
-    estimate_mw: 0.000086
-    thread_name: "Scheduler Threa"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5428
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000733
-    estimate_mw: 0.000085
-    thread_name: "WM.task-2"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5429
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000730
-    estimate_mw: 0.000084
-    thread_name: "Scheduled BG"
-    process_name: "com.google.android.wearable.sysui"
-    thread_id: 2896
-    process_id: 1926
-  }
-  task_info {
-    estimate_mws: 0.000727
-    estimate_mw: 0.000084
-    thread_name: "DefaultDispatch"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5431
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000726
-    estimate_mw: 0.000084
-    thread_name: "BG Thread #3"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5400
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000724
-    estimate_mw: 0.000084
-    thread_name: "WM.task-1"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5427
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000718
-    estimate_mw: 0.000083
-    thread_name: "binder:5377_1"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5390
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000689
-    estimate_mw: 0.000080
-    thread_name: "DefaultDispatch"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5432
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000669
-    estimate_mw: 0.000077
-    thread_name: "BG Thread #2"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5399
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000630
-    estimate_mw: 0.000073
-    thread_name: "binder:5377_2"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5391
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000583
-    estimate_mw: 0.000067
-    thread_name: "perfetto_hprof_"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5383
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000507
-    estimate_mw: 0.000059
-    thread_name: "Primes-2"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5444
-    process_id: 5377
-  }
-  task_info {
-    estimate_mws: 0.000403
-    estimate_mw: 0.000047
-    thread_name: "FinalizerDaemon"
-    process_name: "com.fitbit.FitbitMobile"
-    thread_id: 5388
-    process_id: 5377
+  metric_version: 4
+  power_model_version: 1
+  period_info {
+    period_id: 1
+    task_info {
+      estimated_mws: 34.416729
+      estimated_mw: 3.979098
+      thread_name: "swapper"
+      thread_id: 0
+      process_id: 0
+    }
+    task_info {
+      estimated_mws: 19.853703
+      estimated_mw: 2.295390
+      idle_transitions_mws: 0.220895
+      thread_name: "RenderThread"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 1986
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 17.530441
+      estimated_mw: 2.026786
+      idle_transitions_mws: 0.028812
+      thread_name: "Jit thread pool"
+      process_name: "system_server"
+      thread_id: 1344
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 16.980274
+      estimated_mw: 1.963178
+      idle_transitions_mws: 0.387957
+      thread_name: "surfaceflinger"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 755
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 14.908094
+      estimated_mw: 1.723603
+      idle_transitions_mws: 0.455047
+      thread_name: ".wearable.sysui"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 1926
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 13.373355
+      estimated_mw: 1.546164
+      idle_transitions_mws: 0.011711
+      thread_name: "binder:685_3"
+      process_name: "/vendor/bin/hw/vendor.qti.hardware.display.composer-service"
+      thread_id: 804
+      process_id: 685
+    }
+    task_info {
+      estimated_mws: 6.747261
+      estimated_mw: 0.780086
+      idle_transitions_mws: 0.021185
+      thread_name: "binder:1302_7"
+      process_name: "system_server"
+      thread_id: 1671
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 6.504173
+      estimated_mw: 0.751981
+      idle_transitions_mws: 0.055166
+      thread_name: "binder:1302_A"
+      process_name: "system_server"
+      thread_id: 2015
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 4.858775
+      estimated_mw: 0.561748
+      idle_transitions_mws: 0.082958
+      thread_name: "android.anim"
+      process_name: "system_server"
+      thread_id: 1419
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 4.769800
+      estimated_mw: 0.551462
+      idle_transitions_mws: 0.094492
+      thread_name: "RenderEngine"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 788
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 4.672233
+      estimated_mw: 0.540181
+      idle_transitions_mws: 0.012303
+      thread_name: "kswapd0"
+      process_name: "kswapd0"
+      thread_id: 63
+      process_id: 63
+    }
+    task_info {
+      estimated_mws: 4.314495
+      estimated_mw: 0.498821
+      thread_name: "lowpool[2]"
+      process_name: "com.google.android.gms"
+      thread_id: 3525
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 4.117818
+      estimated_mw: 0.476083
+      thread_name: "logd.writer"
+      process_name: "/system/bin/logd"
+      thread_id: 221
+      process_id: 211
+    }
+    task_info {
+      estimated_mws: 4.108276
+      estimated_mw: 0.474979
+      idle_transitions_mws: 0.001470
+      thread_name: "binder:1302_17"
+      process_name: "system_server"
+      thread_id: 5202
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 3.723955
+      estimated_mw: 0.430546
+      idle_transitions_mws: 0.046603
+      thread_name: "binder:1302_6"
+      process_name: "system_server"
+      thread_id: 1662
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 3.666289
+      estimated_mw: 0.423879
+      idle_transitions_mws: 0.147155
+      thread_name: "e.watchface.rwf"
+      process_name: "com.google.android.wearable.watchface.rwf"
+      thread_id: 1999
+      process_id: 1999
+    }
+    task_info {
+      estimated_mws: 3.524869
+      estimated_mw: 0.407529
+      idle_transitions_mws: 0.003585
+      thread_name: "killall"
+      process_name: "/system/bin/sh"
+      thread_id: 5620
+      process_id: 5620
+    }
+    task_info {
+      estimated_mws: 3.495762
+      estimated_mw: 0.404163
+      idle_transitions_mws: 0.012035
+      thread_name: "CachedAppOptimi"
+      process_name: "system_server"
+      thread_id: 1773
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 3.459922
+      estimated_mw: 0.400020
+      idle_transitions_mws: 0.022780
+      thread_name: "logcat"
+      process_name: "logcat"
+      thread_id: 1230
+      process_id: 1230
+    }
+    task_info {
+      estimated_mws: 3.429554
+      estimated_mw: 0.396509
+      idle_transitions_mws: 0.020454
+      thread_name: "system_server"
+      process_name: "system_server"
+      thread_id: 1302
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 3.300661
+      estimated_mw: 0.381607
+      idle_transitions_mws: 1.010862
+      thread_name: "crtc_commit:80"
+      process_name: "crtc_commit:80"
+      thread_id: 244
+      process_id: 244
+    }
+    task_info {
+      estimated_mws: 3.194881
+      estimated_mw: 0.369377
+      idle_transitions_mws: 0.163208
+      thread_name: "InputDispatcher"
+      process_name: "system_server"
+      thread_id: 1783
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 3.011913
+      estimated_mw: 0.348223
+      idle_transitions_mws: 0.261953
+      thread_name: "binder:755_1"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 782
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 3.006022
+      estimated_mw: 0.347542
+      idle_transitions_mws: 0.064213
+      thread_name: "android.display"
+      process_name: "system_server"
+      thread_id: 1418
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 2.856301
+      estimated_mw: 0.330232
+      idle_transitions_mws: 0.000982
+      thread_name: "binder:524_2"
+      process_name: "/vendor/bin/mcu_mgmtd"
+      thread_id: 524
+      process_id: 524
+    }
+    task_info {
+      estimated_mws: 2.712443
+      estimated_mw: 0.313600
+      idle_transitions_mws: 0.314323
+      thread_name: "traced_probes"
+      process_name: "/system/bin/traced_probes"
+      thread_id: 904
+      process_id: 904
+    }
+    task_info {
+      estimated_mws: 2.553161
+      estimated_mw: 0.295184
+      idle_transitions_mws: 0.751582
+      thread_name: "kworker/u8:0"
+      process_name: "kworker/u8:0"
+      thread_id: 8
+      process_id: 8
+    }
+    task_info {
+      estimated_mws: 2.487099
+      estimated_mw: 0.287547
+      idle_transitions_mws: 0.509790
+      thread_name: "surfaceflinger"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 883
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 2.386123
+      estimated_mw: 0.275872
+      idle_transitions_mws: 0.002251
+      thread_name: "binder:1302_15"
+      process_name: "system_server"
+      thread_id: 3754
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 2.258779
+      estimated_mw: 0.261149
+      idle_transitions_mws: 0.219690
+      thread_name: "logd.reader.per"
+      process_name: "/system/bin/logd"
+      thread_id: 1274
+      process_id: 211
+    }
+    task_info {
+      estimated_mws: 2.171289
+      estimated_mw: 0.251034
+      idle_transitions_mws: 0.014154
+      thread_name: "RenderThread"
+      process_name: "com.google.android.wearable.watchface.rwf"
+      thread_id: 2301
+      process_id: 1999
+    }
+    task_info {
+      estimated_mws: 2.143151
+      estimated_mw: 0.247781
+      idle_transitions_mws: 0.052887
+      thread_name: "InputReader"
+      process_name: "system_server"
+      thread_id: 1784
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 2.091430
+      estimated_mw: 0.241801
+      idle_transitions_mws: 2.681956
+      thread_name: "rcu_preempt"
+      process_name: "rcu_preempt"
+      thread_id: 14
+      process_id: 14
+    }
+    task_info {
+      estimated_mws: 2.048920
+      estimated_mw: 0.236886
+      idle_transitions_mws: 0.122795
+      thread_name: "binder:1926_4"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 2262
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 1.914560
+      estimated_mw: 0.221352
+      idle_transitions_mws: 0.033359
+      thread_name: "arable.systemui"
+      process_name: "com.google.android.apps.wearable.systemui"
+      thread_id: 2171
+      process_id: 2171
+    }
+    task_info {
+      estimated_mws: 1.854433
+      estimated_mw: 0.214401
+      idle_transitions_mws: 0.046845
+      thread_name: "android.ui"
+      process_name: "system_server"
+      thread_id: 1416
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 1.777087
+      estimated_mw: 0.205458
+      idle_transitions_mws: 0.428240
+      thread_name: "kworker/u8:4"
+      process_name: "kworker/u8:4"
+      thread_id: 431
+      process_id: 431
+    }
+    task_info {
+      estimated_mws: 1.773777
+      estimated_mw: 0.205076
+      idle_transitions_mws: 0.584214
+      thread_name: "TimerDispatch"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 865
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 1.760400
+      estimated_mw: 0.203529
+      idle_transitions_mws: 0.078772
+      thread_name: "ActivityManager"
+      process_name: "system_server"
+      thread_id: 1431
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 1.733169
+      estimated_mw: 0.200381
+      idle_transitions_mws: 0.039163
+      thread_name: "PowerManagerSer"
+      process_name: "system_server"
+      thread_id: 1506
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 1.639501
+      estimated_mw: 0.189551
+      thread_name: "WifiHandlerThre"
+      process_name: "system_server"
+      thread_id: 1818
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 1.631037
+      estimated_mw: 0.188573
+      idle_transitions_mws: 0.170842
+      thread_name: "binder:755_5"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 1987
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 1.605931
+      estimated_mw: 0.185670
+      idle_transitions_mws: 0.400486
+      thread_name: "kgsl_dispatcher"
+      process_name: "kgsl_dispatcher"
+      thread_id: 111
+      process_id: 111
+    }
+    task_info {
+      estimated_mws: 1.564964
+      estimated_mw: 0.180934
+      idle_transitions_mws: 0.000901
+      thread_name: "binder:1302_8"
+      process_name: "system_server"
+      thread_id: 1679
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 1.476619
+      estimated_mw: 0.170720
+      idle_transitions_mws: 0.013637
+      thread_name: "lowpool[5]"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 3489
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 1.470155
+      estimated_mw: 0.169972
+      thread_name: "-Executor] idle"
+      process_name: "com.google.android.gms"
+      thread_id: 5591
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 1.469958
+      estimated_mw: 0.169950
+      idle_transitions_mws: 0.002534
+      thread_name: "binder:1302_B"
+      process_name: "system_server"
+      thread_id: 2033
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 1.390635
+      estimated_mw: 0.160779
+      idle_transitions_mws: 0.083044
+      thread_name: "binder:755_4"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 1125
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 1.327049
+      estimated_mw: 0.153427
+      idle_transitions_mws: 0.029246
+      thread_name: "batterystats-ha"
+      process_name: "system_server"
+      thread_id: 1484
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 1.312721
+      estimated_mw: 0.151771
+      thread_name: "statsd.writer"
+      process_name: "/apex/com.android.os.statsd/bin/statsd"
+      thread_id: 980
+      process_id: 545
+    }
+    task_info {
+      estimated_mws: 1.252738
+      estimated_mw: 0.144836
+      idle_transitions_mws: 0.705073
+      thread_name: "kworker/u8:2"
+      process_name: "kworker/u8:2"
+      thread_id: 62
+      process_id: 62
+    }
+    task_info {
+      estimated_mws: 1.251944
+      estimated_mw: 0.144744
+      idle_transitions_mws: 0.206733
+      thread_name: "app"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 867
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 1.233674
+      estimated_mw: 0.142632
+      idle_transitions_mws: 0.066972
+      thread_name: "system_server"
+      process_name: "system_server"
+      thread_id: 1343
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 1.228813
+      estimated_mw: 0.142070
+      idle_transitions_mws: 0.785543
+      thread_name: "irq/33-4520300."
+      process_name: "irq/33-4520300.qcom,bwmon-ddr"
+      thread_id: 95
+      process_id: 95
+    }
+    task_info {
+      estimated_mws: 1.068197
+      estimated_mw: 0.123500
+      idle_transitions_mws: 0.111670
+      thread_name: "logd.klogd"
+      process_name: "/system/bin/logd"
+      thread_id: 234
+      process_id: 211
+    }
+    task_info {
+      estimated_mws: 1.007070
+      estimated_mw: 0.116433
+      idle_transitions_mws: 0.029416
+      thread_name: "android.fg"
+      process_name: "system_server"
+      thread_id: 1415
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.969980
+      estimated_mw: 0.112144
+      idle_transitions_mws: 0.455999
+      thread_name: "rcuog/0"
+      process_name: "rcuog/0"
+      thread_id: 15
+      process_id: 15
+    }
+    task_info {
+      estimated_mws: 0.952077
+      estimated_mw: 0.110075
+      idle_transitions_mws: 0.102993
+      thread_name: "binder:1926_3"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 1940
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.946746
+      estimated_mw: 0.109458
+      idle_transitions_mws: 0.007196
+      thread_name: "gle.android.gms"
+      process_name: "com.google.android.gms"
+      thread_id: 2856
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.930774
+      estimated_mw: 0.107612
+      idle_transitions_mws: 0.738786
+      thread_name: "crtc_event:80"
+      process_name: "crtc_event:80"
+      thread_id: 245
+      process_id: 245
+    }
+    task_info {
+      estimated_mws: 0.907425
+      estimated_mw: 0.104912
+      idle_transitions_mws: 0.009862
+      thread_name: "binder:755_3"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 1124
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 0.897620
+      estimated_mw: 0.103779
+      idle_transitions_mws: 0.007379
+      thread_name: "init"
+      process_name: "/system/bin/init"
+      thread_id: 143
+      process_id: 1
+    }
+    task_info {
+      estimated_mws: 0.880853
+      estimated_mw: 0.101840
+      idle_transitions_mws: 0.013499
+      thread_name: "wmshell.main"
+      process_name: "com.google.android.apps.wearable.systemui"
+      thread_id: 2260
+      process_id: 2171
+    }
+    task_info {
+      estimated_mws: 0.870598
+      estimated_mw: 0.100654
+      idle_transitions_mws: 0.005937
+      thread_name: "Primes-1"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 1944
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.847641
+      estimated_mw: 0.098000
+      idle_transitions_mws: 0.013271
+      thread_name: "init"
+      process_name: "/system/bin/init"
+      thread_id: 1
+      process_id: 1
+    }
+    task_info {
+      estimated_mws: 0.846054
+      estimated_mw: 0.097817
+      thread_name: "binder:1302_D"
+      process_name: "system_server"
+      thread_id: 2043
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.844958
+      estimated_mw: 0.097690
+      idle_transitions_mws: 0.202229
+      thread_name: "surfaceflinger"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 786
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 0.833920
+      estimated_mw: 0.096414
+      idle_transitions_mws: 0.342121
+      thread_name: "kworker/u8:5"
+      process_name: "kworker/u8:5"
+      thread_id: 5304
+      process_id: 5304
+    }
+    task_info {
+      estimated_mws: 0.780835
+      estimated_mw: 0.090276
+      idle_transitions_mws: 0.182034
+      thread_name: "kworker/2:4"
+      process_name: "kworker/2:4"
+      thread_id: 4995
+      process_id: 4995
+    }
+    task_info {
+      estimated_mws: 0.747755
+      estimated_mw: 0.086452
+      idle_transitions_mws: 0.004701
+      thread_name: "binder:2171_4"
+      process_name: "com.google.android.apps.wearable.systemui"
+      thread_id: 2374
+      process_id: 2171
+    }
+    task_info {
+      estimated_mws: 0.746488
+      estimated_mw: 0.086305
+      idle_transitions_mws: 0.043573
+      thread_name: "binder:1999_5"
+      process_name: "com.google.android.wearable.watchface.rwf"
+      thread_id: 3678
+      process_id: 1999
+    }
+    task_info {
+      estimated_mws: 0.744159
+      estimated_mw: 0.086036
+      idle_transitions_mws: 0.120816
+      thread_name: "servicemanager"
+      process_name: "/system/bin/servicemanager"
+      thread_id: 213
+      process_id: 213
+    }
+    task_info {
+      estimated_mws: 0.717520
+      estimated_mw: 0.082956
+      idle_transitions_mws: 0.004484
+      thread_name: "wmshell.anim"
+      process_name: "com.google.android.apps.wearable.systemui"
+      thread_id: 2269
+      process_id: 2171
+    }
+    task_info {
+      estimated_mws: 0.699681
+      estimated_mw: 0.080894
+      thread_name: "GoogleApiHandle"
+      process_name: "com.google.android.gms"
+      thread_id: 3208
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.675997
+      estimated_mw: 0.078156
+      idle_transitions_mws: 0.003826
+      thread_name: "binder:1302_4"
+      process_name: "system_server"
+      thread_id: 1592
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.647999
+      estimated_mw: 0.074919
+      thread_name: "batterystats-wo"
+      process_name: "system_server"
+      thread_id: 1487
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.640576
+      estimated_mw: 0.074060
+      idle_transitions_mws: 0.005443
+      thread_name: ".gms.persistent"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 1949
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.631830
+      estimated_mw: 0.073049
+      idle_transitions_mws: 0.013282
+      thread_name: "binder:1926_6"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 5211
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.627672
+      estimated_mw: 0.072568
+      idle_transitions_mws: 0.000795
+      thread_name: "DisplayOffloadB"
+      process_name: "system_server"
+      thread_id: 1512
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.627487
+      estimated_mw: 0.072547
+      thread_name: "binder:682_2"
+      process_name: "/vendor/bin/hw/vendor.qti.hardware.display.allocator-service"
+      thread_id: 682
+      process_id: 682
+    }
+    task_info {
+      estimated_mws: 0.624294
+      estimated_mw: 0.072178
+      idle_transitions_mws: 0.431093
+      thread_name: "rcuog/2"
+      process_name: "rcuog/2"
+      thread_id: 37
+      process_id: 37
+    }
+    task_info {
+      estimated_mws: 0.623909
+      estimated_mw: 0.072133
+      idle_transitions_mws: 0.274820
+      thread_name: "kworker/0:6"
+      process_name: "kworker/0:6"
+      thread_id: 586
+      process_id: 586
+    }
+    task_info {
+      estimated_mws: 0.597177
+      estimated_mw: 0.069043
+      thread_name: "diag-router"
+      process_name: "/vendor/bin/diag-router"
+      thread_id: 634
+      process_id: 634
+    }
+    task_info {
+      estimated_mws: 0.582498
+      estimated_mw: 0.067346
+      thread_name: "HeapTaskDaemon"
+      process_name: "com.google.android.gms"
+      thread_id: 2882
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.579675
+      estimated_mw: 0.067019
+      idle_transitions_mws: 0.102756
+      thread_name: "FileWatcherThre"
+      process_name: "/vendor/bin/hw/android.hardware.thermal-service.pixel"
+      thread_id: 1411
+      process_id: 1404
+    }
+    task_info {
+      estimated_mws: 0.568415
+      estimated_mw: 0.065717
+      idle_transitions_mws: 0.004498
+      thread_name: "TaskSnapshotPer"
+      process_name: "system_server"
+      thread_id: 1913
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.565566
+      estimated_mw: 0.065388
+      idle_transitions_mws: 0.016823
+      thread_name: "lmkd"
+      process_name: "/system/bin/lmkd"
+      thread_id: 212
+      process_id: 212
+    }
+    task_info {
+      estimated_mws: 0.554734
+      estimated_mw: 0.064136
+      idle_transitions_mws: 0.001437
+      thread_name: "binder:1949_8"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 3269
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.517529
+      estimated_mw: 0.059834
+      idle_transitions_mws: 0.084239
+      thread_name: "appSf"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 868
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 0.514221
+      estimated_mw: 0.059452
+      idle_transitions_mws: 0.542479
+      thread_name: "kworker/1:1"
+      process_name: "kworker/1:1"
+      thread_id: 47
+      process_id: 47
+    }
+    task_info {
+      estimated_mws: 0.507581
+      estimated_mw: 0.058684
+      idle_transitions_mws: 0.039847
+      thread_name: "android.hardwar"
+      process_name: "/vendor/bin/hw/android.hardware.usb-service.qti"
+      thread_id: 1861
+      process_id: 665
+    }
+    task_info {
+      estimated_mws: 0.504068
+      estimated_mw: 0.058278
+      idle_transitions_mws: 0.058645
+      thread_name: "Primes-Jank"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 2389
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.493578
+      estimated_mw: 0.057065
+      idle_transitions_mws: 0.006902
+      thread_name: "binder:2171_3"
+      process_name: "com.google.android.apps.wearable.systemui"
+      thread_id: 2235
+      process_id: 2171
+    }
+    task_info {
+      estimated_mws: 0.490345
+      estimated_mw: 0.056691
+      idle_transitions_mws: 0.005860
+      thread_name: "traced"
+      process_name: "/system/bin/traced"
+      thread_id: 905
+      process_id: 905
+    }
+    task_info {
+      estimated_mws: 0.468415
+      estimated_mw: 0.054156
+      thread_name: "eduling.default"
+      process_name: "system_server"
+      thread_id: 1761
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.462913
+      estimated_mw: 0.053520
+      idle_transitions_mws: 0.021445
+      thread_name: "binder:545_2"
+      process_name: "/apex/com.android.os.statsd/bin/statsd"
+      thread_id: 553
+      process_id: 545
+    }
+    task_info {
+      estimated_mws: 0.462537
+      estimated_mw: 0.053476
+      thread_name: "User"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 2234
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.454063
+      estimated_mw: 0.052497
+      idle_transitions_mws: 0.032182
+      thread_name: "putmethod.latin"
+      process_name: "com.google.android.inputmethod.latin"
+      thread_id: 4997
+      process_id: 4997
+    }
+    task_info {
+      estimated_mws: 0.450612
+      estimated_mw: 0.052098
+      thread_name: "ueventd"
+      process_name: "/system/bin/ueventd"
+      thread_id: 145
+      process_id: 145
+    }
+    task_info {
+      estimated_mws: 0.448044
+      estimated_mw: 0.051801
+      thread_name: "wpa_supplicant"
+      process_name: "/vendor/bin/hw/wpa_supplicant"
+      thread_id: 5214
+      process_id: 5214
+    }
+    task_info {
+      estimated_mws: 0.431304
+      estimated_mw: 0.049865
+      idle_transitions_mws: 0.223655
+      thread_name: "rcuop/0"
+      process_name: "rcuop/0"
+      thread_id: 16
+      process_id: 16
+    }
+    task_info {
+      estimated_mws: 0.416635
+      estimated_mw: 0.048169
+      idle_transitions_mws: 0.003314
+      thread_name: "Jit thread pool"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 1933
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.404592
+      estimated_mw: 0.046777
+      thread_name: "pixelstats-vend"
+      process_name: "/vendor/bin/pixelstats-vendor"
+      thread_id: 267
+      process_id: 255
+    }
+    task_info {
+      estimated_mws: 0.396838
+      estimated_mw: 0.045880
+      idle_transitions_mws: 0.096821
+      thread_name: "irq/236-NVT-ts"
+      process_name: "irq/236-NVT-ts"
+      thread_id: 505
+      process_id: 505
+    }
+    task_info {
+      estimated_mws: 0.393249
+      estimated_mw: 0.045466
+      idle_transitions_mws: 0.004124
+      thread_name: "nanohub"
+      process_name: "nanohub"
+      thread_id: 297
+      process_id: 297
+    }
+    task_info {
+      estimated_mws: 0.376686
+      estimated_mw: 0.043551
+      idle_transitions_mws: 0.007074
+      thread_name: "android.bg"
+      process_name: "system_server"
+      thread_id: 1430
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.375869
+      estimated_mw: 0.043456
+      idle_transitions_mws: 0.002342
+      thread_name: "chre"
+      process_name: "/vendor/bin/chre"
+      thread_id: 1041
+      process_id: 1041
+    }
+    task_info {
+      estimated_mws: 0.373520
+      estimated_mw: 0.043185
+      idle_transitions_mws: 0.003289
+      thread_name: "lowpool[1]"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 2279
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.366001
+      estimated_mw: 0.042315
+      idle_transitions_mws: 0.018638
+      thread_name: "TracingMuxer"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 783
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 0.359494
+      estimated_mw: 0.041563
+      idle_transitions_mws: 0.136611
+      thread_name: "kgsl-events"
+      process_name: "kgsl-events"
+      thread_id: 109
+      process_id: 109
+    }
+    task_info {
+      estimated_mws: 0.359130
+      estimated_mw: 0.041521
+      thread_name: "IpClient.wlan0"
+      process_name: "com.android.networkstack.process"
+      thread_id: 5216
+      process_id: 2049
+    }
+    task_info {
+      estimated_mws: 0.346517
+      estimated_mw: 0.040063
+      idle_transitions_mws: 0.001877
+      thread_name: "binder:257_5"
+      process_name: "/system/bin/hw/android.system.suspend-service"
+      thread_id: 1491
+      process_id: 257
+    }
+    task_info {
+      estimated_mws: 0.341200
+      estimated_mw: 0.039448
+      idle_transitions_mws: 0.001228
+      thread_name: "binder:1901_3"
+      process_name: "/vendor/bin/hw/android.hardware.wifi-service-lazy"
+      thread_id: 1905
+      process_id: 1901
+    }
+    task_info {
+      estimated_mws: 0.335534
+      estimated_mw: 0.038793
+      thread_name: "binder:740_1"
+      process_name: "/system/bin/audioserver"
+      thread_id: 821
+      process_id: 740
+    }
+    task_info {
+      estimated_mws: 0.331405
+      estimated_mw: 0.038316
+      idle_transitions_mws: 0.001044
+      thread_name: "BG"
+      process_name: "com.google.wear.services"
+      thread_id: 2023
+      process_id: 1948
+    }
+    task_info {
+      estimated_mws: 0.326344
+      estimated_mw: 0.037730
+      idle_transitions_mws: 0.045342
+      thread_name: "kworker/0:5H"
+      process_name: "kworker/0:5H"
+      thread_id: 1337
+      process_id: 1337
+    }
+    task_info {
+      estimated_mws: 0.322384
+      estimated_mw: 0.037273
+      idle_transitions_mws: 0.002751
+      thread_name: "binder:755_2"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 784
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 0.319511
+      estimated_mw: 0.036940
+      thread_name: "audioserver"
+      process_name: "/system/bin/audioserver"
+      thread_id: 740
+      process_id: 740
+    }
+    task_info {
+      estimated_mws: 0.310996
+      estimated_mw: 0.035956
+      idle_transitions_mws: 0.007033
+      thread_name: "binder:1949_2"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 1978
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.302115
+      estimated_mw: 0.034929
+      idle_transitions_mws: 0.000843
+      thread_name: "-Executor] idle"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 5602
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.301578
+      estimated_mw: 0.034867
+      thread_name: "pool-11-thread-"
+      process_name: "com.google.android.wearable.healthservices"
+      thread_id: 3329
+      process_id: 3028
+    }
+    task_info {
+      estimated_mws: 0.299169
+      estimated_mw: 0.034589
+      thread_name: "android.io"
+      process_name: "system_server"
+      thread_id: 1417
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.296825
+      estimated_mw: 0.034318
+      thread_name: "binder:1901_3"
+      process_name: "/vendor/bin/hw/android.hardware.wifi-service-lazy"
+      thread_id: 5205
+      process_id: 1901
+    }
+    task_info {
+      estimated_mws: 0.294242
+      estimated_mw: 0.034019
+      idle_transitions_mws: 0.142357
+      thread_name: "rcuop/1"
+      process_name: "rcuop/1"
+      thread_id: 30
+      process_id: 30
+    }
+    task_info {
+      estimated_mws: 0.286642
+      estimated_mw: 0.033140
+      thread_name: "binder:1948_6"
+      process_name: "com.google.wear.services"
+      thread_id: 5315
+      process_id: 1948
+    }
+    task_info {
+      estimated_mws: 0.285983
+      estimated_mw: 0.033064
+      thread_name: "AssistantHandle"
+      process_name: "com.google.android.wearable.assistant"
+      thread_id: 4081
+      process_id: 4038
+    }
+    task_info {
+      estimated_mws: 0.283378
+      estimated_mw: 0.032763
+      idle_transitions_mws: 0.023300
+      thread_name: "binder:1999_1"
+      process_name: "com.google.android.wearable.watchface.rwf"
+      thread_id: 2016
+      process_id: 1999
+    }
+    task_info {
+      estimated_mws: 0.279959
+      estimated_mw: 0.032367
+      idle_transitions_mws: 0.001981
+      thread_name: "binder:2182_7"
+      process_name: "com.android.phone"
+      thread_id: 2694
+      process_id: 2182
+    }
+    task_info {
+      estimated_mws: 0.279816
+      estimated_mw: 0.032351
+      idle_transitions_mws: 0.055476
+      thread_name: "kworker/3:2H"
+      process_name: "kworker/3:2H"
+      thread_id: 226
+      process_id: 226
+    }
+    task_info {
+      estimated_mws: 0.277230
+      estimated_mw: 0.032052
+      idle_transitions_mws: 0.002913
+      thread_name: "BG"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 3005
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.274735
+      estimated_mw: 0.031764
+      thread_name: "lowpool[3]"
+      process_name: "com.google.android.gms"
+      thread_id: 3527
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.267749
+      estimated_mw: 0.030956
+      idle_transitions_mws: 0.038805
+      thread_name: "hvdcp_opti"
+      process_name: "/vendor/bin/hvdcp_opti"
+      thread_id: 1276
+      process_id: 1270
+    }
+    task_info {
+      estimated_mws: 0.262081
+      estimated_mw: 0.030301
+      idle_transitions_mws: 0.190243
+      thread_name: "binder:1926_3"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 2022
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.259248
+      estimated_mw: 0.029973
+      idle_transitions_mws: 0.172554
+      thread_name: "kworker/3:5"
+      process_name: "kworker/3:5"
+      thread_id: 104
+      process_id: 104
+    }
+    task_info {
+      estimated_mws: 0.256714
+      estimated_mw: 0.029680
+      idle_transitions_mws: 0.002836
+      thread_name: "binder:257_2"
+      process_name: "/system/bin/hw/android.system.suspend-service"
+      thread_id: 264
+      process_id: 257
+    }
+    task_info {
+      estimated_mws: 0.247037
+      estimated_mw: 0.028561
+      idle_transitions_mws: 0.088455
+      thread_name: "SDM_EventThread"
+      process_name: "/vendor/bin/hw/vendor.qti.hardware.display.composer-service"
+      thread_id: 727
+      process_id: 685
+    }
+    task_info {
+      estimated_mws: 0.244112
+      estimated_mw: 0.028223
+      thread_name: "POSIX timer 2"
+      process_name: "/vendor/bin/hw/android.hardware.sensors-service.multihal"
+      thread_id: 1600
+      process_id: 664
+    }
+    task_info {
+      estimated_mws: 0.242754
+      estimated_mw: 0.028066
+      idle_transitions_mws: 0.005424
+      thread_name: "binder:2856_4"
+      process_name: "com.google.android.gms"
+      thread_id: 3679
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.241348
+      estimated_mw: 0.027904
+      thread_name: "pool-2-thread-1"
+      process_name: "com.android.networkstack.process"
+      thread_id: 2416
+      process_id: 2049
+    }
+    task_info {
+      estimated_mws: 0.231145
+      estimated_mw: 0.026724
+      idle_transitions_mws: 0.081316
+      thread_name: "rcuop/3"
+      process_name: "rcuop/3"
+      thread_id: 45
+      process_id: 45
+    }
+    task_info {
+      estimated_mws: 0.230341
+      estimated_mw: 0.026631
+      idle_transitions_mws: 0.003605
+      thread_name: "f2fs_ckpt-254:4"
+      process_name: "f2fs_ckpt-254:43"
+      thread_id: 347
+      process_id: 347
+    }
+    task_info {
+      estimated_mws: 0.229722
+      estimated_mw: 0.026559
+      thread_name: "OomAdjuster"
+      process_name: "system_server"
+      thread_id: 1482
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.226417
+      estimated_mw: 0.026177
+      idle_transitions_mws: 0.001358
+      thread_name: "binder:740_6"
+      process_name: "/system/bin/audioserver"
+      thread_id: 2639
+      process_id: 740
+    }
+    task_info {
+      estimated_mws: 0.226007
+      estimated_mw: 0.026130
+      idle_transitions_mws: 0.054115
+      thread_name: "rcuop/2"
+      process_name: "rcuop/2"
+      thread_id: 38
+      process_id: 38
+    }
+    task_info {
+      estimated_mws: 0.225133
+      estimated_mw: 0.026029
+      idle_transitions_mws: 0.065811
+      thread_name: "kworker/0:7"
+      process_name: "kworker/0:7"
+      thread_id: 598
+      process_id: 598
+    }
+    task_info {
+      estimated_mws: 0.212788
+      estimated_mw: 0.024602
+      idle_transitions_mws: 0.006439
+      thread_name: "queued-work-loo"
+      process_name: "system_server"
+      thread_id: 1886
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.202562
+      estimated_mw: 0.023419
+      thread_name: "pool-13-thread-"
+      process_name: "com.google.android.wearable.healthservices"
+      thread_id: 3327
+      process_id: 3028
+    }
+    task_info {
+      estimated_mws: 0.201687
+      estimated_mw: 0.023318
+      idle_transitions_mws: 0.001195
+      thread_name: "WearSdkThread"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 2207
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.201119
+      estimated_mw: 0.023252
+      thread_name: "qrtr_ns"
+      process_name: "qrtr_ns"
+      thread_id: 88
+      process_id: 88
+    }
+    task_info {
+      estimated_mws: 0.200639
+      estimated_mw: 0.023197
+      thread_name: "binder:740_7"
+      process_name: "/system/bin/audioserver"
+      thread_id: 5206
+      process_id: 740
+    }
+    task_info {
+      estimated_mws: 0.196587
+      estimated_mw: 0.022729
+      idle_transitions_mws: 0.002857
+      thread_name: "binder:4997_4"
+      process_name: "com.google.android.inputmethod.latin"
+      thread_id: 5122
+      process_id: 4997
+    }
+    task_info {
+      estimated_mws: 0.194754
+      estimated_mw: 0.022517
+      thread_name: "m.android.phone"
+      process_name: "com.android.phone"
+      thread_id: 2182
+      process_id: 2182
+    }
+    task_info {
+      estimated_mws: 0.192336
+      estimated_mw: 0.022237
+      idle_transitions_mws: 0.006386
+      thread_name: "HwcAsyncWorker"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 835
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 0.190522
+      estimated_mw: 0.022027
+      thread_name: "binder:636_2"
+      process_name: "/vendor/bin/hw/android.hardware.audio.service"
+      thread_id: 636
+      process_id: 636
+    }
+    task_info {
+      estimated_mws: 0.188908
+      estimated_mw: 0.021841
+      thread_name: "SettingsProvide"
+      process_name: "system_server"
+      thread_id: 1771
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.181172
+      estimated_mw: 0.020946
+      thread_name: "binder:1302_2"
+      process_name: "system_server"
+      thread_id: 1350
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.177579
+      estimated_mw: 0.020531
+      idle_transitions_mws: 0.111479
+      thread_name: "RegSampIdle"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 872
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 0.168738
+      estimated_mw: 0.019509
+      thread_name: "ice] processing"
+      process_name: "com.google.android.gms"
+      thread_id: 3238
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.162808
+      estimated_mw: 0.018823
+      thread_name: "binder:682_3"
+      process_name: "/vendor/bin/hw/vendor.qti.hardware.display.allocator-service"
+      thread_id: 2308
+      process_id: 682
+    }
+    task_info {
+      estimated_mws: 0.158857
+      estimated_mw: 0.018366
+      thread_name: "binder:678_3"
+      process_name: "/apex/com.google.wearable.wac.whshal/bin/hw/vendor.google.wearable.wac.whshal@2.0-service"
+      thread_id: 1884
+      process_id: 678
+    }
+    task_info {
+      estimated_mws: 0.158692
+      estimated_mw: 0.018347
+      idle_transitions_mws: 0.016263
+      thread_name: "binder:650_4"
+      process_name: "/vendor/bin/hw/android.hardware.gnss-aidl-service-qti"
+      thread_id: 5498
+      process_id: 650
+    }
+    task_info {
+      estimated_mws: 0.157956
+      estimated_mw: 0.018262
+      idle_transitions_mws: 0.002306
+      thread_name: "vndservicemanag"
+      process_name: "/vendor/bin/vndservicemanager"
+      thread_id: 215
+      process_id: 215
+    }
+    task_info {
+      estimated_mws: 0.153600
+      estimated_mw: 0.017759
+      thread_name: "GoogleLocationS"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 3355
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.153038
+      estimated_mw: 0.017694
+      thread_name: "TransportThread"
+      process_name: "/vendor/bin/chre"
+      thread_id: 1078
+      process_id: 1041
+    }
+    task_info {
+      estimated_mws: 0.152058
+      estimated_mw: 0.017580
+      idle_transitions_mws: 0.068137
+      thread_name: "kworker/2:1H"
+      process_name: "kworker/2:1H"
+      thread_id: 123
+      process_id: 123
+    }
+    task_info {
+      estimated_mws: 0.148559
+      estimated_mw: 0.017176
+      idle_transitions_mws: 0.002115
+      thread_name: "BG"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 2120
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.148352
+      estimated_mw: 0.017152
+      thread_name: "vendor.google.w"
+      process_name: "/apex/com.google.wearable.wac.whshal/bin/hw/vendor.google.wearable.wac.whshal@2.0-service"
+      thread_id: 1881
+      process_id: 678
+    }
+    task_info {
+      estimated_mws: 0.140864
+      estimated_mw: 0.016286
+      thread_name: "NetworkStats"
+      process_name: "system_server"
+      thread_id: 1814
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.138579
+      estimated_mw: 0.016022
+      idle_transitions_mws: 0.021968
+      thread_name: "binder:969_2"
+      process_name: "/system/vendor/bin/cnd"
+      thread_id: 1011
+      process_id: 969
+    }
+    task_info {
+      estimated_mws: 0.134607
+      estimated_mw: 0.015563
+      idle_transitions_mws: 0.010586
+      thread_name: "dmabuf-deferred"
+      process_name: "dmabuf-deferred-free-worker"
+      thread_id: 69
+      process_id: 69
+    }
+    task_info {
+      estimated_mws: 0.129449
+      estimated_mw: 0.014966
+      thread_name: "highpool[5]"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 3354
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.126973
+      estimated_mw: 0.014680
+      thread_name: "ice] processing"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 2363
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.126830
+      estimated_mw: 0.014663
+      thread_name: "ediator.Toggler"
+      process_name: "system_server"
+      thread_id: 1910
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.125742
+      estimated_mw: 0.014538
+      idle_transitions_mws: 0.064827
+      thread_name: "surfaceflinger"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 875
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 0.123833
+      estimated_mw: 0.014317
+      thread_name: "wificond"
+      process_name: "/system/bin/wificond"
+      thread_id: 964
+      process_id: 964
+    }
+    task_info {
+      estimated_mws: 0.123248
+      estimated_mw: 0.014249
+      thread_name: "MobileDataStats"
+      process_name: "system_server"
+      thread_id: 1912
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.119885
+      estimated_mw: 0.013860
+      thread_name: "GlobalScheduler"
+      process_name: "com.google.android.gms"
+      thread_id: 3156
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.119479
+      estimated_mw: 0.013814
+      idle_transitions_mws: 0.000930
+      thread_name: "RenderThread"
+      thread_id: 5599
+    }
+    task_info {
+      estimated_mws: 0.119243
+      estimated_mw: 0.013786
+      idle_transitions_mws: 0.008398
+      thread_name: "TouchTimer"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 866
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 0.114249
+      estimated_mw: 0.013209
+      thread_name: "binder:4038_1"
+      process_name: "com.google.android.wearable.assistant"
+      thread_id: 4050
+      process_id: 4038
+    }
+    task_info {
+      estimated_mws: 0.112705
+      estimated_mw: 0.013030
+      idle_transitions_mws: 0.011129
+      thread_name: "displayoffload@"
+      process_name: "/vendor/bin/hw/vendor.google_clockwork.displayoffload@2.0-service.1p"
+      thread_id: 937
+      process_id: 937
+    }
+    task_info {
+      estimated_mws: 0.111279
+      estimated_mw: 0.012866
+      idle_transitions_mws: 0.003661
+      thread_name: "adbd"
+      process_name: "/apex/com.android.adbd/bin/adbd"
+      thread_id: 5544
+      process_id: 5544
+    }
+    task_info {
+      estimated_mws: 0.108114
+      estimated_mw: 0.012500
+      thread_name: "kworker/1:2H"
+      process_name: "kworker/1:2H"
+      thread_id: 300
+      process_id: 300
+    }
+    task_info {
+      estimated_mws: 0.107442
+      estimated_mw: 0.012422
+      idle_transitions_mws: 0.003376
+      thread_name: "RenderThread"
+      thread_id: 5584
+    }
+    task_info {
+      estimated_mws: 0.105863
+      estimated_mw: 0.012239
+      idle_transitions_mws: 0.006458
+      thread_name: "Primes-2"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 1946
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.104306
+      estimated_mw: 0.012059
+      thread_name: "iptables-restor"
+      process_name: "/system/bin/iptables-restore"
+      thread_id: 558
+      process_id: 558
+    }
+    task_info {
+      estimated_mws: 0.104093
+      estimated_mw: 0.012035
+      thread_name: "RenderThread"
+      process_name: "system_server"
+      thread_id: 5223
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.102912
+      estimated_mw: 0.011898
+      idle_transitions_mws: 0.006649
+      thread_name: "irq/168-nanohub"
+      process_name: "irq/168-nanohub-irq1"
+      thread_id: 296
+      process_id: 296
+    }
+    task_info {
+      estimated_mws: 0.102167
+      estimated_mw: 0.011812
+      idle_transitions_mws: 0.003890
+      thread_name: "RenderThread"
+      thread_id: 5604
+    }
+    task_info {
+      estimated_mws: 0.101945
+      estimated_mw: 0.011786
+      thread_name: "ksoftirqd/2"
+      process_name: "ksoftirqd/2"
+      thread_id: 34
+      process_id: 34
+    }
+    task_info {
+      estimated_mws: 0.101282
+      estimated_mw: 0.011710
+      thread_name: "PhotonicModulat"
+      process_name: "system_server"
+      thread_id: 1899
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.100285
+      estimated_mw: 0.011595
+      thread_name: "ip6tables-resto"
+      process_name: "/system/bin/ip6tables-restore"
+      thread_id: 559
+      process_id: 559
+    }
+    task_info {
+      estimated_mws: 0.099432
+      estimated_mw: 0.011496
+      thread_name: "init"
+      process_name: "/system/bin/init"
+      thread_id: 144
+      process_id: 144
+    }
+    task_info {
+      estimated_mws: 0.096314
+      estimated_mw: 0.011135
+      thread_name: "FrameworkReceiv"
+      process_name: ".qtidataservices"
+      thread_id: 2793
+      process_id: 2118
+    }
+    task_info {
+      estimated_mws: 0.095442
+      estimated_mw: 0.011035
+      thread_name: "Jit thread pool"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 1969
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.094448
+      estimated_mw: 0.010920
+      thread_name: "pool-14-thread-"
+      process_name: "com.google.android.wearable.healthservices"
+      thread_id: 3314
+      process_id: 3028
+    }
+    task_info {
+      estimated_mws: 0.093937
+      estimated_mw: 0.010861
+      thread_name: "binder:1926_2"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 1939
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.093621
+      estimated_mw: 0.010824
+      thread_name: "ChreMsgHandler"
+      process_name: "/vendor/bin/chre"
+      thread_id: 1080
+      process_id: 1041
+    }
+    task_info {
+      estimated_mws: 0.091738
+      estimated_mw: 0.010606
+      idle_transitions_mws: 0.001106
+      thread_name: "DispatcherModul"
+      process_name: "/vendor/bin/hw/qcrilNrd"
+      thread_id: 1673
+      process_id: 1062
+    }
+    task_info {
+      estimated_mws: 0.091698
+      estimated_mw: 0.010602
+      idle_transitions_mws: 0.047196
+      thread_name: "irq/234-pixart_"
+      process_name: "irq/234-pixart_pat9126_irq"
+      thread_id: 500
+      process_id: 500
+    }
+    task_info {
+      estimated_mws: 0.090883
+      estimated_mw: 0.010507
+      thread_name: "scheduler_threa"
+      process_name: "scheduler_thread"
+      thread_id: 5198
+      process_id: 5198
+    }
+    task_info {
+      estimated_mws: 0.089001
+      estimated_mw: 0.010290
+      thread_name: "binder:2085_4"
+      process_name: "com.google.android.bluetooth"
+      thread_id: 2713
+      process_id: 2085
+    }
+    task_info {
+      estimated_mws: 0.086934
+      estimated_mw: 0.010051
+      idle_transitions_mws: 0.001387
+      thread_name: "binder:3028_5"
+      process_name: "com.google.android.wearable.healthservices"
+      thread_id: 5434
+      process_id: 3028
+    }
+    task_info {
+      estimated_mws: 0.085941
+      estimated_mw: 0.009936
+      thread_name: "binder:2670_6"
+      process_name: "com.android.nfc"
+      thread_id: 3159
+      process_id: 2670
+    }
+    task_info {
+      estimated_mws: 0.083859
+      estimated_mw: 0.009695
+      idle_transitions_mws: 0.289424
+      thread_name: "psimon"
+      process_name: "psimon"
+      thread_id: 1480
+      process_id: 1480
+    }
+    task_info {
+      estimated_mws: 0.083773
+      estimated_mw: 0.009685
+      thread_name: "binder:233_2"
+      process_name: "/system/bin/vold"
+      thread_id: 252
+      process_id: 233
+    }
+    task_info {
+      estimated_mws: 0.080959
+      estimated_mw: 0.009360
+      thread_name: "binder:2049_2"
+      process_name: "com.android.networkstack.process"
+      thread_id: 2068
+      process_id: 2049
+    }
+    task_info {
+      estimated_mws: 0.080298
+      estimated_mw: 0.009284
+      thread_name: "netd"
+      process_name: "/system/bin/netd"
+      thread_id: 568
+      process_id: 546
+    }
+    task_info {
+      estimated_mws: 0.080265
+      estimated_mw: 0.009280
+      thread_name: "UEventObserver"
+      process_name: "system_server"
+      thread_id: 1857
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.079337
+      estimated_mw: 0.009173
+      thread_name: "RenderThread"
+      thread_id: 5619
+    }
+    task_info {
+      estimated_mws: 0.078696
+      estimated_mw: 0.009098
+      thread_name: "pool-8-thread-1"
+      process_name: "com.google.android.gms"
+      thread_id: 3102
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.077362
+      estimated_mw: 0.008944
+      idle_transitions_mws: 0.019002
+      thread_name: "mcu_mgmtd"
+      process_name: "/vendor/bin/mcu_mgmtd"
+      thread_id: 594
+      process_id: 524
+    }
+    task_info {
+      estimated_mws: 0.074501
+      estimated_mw: 0.008613
+      thread_name: "spi0"
+      process_name: "spi0"
+      thread_id: 295
+      process_id: 295
+    }
+    task_info {
+      estimated_mws: 0.073914
+      estimated_mw: 0.008546
+      thread_name: "com.android.nfc"
+      process_name: "com.android.nfc"
+      thread_id: 2670
+      process_id: 2670
+    }
+    task_info {
+      estimated_mws: 0.072671
+      estimated_mw: 0.008402
+      idle_transitions_mws: 0.063131
+      thread_name: "rcu_exp_gp_kthr"
+      process_name: "rcu_exp_gp_kthread_worker"
+      thread_id: 19
+      process_id: 19
+    }
+    task_info {
+      estimated_mws: 0.070587
+      estimated_mw: 0.008161
+      idle_transitions_mws: 0.005993
+      thread_name: "adbd"
+      process_name: "/apex/com.android.adbd/bin/adbd"
+      thread_id: 5546
+      process_id: 5544
+    }
+    task_info {
+      estimated_mws: 0.070105
+      estimated_mw: 0.008105
+      thread_name: "servicemanager"
+      thread_id: 5598
+    }
+    task_info {
+      estimated_mws: 0.069214
+      estimated_mw: 0.008002
+      idle_transitions_mws: 0.001447
+      thread_name: "android.imms"
+      process_name: "system_server"
+      thread_id: 1791
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.068600
+      estimated_mw: 0.007931
+      thread_name: "lowpool[2]"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 2321
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.068467
+      estimated_mw: 0.007916
+      thread_name: "FileObserver"
+      process_name: "com.google.android.gms"
+      thread_id: 3035
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.068316
+      estimated_mw: 0.007898
+      thread_name: "binder:546_3"
+      process_name: "/system/bin/netd"
+      thread_id: 546
+      process_id: 546
+    }
+    task_info {
+      estimated_mws: 0.066410
+      estimated_mw: 0.007678
+      thread_name: "pool-51-thread-"
+      process_name: "com.google.android.gms"
+      thread_id: 4215
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.064761
+      estimated_mw: 0.007487
+      thread_name: "AudioService"
+      process_name: "system_server"
+      thread_id: 1844
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.064339
+      estimated_mw: 0.007439
+      idle_transitions_mws: 0.003289
+      thread_name: "adbd"
+      process_name: "/apex/com.android.adbd/bin/adbd"
+      thread_id: 5545
+      process_id: 5544
+    }
+    task_info {
+      estimated_mws: 0.063188
+      estimated_mw: 0.007305
+      thread_name: "droid.bluetooth"
+      process_name: "com.google.android.bluetooth"
+      thread_id: 2085
+      process_id: 2085
+    }
+    task_info {
+      estimated_mws: 0.061069
+      estimated_mw: 0.007061
+      thread_name: "msm_irqbalance"
+      process_name: "/vendor/bin/msm_irqbalance"
+      thread_id: 2466
+      process_id: 2466
+    }
+    task_info {
+      estimated_mws: 0.060920
+      estimated_mw: 0.007043
+      idle_transitions_mws: 0.003380
+      thread_name: "BackgroundInsta"
+      process_name: "system_server"
+      thread_id: 1875
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.059901
+      estimated_mw: 0.006925
+      idle_transitions_mws: 0.008904
+      thread_name: "ConnectivitySer"
+      process_name: "system_server"
+      thread_id: 1827
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.059295
+      estimated_mw: 0.006855
+      idle_transitions_mws: 0.018518
+      thread_name: "pool-1-thread-1"
+      process_name: "system_server"
+      thread_id: 1873
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.059196
+      estimated_mw: 0.006844
+      thread_name: "binder:3028_1"
+      process_name: "com.google.android.wearable.healthservices"
+      thread_id: 3049
+      process_id: 3028
+    }
+    task_info {
+      estimated_mws: 0.058807
+      estimated_mw: 0.006799
+      idle_transitions_mws: 0.001108
+      thread_name: "roid.apps.scone"
+      process_name: "com.google.android.apps.scone"
+      thread_id: 5245
+      process_id: 5245
+    }
+    task_info {
+      estimated_mws: 0.058053
+      estimated_mw: 0.006712
+      thread_name: "UsbFfs-worker"
+      process_name: "/apex/com.android.adbd/bin/adbd"
+      thread_id: 5560
+      process_id: 5544
+    }
+    task_info {
+      estimated_mws: 0.057400
+      estimated_mw: 0.006636
+      idle_transitions_mws: 0.034371
+      thread_name: "android.hardwar"
+      process_name: "/vendor/bin/hw/android.hardware.health-service.eos"
+      thread_id: 1271
+      process_id: 1271
+    }
+    task_info {
+      estimated_mws: 0.056947
+      estimated_mw: 0.006584
+      idle_transitions_mws: 0.002094
+      thread_name: "bgres-controlle"
+      process_name: "system_server"
+      thread_id: 1495
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.056879
+      estimated_mw: 0.006576
+      thread_name: "netd"
+      process_name: "/system/bin/netd"
+      thread_id: 569
+      process_id: 546
+    }
+    task_info {
+      estimated_mws: 0.055642
+      estimated_mw: 0.006433
+      thread_name: "system_server"
+      thread_id: 5590
+    }
+    task_info {
+      estimated_mws: 0.054764
+      estimated_mw: 0.006332
+      thread_name: "binder:2049_4"
+      process_name: "com.android.networkstack.process"
+      thread_id: 2083
+      process_id: 2049
+    }
+    task_info {
+      estimated_mws: 0.053127
+      estimated_mw: 0.006142
+      thread_name: "binder:1949_4"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 2302
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.052984
+      estimated_mw: 0.006126
+      idle_transitions_mws: 0.001354
+      thread_name: "oid.grilservice"
+      process_name: "com.google.android.grilservice"
+      thread_id: 2129
+      process_id: 2129
+    }
+    task_info {
+      estimated_mws: 0.052980
+      estimated_mw: 0.006125
+      thread_name: "binder:2856_9"
+      process_name: "com.google.android.gms"
+      thread_id: 5585
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.052715
+      estimated_mw: 0.006095
+      thread_name: "StateService"
+      process_name: "com.google.android.apps.scone"
+      thread_id: 5269
+      process_id: 5245
+    }
+    task_info {
+      estimated_mws: 0.052232
+      estimated_mw: 0.006039
+      thread_name: "vndservicemanag"
+      thread_id: 5597
+    }
+    task_info {
+      estimated_mws: 0.052027
+      estimated_mw: 0.006015
+      thread_name: "binder:1949_9"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 3359
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.051717
+      estimated_mw: 0.005979
+      idle_transitions_mws: 0.019339
+      thread_name: "Ipc-5004:1"
+      process_name: "/vendor/bin/hw/android.hardware.gnss-aidl-service-qti"
+      thread_id: 5483
+      process_id: 650
+    }
+    task_info {
+      estimated_mws: 0.049546
+      estimated_mw: 0.005728
+      thread_name: "PackageManager"
+      process_name: "system_server"
+      thread_id: 1530
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.048227
+      estimated_mw: 0.005576
+      thread_name: "android.hardwar"
+      process_name: "/vendor/bin/hw/android.hardware.power-service"
+      thread_id: 660
+      process_id: 660
+    }
+    task_info {
+      estimated_mws: 0.047454
+      estimated_mw: 0.005486
+      thread_name: "BluetoothScanMa"
+      process_name: "com.google.android.bluetooth"
+      thread_id: 2609
+      process_id: 2085
+    }
+    task_info {
+      estimated_mws: 0.046472
+      estimated_mw: 0.005373
+      thread_name: "subsystem_ramdu"
+      process_name: "/system/vendor/bin/subsystem_ramdump"
+      thread_id: 816
+      process_id: 799
+    }
+    task_info {
+      estimated_mws: 0.046295
+      estimated_mw: 0.005352
+      thread_name: "Ipc-5004:2"
+      process_name: "/vendor/bin/hw/android.hardware.gnss-aidl-service-qti"
+      thread_id: 5484
+      process_id: 650
+    }
+    task_info {
+      estimated_mws: 0.045805
+      estimated_mw: 0.005296
+      thread_name: "binder:975_2"
+      process_name: "/vendor/bin/imsdaemon"
+      thread_id: 1047
+      process_id: 975
+    }
+    task_info {
+      estimated_mws: 0.045282
+      estimated_mw: 0.005235
+      idle_transitions_mws: 0.000972
+      thread_name: ".healthservices"
+      process_name: "com.google.android.wearable.healthservices"
+      thread_id: 3028
+      process_id: 3028
+    }
+    task_info {
+      estimated_mws: 0.045087
+      estimated_mw: 0.005213
+      idle_transitions_mws: 0.001817
+      thread_name: "queued-work-loo"
+      process_name: "com.google.android.gms"
+      thread_id: 3236
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.043921
+      estimated_mw: 0.005078
+      thread_name: "powerstateservi"
+      process_name: "/vendor/bin/hw/vendor.qti.hardware.powerstateservice@1.0-service"
+      thread_id: 276
+      process_id: 269
+    }
+    task_info {
+      estimated_mws: 0.043653
+      estimated_mw: 0.005047
+      idle_transitions_mws: 0.045128
+      thread_name: "wlan_logging_th"
+      process_name: "wlan_logging_thread"
+      thread_id: 368
+      process_id: 368
+    }
+    task_info {
+      estimated_mws: 0.043574
+      estimated_mw: 0.005038
+      thread_name: "FlpThread"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 3279
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.043436
+      estimated_mw: 0.005022
+      thread_name: "Light-P0-2"
+      process_name: "com.google.android.inputmethod.latin"
+      thread_id: 5071
+      process_id: 4997
+    }
+    task_info {
+      estimated_mws: 0.042985
+      estimated_mw: 0.004970
+      idle_transitions_mws: 0.004250
+      thread_name: "binder:5245_4"
+      process_name: "com.google.android.apps.scone"
+      thread_id: 5270
+      process_id: 5245
+    }
+    task_info {
+      estimated_mws: 0.042946
+      estimated_mw: 0.004965
+      thread_name: "HWC_UeventThrea"
+      process_name: "/vendor/bin/hw/vendor.qti.hardware.display.composer-service"
+      thread_id: 717
+      process_id: 685
+    }
+    task_info {
+      estimated_mws: 0.042762
+      estimated_mw: 0.004944
+      thread_name: "pool-12-thread-"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 2371
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.042752
+      estimated_mw: 0.004943
+      thread_name: "android.hardwar"
+      process_name: "/vendor/bin/hw/android.hardware.contexthub-service.wac"
+      thread_id: 668
+      process_id: 644
+    }
+    task_info {
+      estimated_mws: 0.042468
+      estimated_mw: 0.004910
+      thread_name: "binder:1948_3"
+      process_name: "com.google.wear.services"
+      thread_id: 1976
+      process_id: 1948
+    }
+    task_info {
+      estimated_mws: 0.042220
+      estimated_mw: 0.004881
+      thread_name: "netlink socket"
+      process_name: "/system/vendor/bin/ipacm"
+      thread_id: 538
+      process_id: 523
+    }
+    task_info {
+      estimated_mws: 0.040507
+      estimated_mw: 0.004683
+      idle_transitions_mws: 0.020051
+      thread_name: "RegionSampling"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 871
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 0.040385
+      estimated_mw: 0.004669
+      thread_name: "TransportThread"
+      process_name: "/vendor/bin/hw/android.hardware.sensors-service.multihal"
+      thread_id: 794
+      process_id: 664
+    }
+    task_info {
+      estimated_mws: 0.040349
+      estimated_mw: 0.004665
+      thread_name: "binder:2856_1"
+      process_name: "com.google.android.gms"
+      thread_id: 2898
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.040180
+      estimated_mw: 0.004645
+      thread_name: "servicemanager"
+      thread_id: 5595
+    }
+    task_info {
+      estimated_mws: 0.039525
+      estimated_mw: 0.004570
+      thread_name: "pd-mapper"
+      process_name: "/vendor/bin/pd-mapper"
+      thread_id: 752
+      process_id: 725
+    }
+    task_info {
+      estimated_mws: 0.039196
+      estimated_mw: 0.004532
+      thread_name: "vndservicemanag"
+      thread_id: 5618
+    }
+    task_info {
+      estimated_mws: 0.039101
+      estimated_mw: 0.004521
+      idle_transitions_mws: 0.001158
+      thread_name: "vndservicemanag"
+      thread_id: 5605
+    }
+    task_info {
+      estimated_mws: 0.038960
+      estimated_mw: 0.004504
+      thread_name: "WifiScanningSer"
+      process_name: "system_server"
+      thread_id: 1823
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.038580
+      estimated_mw: 0.004460
+      thread_name: "cnss-daemon"
+      process_name: "/system/vendor/bin/cnss-daemon"
+      thread_id: 5204
+      process_id: 1009
+    }
+    task_info {
+      estimated_mws: 0.038053
+      estimated_mw: 0.004399
+      idle_transitions_mws: 0.002046
+      thread_name: "shell svc 5620"
+      process_name: "/apex/com.android.adbd/bin/adbd"
+      thread_id: 5622
+      process_id: 5544
+    }
+    task_info {
+      estimated_mws: 0.037116
+      estimated_mw: 0.004291
+      thread_name: "rmt_storage"
+      process_name: "/vendor/bin/rmt_storage"
+      thread_id: 758
+      process_id: 758
+    }
+    task_info {
+      estimated_mws: 0.036357
+      estimated_mw: 0.004203
+      idle_transitions_mws: 0.164265
+      thread_name: "halt_drain_rqs"
+      process_name: "halt_drain_rqs"
+      thread_id: 105
+      process_id: 105
+    }
+    task_info {
+      estimated_mws: 0.035907
+      estimated_mw: 0.004151
+      thread_name: "BG Thread #2"
+      process_name: "com.google.android.wearable.assistant"
+      thread_id: 4106
+      process_id: 4038
+    }
+    task_info {
+      estimated_mws: 0.035876
+      estimated_mw: 0.004148
+      idle_transitions_mws: 0.007692
+      thread_name: "-Executor] idle"
+      process_name: "com.google.android.gms"
+      thread_id: 5592
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.035444
+      estimated_mw: 0.004098
+      thread_name: "tftp_server"
+      process_name: "/vendor/bin/tftp_server"
+      thread_id: 759
+      process_id: 759
+    }
+    task_info {
+      estimated_mws: 0.035386
+      estimated_mw: 0.004091
+      thread_name: "FinalizerWatchd"
+      process_name: "com.google.android.gms"
+      thread_id: 2887
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.035171
+      estimated_mw: 0.004066
+      idle_transitions_mws: 0.012569
+      thread_name: "servicemanager"
+      thread_id: 5606
+    }
+    task_info {
+      estimated_mws: 0.035157
+      estimated_mw: 0.004065
+      thread_name: "Blocking Thread"
+      process_name: "com.google.android.wearable.assistant"
+      thread_id: 5587
+      process_id: 4038
+    }
+    task_info {
+      estimated_mws: 0.035034
+      estimated_mw: 0.004050
+      thread_name: "vndservicemanag"
+      thread_id: 5611
+    }
+    task_info {
+      estimated_mws: 0.034307
+      estimated_mw: 0.003966
+      thread_name: "vndservicemanag"
+      thread_id: 5593
+    }
+    task_info {
+      estimated_mws: 0.034030
+      estimated_mw: 0.003934
+      thread_name: "servicemanager"
+      thread_id: 5621
+    }
+    task_info {
+      estimated_mws: 0.032631
+      estimated_mw: 0.003773
+      thread_name: "binder:685_3"
+      thread_id: 5586
+    }
+    task_info {
+      estimated_mws: 0.031847
+      estimated_mw: 0.003682
+      idle_transitions_mws: 0.000891
+      thread_name: "radioext@1.0-se"
+      process_name: "/vendor/bin/hw/vendor.google.radioext@1.0-service"
+      thread_id: 676
+      process_id: 676
+    }
+    task_info {
+      estimated_mws: 0.031818
+      estimated_mw: 0.003679
+      idle_transitions_mws: 0.001158
+      thread_name: "BgBroadcastRegi"
+      process_name: "com.google.wear.services"
+      thread_id: 2017
+      process_id: 1948
+    }
+    task_info {
+      estimated_mws: 0.031204
+      estimated_mw: 0.003608
+      idle_transitions_mws: 0.002208
+      thread_name: "DefaultExecutor"
+      process_name: "com.google.android.wearable.watchface.rwf"
+      thread_id: 5600
+      process_id: 1999
+    }
+    task_info {
+      estimated_mws: 0.030138
+      estimated_mw: 0.003484
+      thread_name: "Light-P0-1"
+      process_name: "com.google.android.inputmethod.latin"
+      thread_id: 5064
+      process_id: 4997
+    }
+    task_info {
+      estimated_mws: 0.029778
+      estimated_mw: 0.003443
+      thread_name: "binder:978_2"
+      process_name: "/system/vendor/bin/nicmd"
+      thread_id: 1368
+      process_id: 978
+    }
+    task_info {
+      estimated_mws: 0.029627
+      estimated_mw: 0.003425
+      thread_name: "atchdog.monitor"
+      process_name: "system_server"
+      thread_id: 1414
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.029590
+      estimated_mw: 0.003421
+      idle_transitions_mws: 0.012793
+      thread_name: "UsfHalWorker"
+      process_name: "/vendor/bin/hw/android.hardware.sensors-service.multihal"
+      thread_id: 792
+      process_id: 664
+    }
+    task_info {
+      estimated_mws: 0.028316
+      estimated_mw: 0.003274
+      idle_transitions_mws: 0.003170
+      thread_name: "binder:1999_5"
+      process_name: "com.google.android.wearable.watchface.rwf"
+      thread_id: 4985
+      process_id: 1999
+    }
+    task_info {
+      estimated_mws: 0.028097
+      estimated_mw: 0.003248
+      thread_name: "SatelliteContro"
+      process_name: "com.android.phone"
+      thread_id: 2382
+      process_id: 2182
+    }
+    task_info {
+      estimated_mws: 0.027888
+      estimated_mw: 0.003224
+      idle_transitions_mws: 0.009315
+      thread_name: "irq/199-dwc3"
+      process_name: "irq/199-dwc3"
+      thread_id: 5559
+      process_id: 5559
+    }
+    task_info {
+      estimated_mws: 0.027797
+      estimated_mw: 0.003214
+      thread_name: "pm-service"
+      process_name: "/vendor/bin/pm-service"
+      thread_id: 745
+      process_id: 730
+    }
+    task_info {
+      estimated_mws: 0.027301
+      estimated_mw: 0.003156
+      idle_transitions_mws: 0.001040
+      thread_name: "-Executor] idle"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 5603
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.027287
+      estimated_mw: 0.003155
+      idle_transitions_mws: 0.002419
+      thread_name: "perfetto"
+      process_name: "perfetto"
+      thread_id: 5581
+      process_id: 5581
+    }
+    task_info {
+      estimated_mws: 0.026942
+      estimated_mw: 0.003115
+      thread_name: "ExeSeq-P10-1"
+      process_name: "com.google.android.inputmethod.latin"
+      thread_id: 5074
+      process_id: 4997
+    }
+    task_info {
+      estimated_mws: 0.026776
+      estimated_mw: 0.003096
+      thread_name: "binder:978_2"
+      process_name: "/system/vendor/bin/nicmd"
+      thread_id: 1364
+      process_id: 978
+    }
+    task_info {
+      estimated_mws: 0.026328
+      estimated_mw: 0.003044
+      thread_name: "HwBinder:2129_1"
+      process_name: "com.google.android.grilservice"
+      thread_id: 3649
+      process_id: 2129
+    }
+    task_info {
+      estimated_mws: 0.026013
+      estimated_mw: 0.003008
+      thread_name: "DefaultExecutor"
+      process_name: "com.google.android.wearable.watchface.rwf"
+      thread_id: 5588
+      process_id: 1999
+    }
+    task_info {
+      estimated_mws: 0.025701
+      estimated_mw: 0.002971
+      thread_name: "rkstack.process"
+      process_name: "com.android.networkstack.process"
+      thread_id: 2049
+      process_id: 2049
+    }
+    task_info {
+      estimated_mws: 0.024843
+      estimated_mw: 0.002872
+      idle_transitions_mws: 0.001271
+      thread_name: "hwuiTask1"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 1997
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.024811
+      estimated_mw: 0.002868
+      idle_transitions_mws: 0.000632
+      thread_name: "pool-1-thread-1"
+      process_name: "com.google.android.apps.scone"
+      thread_id: 5271
+      process_id: 5245
+    }
+    task_info {
+      estimated_mws: 0.024633
+      estimated_mw: 0.002848
+      thread_name: "binder:978_2"
+      process_name: "/system/vendor/bin/nicmd"
+      thread_id: 1360
+      process_id: 978
+    }
+    task_info {
+      estimated_mws: 0.023564
+      estimated_mw: 0.002724
+      thread_name: "binder:978_2"
+      process_name: "/system/vendor/bin/nicmd"
+      thread_id: 1366
+      process_id: 978
+    }
+    task_info {
+      estimated_mws: 0.023405
+      estimated_mw: 0.002706
+      thread_name: "cnss-daemon"
+      process_name: "/system/vendor/bin/cnss-daemon"
+      thread_id: 5613
+      process_id: 1009
+    }
+    task_info {
+      estimated_mws: 0.023393
+      estimated_mw: 0.002705
+      thread_name: "servicemanager"
+      thread_id: 5608
+    }
+    task_info {
+      estimated_mws: 0.022813
+      estimated_mw: 0.002638
+      thread_name: "android.hardwar"
+      process_name: "/vendor/bin/hw/android.hardware.contexthub-service.wac"
+      thread_id: 644
+      process_id: 644
+    }
+    task_info {
+      estimated_mws: 0.022774
+      estimated_mw: 0.002633
+      thread_name: "binder:978_2"
+      process_name: "/system/vendor/bin/nicmd"
+      thread_id: 1362
+      process_id: 978
+    }
+    task_info {
+      estimated_mws: 0.022714
+      estimated_mw: 0.002626
+      thread_name: "binder:685_3"
+      thread_id: 5594
+    }
+    task_info {
+      estimated_mws: 0.022642
+      estimated_mw: 0.002618
+      thread_name: "binder:978_2"
+      process_name: "/system/vendor/bin/nicmd"
+      thread_id: 1358
+      process_id: 978
+    }
+    task_info {
+      estimated_mws: 0.022617
+      estimated_mw: 0.002615
+      thread_name: "vndservicemanag"
+      thread_id: 5607
+    }
+    task_info {
+      estimated_mws: 0.021814
+      estimated_mw: 0.002522
+      idle_transitions_mws: 0.002423
+      thread_name: "it.FitbitMobile"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5377
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.021307
+      estimated_mw: 0.002463
+      thread_name: "SysUiBg"
+      process_name: "com.google.android.apps.wearable.systemui"
+      thread_id: 2294
+      process_id: 2171
+    }
+    task_info {
+      estimated_mws: 0.020941
+      estimated_mw: 0.002421
+      thread_name: "-Executor] idle"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 5623
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.020393
+      estimated_mw: 0.002358
+      thread_name: "vndservicemanag"
+      thread_id: 5582
+    }
+    task_info {
+      estimated_mws: 0.019946
+      estimated_mw: 0.002306
+      idle_transitions_mws: 0.009476
+      thread_name: "qcom,system-poo"
+      process_name: "qcom,system-pool-refill-thread"
+      thread_id: 81
+      process_id: 81
+    }
+    task_info {
+      estimated_mws: 0.019901
+      estimated_mw: 0.002301
+      idle_transitions_mws: 0.000916
+      thread_name: "binder:2129_9"
+      process_name: "com.google.android.grilservice"
+      thread_id: 5203
+      process_id: 2129
+    }
+    task_info {
+      estimated_mws: 0.019723
+      estimated_mw: 0.002280
+      thread_name: "servicemanager"
+      thread_id: 5610
+    }
+    task_info {
+      estimated_mws: 0.019537
+      estimated_mw: 0.002259
+      thread_name: "cnss-daemon"
+      process_name: "/system/vendor/bin/cnss-daemon"
+      thread_id: 1009
+      process_id: 1009
+    }
+    task_info {
+      estimated_mws: 0.019390
+      estimated_mw: 0.002242
+      thread_name: "pool-9-thread-1"
+      process_name: "com.google.android.wearable.watchface.rwf"
+      thread_id: 2073
+      process_id: 1999
+    }
+    task_info {
+      estimated_mws: 0.019219
+      estimated_mw: 0.002222
+      thread_name: "binder:1302_3"
+      process_name: "system_server"
+      thread_id: 1433
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.019059
+      estimated_mw: 0.002204
+      thread_name: "mcu_mgmtd"
+      process_name: "/vendor/bin/mcu_mgmtd"
+      thread_id: 587
+      process_id: 524
+    }
+    task_info {
+      estimated_mws: 0.018619
+      estimated_mw: 0.002153
+      thread_name: "WCMTelemetryLog"
+      process_name: "system_server"
+      thread_id: 1906
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.018508
+      estimated_mw: 0.002140
+      thread_name: "droid.tethering"
+      process_name: "com.android.networkstack.process"
+      thread_id: 2158
+      process_id: 2049
+    }
+    task_info {
+      estimated_mws: 0.018055
+      estimated_mw: 0.002087
+      thread_name: "DefaultWallpape"
+      process_name: "com.google.android.wearable.watchface.rwf"
+      thread_id: 2082
+      process_id: 1999
+    }
+    task_info {
+      estimated_mws: 0.017725
+      estimated_mw: 0.002049
+      idle_transitions_mws: 0.001379
+      thread_name: "HeapTaskDaemon"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5386
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.017125
+      estimated_mw: 0.001980
+      idle_transitions_mws: 0.001201
+      thread_name: "WearConnectionT"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 2172
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.016887
+      estimated_mw: 0.001952
+      idle_transitions_mws: 0.001112
+      thread_name: "wear-services-w"
+      process_name: "com.google.wear.services"
+      thread_id: 2029
+      process_id: 1948
+    }
+    task_info {
+      estimated_mws: 0.016252
+      estimated_mw: 0.001879
+      thread_name: "BG Thread #1"
+      process_name: "com.google.android.wearable.assistant"
+      thread_id: 4080
+      process_id: 4038
+    }
+    task_info {
+      estimated_mws: 0.016215
+      estimated_mw: 0.001875
+      idle_transitions_mws: 0.001249
+      thread_name: "GlobalScheduler"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 2276
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.016141
+      estimated_mw: 0.001866
+      thread_name: "pool-31-thread-"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 5617
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.016069
+      estimated_mw: 0.001858
+      idle_transitions_mws: 0.005041
+      thread_name: "servicemanager"
+      thread_id: 5583
+    }
+    task_info {
+      estimated_mws: 0.015376
+      estimated_mw: 0.001778
+      idle_transitions_mws: 0.007814
+      thread_name: "RenderEngine"
+      process_name: "/system/bin/surfaceflinger"
+      thread_id: 5601
+      process_id: 755
+    }
+    task_info {
+      estimated_mws: 0.015374
+      estimated_mw: 0.001777
+      thread_name: "pool-11-thread-"
+      process_name: "com.google.android.wearable.healthservices"
+      thread_id: 5596
+      process_id: 3028
+    }
+    task_info {
+      estimated_mws: 0.015097
+      estimated_mw: 0.001745
+      idle_transitions_mws: 0.000833
+      thread_name: "dsi_err_workq"
+      process_name: "dsi_err_workq"
+      thread_id: 5589
+      process_id: 5589
+    }
+    task_info {
+      estimated_mws: 0.015062
+      estimated_mw: 0.001741
+      thread_name: "InteractionJank"
+      process_name: "com.google.android.apps.wearable.systemui"
+      thread_id: 2300
+      process_id: 2171
+    }
+    task_info {
+      estimated_mws: 0.015034
+      estimated_mw: 0.001738
+      thread_name: "vndservicemanag"
+      thread_id: 5609
+    }
+    task_info {
+      estimated_mws: 0.014592
+      estimated_mw: 0.001687
+      idle_transitions_mws: 0.000734
+      thread_name: "hwuiTask0"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 1996
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.014520
+      estimated_mw: 0.001679
+      thread_name: "tworkPolicy.uid"
+      process_name: "system_server"
+      thread_id: 1817
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.014278
+      estimated_mw: 0.001651
+      thread_name: "highpool[10]"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 3417
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.014123
+      estimated_mw: 0.001633
+      idle_transitions_mws: 0.010036
+      thread_name: "LowMemThread"
+      process_name: "system_server"
+      thread_id: 1481
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.014026
+      estimated_mw: 0.001622
+      thread_name: "pixelstats-vend"
+      process_name: "/vendor/bin/pixelstats-vendor"
+      thread_id: 266
+      process_id: 255
+    }
+    task_info {
+      estimated_mws: 0.013579
+      estimated_mw: 0.001570
+      idle_transitions_mws: 0.001160
+      thread_name: "kworker/u9:0"
+      process_name: "kworker/u9:0"
+      thread_id: 64
+      process_id: 64
+    }
+    task_info {
+      estimated_mws: 0.013322
+      estimated_mw: 0.001540
+      idle_transitions_mws: 0.011775
+      thread_name: "migration/1"
+      process_name: "migration/1"
+      thread_id: 25
+      process_id: 25
+    }
+    task_info {
+      estimated_mws: 0.013321
+      estimated_mw: 0.001540
+      thread_name: "GlobalDispatchi"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 2290
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.013127
+      estimated_mw: 0.001518
+      thread_name: "main"
+      process_name: "/vendor/bin/hw/qcrilNrd"
+      thread_id: 1568
+      process_id: 1062
+    }
+    task_info {
+      estimated_mws: 0.012863
+      estimated_mw: 0.001487
+      thread_name: "servicemanager"
+      thread_id: 5612
+    }
+    task_info {
+      estimated_mws: 0.012835
+      estimated_mw: 0.001484
+      thread_name: "qtidataservices"
+      process_name: ".qtidataservices"
+      thread_id: 2846
+      process_id: 2118
+    }
+    task_info {
+      estimated_mws: 0.012734
+      estimated_mw: 0.001472
+      thread_name: "shortcut"
+      process_name: "system_server"
+      thread_id: 1874
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.012433
+      estimated_mw: 0.001437
+      idle_transitions_mws: 0.001905
+      thread_name: "irq/25-mmc0"
+      process_name: "irq/25-mmc0"
+      thread_id: 120
+      process_id: 120
+    }
+    task_info {
+      estimated_mws: 0.012244
+      estimated_mw: 0.001416
+      thread_name: "GlobalDispatchi"
+      process_name: "com.google.android.gms"
+      thread_id: 3155
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.012130
+      estimated_mw: 0.001402
+      thread_name: "ConnectivityThr"
+      process_name: "com.google.android.gms"
+      thread_id: 4172
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.011932
+      estimated_mw: 0.001379
+      thread_name: "RenderThread"
+      thread_id: 5616
+    }
+    task_info {
+      estimated_mws: 0.011783
+      estimated_mw: 0.001362
+      thread_name: "AGMIPC@1.0-serv"
+      process_name: "/vendor/bin/hw/vendor.qti.hardware.AGMIPC@1.0-service"
+      thread_id: 1454
+      process_id: 1446
+    }
+    task_info {
+      estimated_mws: 0.011706
+      estimated_mw: 0.001353
+      thread_name: "binder:2171_2"
+      process_name: "com.google.android.apps.wearable.systemui"
+      thread_id: 2209
+      process_id: 2171
+    }
+    task_info {
+      estimated_mws: 0.011675
+      estimated_mw: 0.001350
+      thread_name: "radioext@1.0-se"
+      process_name: "/vendor/bin/hw/vendor.google.radioext@1.0-service"
+      thread_id: 714
+      process_id: 676
+    }
+    task_info {
+      estimated_mws: 0.011615
+      estimated_mw: 0.001343
+      thread_name: "servicemanager"
+      thread_id: 5615
+    }
+    task_info {
+      estimated_mws: 0.011467
+      estimated_mw: 0.001326
+      thread_name: "LocApiMsgTask"
+      process_name: "/vendor/bin/hw/android.hardware.gnss-aidl-service-qti"
+      thread_id: 694
+      process_id: 650
+    }
+    task_info {
+      estimated_mws: 0.011318
+      estimated_mw: 0.001309
+      thread_name: "LocApiMsgTask"
+      process_name: "xtra-daemon"
+      thread_id: 1090
+      process_id: 1031
+    }
+    task_info {
+      estimated_mws: 0.011273
+      estimated_mw: 0.001303
+      thread_name: "vndservicemanag"
+      thread_id: 5614
+    }
+    task_info {
+      estimated_mws: 0.011024
+      estimated_mw: 0.001275
+      idle_transitions_mws: 0.004820
+      thread_name: "TimerThread"
+      process_name: "/system/bin/audioserver"
+      thread_id: 1486
+      process_id: 740
+    }
+    task_info {
+      estimated_mws: 0.010869
+      estimated_mw: 0.001257
+      idle_transitions_mws: 0.001558
+      thread_name: "irq/26-4744000."
+      process_name: "irq/26-4744000.sdhci"
+      thread_id: 117
+      process_id: 117
+    }
+    task_info {
+      estimated_mws: 0.010764
+      estimated_mw: 0.001245
+      idle_transitions_mws: 0.003066
+      thread_name: "SurfaceSyncGrou"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 1994
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.010731
+      estimated_mw: 0.001241
+      idle_transitions_mws: 0.009927
+      thread_name: "migration/3"
+      process_name: "migration/3"
+      thread_id: 40
+      process_id: 40
+    }
+    task_info {
+      estimated_mws: 0.010156
+      estimated_mw: 0.001174
+      thread_name: "id.wearable.app"
+      process_name: "com.google.android.wearable.app"
+      thread_id: 3857
+      process_id: 3857
+    }
+    task_info {
+      estimated_mws: 0.009691
+      estimated_mw: 0.001120
+      thread_name: "ksoftirqd/0"
+      process_name: "ksoftirqd/0"
+      thread_id: 13
+      process_id: 13
+    }
+    task_info {
+      estimated_mws: 0.009668
+      estimated_mw: 0.001118
+      thread_name: "cnss-daemon"
+      process_name: "/system/vendor/bin/cnss-daemon"
+      thread_id: 1052
+      process_id: 1009
+    }
+    task_info {
+      estimated_mws: 0.009639
+      estimated_mw: 0.001114
+      idle_transitions_mws: 0.002373
+      thread_name: "kworker/u9:2"
+      process_name: "kworker/u9:2"
+      thread_id: 338
+      process_id: 338
+    }
+    task_info {
+      estimated_mws: 0.009404
+      estimated_mw: 0.001087
+      thread_name: "binder:2856_3"
+      process_name: "com.google.android.gms"
+      thread_id: 3157
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.009364
+      estimated_mw: 0.001083
+      thread_name: "binder:969_3"
+      process_name: "/system/vendor/bin/cnd"
+      thread_id: 1013
+      process_id: 969
+    }
+    task_info {
+      estimated_mws: 0.008837
+      estimated_mw: 0.001022
+      thread_name: "binder:2118_2"
+      process_name: ".qtidataservices"
+      thread_id: 2142
+      process_id: 2118
+    }
+    task_info {
+      estimated_mws: 0.008775
+      estimated_mw: 0.001015
+      thread_name: "thermal-engine-"
+      process_name: "/vendor/bin/thermal-engine-v2"
+      thread_id: 2520
+      process_id: 2493
+    }
+    task_info {
+      estimated_mws: 0.008724
+      estimated_mw: 0.001009
+      thread_name: "binder:2856_7"
+      process_name: "com.google.android.gms"
+      thread_id: 4825
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.008275
+      estimated_mw: 0.000957
+      thread_name: "binder:2856_6"
+      process_name: "com.google.android.gms"
+      thread_id: 4824
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.008136
+      estimated_mw: 0.000941
+      thread_name: "binder:3857_3"
+      process_name: "com.google.android.wearable.app"
+      thread_id: 3872
+      process_id: 3857
+    }
+    task_info {
+      estimated_mws: 0.007913
+      estimated_mw: 0.000915
+      thread_name: "binder:975_3"
+      process_name: "/vendor/bin/imsdaemon"
+      thread_id: 1630
+      process_id: 975
+    }
+    task_info {
+      estimated_mws: 0.007892
+      estimated_mw: 0.000912
+      thread_name: "time_daemon"
+      process_name: "/vendor/bin/time_daemon"
+      thread_id: 525
+      process_id: 522
+    }
+    task_info {
+      estimated_mws: 0.007750
+      estimated_mw: 0.000896
+      thread_name: "binder:978_2"
+      process_name: "/system/vendor/bin/nicmd"
+      thread_id: 1044
+      process_id: 978
+    }
+    task_info {
+      estimated_mws: 0.007591
+      estimated_mw: 0.000878
+      thread_name: "binder:2856_5"
+      process_name: "com.google.android.gms"
+      thread_id: 3681
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.007392
+      estimated_mw: 0.000855
+      thread_name: "binder:2182_1"
+      process_name: "com.android.phone"
+      thread_id: 2231
+      process_id: 2182
+    }
+    task_info {
+      estimated_mws: 0.007384
+      estimated_mw: 0.000854
+      thread_name: "binder:2171_5"
+      process_name: "com.google.android.apps.wearable.systemui"
+      thread_id: 2638
+      process_id: 2171
+    }
+    task_info {
+      estimated_mws: 0.007245
+      estimated_mw: 0.000838
+      idle_transitions_mws: 0.000922
+      thread_name: "qrtr_rx"
+      process_name: "qrtr_rx"
+      thread_id: 1556
+      process_id: 1556
+    }
+    task_info {
+      estimated_mws: 0.007086
+      estimated_mw: 0.000819
+      thread_name: "radioext@1.0-se"
+      process_name: "/vendor/bin/hw/vendor.google.radioext@1.0-service"
+      thread_id: 1605
+      process_id: 676
+    }
+    task_info {
+      estimated_mws: 0.006850
+      estimated_mw: 0.000792
+      idle_transitions_mws: 0.005140
+      thread_name: "hwservicemanage"
+      process_name: "/system/system_ext/bin/hwservicemanager"
+      thread_id: 214
+      process_id: 214
+    }
+    task_info {
+      estimated_mws: 0.006731
+      estimated_mw: 0.000778
+      thread_name: "rcub/0"
+      process_name: "rcub/0"
+      thread_id: 17
+      process_id: 17
+    }
+    task_info {
+      estimated_mws: 0.006663
+      estimated_mw: 0.000770
+      thread_name: "binder:2856_8"
+      process_name: "com.google.android.gms"
+      thread_id: 4826
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.006650
+      estimated_mw: 0.000769
+      thread_name: "kthreadd"
+      process_name: "kthreadd"
+      thread_id: 2
+      process_id: 2
+    }
+    task_info {
+      estimated_mws: 0.006574
+      estimated_mw: 0.000760
+      thread_name: "binder:233_2"
+      process_name: "/system/bin/vold"
+      thread_id: 233
+      process_id: 233
+    }
+    task_info {
+      estimated_mws: 0.006115
+      estimated_mw: 0.000707
+      thread_name: "cds_ol_rx_threa"
+      process_name: "cds_ol_rx_thread"
+      thread_id: 5199
+      process_id: 5199
+    }
+    task_info {
+      estimated_mws: 0.005829
+      estimated_mw: 0.000674
+      thread_name: "NsdService"
+      process_name: "system_server"
+      thread_id: 1831
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.005734
+      estimated_mw: 0.000663
+      thread_name: "binder:978_2"
+      process_name: "/system/vendor/bin/nicmd"
+      thread_id: 1367
+      process_id: 978
+    }
+    task_info {
+      estimated_mws: 0.005699
+      estimated_mw: 0.000659
+      thread_name: "Scheduled BG"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 2895
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.005530
+      estimated_mw: 0.000639
+      thread_name: "binder:2856_2"
+      process_name: "com.google.android.gms"
+      thread_id: 2903
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.005484
+      estimated_mw: 0.000634
+      thread_name: "binder:978_2"
+      process_name: "/system/vendor/bin/nicmd"
+      thread_id: 1361
+      process_id: 978
+    }
+    task_info {
+      estimated_mws: 0.005366
+      estimated_mw: 0.000620
+      thread_name: "binder:978_2"
+      process_name: "/system/vendor/bin/nicmd"
+      thread_id: 1357
+      process_id: 978
+    }
+    task_info {
+      estimated_mws: 0.005364
+      estimated_mw: 0.000620
+      thread_name: "FileObserver"
+      process_name: "system_server"
+      thread_id: 1498
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.005278
+      estimated_mw: 0.000610
+      thread_name: "binder:978_2"
+      process_name: "/system/vendor/bin/nicmd"
+      thread_id: 1359
+      process_id: 978
+    }
+    task_info {
+      estimated_mws: 0.005261
+      estimated_mw: 0.000608
+      thread_name: "Lite Thread #0"
+      process_name: "com.google.android.wearable.assistant"
+      thread_id: 4109
+      process_id: 4038
+    }
+    task_info {
+      estimated_mws: 0.005011
+      estimated_mw: 0.000579
+      thread_name: "kworker/3:1H"
+      process_name: "kworker/3:1H"
+      thread_id: 122
+      process_id: 122
+    }
+    task_info {
+      estimated_mws: 0.004996
+      estimated_mw: 0.000578
+      thread_name: "binder:978_2"
+      process_name: "/system/vendor/bin/nicmd"
+      thread_id: 1365
+      process_id: 978
+    }
+    task_info {
+      estimated_mws: 0.004986
+      estimated_mw: 0.000576
+      idle_transitions_mws: 0.031459
+      thread_name: "Scheduled BG"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 2890
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.004877
+      estimated_mw: 0.000564
+      thread_name: "perfetto_hprof_"
+      process_name: "com.google.android.gms"
+      thread_id: 2880
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.004761
+      estimated_mw: 0.000550
+      thread_name: "backup-0"
+      process_name: "system_server"
+      thread_id: 2660
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.004757
+      estimated_mw: 0.000550
+      thread_name: "tts-player-0"
+      process_name: "com.google.android.wearable.assistant"
+      thread_id: 4209
+      process_id: 4038
+    }
+    task_info {
+      estimated_mws: 0.004444
+      estimated_mw: 0.000514
+      thread_name: "Signal Catcher"
+      process_name: "com.google.android.gms"
+      thread_id: 2878
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.004331
+      estimated_mw: 0.000501
+      thread_name: "binder:978_2"
+      process_name: "/system/vendor/bin/nicmd"
+      thread_id: 1363
+      process_id: 978
+    }
+    task_info {
+      estimated_mws: 0.004292
+      estimated_mw: 0.000496
+      idle_transitions_mws: 0.005500
+      thread_name: "f2fs_discard-25"
+      process_name: "f2fs_discard-254:43"
+      thread_id: 349
+      process_id: 349
+    }
+    task_info {
+      estimated_mws: 0.004283
+      estimated_mw: 0.000495
+      idle_transitions_mws: 0.009490
+      thread_name: "irq/24-glink-na"
+      process_name: "irq/24-glink-native-rpm-glink"
+      thread_id: 86
+      process_id: 86
+    }
+    task_info {
+      estimated_mws: 0.004252
+      estimated_mw: 0.000492
+      idle_transitions_mws: 0.001945
+      thread_name: "pool-4-thread-1"
+      process_name: "system_server"
+      thread_id: 1774
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.004010
+      estimated_mw: 0.000464
+      thread_name: "PasspointProvis"
+      process_name: "system_server"
+      thread_id: 1821
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.003934
+      estimated_mw: 0.000455
+      idle_transitions_mws: 0.001222
+      thread_name: "binder:5377_5"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5573
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.003880
+      estimated_mw: 0.000449
+      thread_name: "BG Thread #3"
+      process_name: "com.google.android.wearable.assistant"
+      thread_id: 4107
+      process_id: 4038
+    }
+    task_info {
+      estimated_mws: 0.003872
+      estimated_mw: 0.000448
+      thread_name: "TransportThread"
+      process_name: "/vendor/bin/mcu_mgmtd"
+      thread_id: 3540
+      process_id: 524
+    }
+    task_info {
+      estimated_mws: 0.003835
+      estimated_mw: 0.000443
+      thread_name: "kworker/1:1H"
+      process_name: "kworker/1:1H"
+      thread_id: 127
+      process_id: 127
+    }
+    task_info {
+      estimated_mws: 0.003754
+      estimated_mw: 0.000434
+      thread_name: "watchdog"
+      process_name: "system_server"
+      thread_id: 1421
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.003628
+      estimated_mw: 0.000419
+      thread_name: "PackageInstalle"
+      process_name: "system_server"
+      thread_id: 1744
+      process_id: 1302
+    }
+    task_info {
+      estimated_mws: 0.003469
+      estimated_mw: 0.000401
+      thread_name: "Lite Thread #1"
+      process_name: "com.google.android.wearable.assistant"
+      thread_id: 4118
+      process_id: 4038
+    }
+    task_info {
+      estimated_mws: 0.003393
+      estimated_mw: 0.000392
+      idle_transitions_mws: 0.009600
+      thread_name: "FinalizerWatchd"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5389
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.003365
+      estimated_mw: 0.000389
+      thread_name: "DFacilitator-1"
+      process_name: "com.google.android.inputmethod.latin"
+      thread_id: 5128
+      process_id: 4997
+    }
+    task_info {
+      estimated_mws: 0.003243
+      estimated_mw: 0.000375
+      thread_name: "pool-8-thread-1"
+      process_name: "com.google.android.wearable.healthservices"
+      thread_id: 3308
+      process_id: 3028
+    }
+    task_info {
+      estimated_mws: 0.002873
+      estimated_mw: 0.000332
+      thread_name: "lowpool[1]"
+      process_name: "com.google.android.gms"
+      thread_id: 3503
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.002842
+      estimated_mw: 0.000329
+      thread_name: "ReferenceQueueD"
+      process_name: "com.google.android.gms"
+      thread_id: 2883
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.002778
+      estimated_mw: 0.000321
+      thread_name: "ipacm-diag"
+      process_name: "/system/vendor/bin/ipacm-diag"
+      thread_id: 976
+      process_id: 976
+    }
+    task_info {
+      estimated_mws: 0.002739
+      estimated_mw: 0.000317
+      idle_transitions_mws: 0.005358
+      thread_name: "migration/2"
+      process_name: "migration/2"
+      thread_id: 32
+      process_id: 32
+    }
+    task_info {
+      estimated_mws: 0.002654
+      estimated_mw: 0.000307
+      thread_name: "qrtr_rx"
+      process_name: "qrtr_rx"
+      thread_id: 564
+      process_id: 564
+    }
+    task_info {
+      estimated_mws: 0.002601
+      estimated_mw: 0.000301
+      thread_name: "card0-crtc0"
+      process_name: "card0-crtc0"
+      thread_id: 247
+      process_id: 247
+    }
+    task_info {
+      estimated_mws: 0.002574
+      estimated_mw: 0.000298
+      thread_name: "pool-7-thread-3"
+      process_name: "com.google.android.wearable.healthservices"
+      thread_id: 5435
+      process_id: 3028
+    }
+    task_info {
+      estimated_mws: 0.002458
+      estimated_mw: 0.000284
+      thread_name: "RenderThread"
+      process_name: "com.google.android.apps.wearable.systemui"
+      thread_id: 2319
+      process_id: 2171
+    }
+    task_info {
+      estimated_mws: 0.002443
+      estimated_mw: 0.000282
+      thread_name: "highpool[0]"
+      process_name: "com.google.android.gms"
+      thread_id: 3154
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.002437
+      estimated_mw: 0.000282
+      thread_name: "lowpool[0]"
+      process_name: "com.google.android.gms"
+      thread_id: 3478
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.002241
+      estimated_mw: 0.000259
+      thread_name: "queued-work-loo"
+      process_name: "com.google.android.gms.persistent"
+      thread_id: 3533
+      process_id: 1949
+    }
+    task_info {
+      estimated_mws: 0.002222
+      estimated_mw: 0.000257
+      thread_name: "arch_disk_io_2"
+      process_name: "com.google.android.gms"
+      thread_id: 4174
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.002160
+      estimated_mw: 0.000250
+      thread_name: "binder:523_2"
+      process_name: "/system/vendor/bin/ipacm"
+      thread_id: 537
+      process_id: 523
+    }
+    task_info {
+      estimated_mws: 0.002129
+      estimated_mw: 0.000246
+      thread_name: "Jit thread pool"
+      process_name: "com.google.android.gms"
+      thread_id: 2881
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.002082
+      estimated_mw: 0.000241
+      thread_name: "pool-48-thread-"
+      process_name: "com.google.android.gms"
+      thread_id: 4110
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.002071
+      estimated_mw: 0.000239
+      thread_name: "RenderThread"
+      process_name: "com.google.android.inputmethod.latin"
+      thread_id: 5120
+      process_id: 4997
+    }
+    task_info {
+      estimated_mws: 0.002020
+      estimated_mw: 0.000233
+      thread_name: "arch_disk_io_0"
+      process_name: "com.google.android.gms"
+      thread_id: 4031
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.001985
+      estimated_mw: 0.000229
+      thread_name: "arch_disk_io_1"
+      process_name: "com.google.android.gms"
+      thread_id: 4034
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.001856
+      estimated_mw: 0.000215
+      thread_name: "BG Thread #0"
+      process_name: "com.google.android.wearable.assistant"
+      thread_id: 4061
+      process_id: 4038
+    }
+    task_info {
+      estimated_mws: 0.001782
+      estimated_mw: 0.000206
+      thread_name: "binder:1926_1"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 1938
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.001776
+      estimated_mw: 0.000205
+      thread_name: "highpool[3]"
+      process_name: "com.google.android.gms"
+      thread_id: 3470
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.001776
+      estimated_mw: 0.000205
+      idle_transitions_mws: 0.005770
+      thread_name: "migration/0"
+      process_name: "migration/0"
+      thread_id: 21
+      process_id: 21
+    }
+    task_info {
+      estimated_mws: 0.001772
+      estimated_mw: 0.000205
+      thread_name: "AsyncTask #2"
+      process_name: "com.google.android.gms"
+      thread_id: 4164
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.001720
+      estimated_mw: 0.000199
+      thread_name: "arch_disk_io_3"
+      process_name: "com.google.android.gms"
+      thread_id: 4175
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.001562
+      estimated_mw: 0.000181
+      idle_transitions_mws: 0.001329
+      thread_name: "POSIX timer 0"
+      process_name: "/vendor/bin/hw/android.hardware.sensors-service.multihal"
+      thread_id: 850
+      process_id: 664
+    }
+    task_info {
+      estimated_mws: 0.001520
+      estimated_mw: 0.000176
+      thread_name: "ksoftirqd/3"
+      process_name: "ksoftirqd/3"
+      thread_id: 42
+      process_id: 42
+    }
+    task_info {
+      estimated_mws: 0.001401
+      estimated_mw: 0.000162
+      thread_name: "Primes-1"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5394
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.001320
+      estimated_mw: 0.000153
+      thread_name: "binder:5377_3"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5392
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.001316
+      estimated_mw: 0.000152
+      idle_transitions_mws: 0.008908
+      thread_name: "msm-watchdog"
+      process_name: "msm-watchdog"
+      thread_id: 76
+      process_id: 76
+    }
+    task_info {
+      estimated_mws: 0.001222
+      estimated_mw: 0.000141
+      thread_name: "Lite Thread #1"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5421
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.001220
+      estimated_mw: 0.000141
+      thread_name: "Signal Catcher"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5382
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.001179
+      estimated_mw: 0.000136
+      thread_name: "GoogleApiHandle"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5398
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.001127
+      estimated_mw: 0.000130
+      thread_name: "binder:2171_6"
+      process_name: "com.google.android.apps.wearable.systemui"
+      thread_id: 2678
+      process_id: 2171
+    }
+    task_info {
+      estimated_mws: 0.001103
+      estimated_mw: 0.000128
+      thread_name: "Blocking Thread"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5574
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.001055
+      estimated_mw: 0.000122
+      thread_name: "WM.task-3"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5430
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000990
+      estimated_mw: 0.000114
+      thread_name: "highpool[1]"
+      process_name: "com.google.android.gms"
+      thread_id: 3373
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.000984
+      estimated_mw: 0.000114
+      thread_name: "Primes-nativecr"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5397
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000961
+      estimated_mw: 0.000111
+      thread_name: "binder:740_4"
+      process_name: "/system/bin/audioserver"
+      thread_id: 2183
+      process_id: 740
+    }
+    task_info {
+      estimated_mws: 0.000954
+      estimated_mw: 0.000110
+      thread_name: "Lite Thread #0"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5404
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000927
+      estimated_mw: 0.000107
+      thread_name: "BG Thread #0"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5395
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000926
+      estimated_mw: 0.000107
+      thread_name: "FinalizerDaemon"
+      process_name: "com.google.android.gms"
+      thread_id: 2885
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.000887
+      estimated_mw: 0.000103
+      thread_name: "BG Thread #1"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5396
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000865
+      estimated_mw: 0.000100
+      thread_name: "Jit thread pool"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5385
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000858
+      estimated_mw: 0.000099
+      thread_name: "highpool[2]"
+      process_name: "com.google.android.gms"
+      thread_id: 3375
+      process_id: 2856
+    }
+    task_info {
+      estimated_mws: 0.000855
+      estimated_mw: 0.000099
+      thread_name: "ConnectivityThr"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5423
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000828
+      estimated_mw: 0.000096
+      thread_name: "Profile Saver"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5393
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000808
+      estimated_mw: 0.000093
+      idle_transitions_mws: 0.000732
+      thread_name: "ReferenceQueueD"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5387
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000803
+      estimated_mw: 0.000093
+      idle_transitions_mws: 0.000679
+      thread_name: "binder:5377_4"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5433
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000783
+      estimated_mw: 0.000091
+      thread_name: "ksoftirqd/1"
+      process_name: "ksoftirqd/1"
+      thread_id: 27
+      process_id: 27
+    }
+    task_info {
+      estimated_mws: 0.000782
+      estimated_mw: 0.000090
+      thread_name: "HsConnectionMan"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5422
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000769
+      estimated_mw: 0.000089
+      thread_name: "ADB-JDWP Connec"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5384
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000742
+      estimated_mw: 0.000086
+      thread_name: "Scheduler Threa"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5428
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000733
+      estimated_mw: 0.000085
+      thread_name: "WM.task-2"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5429
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000730
+      estimated_mw: 0.000084
+      thread_name: "Scheduled BG"
+      process_name: "com.google.android.wearable.sysui"
+      thread_id: 2896
+      process_id: 1926
+    }
+    task_info {
+      estimated_mws: 0.000727
+      estimated_mw: 0.000084
+      thread_name: "DefaultDispatch"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5431
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000726
+      estimated_mw: 0.000084
+      thread_name: "BG Thread #3"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5400
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000724
+      estimated_mw: 0.000084
+      thread_name: "WM.task-1"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5427
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000718
+      estimated_mw: 0.000083
+      thread_name: "binder:5377_1"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5390
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000689
+      estimated_mw: 0.000080
+      thread_name: "DefaultDispatch"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5432
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000669
+      estimated_mw: 0.000077
+      thread_name: "BG Thread #2"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5399
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000630
+      estimated_mw: 0.000073
+      idle_transitions_mws: 0.008723
+      thread_name: "binder:5377_2"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5391
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000583
+      estimated_mw: 0.000067
+      thread_name: "perfetto_hprof_"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5383
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000507
+      estimated_mw: 0.000059
+      thread_name: "Primes-2"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5444
+      process_id: 5377
+    }
+    task_info {
+      estimated_mws: 0.000403
+      estimated_mw: 0.000047
+      thread_name: "FinalizerDaemon"
+      process_name: "com.fitbit.FitbitMobile"
+      thread_id: 5388
+      process_id: 5377
+    }
   }
 }
diff --git a/test/trace_processor/diff_tests/metrics/chrome/tests_scroll_jank.py b/test/trace_processor/diff_tests/metrics/chrome/tests_scroll_jank.py
index 597cdcd..1ce67ab 100644
--- a/test/trace_processor/diff_tests/metrics/chrome/tests_scroll_jank.py
+++ b/test/trace_processor/diff_tests/metrics/chrome/tests_scroll_jank.py
@@ -492,7 +492,7 @@
         INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_v3;
 
         SELECT
-          HAS_DESCENDANT_SLICE_WITH_NAME(
+          _HAS_DESCENDANT_SLICE_WITH_NAME(
             (SELECT id from slice where dur = 60156000),
             'SwapEndToPresentationCompositorFrame') AS has_descendant;
         """,
@@ -510,7 +510,7 @@
         INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_v3;
 
         SELECT
-          HAS_DESCENDANT_SLICE_WITH_NAME(
+          _HAS_DESCENDANT_SLICE_WITH_NAME(
             (SELECT id from slice where dur = 77247000),
             'SwapEndToPresentationCompositorFrame') AS has_descendant;
         """,
diff --git a/test/trace_processor/diff_tests/metrics/codecs/codec-framedecoder-trace.out b/test/trace_processor/diff_tests/metrics/codecs/codec-framedecoder-trace.out
index 1ba2a49..2873b57 100644
--- a/test/trace_processor/diff_tests/metrics/codecs/codec-framedecoder-trace.out
+++ b/test/trace_processor/diff_tests/metrics/codecs/codec-framedecoder-trace.out
@@ -3,7 +3,6 @@
     process_name: "/system/bin/mediaserver"
     thread_name: "CodecLooper"
     thread_cpu_ns: 40782760
-    num_threads: 1
     core_data {
       type: "bigger"
       metrics {
@@ -32,6 +31,7 @@
       thread_name: "CodecLooper"
       total_cpu_ns: 6505730
       running_cpu_ns: 6224689
+      cpu_cycles: 4
     }
   }
   codec_function {
@@ -41,6 +41,7 @@
       thread_name: "HwBinder:1465_4"
       total_cpu_ns: 805417
       running_cpu_ns: 805417
+      cpu_cycles: 1
     }
   }
   codec_function {
@@ -50,6 +51,7 @@
       thread_name: "CodecLooper"
       total_cpu_ns: 34476253
       running_cpu_ns: 19314740
+      cpu_cycles: 0
     }
   }
   codec_function {
@@ -79,13 +81,6 @@
       running_cpu_ns: 17917404
     }
   }
-  codec_function {
-    codec_string: "FrameDecoderBenchmark#frameDecoderBenchmarkTest"
-    process_name: "android.platform.test.scenario"
-    detail {
-      thread_name: "roidJUnitRunner"
-      total_cpu_ns: -1
-      running_cpu_ns: -1
-    }
+  energy {
   }
 }
diff --git a/test/trace_processor/diff_tests/metrics/graphics/tests.py b/test/trace_processor/diff_tests/metrics/graphics/tests.py
index 28a1f66..cc514a2 100644
--- a/test/trace_processor/diff_tests/metrics/graphics/tests.py
+++ b/test/trace_processor/diff_tests/metrics/graphics/tests.py
@@ -20,23 +20,6 @@
 
 
 class GraphicsMetrics(TestSuite):
-  # Android SurfaceFlinger metrics
-  def test_frame_missed_event_frame_missed(self):
-    return DiffTestBlueprint(
-        trace=Path('frame_missed.py'),
-        query="""
-        SELECT RUN_METRIC('android/android_surfaceflinger.sql');
-
-        SELECT ts, dur
-        FROM android_surfaceflinger_event;
-        """,
-        out=Csv("""
-        "ts","dur"
-        100,1
-        102,1
-        103,1
-        """))
-
   def test_frame_missed_metrics(self):
     return DiffTestBlueprint(
         trace=Path('frame_missed.py'),
diff --git a/test/trace_processor/diff_tests/metrics/memory/trace_metadata.out b/test/trace_processor/diff_tests/metrics/memory/trace_metadata.out
index e26a764..b6c0845 100644
--- a/test/trace_processor/diff_tests/metrics/memory/trace_metadata.out
+++ b/test/trace_processor/diff_tests/metrics/memory/trace_metadata.out
@@ -3,5 +3,8 @@
   trace_uuid: "00000000-0000-0000-e77f-20a2204c2a49",
   trace_size_bytes: 6365447
   trace_config_pbtxt: "buffers {\n  size_kb: 32768\n  fill_policy: UNSPECIFIED\n}\ndata_sources {\n  config {\n    name: \"linux.ftrace\"\n    target_buffer: 0\n    trace_duration_ms: 0\n    tracing_session_id: 0\n    ftrace_config {\n      ftrace_events: \"print\"\n      ftrace_events: \"sched_switch\"\n      ftrace_events: \"rss_stat\"\n      ftrace_events: \"ion_heap_shrink\"\n      ftrace_events: \"ion_heap_grow\"\n      atrace_categories: \"am\"\n      atrace_categories: \"dalvik\"\n      buffer_size_kb: 0\n      drain_period_ms: 0\n    }\n    chrome_config {\n      trace_config: \"\"\n    }\n    inode_file_config {\n      scan_interval_ms: 0\n      scan_delay_ms: 0\n      scan_batch_size: 0\n      do_not_scan: false\n    }\n    process_stats_config {\n      scan_all_processes_on_start: false\n      record_thread_names: false\n      proc_stats_poll_ms: 0\n    }\n    sys_stats_config {\n      meminfo_period_ms: 0\n      vmstat_period_ms: 0\n      stat_period_ms: 0\n    }\n    heapprofd_config {\n      sampling_interval_bytes: 0\n      all: false\n      continuous_dump_config {\n        dump_phase_ms: 0\n        dump_interval_ms: 0\n      }\n    }\n    legacy_config: \"\"\n  }\n}\ndata_sources {\n  config {\n    name: \"linux.process_stats\"\n    target_buffer: 0\n    trace_duration_ms: 0\n    tracing_session_id: 0\n    ftrace_config {\n      buffer_size_kb: 0\n      drain_period_ms: 0\n    }\n    chrome_config {\n      trace_config: \"\"\n    }\n    inode_file_config {\n      scan_interval_ms: 0\n      scan_delay_ms: 0\n      scan_batch_size: 0\n      do_not_scan: false\n    }\n    process_stats_config {\n      scan_all_processes_on_start: false\n      record_thread_names: false\n      proc_stats_poll_ms: 100\n    }\n    sys_stats_config {\n      meminfo_period_ms: 0\n      vmstat_period_ms: 0\n      stat_period_ms: 0\n    }\n    heapprofd_config {\n      sampling_interval_bytes: 0\n      all: false\n      continuous_dump_config {\n        dump_phase_ms: 0\n        dump_interval_ms: 0\n      }\n    }\n    legacy_config: \"\"\n  }\n}\ndata_sources {\n  config {\n    name: \"linux.sys_stats\"\n    target_buffer: 0\n    trace_duration_ms: 0\n    tracing_session_id: 0\n    ftrace_config {\n      buffer_size_kb: 0\n      drain_period_ms: 0\n    }\n    chrome_config {\n      trace_config: \"\"\n    }\n    inode_file_config {\n      scan_interval_ms: 0\n      scan_delay_ms: 0\n      scan_batch_size: 0\n      do_not_scan: false\n    }\n    process_stats_config {\n      scan_all_processes_on_start: false\n      record_thread_names: false\n      proc_stats_poll_ms: 0\n    }\n    sys_stats_config {\n      meminfo_period_ms: 50\n      meminfo_counters: MEMINFO_MEM_AVAILABLE\n      meminfo_counters: MEMINFO_SWAP_CACHED\n      meminfo_counters: MEMINFO_ACTIVE\n      meminfo_counters: MEMINFO_INACTIVE\n      vmstat_period_ms: 0\n      stat_period_ms: 0\n    }\n    heapprofd_config {\n      sampling_interval_bytes: 0\n      all: false\n      continuous_dump_config {\n        dump_phase_ms: 0\n        dump_interval_ms: 0\n      }\n    }\n    legacy_config: \"\"\n  }\n}\nduration_ms: 10000\nenable_extra_guardrails: false\nlockdown_mode: LOCKDOWN_UNCHANGED\nstatsd_metadata {\n  triggering_alert_id: 0\n  triggering_config_uid: 0\n  triggering_config_id: 0\n}\nwrite_into_file: false\nfile_write_period_ms: 0\nmax_file_size_bytes: 0\nguardrail_overrides {\n  max_upload_per_day_bytes: 0\n}\ndeferred_start: false",
-  sched_duration_ns: 9452761359
+  sched_duration_ns: 9452761359,
+  suspend_count: 0,
+  data_loss_count: 0,
+  error_count: 1
 }
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup.out b/test/trace_processor/diff_tests/metrics/startup/android_startup.out
index 29e6a3c..ec2b075 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup.out
@@ -1,6 +1,7 @@
 android_startup {
   startup {
     startup_id: 0
+    cpu_count: 2
     package_name: "com.google.android.calendar"
     process_name: "com.google.android.calendar"
     zygote_new_process: false
@@ -77,10 +78,15 @@
       }
       launch_dur: 108
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 130
+          end_timestamp: 210
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 130
         end_timestamp: 210
-        thread_utid: 3
-        thread_name: "com.google.android.calendar"
       }
     }
     startup_type: "warm"
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.out
index 9dba7b6..8b53348 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.out
@@ -57,6 +57,11 @@
         dur_ns: 55
         dur_ms: 5.5e-05
       }
+      time_class_initialization {
+        dur_ns: 2
+        dur_ms: 2e-06
+      }
+      class_initialization_count: 2
     }
     activity_hosting_process_count: 1
     process {
@@ -126,6 +131,7 @@
     dlopen_file: "libandroid.so"
     dlopen_file: "libandroid2.so"
     startup_type: "hot"
+    cpu_count: 1
     slow_start_reason: "GC Activity"
     slow_start_reason: "Main Thread - Time spent in OpenDexFilesFromOat*"
     slow_start_reason: "Main Thread - Binder transactions blocked"
@@ -142,10 +148,16 @@
       }
       launch_dur: 999999900
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 340
+          end_timestamp: 390
+          slice_id: 20
+          slice_name: "CollectorTransition mark sweep GC"
+          process_pid: 3
+          thread_tid: 5
+        }
         start_timestamp: 340
         end_timestamp: 390
-        slice_id: 18
-        slice_name: "CollectorTransition mark sweep GC"
       }
     }
     slow_start_reason_with_details {
@@ -163,16 +175,24 @@
       }
       launch_dur: 999999900
       trace_slice_sections {
-        start_timestamp: 170
-        end_timestamp: 500000000
-        slice_id: 9
-        slice_name: "OpenDexFilesFromOat(something else)"
-      }
-      trace_slice_sections {
+        slice_section {
+          start_timestamp: 170
+          end_timestamp: 500000000
+          slice_id: 9
+          slice_name: "OpenDexFilesFromOat(something else)"
+          process_pid: 3
+          thread_tid: 3
+        }
+        slice_section {
+          start_timestamp: 150
+          end_timestamp: 165
+          slice_id: 5
+          slice_name: "OpenDexFilesFromOat(something)"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 150
-        end_timestamp: 165
-        slice_id: 5
-        slice_name: "OpenDexFilesFromOat(something)"
+        end_timestamp: 500000000
       }
     }
     slow_start_reason_with_details {
@@ -188,10 +208,16 @@
       }
       launch_dur: 999999900
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 10000000
+          end_timestamp: 50000000
+          slice_id: 21
+          slice_name: "binder transaction"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 10000000
         end_timestamp: 50000000
-        slice_id: 19
-        slice_name: "binder transaction"
       }
     }
   }
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.py
index dc3e0ab..2b98863 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.py
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.py
@@ -107,6 +107,15 @@
 trace.add_atrace_begin(ts=270, pid=APP_PID, tid=APP_TID, buf='VerifyClass dl')
 trace.add_atrace_end(ts=275, pid=APP_PID, tid=APP_TID)
 
+# class init slices within the startup.
+# class init slices will start with an L and end with a semicolon,
+# e.g. Lkotlin/text/MatchResult;
+trace.add_atrace_begin(ts=276, pid=APP_PID, tid=APP_TID, buf='Landroid/dummy;')
+trace.add_atrace_end(ts=277, pid=APP_PID, tid=APP_TID)
+
+trace.add_atrace_begin(ts=278, pid=APP_PID, tid=APP_TID, buf='Lcom/dummy;')
+trace.add_atrace_end(ts=279, pid=APP_PID, tid=APP_TID)
+
 # VerifyClass slice outside the startup.
 trace.add_atrace_begin(ts=55, pid=APP_PID, tid=APP_TID, buf='VerifyClass xf')
 trace.add_atrace_end(ts=65, pid=APP_PID, tid=APP_TID)
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.out
index 07eded2..a4e1074 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.out
@@ -1,6 +1,7 @@
 android_startup {
   startup {
     startup_id: 0
+    cpu_count: 1
     package_name: "com.some.app"
     process_name: "com.some.app"
     zygote_new_process: false
@@ -106,10 +107,16 @@
       }
       launch_dur: 999999900000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 340000000000
+          end_timestamp: 390000000000
+          slice_id: 91
+          slice_name: "CollectorTransition mark sweep GC"
+          process_pid: 3
+          thread_tid: 5
+        }
         start_timestamp: 340000000000
         end_timestamp: 390000000000
-        slice_id: 91
-        slice_name: "CollectorTransition mark sweep GC"
       }
     }
     slow_start_reason_with_details {
@@ -126,22 +133,29 @@
       }
       launch_dur: 999999900000000000
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 155000000000
+          end_timestamp: 165000000000
+          thread_name: "Jit thread pool"
+          process_pid: 3
+          thread_tid: 4
+        }
+        thread_section {
+          start_timestamp: 170000000000
+          end_timestamp: 175000000000
+          thread_name: "Jit thread pool"
+          process_pid: 3
+          thread_tid: 4
+        }
+        thread_section {
+          start_timestamp: 185000000000
+          end_timestamp: 190000000000
+          thread_name: "Jit thread pool"
+          process_pid: 3
+          thread_tid: 4
+        }
         start_timestamp: 155000000000
-        end_timestamp: 165000000000
-        thread_utid: 4
-        thread_name: "Jit thread pool"
-      }
-      trace_thread_sections {
-        start_timestamp: 170000000000
-        end_timestamp: 175000000000
-        thread_utid: 4
-        thread_name: "Jit thread pool"
-      }
-      trace_thread_sections {
-        start_timestamp: 185000000000
         end_timestamp: 190000000000
-        thread_utid: 4
-        thread_name: "Jit thread pool"
       }
     }
     slow_start_reason_with_details {
@@ -158,22 +172,32 @@
       }
       launch_dur: 999999900000000000
       trace_slice_sections {
-        start_timestamp: 200000000000
-        end_timestamp: 210000000000
-        slice_id: 84
-        slice_name: "JIT compiling nothing"
-      }
-      trace_slice_sections {
+        slice_section {
+          start_timestamp: 200000000000
+          end_timestamp: 210000000000
+          slice_id: 84
+          slice_name: "JIT compiling nothing"
+          process_pid: 3
+          thread_tid: 3
+        }
+        slice_section {
+          start_timestamp: 100000000000
+          end_timestamp: 101000000000
+          slice_id: 9
+          slice_name: "JIT compiling something"
+          process_pid: 3
+          thread_tid: 4
+        }
+        slice_section {
+          start_timestamp: 101000000000
+          end_timestamp: 102000000000
+          slice_id: 10
+          slice_name: "JIT compiling something"
+          process_pid: 3
+          thread_tid: 4
+        }
         start_timestamp: 100000000000
-        end_timestamp: 101000000000
-        slice_id: 9
-        slice_name: "JIT compiling something"
-      }
-      trace_slice_sections {
-        start_timestamp: 101000000000
-        end_timestamp: 102000000000
-        slice_id: 10
-        slice_name: "JIT compiling something"
+        end_timestamp: 210000000000
       }
     }
   }
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_battery.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_battery.py
index 3727a12..49ad31f 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_battery.py
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_battery.py
@@ -18,9 +18,9 @@
 import synth_common
 
 trace = synth_common.create_trace()
-trace.add_battery_counters(20, 52, 0.2, 10, 12)
-trace.add_battery_counters(52, 32, 0.8, 8, 93)
-trace.add_battery_counters(80, 15, 0.5, 9, 5)
-trace.add_battery_counters_no_curr_ua(92, 21, 0.3, 25)
+trace.add_battery_counters(20, 52, 0.2, 990000, 12, 11800000)
+trace.add_battery_counters(52, 32, 0.8, 710000, 93, 11900000)
+trace.add_battery_counters(80, 15, 0.5, 510000, 5, 12000000)
+trace.add_battery_counters_no_curr_ua(92, 21, 0.3, 25, 12000000)
 
 sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.out
index 01a8a83..04b8915 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.out
@@ -1,6 +1,7 @@
 android_startup {
   startup {
     startup_id: 0
+    cpu_count: 1
     package_name: "com.google.android.calendar"
     process_name: "com.google.android.calendar"
     zygote_new_process: true
@@ -127,10 +128,16 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 204000000000
+          end_timestamp: 205000000000
+          slice_id: 13
+          slice_name: "location=/system/framework/oat/arm/com.google.android.calendar.odex status=up-to-date filter=speed reason=install-dm"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 204000000000
         end_timestamp: 205000000000
-        slice_id: 13
-        slice_name: "location=/system/framework/oat/arm/com.google.android.calendar.odex status=up-to-date filter=speed reason=install-dm"
       }
     }
     slow_start_reason_with_details {
@@ -146,10 +153,16 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 200000000000
+          end_timestamp: 202000000000
+          slice_id: 12
+          slice_name: "location=error status=io-error-no-oat filter=run-from-apk reason=unknown"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 200000000000
         end_timestamp: 202000000000
-        slice_id: 12
-        slice_name: "location=error status=io-error-no-oat filter=run-from-apk reason=unknown"
       }
     }
     slow_start_reason_with_details {
@@ -166,10 +179,16 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 185000000000
+          end_timestamp: 187000000000
+          slice_id: 4
+          slice_name: "bindApplication"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 185000000000
         end_timestamp: 187000000000
-        slice_id: 4
-        slice_name: "bindApplication"
       }
     }
     slow_start_reason_with_details {
@@ -186,16 +205,24 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 188000000000
+          end_timestamp: 189000000000
+          slice_id: 6
+          slice_name: "inflate"
+          process_pid: 3
+          thread_tid: 3
+        }
+        slice_section {
+          start_timestamp: 191000000000
+          end_timestamp: 192000000000
+          slice_id: 8
+          slice_name: "inflate"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 188000000000
-        end_timestamp: 189000000000
-        slice_id: 6
-        slice_name: "inflate"
-      }
-      trace_slice_sections {
-        start_timestamp: 191000000000
         end_timestamp: 192000000000
-        slice_id: 8
-        slice_name: "inflate"
       }
     }
     slow_start_reason_with_details {
@@ -212,10 +239,16 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 188000000000
+          end_timestamp: 189000000000
+          slice_id: 7
+          slice_name: "ResourcesManager#getResources"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 188000000000
         end_timestamp: 189000000000
-        slice_id: 7
-        slice_name: "ResourcesManager#getResources"
       }
     }
     slow_start_reason_with_details {
@@ -232,10 +265,15 @@
       }
       launch_dur: 108000000000
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 205000000000
+          end_timestamp: 210000000000
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 205000000000
         end_timestamp: 210000000000
-        thread_utid: 3
-        thread_name: "com.google.android.calendar"
       }
     }
     startup_type: "cold"
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.out
index d972c12..f2c7123 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.out
@@ -1,6 +1,7 @@
 android_startup {
   startup {
     startup_id: 0
+    cpu_count: 1
     package_name: "com.google.android.calendar"
     process_name: "com.google.android.calendar"
     zygote_new_process: true
@@ -126,10 +127,16 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 200000000000
+          end_timestamp: 202000000000
+          slice_id: 12
+          slice_name: "location=error status=io-error-no-oat filter=run-from-apk reason=unknown"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 200000000000
         end_timestamp: 202000000000
-        slice_id: 12
-        slice_name: "location=error status=io-error-no-oat filter=run-from-apk reason=unknown"
       }
     }
     slow_start_reason_with_details {
@@ -146,10 +153,16 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 185000000000
+          end_timestamp: 195000000000
+          slice_id: 4
+          slice_name: "bindApplication"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 185000000000
         end_timestamp: 195000000000
-        slice_id: 4
-        slice_name: "bindApplication"
       }
     }
     slow_start_reason_with_details {
@@ -166,16 +179,24 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
-        start_timestamp: 190000000000
-        end_timestamp: 192000000000
-        slice_id: 8
-        slice_name: "inflate"
-      }
-      trace_slice_sections {
+        slice_section {
+          start_timestamp: 190000000000
+          end_timestamp: 192000000000
+          slice_id: 8
+          slice_name: "inflate"
+          process_pid: 3
+          thread_tid: 3
+        }
+        slice_section {
+          start_timestamp: 188000000000
+          end_timestamp: 189000000000
+          slice_id: 7
+          slice_name: "inflate"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 188000000000
-        end_timestamp: 189000000000
-        slice_id: 7
-        slice_name: "inflate"
+        end_timestamp: 192000000000
       }
     }
     slow_start_reason_with_details {
@@ -192,10 +213,16 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 187000000000
+          end_timestamp: 192000000000
+          slice_id: 5
+          slice_name: "ResourcesManager#getResources"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 187000000000
         end_timestamp: 192000000000
-        slice_id: 5
-        slice_name: "ResourcesManager#getResources"
       }
     }
     slow_start_reason_with_details {
@@ -212,10 +239,15 @@
       }
       launch_dur: 108000000000
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 205000000000
+          end_timestamp: 210000000000
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 205000000000
         end_timestamp: 210000000000
-        thread_utid: 3
-        thread_name: "com.google.android.calendar"
       }
     }
     startup_type: "cold"
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast.out
index a0753b7..fc0f086 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast.out
@@ -1,6 +1,7 @@
 android_startup {
   startup {
     startup_id: 1
+    cpu_count: 0
     package_name: "com.google.android.calendar"
     zygote_new_process: false
     to_first_frame {
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.out
index 2fe9ca8..6cb97e0 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.out
@@ -1,6 +1,7 @@
 android_startup {
   startup {
     startup_id: 1
+    cpu_count: 0
     package_name: "com.google.android.calendar"
     zygote_new_process: false
     to_first_frame {
@@ -45,22 +46,29 @@
       }
       launch_dur: 100
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 105
+          end_timestamp: 106
+          slice_id: 6
+          slice_name: "Broadcast dispatched from android (2005:system/1000) x"
+          thread_tid: 1
+        }
+        slice_section {
+          start_timestamp: 106
+          end_timestamp: 107
+          slice_id: 8
+          slice_name: "Broadcast dispatched from android (2005:system/1000) x"
+          thread_tid: 1
+        }
+        slice_section {
+          start_timestamp: 107
+          end_timestamp: 108
+          slice_id: 10
+          slice_name: "Broadcast dispatched from android (2005:system/1000) x"
+          thread_tid: 1
+        }
         start_timestamp: 105
-        end_timestamp: 106
-        slice_id: 6
-        slice_name: "Broadcast dispatched from android (2005:system/1000) x"
-      }
-      trace_slice_sections {
-        start_timestamp: 106
-        end_timestamp: 107
-        slice_id: 8
-        slice_name: "Broadcast dispatched from android (2005:system/1000) x"
-      }
-      trace_slice_sections {
-        start_timestamp: 107
         end_timestamp: 108
-        slice_id: 10
-        slice_name: "Broadcast dispatched from android (2005:system/1000) x"
       }
     }
     slow_start_reason_with_details {
@@ -77,22 +85,29 @@
       }
       launch_dur: 100
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 100
+          end_timestamp: 101
+          slice_id: 1
+          slice_name: "broadcastReceiveReg: x"
+          thread_tid: 2
+        }
+        slice_section {
+          start_timestamp: 101
+          end_timestamp: 102
+          slice_id: 2
+          slice_name: "broadcastReceiveReg: x"
+          thread_tid: 2
+        }
+        slice_section {
+          start_timestamp: 102
+          end_timestamp: 103
+          slice_id: 3
+          slice_name: "broadcastReceiveReg: x"
+          thread_tid: 2
+        }
         start_timestamp: 100
-        end_timestamp: 101
-        slice_id: 1
-        slice_name: "broadcastReceiveReg: x"
-      }
-      trace_slice_sections {
-        start_timestamp: 101
-        end_timestamp: 102
-        slice_id: 2
-        slice_name: "broadcastReceiveReg: x"
-      }
-      trace_slice_sections {
-        start_timestamp: 102
         end_timestamp: 103
-        slice_id: 3
-        slice_name: "broadcastReceiveReg: x"
       }
     }
   }
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat.out
index 7c886eb..efcbb30 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat.out
@@ -1,6 +1,7 @@
 android_startup {
   startup {
     startup_id: 1
+    cpu_count: 1
     package_name: "com.google.android.calendar"
     zygote_new_process: false
     to_first_frame {
@@ -32,6 +33,7 @@
   }
   startup {
     startup_id: 2
+    cpu_count: 1
     package_name: "com.google.android.calculator"
     zygote_new_process: false
     to_first_frame {
@@ -79,6 +81,7 @@
   }
   startup {
     startup_id: 3
+    cpu_count: 1
     package_name: "com.google.android.deskclock"
     zygote_new_process: false
     to_first_frame {
@@ -126,6 +129,7 @@
   }
   startup {
     startup_id: 4
+    cpu_count: 1
     package_name: "com.google.android.gm"
     zygote_new_process: false
     to_first_frame {
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat_slow.out
index 9be93fc..8502243 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat_slow.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat_slow.out
@@ -1,6 +1,7 @@
 android_startup {
   startup {
     startup_id: 1
+    cpu_count: 1
     package_name: "com.google.android.calendar"
     zygote_new_process: false
     to_first_frame {
@@ -32,6 +33,7 @@
   }
   startup {
     startup_id: 2
+    cpu_count: 1
     package_name: "com.google.android.calculator"
     zygote_new_process: false
     to_first_frame {
@@ -79,6 +81,7 @@
   }
   startup {
     startup_id: 3
+    cpu_count: 1
     package_name: "com.google.android.deskclock"
     zygote_new_process: false
     to_first_frame {
@@ -158,6 +161,7 @@
   }
   startup {
     startup_id: 4
+    cpu_count: 1
     package_name: "com.google.android.gm"
     zygote_new_process: false
     to_first_frame {
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention.out
index 7c638b5..8ee0bbe 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention.out
@@ -1,6 +1,7 @@
 android_startup {
   startup {
     startup_id: 1
+    cpu_count: 0
     package_name: "com.google.android.calendar"
     process_name: "com.google.android.calendar"
     zygote_new_process: false
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.out
index 61782f8..f64dceb 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.out
@@ -1,6 +1,7 @@
 android_startup {
   startup {
     startup_id: 1
+    cpu_count: 0
     package_name: "com.google.android.calendar"
     process_name: "com.google.android.calendar"
     zygote_new_process: false
@@ -81,10 +82,16 @@
       }
       launch_dur: 100000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 112000000000
+          end_timestamp: 115000000000
+          slice_id: 1
+          slice_name: "bindApplication"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 112000000000
         end_timestamp: 115000000000
-        slice_id: 1
-        slice_name: "bindApplication"
       }
     }
     slow_start_reason_with_details {
@@ -101,6 +108,34 @@
         dur: 27000000000
       }
       launch_dur: 100000000000
+      trace_slice_sections {
+        slice_section {
+          start_timestamp: 140000000000
+          end_timestamp: 157000000000
+          slice_id: 5
+          slice_name: "Lock contention on a monitor lock (owner tid: 2)"
+          process_pid: 3
+          thread_tid: 3
+        }
+        slice_section {
+          start_timestamp: 120000000000
+          end_timestamp: 130000000000
+          slice_id: 4
+          slice_name: "Lock contention on thread list lock (owner tid: 2)"
+          process_pid: 3
+          thread_tid: 3
+        }
+        slice_section {
+          start_timestamp: 155000000000
+          end_timestamp: 160000000000
+          slice_id: 6
+          slice_name: "Lock contention on a monitor lock (owner tid: 3)"
+          process_pid: 3
+          thread_tid: 4
+        }
+        start_timestamp: 120000000000
+        end_timestamp: 160000000000
+      }
     }
     slow_start_reason_with_details {
      reason_id: MAIN_THREAD_MONITOR_CONTENTION
@@ -116,6 +151,26 @@
        dur: 17000000000
      }
      launch_dur: 100000000000
+     trace_slice_sections {
+       slice_section {
+         start_timestamp: 140000000000
+         end_timestamp: 157000000000
+         slice_id: 5
+         slice_name: "Lock contention on a monitor lock (owner tid: 2)"
+         process_pid: 3
+         thread_tid: 3
+       }
+       slice_section {
+         start_timestamp: 155000000000
+         end_timestamp: 160000000000
+         slice_id: 6
+         slice_name: "Lock contention on a monitor lock (owner tid: 3)"
+         process_pid: 3
+         thread_tid: 4
+       }
+       start_timestamp: 140000000000
+       end_timestamp: 160000000000
+     }
     }
     startup_type: "cold"
   }
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_minsdk33.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_minsdk33.out
index fdd584f..bba8cc1 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_minsdk33.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_minsdk33.out
@@ -1,6 +1,7 @@
 android_startup {
   startup {
     startup_id: 1
+    cpu_count: 0
     package_name: "com.google.android.calendar"
     zygote_new_process: false
     to_first_frame {
@@ -46,6 +47,7 @@
   }
   startup {
     startup_id: 2
+    cpu_count: 0
     package_name: "com.google.android.calendar"
     process_name: "com.google.android.calendar"
     zygote_new_process: false
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.out
index be657e6..17d426d 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.out
@@ -1,6 +1,7 @@
 android_startup {
   startup {
     startup_id: 0
+    cpu_count: 0
     package_name: "com.google.android.calendar"
     process_name: "com.google.android.calendar:debug"
     zygote_new_process: false
@@ -76,15 +77,21 @@
       }
       launch_dur: 7
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 103
+          end_timestamp: 107
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 103
         end_timestamp: 107
-        thread_utid: 3
-        thread_name: "com.google.android.calendar"
       }
     }
   }
   startup {
     startup_id: 1
+    cpu_count: 0
     package_name: "com.google.android.calendar"
     process_name: "com.google.android.calendar"
     zygote_new_process: false
@@ -161,10 +168,15 @@
       }
       launch_dur: 7
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 203
+          end_timestamp: 207
+          thread_name: "com.google.android.calendar"
+          process_pid: 4
+          thread_tid: 4
+        }
         start_timestamp: 203
         end_timestamp: 207
-        thread_utid: 4
-        thread_name: "com.google.android.calendar"
       }
     }
   }
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_slow.out
index 2bd4bd8..0ae5b29 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_slow.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_slow.out
@@ -1,6 +1,7 @@
 android_startup {
   startup {
     startup_id: 0
+    cpu_count: 2
     package_name: "com.google.android.calendar"
     process_name: "com.google.android.calendar"
     zygote_new_process: false
@@ -80,10 +81,15 @@
       }
       launch_dur: 108000000000
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 130000000000
+          end_timestamp: 210000000000
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 130000000000
         end_timestamp: 210000000000
-        thread_utid: 3
-        thread_name: "com.google.android.calendar"
       }
     }
     slow_start_reason_with_details {
@@ -100,10 +106,15 @@
       }
       launch_dur: 108000000000
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 120000000000
+          end_timestamp: 125000000000
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 120000000000
         end_timestamp: 125000000000
-        thread_utid: 3
-        thread_name: "com.google.android.calendar"
       }
     }
     slow_start_reason_with_details {
@@ -120,10 +131,15 @@
       }
       launch_dur: 108000000000
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 125000000000
+          end_timestamp: 130000000000
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 125000000000
         end_timestamp: 130000000000
-        thread_utid: 3
-        thread_name: "com.google.android.calendar"
       }
     }
     slow_start_reason_with_details {
@@ -140,10 +156,15 @@
       }
       launch_dur: 108000000000
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 130000000000
+          end_timestamp: 210000000000
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 130000000000
         end_timestamp: 210000000000
-        thread_utid: 3
-        thread_name: "com.google.android.calendar"
       }
     }
   }
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.out
index 0340775..1f2000d 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.out
@@ -1,6 +1,7 @@
 android_startup {
   startup {
     startup_id: 1
+    cpu_count: 0
     package_name: "com.google.android.calendar"
     zygote_new_process: false
     to_first_frame {
@@ -43,10 +44,15 @@
       }
       launch_dur: 100
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 130
+          end_timestamp: 133
+          slice_id: 1
+          slice_name: "KeyguardUpdateMonitor#onAuthenticationSucceeded"
+          thread_tid: 2
+        }
         start_timestamp: 130
         end_timestamp: 133
-        slice_id: 1
-        slice_name: "KeyguardUpdateMonitor#onAuthenticationSucceeded"
       }
     }
   }
diff --git a/test/trace_processor/diff_tests/metrics/startup/tests_metrics.py b/test/trace_processor/diff_tests/metrics/startup/tests_metrics.py
index 99911da..88fae90 100644
--- a/test/trace_processor/diff_tests/metrics/startup/tests_metrics.py
+++ b/test/trace_processor/diff_tests/metrics/startup/tests_metrics.py
@@ -80,21 +80,21 @@
               timestamp_ns: 20
               charge_counter_uah: 52
               capacity_percent: 0.2
-              current_ua: 10
+              current_ua: 990000
               current_avg_ua: 12
            }
            battery_counters {
               timestamp_ns: 52
               charge_counter_uah: 32
               capacity_percent: 0.8
-              current_ua: 8
+              current_ua: 710000
               current_avg_ua: 93
            }
            battery_counters {
               timestamp_ns: 80
               charge_counter_uah: 15
               capacity_percent: 0.5
-              current_ua: 9
+              current_ua: 510000
               current_avg_ua: 5
            }
            battery_counters {
@@ -103,6 +103,10 @@
               capacity_percent: 0.3
               current_avg_ua: 25
            }
+           battery_aggregates {
+              avg_power_mw: 9497.722222222223
+              energy_usage_estimate: 1.3017599999999998
+           }
         }
         """))
 
diff --git a/test/trace_processor/diff_tests/metrics/startup/ttid_and_ttfd.out b/test/trace_processor/diff_tests/metrics/startup/ttid_and_ttfd.out
index 8999dd6..624b34d 100644
--- a/test/trace_processor/diff_tests/metrics/startup/ttid_and_ttfd.out
+++ b/test/trace_processor/diff_tests/metrics/startup/ttid_and_ttfd.out
@@ -94,5 +94,6 @@
     startup_type: "warm"
     time_to_initial_display: 62373965
     time_to_full_display: 555968701
+    cpu_count: 8
     }
 }
diff --git a/test/trace_processor/diff_tests/parser/android/android_system_property_slice.out b/test/trace_processor/diff_tests/parser/android/android_system_property_slice.out
deleted file mode 100644
index 4783a2c..0000000
--- a/test/trace_processor/diff_tests/parser/android/android_system_property_slice.out
+++ /dev/null
@@ -1,3 +0,0 @@
-"type","name","id","ts","dur","type","name"
-"track","DeviceStateChanged",0,1000,0,"__intrinsic_slice","some_state_from_sysprops"
-"track","DeviceStateChanged",1,3000,0,"__intrinsic_slice","some_state_from_atrace"
diff --git a/test/trace_processor/diff_tests/parser/android/surfaceflinger_layers.textproto b/test/trace_processor/diff_tests/parser/android/surfaceflinger_layers.textproto
index 7afbb6a..c8da434 100644
--- a/test/trace_processor/diff_tests/parser/android/surfaceflinger_layers.textproto
+++ b/test/trace_processor/diff_tests/parser/android/surfaceflinger_layers.textproto
@@ -48,12 +48,9 @@
         children: 44
         children: 77
         children: 87
-        type: "Layer"
         layer_stack: 0
         z: 0
         crop {
-          left: 0
-          top: 0
           right: -1
           bottom: -1
         }
@@ -393,6 +390,18 @@
       }
       is_virtual: false
     }
+    displays {
+      id: 4619827677550801153
+      name: "Common Panel"
+      size {
+        w: 1080
+        h: 2400
+      }
+      layer_stack_space_rect {
+        right: 1080
+        bottom: 2400
+      }
+    }
     vsync_id: 24767
   }
   trusted_uid: 1000
diff --git a/test/trace_processor/diff_tests/parser/android/tests.py b/test/trace_processor/diff_tests/parser/android/tests.py
index 6a20f59..ab9e4a1 100644
--- a/test/trace_processor/diff_tests/parser/android/tests.py
+++ b/test/trace_processor/diff_tests/parser/android/tests.py
@@ -106,11 +106,15 @@
         }
         """),
         query="""
-        SELECT t.type, t.name, s.id, s.ts, s.dur, s.type, s.name
+        SELECT t.name, s.id, s.ts, s.dur, s.type, s.name
         FROM track t JOIN slice s ON s.track_id = t.id
         WHERE t.name = 'DeviceStateChanged';
         """,
-        out=Path('android_system_property_slice.out'))
+        out=Csv("""
+        "name","id","ts","dur","type","name"
+        "DeviceStateChanged",0,1000,0,"__intrinsic_slice","some_state_from_sysprops"
+        "DeviceStateChanged",1,3000,0,"__intrinsic_slice","some_state_from_atrace"
+        """))
 
   def test_binder_txn_sync_good(self):
     return DiffTestBlueprint(
diff --git a/test/trace_processor/diff_tests/parser/android/tests_android_input_event.py b/test/trace_processor/diff_tests/parser/android/tests_android_input_event.py
index c7c8e96..4f91501 100644
--- a/test/trace_processor/diff_tests/parser/android/tests_android_input_event.py
+++ b/test/trace_processor/diff_tests/parser/android/tests_android_input_event.py
@@ -282,3 +282,24 @@
         27,"vsync_id","0"
         27,"window_id","0"
         """))
+
+  def test_tables_have_raw_protos(self):
+    return DiffTestBlueprint(
+        trace=Path('input_event_trace.textproto'),
+        query="""
+        INCLUDE PERFETTO MODULE android.input;
+        SELECT COUNT(*) FROM android_key_events
+        WHERE base64_proto IS NOT NULL AND base64_proto_id IS NOT NULL
+        UNION ALL
+        SELECT COUNT(*) FROM android_motion_events
+        WHERE base64_proto IS NOT NULL AND base64_proto_id IS NOT NULL
+        UNION ALL
+        SELECT COUNT(*) FROM android_input_event_dispatch
+        WHERE base64_proto IS NOT NULL AND base64_proto_id IS NOT NULL
+        """,
+        out=Csv("""
+        "COUNT(*)"
+        2
+        6
+        30
+        """))
diff --git a/test/trace_processor/diff_tests/parser/android/tests_bugreport.py b/test/trace_processor/diff_tests/parser/android/tests_bugreport.py
index d67867d..b52f5f8 100644
--- a/test/trace_processor/diff_tests/parser/android/tests_bugreport.py
+++ b/test/trace_processor/diff_tests/parser/android/tests_bugreport.py
@@ -60,18 +60,36 @@
         """,
         out=Path('android_bugreport_dumpsys_test.out'))
 
-  def test_android_bugreport_trace_types(self):
+  def test_android_bugreport_parse_order(self):
     return DiffTestBlueprint(
         trace=DataPath('bugreport-crosshatch-SPB5.zip'),
         query="""
         SELECT *
         FROM __intrinsic_trace_file
-        ORDER BY id
+        WHERE trace_type <> "unknown"
+        ORDER BY processing_order
         """,
         out=Csv("""
-        "id","type","parent_id","name","size","trace_type"
-        0,"__intrinsic_trace_file","[NULL]","[NULL]",6220586,"zip"
-        1,"__intrinsic_trace_file",0,"FS/data/misc/logd/logcat.01",2169697,"android_logcat"
-        2,"__intrinsic_trace_file",0,"FS/data/misc/logd/logcat",2152073,"android_logcat"
-        3,"__intrinsic_trace_file",0,"bugreport-crosshatch-SPB5.210812.002-2021-08-24-23-35-40.txt",43132864,"android_dumpstate"
-        """))
\ No newline at end of file
+        "id","type","parent_id","name","size","trace_type","processing_order"
+        0,"__intrinsic_trace_file","[NULL]","[NULL]",6220586,"zip",0
+        16,"__intrinsic_trace_file",0,"FS/data/misc/logd/logcat.01",2169697,"android_logcat",1
+        15,"__intrinsic_trace_file",0,"FS/data/misc/logd/logcat",2152073,"android_logcat",2
+        1,"__intrinsic_trace_file",0,"bugreport-crosshatch-SPB5.210812.002-2021-08-24-23-35-40.txt",43132864,"android_dumpstate",3
+        """))
+
+  def test_android_bugreport_trace_types(self):
+    return DiffTestBlueprint(
+        trace=DataPath('bugreport-crosshatch-SPB5.zip'),
+        query="""
+        SELECT trace_type, count(*) AS cnt, sum(size) AS total_size
+        FROM __intrinsic_trace_file
+        GROUP BY trace_type
+        ORDER BY trace_type
+        """,
+        out=Csv("""
+        "trace_type","cnt","total_size"
+        "android_dumpstate",1,43132864
+        "android_logcat",2,4321770
+        "unknown",2452,626115
+        "zip",1,6220586
+        """))
diff --git a/test/trace_processor/diff_tests/parser/android/tests_inputmethod_clients.py b/test/trace_processor/diff_tests/parser/android/tests_inputmethod_clients.py
index 74b2760..282a5b3 100644
--- a/test/trace_processor/diff_tests/parser/android/tests_inputmethod_clients.py
+++ b/test/trace_processor/diff_tests/parser/android/tests_inputmethod_clients.py
@@ -63,3 +63,16 @@
         "client.ime_insets_source_consumer.insets_source_consumer.source_control.leash.hash_code","135479902"
         "client.ime_insets_source_consumer.insets_source_consumer.source_control.leash.layerId","105"
         """))
+
+  def test_table_has_raw_protos(self):
+    return DiffTestBlueprint(
+        trace=Path('inputmethod_clients.textproto'),
+        query="""
+        INCLUDE PERFETTO MODULE android.winscope.inputmethod;
+        SELECT COUNT(*) FROM android_inputmethod_clients
+        WHERE base64_proto IS NOT NULL AND base64_proto_id IS NOT NULL
+        """,
+        out=Csv("""
+        "COUNT(*)"
+        2
+        """))
diff --git a/test/trace_processor/diff_tests/parser/android/tests_inputmethod_manager_service.py b/test/trace_processor/diff_tests/parser/android/tests_inputmethod_manager_service.py
index 612f5b8..1295cfe 100644
--- a/test/trace_processor/diff_tests/parser/android/tests_inputmethod_manager_service.py
+++ b/test/trace_processor/diff_tests/parser/android/tests_inputmethod_manager_service.py
@@ -66,3 +66,16 @@
         "input_method_manager_service.system_ready","true"
         "where","InputMethodManagerService#startInputOrWindowGainedFocus"
         """))
+
+  def test_table_has_raw_protos(self):
+    return DiffTestBlueprint(
+        trace=Path('inputmethod_manager_service.textproto'),
+        query="""
+        INCLUDE PERFETTO MODULE android.winscope.inputmethod;
+        SELECT COUNT(*) FROM android_inputmethod_manager_service
+        WHERE base64_proto IS NOT NULL AND base64_proto_id IS NOT NULL
+        """,
+        out=Csv("""
+        "COUNT(*)"
+        2
+        """))
diff --git a/test/trace_processor/diff_tests/parser/android/tests_inputmethod_service.py b/test/trace_processor/diff_tests/parser/android/tests_inputmethod_service.py
index 4ec909c..164fa8c 100644
--- a/test/trace_processor/diff_tests/parser/android/tests_inputmethod_service.py
+++ b/test/trace_processor/diff_tests/parser/android/tests_inputmethod_service.py
@@ -64,3 +64,16 @@
         "input_method_service.token","android.os.BinderProxy@50043d1"
         "where","InputMethodService#doFinishInput"
         """))
+
+  def test_table_has_raw_protos(self):
+    return DiffTestBlueprint(
+        trace=Path('inputmethod_service.textproto'),
+        query="""
+        INCLUDE PERFETTO MODULE android.winscope.inputmethod;
+        SELECT COUNT(*) FROM android_inputmethod_service
+        WHERE base64_proto IS NOT NULL AND base64_proto_id IS NOT NULL
+        """,
+        out=Csv("""
+        "COUNT(*)"
+        2
+        """))
diff --git a/test/trace_processor/diff_tests/parser/android/tests_protolog.py b/test/trace_processor/diff_tests/parser/android/tests_protolog.py
index a03ed56..70ea6e5 100644
--- a/test/trace_processor/diff_tests/parser/android/tests_protolog.py
+++ b/test/trace_processor/diff_tests/parser/android/tests_protolog.py
@@ -27,10 +27,10 @@
         query="SELECT id, ts, level, tag, message, location, stacktrace FROM protolog;",
         out=Csv("""
         "id","ts","level","tag","message","location","stacktrace"
-        0,857384100,"DEBUG","MyFirstGroup","Test message with a string (MyTestString), an int (1776), a double 8.88, and a boolean true.","com/test/TestClass.java:123","A STACK TRACE"
-        1,857384110,"WARN","MySecondGroup","Test message with different int formats: 1776, 0o3360, 0x6f0, 888.000000, 8.880000e+02.","com/test/TestClass.java:567","[NULL]"
+        0,857384100,"DEBUG","MyFirstGroup","Test message with a string (MyTestString), an int (888), a double 8.88, and a boolean true.","com/test/TestClass.java:123","A STACK TRACE"
+        1,857384110,"WARN","MySecondGroup","Test message with different int formats: 888, 0o1570, 0x378, 888.000000, 8.880000e+02.","com/test/TestClass.java:567","[NULL]"
         2,857384130,"ERROR","MyThirdGroup","Message re-using interned string 'MyOtherTestString' == 'MyOtherTestString', but 'SomeOtherTestString' != 'MyOtherTestString'","com/test/TestClass.java:527","[NULL]"
-        3,857384140,"VERBOSE","MyNonProcessedGroup","My non-processed proto message with a string (MyTestString), an int (1776), a double 8.88, and a boolean true.","[NULL]","[NULL]"
+        3,857384140,"VERBOSE","MyNonProcessedGroup","My non-processed proto message with a string (MyTestString), an int (888), a double 8.88, and a boolean true.","[NULL]","[NULL]"
         """))
 
   def test_handles_packet_loss(self):
diff --git a/test/trace_processor/diff_tests/parser/android/tests_shell_transitions.py b/test/trace_processor/diff_tests/parser/android/tests_shell_transitions.py
index a8e0328..b691bfb 100644
--- a/test/trace_processor/diff_tests/parser/android/tests_shell_transitions.py
+++ b/test/trace_processor/diff_tests/parser/android/tests_shell_transitions.py
@@ -74,6 +74,18 @@
         "type","1"
         """))
 
+  def test_shell_transitions_table_has_raw_protos(self):
+    return DiffTestBlueprint(
+        trace=Path('shell_transitions.textproto'),
+        query="""
+        SELECT COUNT(*) FROM window_manager_shell_transitions
+        WHERE base64_proto IS NOT NULL AND base64_proto_id IS NOT NULL
+        """,
+        out=Csv("""
+        "COUNT(*)"
+        6
+        """))
+
   def test_has_shell_handlers(self):
     return DiffTestBlueprint(
         trace=Path('shell_handlers.textproto'),
@@ -89,3 +101,15 @@
       2,"RecentsTransitionHandler"
       3,"FreeformTaskTransitionHandler"
       """))
+
+  def test_shell_handlers_table_has_raw_protos(self):
+    return DiffTestBlueprint(
+        trace=Path('shell_handlers.textproto'),
+        query="""
+        SELECT COUNT(*) FROM window_manager_shell_transition_handlers
+        WHERE base64_proto IS NOT NULL AND base64_proto_id IS NOT NULL
+        """,
+        out=Csv("""
+        "COUNT(*)"
+        3
+        """))
diff --git a/test/trace_processor/diff_tests/parser/android/tests_surfaceflinger_layers.py b/test/trace_processor/diff_tests/parser/android/tests_surfaceflinger_layers.py
index ac1fcab..6ea3ff3 100644
--- a/test/trace_processor/diff_tests/parser/android/tests_surfaceflinger_layers.py
+++ b/test/trace_processor/diff_tests/parser/android/tests_surfaceflinger_layers.py
@@ -81,3 +81,19 @@
         2,1,"surfaceflinger_layer"
         3,1,"surfaceflinger_layer"
         """))
+
+  def test_tables_have_raw_protos(self):
+    return DiffTestBlueprint(
+        trace=Path('surfaceflinger_layers.textproto'),
+        query="""
+        SELECT COUNT(*) FROM surfaceflinger_layers_snapshot
+        WHERE base64_proto IS NOT NULL AND base64_proto_id IS NOT NULL
+        UNION ALL
+        SELECT COUNT(*) FROM surfaceflinger_layer
+        WHERE base64_proto IS NOT NULL AND base64_proto_id IS NOT NULL
+        """,
+        out=Csv("""
+        "COUNT(*)"
+        2
+        4
+        """))
diff --git a/test/trace_processor/diff_tests/parser/android/tests_surfaceflinger_transactions.py b/test/trace_processor/diff_tests/parser/android/tests_surfaceflinger_transactions.py
index cf83ce4..ff84af8 100644
--- a/test/trace_processor/diff_tests/parser/android/tests_surfaceflinger_transactions.py
+++ b/test/trace_processor/diff_tests/parser/android/tests_surfaceflinger_transactions.py
@@ -80,3 +80,15 @@
         "transactions[0].vsync_id","24769"
         "vsync_id","24776"
         """))
+
+  def test_table_has_raw_protos(self):
+    return DiffTestBlueprint(
+        trace=Path('surfaceflinger_transactions.textproto'),
+        query="""
+        SELECT COUNT(*) FROM surfaceflinger_transactions
+        WHERE base64_proto IS NOT NULL AND base64_proto_id IS NOT NULL
+        """,
+        out=Csv("""
+        "COUNT(*)"
+        2
+        """))
diff --git a/test/trace_processor/diff_tests/parser/android/tests_viewcapture.py b/test/trace_processor/diff_tests/parser/android/tests_viewcapture.py
index e089ecb..a99946e 100644
--- a/test/trace_processor/diff_tests/parser/android/tests_viewcapture.py
+++ b/test/trace_processor/diff_tests/parser/android/tests_viewcapture.py
@@ -79,3 +79,16 @@
         "key","display_value"
         "views[1].class_name","STRING DE-INTERNING ERROR"
         """))
+
+  def test_table_has_raw_protos(self):
+    return DiffTestBlueprint(
+        trace=Path('viewcapture.textproto'),
+        query="""
+        INCLUDE PERFETTO MODULE android.winscope.viewcapture;
+        SELECT COUNT(*) FROM android_viewcapture
+        WHERE base64_proto IS NOT NULL AND base64_proto_id IS NOT NULL
+        """,
+        out=Csv("""
+        "COUNT(*)"
+        2
+        """))
diff --git a/test/trace_processor/diff_tests/parser/android/tests_windowmanager.py b/test/trace_processor/diff_tests/parser/android/tests_windowmanager.py
index 9349b8d..3946458 100644
--- a/test/trace_processor/diff_tests/parser/android/tests_windowmanager.py
+++ b/test/trace_processor/diff_tests/parser/android/tests_windowmanager.py
@@ -63,3 +63,16 @@
         "window_manager_service.policy.keyguard_delegate.screen_state","SCREEN_STATE_ON"
         "window_manager_service.policy.keyguard_draw_complete","true"
         """))
+
+  def test_table_has_raw_protos(self):
+    return DiffTestBlueprint(
+        trace=Path('windowmanager.textproto'),
+        query="""
+        INCLUDE PERFETTO MODULE android.winscope.windowmanager;
+        SELECT COUNT(*) FROM android_windowmanager
+        WHERE base64_proto IS NOT NULL AND base64_proto_id IS NOT NULL
+        """,
+        out=Csv("""
+        "COUNT(*)"
+        2
+        """))
diff --git a/test/trace_processor/diff_tests/parser/art_method/tests.py b/test/trace_processor/diff_tests/parser/art_method/tests.py
new file mode 100644
index 0000000..1a62b42
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/art_method/tests.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+# 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 a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import DataPath
+from python.generators.diff_tests.testing import Csv
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class ArtMethodParser(TestSuite):
+
+  def test_art_method_smoke(self):
+    return DiffTestBlueprint(
+        trace=DataPath('art-method-tracing.trace'),
+        query="""
+          INCLUDE PERFETTO MODULE slices.with_context;
+
+          SELECT ts, dur, name, thread_name, extract_arg(arg_set_id, 'pathname') AS pathname
+          FROM thread_slice
+          ORDER BY dur desc
+          LIMIT 10
+        """,
+        out=Csv('''
+          "ts","dur","name","thread_name","pathname"
+          430421819633000,182000,"androidx.benchmark.MethodTracing.start: (Ljava/lang/String;)Landroidx/benchmark/Profiler$ResultFile;","Instr: androidx.test.runner.AndroidJUnitRunner","Profiler.kt"
+          430421819633000,175000,"androidx.benchmark.ProfilerKt.startRuntimeMethodTracing: (Ljava/lang/String;Z)Landroidx/benchmark/Profiler$ResultFile;","Instr: androidx.test.runner.AndroidJUnitRunner","Profiler.kt"
+          430421819634000,67000,"android.os.Debug.startMethodTracing: (Ljava/lang/String;II)V","Instr: androidx.test.runner.AndroidJUnitRunner","Debug.java"
+          430421819635000,62000,"dalvik.system.VMDebug.startMethodTracing: (Ljava/lang/String;IIZI)V","Instr: androidx.test.runner.AndroidJUnitRunner","VMDebug.java"
+          430421819636000,57000,"dalvik.system.VMDebug.startMethodTracingFilename: (Ljava/lang/String;IIZI)V","Instr: androidx.test.runner.AndroidJUnitRunner","VMDebug.java"
+          430421819788000,19000,"androidx.benchmark.Profiler$ResultFile.<init>: (Ljava/lang/String;Ljava/lang/String;)V","Instr: androidx.test.runner.AndroidJUnitRunner","Profiler.kt"
+          430421819795000,2000,"kotlin.jvm.internal.Intrinsics.checkNotNullParameter: (Ljava/lang/Object;Ljava/lang/String;)V","Instr: androidx.test.runner.AndroidJUnitRunner","Intrinsics.java"
+          430421819817000,2000,"androidx.benchmark.vmtrace.ArtTraceTest.myTracedMethod: ()V","Instr: androidx.test.runner.AndroidJUnitRunner","ArtTraceTest.kt"
+          430421819801000,1000,"kotlin.jvm.internal.Intrinsics.checkNotNullParameter: (Ljava/lang/Object;Ljava/lang/String;)V","Instr: androidx.test.runner.AndroidJUnitRunner","Intrinsics.java"
+          430421819804000,1000,"java.lang.Object.<init>: ()V","Instr: androidx.test.runner.AndroidJUnitRunner","Object.java"
+        '''))
+
+  def test_art_method_streaming_smoke(self):
+    return DiffTestBlueprint(
+        trace=DataPath('art-method-tracing-streaming.trace'),
+        query="""
+          INCLUDE PERFETTO MODULE slices.with_context;
+
+          SELECT ts, dur, name, thread_name, extract_arg(arg_set_id, 'pathname') AS pathname
+          FROM thread_slice
+          ORDER BY dur desc
+          LIMIT 10
+        """,
+        out=Csv('''
+          "ts","dur","name","thread_name","pathname"
+          793682939000,26513017000,"java.util.concurrent.ThreadPoolExecutor.getTask: ()Ljava/lang/Runnable;","AsyncTask #1","ThreadPoolExecutor.java"
+          793682939000,26513017000,"java.util.concurrent.LinkedBlockingQueue.take: ()Ljava/lang/Object;","AsyncTask #1","LinkedBlockingQueue.java"
+          793682939000,26513017000,"java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await: ()V","AsyncTask #1","AbstractQueuedSynchronizer.java"
+          793682939000,26513017000,"java.util.concurrent.locks.LockSupport.park: (Ljava/lang/Object;)V","AsyncTask #1","LockSupport.java"
+          793682939000,26513017000,"sun.misc.Unsafe.park: (ZJ)V","AsyncTask #1","Unsafe.java"
+          793682939000,26513017000,"java.lang.Thread.parkFor$: (J)V","AsyncTask #1","Thread.java"
+          793682939000,26513017000,"java.lang.Object.wait: (JI)V","AsyncTask #1","Object.java"
+          810910588000,13004716000,"java.lang.Object.wait: ()V","ReferenceQueueDaemon","Object.java"
+          808685761000,12599705000,"java.lang.Object.wait: (JI)V","OkHttp ConnectionPool","Object.java"
+          800148789000,10759203000,"java.lang.Object.wait: ()V","ReferenceQueueDaemon","Object.java"
+        '''))
diff --git a/test/trace_processor/diff_tests/parser/chrome/tests_v8.py b/test/trace_processor/diff_tests/parser/chrome/tests_v8.py
index d4a0dc4..87c8584 100644
--- a/test/trace_processor/diff_tests/parser/chrome/tests_v8.py
+++ b/test/trace_processor/diff_tests/parser/chrome/tests_v8.py
@@ -100,3 +100,59 @@
 0
 """),
     )
+
+  def test_v8_cpu_samples(self):
+    return DiffTestBlueprint(
+        trace=DataPath('v8-samples.pftrace'),
+        query='''
+          include perfetto module stacks.cpu_profiling;
+
+          select name, source_file, self_count
+          from cpu_profiling_summary_tree
+          where self_count >= 15
+          order by self_count desc, source_file
+        ''',
+        out=Csv('''
+        "name","source_file","self_count"
+        "(program)","[NULL]",17083
+        "(program)","[NULL]",15399
+        "(program)","[NULL]",9853
+        "(program)","[NULL]",9391
+        "(program)","[NULL]",7299
+        "(program)","[NULL]",5245
+        "(program)","[NULL]",2443
+        "(garbage collector)","[NULL]",107
+        "_.mg","chrome-untrusted://new-tab-page/one-google-bar?paramsencoded=",38
+        "(garbage collector)","[NULL]",34
+        "","https://www.google.com/xjs/_/js/k=xjs.hd.en.nSJdbfIGUiE.O/am=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAACAEKAAAABR4AAAAgAAAAAAAAAAQIAQDEAQAAAgA4AAAEAQAEABQQAAAKEATgUTYAgAAwAQAIAAAQAAACQAAACAAAAAMAACAIAAAAAKAAAAAAAAAAAAAAAAAAYAABBAAAAAAAAAAAAIACAAAAoAMAAAAAgAAAgIAAANghAwgAAAQAAACgDwCCB8AghQcAAAAAAAAAAAAAAAKQIJgLCSgIQAAAAAAAAAAAAAAAAACkpIkLCw/d=1/ed=1/dg=3/br=1/rs=ACT90oH8sSQRHJq5R0DO9ABVW-vZJa5Baw/ee=ALeJib:B8gLwd;AfeaP:TkrAjf;BMxAGc:E5bFse;BgS6mb:fidj5d;BjwMce:cXX2Wb;CxXAWb:YyRLvc;DULqB:RKfG5c;Dkk6ge:wJqrrd;DpcR3d:zL72xf;EABSZ:MXZt9d;ESrPQc:mNTJvc;EVNhjf:pw70Gc;EmZ2Bf:zr1jrb;EnlcNd:WeHg4;Erl4fe:FloWmf,FloWmf;F9mqte:UoRcbe;Fmv9Nc:O1Tzwc;G0KhTb:LIaoZ;G6wU6e:hezEbd;GleZL:J1A7Od;HMDDWe:G8QUdb;HoYVKb:PkDN7e;HqeXPd:cmbnH;IBADCc:RYquRb;IZrNqe:P8ha2c;IoGlCf:b5lhvb;IsdWVc:qzxzOb;JXS8fb:Qj0suc;JbMT3:M25sS;JsbNhc:Xd8iUd;KOxcK:OZqGte;KQzWid:ZMKkN;KcokUb:KiuZBf;KpRAue:Tia57b;LBgRLc:SdcwHb,XVMNvd;LEikZe:byfTOb,lsjVmc;LXA8b:q7OdKd;LsNahb:ucGLNb;Me32dd:MEeYgc;NPKaK:SdcwHb;NSEoX:lazG7b;Np8Qkd:Dpx6qc;Nyt6ic:jn2sGd;OgagBe:",33
+        "_.m.Ddb","https://www.google.com/xjs/_/js/k=xjs.hd.en.nSJdbfIGUiE.O/am=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAACAEKAAAABR4AAAAgAAAAAAAAAAQIAQDEAQAAAgA4AAAEAQAEABQQAAAKEATgUTYAgAAwAQAIAAAQAAACQAAACAAAAAMAACAIAAAAAKAAAAAAAAAAAAAAAAAAYAABBAAAAAAAAAAAAIACAAAAoAMAAAAAgAAAgIAAANghAwgAAAQAAACgDwCCB8AghQcAAAAAAAAAAAAAAAKQIJgLCSgIQAAAAAAAAAAAAAAAAACkpIkLCw/d=1/ed=1/dg=3/br=1/rs=ACT90oH8sSQRHJq5R0DO9ABVW-vZJa5Baw/ee=ALeJib:B8gLwd;AfeaP:TkrAjf;BMxAGc:E5bFse;BgS6mb:fidj5d;BjwMce:cXX2Wb;CxXAWb:YyRLvc;DULqB:RKfG5c;Dkk6ge:wJqrrd;DpcR3d:zL72xf;EABSZ:MXZt9d;ESrPQc:mNTJvc;EVNhjf:pw70Gc;EmZ2Bf:zr1jrb;EnlcNd:WeHg4;Erl4fe:FloWmf,FloWmf;F9mqte:UoRcbe;Fmv9Nc:O1Tzwc;G0KhTb:LIaoZ;G6wU6e:hezEbd;GleZL:J1A7Od;HMDDWe:G8QUdb;HoYVKb:PkDN7e;HqeXPd:cmbnH;IBADCc:RYquRb;IZrNqe:P8ha2c;IoGlCf:b5lhvb;IsdWVc:qzxzOb;JXS8fb:Qj0suc;JbMT3:M25sS;JsbNhc:Xd8iUd;KOxcK:OZqGte;KQzWid:ZMKkN;KcokUb:KiuZBf;KpRAue:Tia57b;LBgRLc:SdcwHb,XVMNvd;LEikZe:byfTOb,lsjVmc;LXA8b:q7OdKd;LsNahb:ucGLNb;Me32dd:MEeYgc;NPKaK:SdcwHb;NSEoX:lazG7b;Np8Qkd:Dpx6qc;Nyt6ic:jn2sGd;OgagBe:",18
+        "da","https://www.google.com/",15
+        '''))
+
+  def test_v8_cpu_samples_json(self):
+    return DiffTestBlueprint(
+        trace=DataPath('v8-samples.json'),
+        query='''
+          include perfetto module stacks.cpu_profiling;
+
+          select name, source_file, self_count
+          from cpu_profiling_summary_tree
+          where self_count >= 15
+          order by self_count desc, name
+        ''',
+        out=Csv('''
+        "name","source_file","self_count"
+        "(program)","[NULL]",17083
+        "(program)","[NULL]",15399
+        "(program)","[NULL]",9853
+        "(program)","[NULL]",9391
+        "(program)","[NULL]",7299
+        "(program)","[NULL]",5245
+        "(program)","[NULL]",2443
+        "(garbage collector)","[NULL]",107
+        "_.mg","chrome-untrusted://new-tab-page/one-google-bar?paramsencoded=",38
+        "(garbage collector)","[NULL]",34
+        "","https://www.google.com/xjs/_/js/k=xjs.hd.en.nSJdbfIGUiE.O/am=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAACAEKAAAABR4AAAAgAAAAAAAAAAQIAQDEAQAAAgA4AAAEAQAEABQQAAAKEATgUTYAgAAwAQAIAAAQAAACQAAACAAAAAMAACAIAAAAAKAAAAAAAAAAAAAAAAAAYAABBAAAAAAAAAAAAIACAAAAoAMAAAAAgAAAgIAAANghAwgAAAQAAACgDwCCB8AghQcAAAAAAAAAAAAAAAKQIJgLCSgIQAAAAAAAAAAAAAAAAACkpIkLCw/d=1/ed=1/dg=3/br=1/rs=ACT90oH8sSQRHJq5R0DO9ABVW-vZJa5Baw/ee=ALeJib:B8gLwd;AfeaP:TkrAjf;BMxAGc:E5bFse;BgS6mb:fidj5d;BjwMce:cXX2Wb;CxXAWb:YyRLvc;DULqB:RKfG5c;Dkk6ge:wJqrrd;DpcR3d:zL72xf;EABSZ:MXZt9d;ESrPQc:mNTJvc;EVNhjf:pw70Gc;EmZ2Bf:zr1jrb;EnlcNd:WeHg4;Erl4fe:FloWmf,FloWmf;F9mqte:UoRcbe;Fmv9Nc:O1Tzwc;G0KhTb:LIaoZ;G6wU6e:hezEbd;GleZL:J1A7Od;HMDDWe:G8QUdb;HoYVKb:PkDN7e;HqeXPd:cmbnH;IBADCc:RYquRb;IZrNqe:P8ha2c;IoGlCf:b5lhvb;IsdWVc:qzxzOb;JXS8fb:Qj0suc;JbMT3:M25sS;JsbNhc:Xd8iUd;KOxcK:OZqGte;KQzWid:ZMKkN;KcokUb:KiuZBf;KpRAue:Tia57b;LBgRLc:SdcwHb,XVMNvd;LEikZe:byfTOb,lsjVmc;LXA8b:q7OdKd;LsNahb:ucGLNb;Me32dd:MEeYgc;NPKaK:SdcwHb;NSEoX:lazG7b;Np8Qkd:Dpx6qc;Nyt6ic:jn2sGd;OgagBe:",33
+        "_.m.Ddb","https://www.google.com/xjs/_/js/k=xjs.hd.en.nSJdbfIGUiE.O/am=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAACAEKAAAABR4AAAAgAAAAAAAAAAQIAQDEAQAAAgA4AAAEAQAEABQQAAAKEATgUTYAgAAwAQAIAAAQAAACQAAACAAAAAMAACAIAAAAAKAAAAAAAAAAAAAAAAAAYAABBAAAAAAAAAAAAIACAAAAoAMAAAAAgAAAgIAAANghAwgAAAQAAACgDwCCB8AghQcAAAAAAAAAAAAAAAKQIJgLCSgIQAAAAAAAAAAAAAAAAACkpIkLCw/d=1/ed=1/dg=3/br=1/rs=ACT90oH8sSQRHJq5R0DO9ABVW-vZJa5Baw/ee=ALeJib:B8gLwd;AfeaP:TkrAjf;BMxAGc:E5bFse;BgS6mb:fidj5d;BjwMce:cXX2Wb;CxXAWb:YyRLvc;DULqB:RKfG5c;Dkk6ge:wJqrrd;DpcR3d:zL72xf;EABSZ:MXZt9d;ESrPQc:mNTJvc;EVNhjf:pw70Gc;EmZ2Bf:zr1jrb;EnlcNd:WeHg4;Erl4fe:FloWmf,FloWmf;F9mqte:UoRcbe;Fmv9Nc:O1Tzwc;G0KhTb:LIaoZ;G6wU6e:hezEbd;GleZL:J1A7Od;HMDDWe:G8QUdb;HoYVKb:PkDN7e;HqeXPd:cmbnH;IBADCc:RYquRb;IZrNqe:P8ha2c;IoGlCf:b5lhvb;IsdWVc:qzxzOb;JXS8fb:Qj0suc;JbMT3:M25sS;JsbNhc:Xd8iUd;KOxcK:OZqGte;KQzWid:ZMKkN;KcokUb:KiuZBf;KpRAue:Tia57b;LBgRLc:SdcwHb,XVMNvd;LEikZe:byfTOb,lsjVmc;LXA8b:q7OdKd;LsNahb:ucGLNb;Me32dd:MEeYgc;NPKaK:SdcwHb;NSEoX:lazG7b;Np8Qkd:Dpx6qc;Nyt6ic:jn2sGd;OgagBe:",18
+        "da","https://www.google.com/",15
+        '''))
diff --git a/test/trace_processor/diff_tests/parser/ftrace/ftrace_crop_tests.py b/test/trace_processor/diff_tests/parser/ftrace/ftrace_crop_tests.py
index d69bce7..9863d10 100644
--- a/test/trace_processor/diff_tests/parser/ftrace/ftrace_crop_tests.py
+++ b/test/trace_processor/diff_tests/parser/ftrace/ftrace_crop_tests.py
@@ -21,12 +21,55 @@
 class FtraceCrop(TestSuite):
 
   # Expect the first begin event on cpu1 gets suppressed as it is below the
-  # maximum of last_read_event_timestamps.
+  # maximum of previous_bundle_end_timestamps.
   def test_crop_atrace_slice(self):
     return DiffTestBlueprint(
         trace=TextProto(r"""
         packet { ftrace_events {
           cpu: 1
+          previous_bundle_end_timestamp: 1000
+          event {
+            timestamp: 1500
+            pid: 42
+            print { buf: "B|42|FilteredOut\n" }
+          }
+          event {
+            timestamp: 2700
+            pid: 42
+            print { buf: "E|42\n" }
+          }
+        }}
+        packet { ftrace_events {
+          cpu: 0
+          previous_bundle_end_timestamp: 2000
+          event {
+            timestamp: 2200
+            pid: 42
+            print { buf: "B|42|Kept\n" }
+          }
+        }}
+        """),
+        query="""
+        select
+          ts,
+          rtrim(extract_arg(raw.arg_set_id, "buf"), char(0x0a)) as raw_print,
+          slice.dur as slice_dur,
+          slice.name as slice_name
+        from raw left join slice using (ts)
+        """,
+        out=Csv("""
+        "ts","raw_print","slice_dur","slice_name"
+        1500,"B|42|FilteredOut","[NULL]","[NULL]"
+        2200,"B|42|Kept",500,"Kept"
+        2700,"E|42","[NULL]","[NULL]"
+        """))
+
+  # As test_crop_atrace_slice, with the older "last_read_event_timestamp" field
+  def test_crop_atrace_slice_legacy_field(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet { ftrace_events {
+          cpu: 1
           last_read_event_timestamp: 1000
           event {
             timestamp: 1500
@@ -66,13 +109,13 @@
 
   # First compact_switch per cpu doesn't generate any events, successive
   # switches generate a |raw| entry, but no scheduling slices until past all
-  # last_read_event_timestamps.
+  # previous_bundle_end_timestamps.
   def test_crop_compact_sched_switch(self):
     return DiffTestBlueprint(
         trace=TextProto(r"""
         packet {
           ftrace_events {
-            last_read_event_timestamp: 1000
+            previous_bundle_end_timestamp: 1000
             cpu: 3
             compact_sched {
               intern_table: "zero:3"
@@ -92,7 +135,7 @@
         }
         packet {
           ftrace_events {
-            last_read_event_timestamp: 0
+            previous_bundle_end_timestamp: 0
             cpu: 6
             compact_sched {
               intern_table: "zero:6"
diff --git a/test/trace_processor/diff_tests/parser/ftrace/kprobes_tests.py b/test/trace_processor/diff_tests/parser/ftrace/kprobes_tests.py
new file mode 100644
index 0000000..9d9c16c
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/ftrace/kprobes_tests.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import Csv, TextProto
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class Kprobes(TestSuite):
+
+  def test_kprobes_slice(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet { ftrace_events {
+          cpu: 1
+          event {
+            timestamp: 1500
+            pid: 42
+            kprobe_event {
+              name: "fuse_file_write_iter"
+              type: KPROBE_TYPE_BEGIN
+            }
+          }
+          event {
+            timestamp: 2700
+            pid: 42
+            kprobe_event {
+              name: "fuse_file_write_iter"
+              type: KPROBE_TYPE_END
+            }
+          }
+        }}
+        """),
+        query="""
+        select
+          ts,
+          dur as slice_dur,
+          slice.name as slice_name
+        from slice
+        """,
+        out=Csv("""
+        "ts","slice_dur","slice_name"
+        1500,1200,"fuse_file_write_iter"
+        """))
+
+  def test_kprobes_instant(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet { ftrace_events {
+          cpu: 1
+          event {
+            timestamp: 1500
+            pid: 42
+            kprobe_event {
+              name: "fuse_file_write_iter"
+              type: KPROBE_TYPE_INSTANT
+            }
+          }
+        }}
+        """),
+        query="""
+        select
+          ts,
+          dur as slice_dur,
+          slice.name as slice_name
+        from slice
+        """,
+        out=Csv("""
+        "ts","slice_dur","slice_name"
+        1500,0,"fuse_file_write_iter"
+        """))
diff --git a/test/trace_processor/diff_tests/parser/fuchsia/tests.py b/test/trace_processor/diff_tests/parser/fuchsia/tests.py
index d7b8b4d..59f2673 100644
--- a/test/trace_processor/diff_tests/parser/fuchsia/tests.py
+++ b/test/trace_processor/diff_tests/parser/fuchsia/tests.py
@@ -238,20 +238,30 @@
         query="""
         SELECT key,int_value,string_value,real_value,value_type,display_value
         FROM args
-        LIMIT 12;
+        GROUP BY key
+        ORDER BY key
         """,
         out=Csv("""
         "key","int_value","string_value","real_value","value_type","display_value"
         "SomeNullArg","[NULL]","null","[NULL]","string","null"
-        "Someuint32",2145,"[NULL]","[NULL]","int","2145"
-        "Someuint64",423621626134123415,"[NULL]","[NULL]","int","423621626134123415"
+        "Somedouble","[NULL]","[NULL]",3.141500,"real","3.1415"
         "Someint32",-7,"[NULL]","[NULL]","int","-7"
         "Someint64",-234516543631231,"[NULL]","[NULL]","int","-234516543631231"
-        "Somedouble","[NULL]","[NULL]",3.141500,"real","3.1415"
+        "Someuint32",2145,"[NULL]","[NULL]","int","2145"
+        "Someuint64",423621626134123415,"[NULL]","[NULL]","int","423621626134123415"
+        "cookie",658,"[NULL]","[NULL]","int","658"
+        "name","[NULL]","example_counter:somedataseries:0","[NULL]","string","example_counter:somedataseries:0"
         "ping","[NULL]","pong","[NULL]","string","pong"
-        "somepointer",3285933758964,"[NULL]","[NULL]","pointer","0x2fd10ea19f4"
-        "someotherpointer",43981,"[NULL]","[NULL]","pointer","0xabcd"
-        "somekoid",18,"[NULL]","[NULL]","int","18"
+        "scope","[NULL]","[NULL]","[NULL]","string","[NULL]"
         "somebool",1,"[NULL]","[NULL]","bool","true"
+        "somekoid",18,"[NULL]","[NULL]","int","18"
         "someotherbool",0,"[NULL]","[NULL]","bool","false"
+        "someotherpointer",43981,"[NULL]","[NULL]","pointer","0xabcd"
+        "somepointer",3285933758964,"[NULL]","[NULL]","pointer","0x2fd10ea19f4"
+        "source","[NULL]","chrome","[NULL]","string","chrome"
+        "source_scope","[NULL]","[NULL]","[NULL]","string","[NULL]"
+        "trace_id",658,"[NULL]","[NULL]","int","658"
+        "trace_id_is_process_scoped",0,"[NULL]","[NULL]","bool","false"
+        "upid",1,"[NULL]","[NULL]","int","1"
+        "utid",1,"[NULL]","[NULL]","int","1"
         """))
diff --git a/test/trace_processor/diff_tests/parser/gecko/tests.py b/test/trace_processor/diff_tests/parser/gecko/tests.py
new file mode 100644
index 0000000..3ef6697
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/gecko/tests.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+# 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 a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import DataPath
+from python.generators.diff_tests.testing import Csv
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class GeckoParser(TestSuite):
+
+  def test_gecko_samples_smoke(self):
+    return DiffTestBlueprint(
+        trace=DataPath('trace_processor_perf_as_gecko.json'),
+        query="""
+          INCLUDE PERFETTO MODULE stacks.cpu_profiling;
+
+          SELECT id, parent_id, name, mapping_name, self_count, cumulative_count
+          FROM cpu_profiling_summary_tree
+          LIMIT 10
+        """,
+        out=Csv('''
+          "id","parent_id","name","mapping_name","self_count","cumulative_count"
+          0,"[NULL]","__libc_start_call_main","/usr/lib/x86_64-linux-gnu/libc.so.6",0,37030
+          1,0,"main","/usr/local/google/home/lalitm/perfetto/out/linux_clang_release/trace_processor_shell",0,37030
+          2,1,"perfetto::trace_processor::(anonymous namespace)::TraceProcessorMain(int, char**)","/usr/local/google/home/lalitm/perfetto/out/linux_clang_release/trace_processor_shell",0,37030
+          3,2,"perfetto::trace_processor::(anonymous namespace)::StartInteractiveShell(perfetto::trace_processor::(anonymous namespace)::InteractiveOptions const&)","/usr/local/google/home/lalitm/perfetto/out/linux_clang_release/trace_processor_shell",0,37029
+          4,3,"read","/usr/lib/x86_64-linux-gnu/libc.so.6",8,8
+          5,3,"cfree@GLIBC_2.2.5","/usr/lib/x86_64-linux-gnu/libc.so.6",1,1
+          6,2,"clock_gettime@@GLIBC_2.17","/usr/lib/x86_64-linux-gnu/libc.so.6",1,1
+          7,3,"perfetto::trace_processor::TraceProcessorImpl::ExecuteQuery(std::__Cr::basic_string<char, std::__Cr::char_traits<char>, std::__Cr::allocator<char> > const&)","/usr/local/google/home/lalitm/perfetto/out/linux_clang_release/trace_processor_shell",0,37020
+          8,7,"perfetto::trace_processor::PerfettoSqlEngine::ExecuteUntilLastStatement(perfetto::trace_processor::SqlSource)","/usr/local/google/home/lalitm/perfetto/out/linux_clang_release/trace_processor_shell",0,37020
+          9,8,"perfetto::trace_processor::PerfettoSqlEngine::ExecuteInclude(perfetto::trace_processor::PerfettoSqlParser::Include const&, perfetto::trace_processor::PerfettoSqlParser const&)","/usr/local/google/home/lalitm/perfetto/out/linux_clang_release/trace_processor_shell",0,37020
+        '''))
+
+  def test_gecko_samples_simpleperf_smoke(self):
+    return DiffTestBlueprint(
+        trace=DataPath('simpleperf_as_gecko.json'),
+        query="""
+          INCLUDE PERFETTO MODULE stacks.cpu_profiling;
+
+          SELECT id, parent_id, name, mapping_name, self_count, cumulative_count
+          FROM cpu_profiling_summary_tree
+          ORDER BY cumulative_count desc
+          LIMIT 10
+        """,
+        out=Csv('''
+          "id","parent_id","name","mapping_name","self_count","cumulative_count"
+          13260,"[NULL]","__start_thread","/apex/com.android.runtime/lib64/bionic/libc.so",0,5551
+          13261,13260,"__pthread_start(void*)","/apex/com.android.runtime/lib64/bionic/libc.so",0,5551
+          13262,13261,"art::Thread::CreateCallbackWithUffdGc(void*)","/apex/com.android.art/lib64/libart.so",0,3043
+          13263,13262,"art::Thread::CreateCallback(void*)","/apex/com.android.art/lib64/libart.so",2,3043
+          13266,13263,"art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)","/apex/com.android.art/lib64/libart.so",0,3036
+          13267,13266,"art_quick_invoke_stub","/apex/com.android.art/lib64/libart.so",0,3036
+          13268,13267,"java.lang.Thread.run","/system/framework/arm64/boot.oat",0,2159
+          0,"[NULL]","__libc_init","/apex/com.android.runtime/lib64/bionic/libc.so",0,1714
+          1,0,"main","/system/bin/app_process64",0,1714
+          2,1,"android::AndroidRuntime::start(char const*, android::Vector<android::String8> const&, bool)","/system/lib64/libandroid_runtime.so",0,1714
+        '''))
diff --git a/test/trace_processor/diff_tests/parser/gzip/tests.py b/test/trace_processor/diff_tests/parser/gzip/tests.py
new file mode 100644
index 0000000..a0ac38f
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/gzip/tests.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+# 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 a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import Csv, DataPath
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class Gzip(TestSuite):
+
+  def test_gzip_multi_stream(self):
+    return DiffTestBlueprint(
+        trace=DataPath('sfgate-gzip-multi-stream.json.gz'),
+        query='''select ts, dur, name from slice limit 10''',
+        out=Csv('''
+        "ts","dur","name"
+        2213649212614000,239000,"ThreadTimers::sharedTimerFiredInternal"
+        2213649212678000,142000,"LayoutView::hitTest"
+        2213649214331000,34000,"ThreadTimers::sharedTimerFiredInternal"
+        2213649215569000,16727000,"ThreadTimers::sharedTimerFiredInternal"
+        2213649216760000,50000,"Node::updateDistribution"
+        2213649217290000,1373000,"StyleElement::processStyleSheet"
+        2213649218908000,4862000,"Document::updateRenderTree"
+        2213649218917000,50000,"Node::updateDistribution"
+        2213649218970000,4796000,"Document::updateStyle"
+        2213649218995000,54000,"RuleSet::addRulesFromSheet"
+        '''))
diff --git a/test/trace_processor/diff_tests/parser/instruments/tests.py b/test/trace_processor/diff_tests/parser/instruments/tests.py
new file mode 100644
index 0000000..5558ec9
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/instruments/tests.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import Csv, Path, DataPath
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+# These diff tests use some locally collected trace.
+class Instruments(TestSuite):
+
+  def test_xml_stacks(self):
+    return DiffTestBlueprint(
+        trace=DataPath('instruments_trace.xml'),
+        query='''
+          WITH
+            child AS (
+              SELECT
+                spc.id AS root,
+                spc.id,
+                spc.parent_id,
+                rel_pc AS path
+              FROM
+                instruments_sample s
+                JOIN stack_profile_callsite spc ON (s.callsite_id = spc.id)
+                JOIN stack_profile_frame f ON (f.id = frame_id)
+              UNION ALL
+              SELECT
+                child.root,
+                parent.id,
+                parent.parent_id,
+                COALESCE(f.rel_pc || ',', '') || child.path AS path
+              FROM
+                child
+                JOIN stack_profile_callsite parent ON (child.parent_id = parent.id)
+                LEFT JOIN stack_profile_frame f ON (f.id = frame_id)
+            )
+          SELECT
+            s.id,
+            s.ts,
+            s.utid,
+            c.path
+          FROM
+            instruments_sample s
+            JOIN child c ON s.callsite_id = c.root
+          WHERE
+            c.parent_id IS NULL
+        ''',
+        out=Csv('''
+          "id","ts","utid","path"
+          0,175685291,1,"23999,34891,37935,334037"
+          1,176684208,1,"24307,28687,265407,160467,120123,391295,336787,8955,340991,392555,136711,5707,7603,10507,207839,207495,23655,17383,23211,208391,6225"
+          2,177685166,1,"24915,16095,15891,32211,91151,26907,87887,60651,28343,29471,30159,11087,36269"
+          3,178683916,1,"24915,16107,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16021"
+          4,179687000,1,"24915,16107,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16005"
+          5,180683708,1,"24915,16107,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16005"
+        '''))
+
+  def test_symbolized_frames(self):
+    return DiffTestBlueprint(
+        trace=DataPath('instruments_trace_with_symbols.zip'),
+        query='''
+          SELECT
+            f.id,
+            m.name,
+            m.build_id,
+            f.rel_pc,
+            s.name,
+            s.source_file,
+            s.line_number
+          FROM
+            stack_profile_frame f
+            JOIN stack_profile_mapping m ON f.mapping = m.id
+            JOIN stack_profile_symbol s ON f.symbol_set_id = s.symbol_set_id
+        ''',
+        out=Csv('''
+          "id","name","build_id","rel_pc","name","source_file","line_number"
+          26,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16095,"main","/tmp/test.cpp",25
+          27,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",15891,"EmitSignpost()","/tmp/test.cpp",8
+          38,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16107,"main","/tmp/test.cpp",27
+          39,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16047,"fib(int)","/tmp/test.cpp",21
+          40,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16021,"fib(int)","/tmp/test.cpp",22
+          41,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16005,"fib(int)","/tmp/test.cpp",15
+        '''))
+
+  def test_symbolized_stacks(self):
+    return DiffTestBlueprint(
+        trace=DataPath('instruments_trace_with_symbols.zip'),
+        query='''
+          WITH
+            frame AS (
+              SELECT
+                f.id AS frame_id,
+                COALESCE(s.name || ':' || s.line_number, f.rel_pc) as name
+              FROM
+                stack_profile_frame f
+                LEFT JOIN stack_profile_symbol s USING (symbol_set_id)
+            ),
+            child AS (
+              SELECT
+                spc.id AS root,
+                spc.id,
+                spc.parent_id,
+                name AS path
+              FROM
+                instruments_sample s
+                JOIN stack_profile_callsite spc ON (s.callsite_id = spc.id)
+                LEFT JOIN frame f USING (frame_id)
+              UNION ALL
+              SELECT
+                child.root,
+                parent.id,
+                parent.parent_id,
+                COALESCE(f.name || ',', '') || child.path AS path
+              FROM
+                child
+                JOIN stack_profile_callsite parent ON (child.parent_id = parent.id)
+                LEFT JOIN frame f USING (frame_id)
+            )
+          SELECT
+            s.id,
+            s.ts,
+            s.utid,
+            c.path
+          FROM
+            instruments_sample s
+            JOIN child c ON s.callsite_id = c.root
+          WHERE
+            c.parent_id IS NULL
+        ''',
+        out=Csv('''
+          "id","ts","utid","path"
+          0,175685291,1,"23999,34891,37935,334037"
+          1,176684208,1,"24307,28687,265407,160467,120123,391295,336787,8955,340991,392555,136711,5707,7603,10507,207839,207495,23655,17383,23211,208391,6225"
+          2,177685166,1,"24915,main:25,EmitSignpost():8,32211,91151,26907,87887,60651,28343,29471,30159,11087,36269"
+          3,178683916,1,"24915,main:27,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):22"
+          4,179687000,1,"24915,main:27,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):15"
+          5,180683708,1,"24915,main:27,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):15"
+        '''))
diff --git a/test/trace_processor/diff_tests/parser/json/tests.py b/test/trace_processor/diff_tests/parser/json/tests.py
index 32993c3..5b981ac 100644
--- a/test/trace_processor/diff_tests/parser/json/tests.py
+++ b/test/trace_processor/diff_tests/parser/json/tests.py
@@ -105,4 +105,48 @@
           "Event1","args.02.step2",2
           "Event2","args.01.step1",1
           "Event2","args.02.step2",2
-        """))
\ No newline at end of file
+        """))
+
+  def test_x_event_order(self):
+    return DiffTestBlueprint(
+        trace=Json('''[
+          {
+            "name": "Child",
+            "ph": "X",
+            "ts": 1,
+            "dur": 5,
+            "pid": 1
+          },
+          {
+            "name": "Parent",
+            "ph": "X",
+            "ts": 1,
+            "dur": 10,
+            "pid": 1,
+            "tid": 1
+          }
+        ]'''),
+        query='''
+          SELECT ts, dur, name, depth
+          FROM slice
+        ''',
+        out=Csv("""
+          "ts","dur","name","depth"
+          1000,10000,"Parent",0
+          1000,5000,"Child",1
+        """))
+
+  def test_json_incomplete(self):
+    return DiffTestBlueprint(
+      trace=Json('''
+        [
+        {"name":"typecheck","ph":"X","ts":4619295550.000,"dur":8000.000,"pid":306339,"tid":3},
+      '''),
+      query='''
+        select ts from slice
+      ''',
+      out=Csv('''
+      "ts"
+      4619295550000
+      ''')
+    )
diff --git a/test/trace_processor/diff_tests/parser/parsing/flow_events_json_v1.json b/test/trace_processor/diff_tests/parser/parsing/flow_events_json_v1.json
index 461aa39..7404a9a 100644
--- a/test/trace_processor/diff_tests/parser/parsing/flow_events_json_v1.json
+++ b/test/trace_processor/diff_tests/parser/parsing/flow_events_json_v1.json
@@ -55,7 +55,7 @@
         "cat": "ipc",
         "pid": 15875,
         "tid": 15895,
-        "ts": 1001,
+        "ts": 1002,
         "ph": "X",
         "name": "Blergh",
         "args": {},
diff --git a/test/trace_processor/diff_tests/parser/parsing/otheruuids.textproto b/test/trace_processor/diff_tests/parser/parsing/otheruuids.textproto
deleted file mode 100644
index e5281e8..0000000
--- a/test/trace_processor/diff_tests/parser/parsing/otheruuids.textproto
+++ /dev/null
@@ -1,46 +0,0 @@
-packet {
-  ftrace_events {
-    cpu: 4
-    event {
-      timestamp: 171311335293
-      pid: 7663
-      print {
-        buf: "N|7663|OtherTraces|finalize-uuid-75e4c6d0-d8f6-4f82-fa4b-9e09c5512288\n"
-      }
-    }
-  }
-  trusted_uid: 9999
-  trusted_packet_sequence_id: 2
-  trusted_pid: 1277
-  previous_packet_dropped: true
-}
-packet {
-  ftrace_events {
-    cpu: 4
-    event {
-      timestamp: 187198579688
-      pid: 7752
-      print {
-        buf: "N|7752|OtherTraces|finalize-uuid-ad836701-3113-3fb1-be4f-f7731e23fbbf\n"
-      }
-    }
-  }
-  trusted_uid: 9999
-  trusted_packet_sequence_id: 2
-  trusted_pid: 1277
-}
-packet {
-  ftrace_events {
-    cpu: 6
-    event {
-      timestamp: 200857707872
-      pid: 7824
-      print {
-        buf: "N|7824|OtherTraces|finalize-uuid-0de1a010-efa1-a081-2345-969b1186a6ab\n"
-      }
-    }
-  }
-  trusted_uid: 9999
-  trusted_packet_sequence_id: 2
-  trusted_pid: 1277
-}
diff --git a/test/trace_processor/diff_tests/parser/parsing/tests.py b/test/trace_processor/diff_tests/parser/parsing/tests.py
index b9d05e4..54707c1 100644
--- a/test/trace_processor/diff_tests/parser/parsing/tests.py
+++ b/test/trace_processor/diff_tests/parser/parsing/tests.py
@@ -1141,18 +1141,6 @@
         9
         """))
 
-  def test_otheruuids_android_other_traces(self):
-    return DiffTestBlueprint(
-        trace=Path('otheruuids.textproto'),
-        query=Metric('android_other_traces'),
-        out=TextProto(r"""
-        android_other_traces {
-          finalized_traces_uuid: "75e4c6d0-d8f6-4f82-fa4b-9e09c5512288"
-          finalized_traces_uuid: "ad836701-3113-3fb1-be4f-f7731e23fbbf"
-          finalized_traces_uuid: "0de1a010-efa1-a081-2345-969b1186a6ab"
-        }
-        """))
-
   # Per-process Binder transaction metrics
   def test_android_binder(self):
     return DiffTestBlueprint(
@@ -1326,6 +1314,35 @@
         "all_data_source_flushed_ns",12345
         """))
 
+  def test_slow_starting_data_sources(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          timestamp: 108227060089867
+          trusted_uid: 679634
+          trusted_packet_sequence_id: 1
+          service_event {
+            slow_starting_data_sources {
+              data_source {
+                producer_name: "producer2"
+                data_source_name: "track_event"
+              }
+              data_source {
+                producer_name: "producer3"
+                data_source_name: "track_event"
+              }
+            }
+          }
+        }
+        """),
+        query="""
+        SELECT str_value FROM metadata WHERE name = 'slow_start_data_source'""",
+        out=Csv("""
+        "str_value"
+        "producer2 track_event"
+        "producer3 track_event"
+        """))
+
   def test_ftrace_abi_errors_skipped_zero_data_length(self):
     return DiffTestBlueprint(
         trace=TextProto(r"""
diff --git a/test/trace_processor/diff_tests/parser/parsing/tests_sys_stats.py b/test/trace_processor/diff_tests/parser/parsing/tests_sys_stats.py
index 8e00929..56e2959 100644
--- a/test/trace_processor/diff_tests/parser/parsing/tests_sys_stats.py
+++ b/test/trace_processor/diff_tests/parser/parsing/tests_sys_stats.py
@@ -13,12 +13,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from python.generators.diff_tests.testing import Path, DataPath, Metric
-from python.generators.diff_tests.testing import Csv, Json, TextProto
+from python.generators.diff_tests.testing import Csv, TextProto
 from python.generators.diff_tests.testing import DiffTestBlueprint
 from python.generators.diff_tests.testing import TestSuite
 
-
 class ParsingSysStats(TestSuite):
 
   def test_cpuidle_stats(self):
@@ -101,3 +99,34 @@
         71625871363623,"x86_pkg_temp",29.000000
         71626000387166,"x86_pkg_temp",31.000000
         """))
+
+  def test_gpufreq(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+    packet {
+      sys_stats {
+        gpufreq_mhz: 300
+      }
+      timestamp: 115835063108
+      trusted_packet_sequence_id: 2
+    }
+    packet {
+      sys_stats {
+        gpufreq_mhz: 350
+      }
+      timestamp: 115900182490
+      trusted_packet_sequence_id: 2
+    }
+    """),
+        query="""
+    SELECT c.ts,
+            t.name,
+            c.value
+    FROM counter_track t
+    JOIN counter c ON t.id = c.track_id
+    """,
+        out=Csv("""
+    "ts","name","value"
+    115835063108,"gpufreq",300.000000
+    115900182490,"gpufreq",350.000000
+    """))
diff --git a/test/trace_processor/diff_tests/parser/parsing/tests_traced_stats.py b/test/trace_processor/diff_tests/parser/parsing/tests_traced_stats.py
index af2f362..275c3f4 100644
--- a/test/trace_processor/diff_tests/parser/parsing/tests_traced_stats.py
+++ b/test/trace_processor/diff_tests/parser/parsing/tests_traced_stats.py
@@ -102,3 +102,65 @@
         1,1
         2,2
         """))
+
+  # Check that dropping all packets leads to
+  # `traced_buf_incremental_sequences_dropped` being set.
+  def test_sequence_all_incremental_dropped(self):
+    return DiffTestBlueprint(
+        trace=TextProto('''
+        packet {
+          trusted_packet_sequence_id: 2
+          previous_packet_dropped: true
+          first_packet_on_sequence: true
+          sequence_flags: 1  # SEQ_INCREMENTAL_STATE_CLEARED
+        }
+        packet {
+          trusted_packet_sequence_id: 2
+          sequence_flags: 2  # SEQ_NEEDS_INCREMENTAL_STATE
+        }
+        packet {
+          trusted_packet_sequence_id: 2
+          sequence_flags: 2  # SEQ_NEEDS_INCREMENTAL_STATE
+        }
+        packet {
+          trusted_packet_sequence_id: 3
+          sequence_flags: 2  # SEQ_NEEDS_INCREMENTAL_STATE
+        }
+        packet {
+          trusted_packet_sequence_id: 3
+          sequence_flags: 2  # SEQ_NEEDS_INCREMENTAL_STATE
+        }
+        packet {
+          trusted_packet_sequence_id: 4
+          sequence_flags: 2  # SEQ_NEEDS_INCREMENTAL_STATE
+        }
+        packet {
+          trusted_uid: 9999
+          trusted_packet_sequence_id: 1
+          trace_stats {
+            writer_stats {
+              sequence_id: 2
+              buffer: 0
+            }
+            writer_stats {
+              sequence_id: 3
+              buffer: 1
+            }
+            writer_stats {
+              sequence_id: 4
+              buffer: 1
+            }
+          }
+        }
+        '''),
+        query='''
+          SELECT idx, value
+          FROM stats
+          WHERE name = 'traced_buf_incremental_sequences_dropped'
+          ORDER BY idx;
+        ''',
+        out=Csv('''
+        "idx","value"
+        0,0
+        1,2
+        '''))
diff --git a/test/trace_processor/diff_tests/parser/perf_text/tests.py b/test/trace_processor/diff_tests/parser/perf_text/tests.py
new file mode 100644
index 0000000..a574d3f
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/perf_text/tests.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+# 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 a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import DataPath
+from python.generators.diff_tests.testing import Csv
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class PerfTextParser(TestSuite):
+
+  def test_perf_text_smoke(self):
+    return DiffTestBlueprint(
+        trace=DataPath('trace_processor_perf_as_text.txt'),
+        query="""
+          INCLUDE PERFETTO MODULE stacks.cpu_profiling;
+
+          SELECT id, parent_id, name, mapping_name, self_count, cumulative_count
+          FROM cpu_profiling_summary_tree
+          LIMIT 10
+        """,
+        out=Csv('''
+          "id","parent_id","name","mapping_name","self_count","cumulative_count"
+          0,"[NULL]","_start","/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",1,2
+          1,0,"[unknown]","[unknown]",1,1
+          2,"[NULL]","_dl_start","/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",1,1
+          3,"[NULL]","_dl_start_user","/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",0,16
+          4,3,"_dl_start","/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",2,5
+          5,4,"[unknown]","[unknown]",3,3
+          6,"[NULL]","[unknown]","[unknown]",0,27
+          7,6,"[unknown]","[unknown]",0,3
+          8,7,"__GI___tunables_init","/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",1,2
+          9,8,"[unknown]","[unknown]",1,1
+        '''))
+
+  def test_perf_text_simpleperf_smoke(self):
+    return DiffTestBlueprint(
+        trace=DataPath('simpleperf_as_text.txt'),
+        query="""
+          INCLUDE PERFETTO MODULE stacks.cpu_profiling;
+
+          SELECT id, parent_id, name, mapping_name, self_count, cumulative_count
+          FROM cpu_profiling_summary_tree
+          LIMIT 10
+        """,
+        out=Csv('''
+          "id","parent_id","name","mapping_name","self_count","cumulative_count"
+          0,"[NULL]","__libc_init","/apex/com.android.runtime/lib64/bionic/libc.so",0,1714
+          1,0,"main","/system/bin/app_process64",0,1714
+          2,1,"android::AndroidRuntime::start(char const*, android::Vector<android::String8> const&, bool)","/system/lib64/libandroid_runtime.so",0,1714
+          3,2,"_JNIEnv::CallStaticVoidMethod(_jclass*, _jmethodID*, ...)","/system/lib64/libandroid_runtime.so",0,1714
+          4,3,"art::JNI<true>::CallStaticVoidMethodV(_JNIEnv*, _jclass*, _jmethodID*, std::__va_list)","/apex/com.android.art/lib64/libart.so",0,1714
+          5,4,"art::JValue art::InvokeWithVarArgs<_jmethodID*>(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jmethodID*, std::__va_list)","/apex/com.android.art/lib64/libart.so",0,1714
+          6,5,"art_quick_invoke_static_stub","/apex/com.android.art/lib64/libart.so",0,1714
+          7,6,"com.android.internal.os.ZygoteInit.main","/system/framework/arm64/boot-framework.oat",0,1714
+          8,7,"com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run","/system/framework/arm64/boot-framework.oat",0,1714
+          9,8,"art_jni_trampoline","/system/framework/arm64/boot.oat",0,1714
+        '''))
diff --git a/test/trace_processor/diff_tests/parser/power/tests_energy_breakdown.py b/test/trace_processor/diff_tests/parser/power/tests_energy_breakdown.py
index d678400..655792c 100644
--- a/test/trace_processor/diff_tests/parser/power/tests_energy_breakdown.py
+++ b/test/trace_processor/diff_tests/parser/power/tests_energy_breakdown.py
@@ -25,8 +25,16 @@
     return DiffTestBlueprint(
         trace=Path('energy_breakdown.textproto'),
         query="""
-        SELECT consumer_id, name, consumer_type, ordinal
-        FROM energy_counter_track;
+        SELECT
+          EXTRACT_ARG(
+            dimension_arg_set_id,
+            'energy_consumer_id'
+          ) AS consumer_id,
+          name,
+          EXTRACT_ARG(source_arg_set_id, 'consumer_type') AS consumer_type,
+          EXTRACT_ARG(source_arg_set_id, 'ordinal') AS ordinal
+        FROM track
+        WHERE classification = 'android_energy_estimation_breakdown';
         """,
         out=Csv("""
         "consumer_id","name","consumer_type","ordinal"
@@ -39,7 +47,7 @@
         query="""
         SELECT ts, value
         FROM counter
-        JOIN energy_counter_track ON counter.track_id = energy_counter_track.id
+        JOIN track ON counter.track_id = track.id
         ORDER BY ts;
         """,
         out=Csv("""
@@ -47,42 +55,18 @@
         1030255882785,98567522.000000
         """))
 
-  def test_energy_breakdown_uid_table(self):
-    return DiffTestBlueprint(
-        trace=Path('energy_breakdown_uid.textproto'),
-        query="""
-        SELECT uid, name
-        FROM uid_counter_track;
-        """,
-        out=Csv("""
-        "uid","name"
-        10234,"GPU"
-        10190,"GPU"
-        10235,"GPU"
-        """))
-
-  def test_energy_breakdown_uid_event(self):
-    return DiffTestBlueprint(
-        trace=Path('energy_breakdown_uid.textproto'),
-        query="""
-        SELECT ts, value
-        FROM counter
-        JOIN uid_counter_track ON counter.track_id = uid_counter_track.id
-        ORDER BY ts;
-        """,
-        out=Csv("""
-        "ts","value"
-        1026753926322,3004536.000000
-        1026753926322,0.000000
-        1026753926322,4002274.000000
-        """))
-
   def test_energy_per_uid_table(self):
     return DiffTestBlueprint(
         trace=Path('energy_breakdown_uid.textproto'),
         query="""
-        SELECT consumer_id, uid
-        FROM energy_per_uid_counter_track;
+        SELECT
+          EXTRACT_ARG(
+            dimension_arg_set_id,
+            'energy_consumer_id'
+          ) AS consumer_id,
+          EXTRACT_ARG(dimension_arg_set_id, 'uid') AS uid
+        FROM track
+        WHERE classification = 'android_energy_estimation_breakdown_per_uid';
         """,
         out=Csv("""
         "consumer_id","uid"
@@ -97,9 +81,12 @@
         trace_modifier=TraceInjector(['android_energy_estimation_breakdown'],
                                      {'machine_id': 1001}),
         query="""
-        SELECT uid, name
-        FROM uid_counter_track
-        WHERE machine_id IS NOT NULL;
+        SELECT
+          EXTRACT_ARG(dimension_arg_set_id, 'uid') AS uid,
+          name
+        FROM track
+        WHERE classification = 'android_energy_estimation_breakdown_per_uid'
+          AND machine_id IS NOT NULL;
         """,
         out=Csv("""
         "uid","name"
@@ -114,9 +101,17 @@
         trace_modifier=TraceInjector(['android_energy_estimation_breakdown'],
                                      {'machine_id': 1001}),
         query="""
-        SELECT consumer_id, name, consumer_type, ordinal
-        FROM energy_counter_track
-        WHERE machine_id IS NOT NULL;
+        SELECT
+          EXTRACT_ARG(
+            dimension_arg_set_id,
+            'energy_consumer_id'
+          ) AS consumer_id,
+          name,
+          EXTRACT_ARG(source_arg_set_id, 'consumer_type') AS consumer_type,
+          EXTRACT_ARG(source_arg_set_id, 'ordinal') AS ordinal
+        FROM track
+        WHERE classification = 'android_energy_estimation_breakdown'
+          AND machine_id IS NOT NULL;
         """,
         out=Csv("""
         "consumer_id","name","consumer_type","ordinal"
diff --git a/test/trace_processor/diff_tests/parser/power/tests_linux_sysfs_power.py b/test/trace_processor/diff_tests/parser/power/tests_linux_sysfs_power.py
index fce058e..046d1c4 100644
--- a/test/trace_processor/diff_tests/parser/power/tests_linux_sysfs_power.py
+++ b/test/trace_processor/diff_tests/parser/power/tests_linux_sysfs_power.py
@@ -132,3 +132,33 @@
         out=Csv("""
         "value"
         """))
+
+  # Test calculating power counter from current and voltage.
+  def test_power_from_current_and_voltage(self):
+    return DiffTestBlueprint(
+        trace=TextProto("""
+        packet {
+          timestamp: 3000000
+          battery {
+            current_ua: 710000
+            voltage_uv: 11900000
+          }
+        }
+        packet {
+          timestamp: 4000000
+          battery {
+            current_ua: 510000
+            voltage_uv: 12000000
+          }
+        }
+        """),
+        query="""
+        SELECT value
+        FROM counters
+        WHERE name = "batt.power_mw"
+        """,
+        out=Csv("""
+        "value"
+        8449.000000
+        6120.000000
+        """))
diff --git a/test/trace_processor/diff_tests/parser/simpleperf/tests.py b/test/trace_processor/diff_tests/parser/simpleperf/tests.py
index f29cba2..ddadbc3 100644
--- a/test/trace_processor/diff_tests/parser/simpleperf/tests.py
+++ b/test/trace_processor/diff_tests/parser/simpleperf/tests.py
@@ -245,3 +245,105 @@
         "main,D,E"
         "main,E"
         '''))
+
+  def test_etm_dummy_parsing(self):
+    return DiffTestBlueprint(
+        trace=DataPath('simpleperf/etm.perf.data.zip'),
+        query='''
+        SELECT name, value
+        FROM stats
+        WHERE name IN (
+          'perf_aux_missing', 'perf_aux_ignored', 'perf_aux_lost',
+          'perf_auxtrace_missing')
+        ORDER BY name
+        ''',
+        out=Csv('''
+        "name","value"
+        "perf_aux_ignored",463744
+        "perf_aux_lost",0
+        "perf_aux_missing",0
+        "perf_auxtrace_missing",0
+        '''))
+
+  def test_spe_operation(self):
+    return DiffTestBlueprint(
+        trace=DataPath('simpleperf/spe.trace.zip'),
+        query='''
+        INCLUDE PERFETTO MODULE linux.perf.spe;
+        SELECT
+          operation,
+          count(*) AS cnt
+        FROM linux_perf_spe_record
+        GROUP BY operation
+        ORDER BY operation
+        ''',
+        out=Csv('''
+        "operation","cnt"
+        "BRANCH",68038
+        "LOAD",54
+        "STORE",47
+        '''))
+
+  def test_spe_pc(self):
+    return DiffTestBlueprint(
+        trace=DataPath('simpleperf/spe.trace.zip'),
+        query='''
+        INCLUDE PERFETTO MODULE linux.perf.spe;
+        SELECT
+          printf('0x%08x', rel_pc + m.start - exact_offset) AS pc,
+          exception_level,
+          COUNT(*) AS cnt
+        FROM linux_perf_spe_record r, stack_profile_frame f
+        ON r.instruction_frame_id = f.id,
+        stack_profile_mapping m
+        ON f.mapping = m.id
+        GROUP BY pc, exception_level
+        HAVING cnt > 1
+        ORDER BY pc, exception_level
+        ''',
+        out=Csv('''
+        "pc","exception_level","cnt"
+        "0x5cfc344464","EL0",2157
+        "0x5cfc344528","EL0",2166
+        "0x5cfc3445c4","EL0",2154
+        "0x5cfc3446c8","EL0",2108
+        "0x5cfc3447a8","EL0",2209
+        "0x5cfc344854","EL0",2178
+        "0x5cfc34492c","EL0",2246
+        "0x5cfc344c14","EL0",4461
+        "0x5cfc344cd0","EL0",4416
+        "0x5cfc344d7c","EL0",4399
+        "0x5cfc344df4","EL0",2
+        "0x5cfc344e90","EL0",4427
+        "0x5cfc3450e8","EL0",8756
+        "0x5cfc345194","EL0",8858
+        "0x5cfc345240","EL0",8776
+        "0x5cfc345354","EL0",8659
+        "0xffffd409990628","EL1",14
+        "0xffffd40999062c","EL1",15
+        "0xffffd40fb0f124","EL1",2
+        '''))
+
+  def test_perf_summary_tree(self):
+    return DiffTestBlueprint(
+        trace=DataPath('simpleperf/perf.data'),
+        query='''
+          INCLUDE PERFETTO MODULE linux.perf.samples;
+
+          SELECT *
+          FROM linux_perf_samples_summary_tree
+          LIMIT 10
+        ''',
+        out=Csv('''
+          "id","parent_id","name","mapping_name","source_file","line_number","self_count","cumulative_count"
+          0,"[NULL]","","/elf","[NULL]","[NULL]",84,84
+          1,"[NULL]","","/elf","[NULL]","[NULL]",69,69
+          2,"[NULL]","","/elf","[NULL]","[NULL]",177,177
+          3,"[NULL]","","/elf","[NULL]","[NULL]",89,89
+          4,"[NULL]","","/t1","[NULL]","[NULL]",70,70
+          5,"[NULL]","","/elf","[NULL]","[NULL]",218,218
+          6,"[NULL]","","/elf","[NULL]","[NULL]",65,65
+          7,"[NULL]","","/elf","[NULL]","[NULL]",70,70
+          8,"[NULL]","","/t1","[NULL]","[NULL]",87,87
+          9,"[NULL]","","/elf","[NULL]","[NULL]",64,64
+        '''))
diff --git a/test/trace_processor/diff_tests/parser/track_event/legacy_async_event.out b/test/trace_processor/diff_tests/parser/track_event/legacy_async_event.out
deleted file mode 100644
index 56a8f2e..0000000
--- a/test/trace_processor/diff_tests/parser/track_event/legacy_async_event.out
+++ /dev/null
@@ -1,15 +0,0 @@
-"track","process","thread","thread_process","ts","dur","category","name","key","string_value","int_value"
-"name1","[NULL]","[NULL]","[NULL]",1000,7000,"cat","name1","debug.arg1","value1","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",1000,7000,"cat","name1","legacy_event.passthrough_utid","[NULL]",1
-"name1","[NULL]","[NULL]","[NULL]",1000,7000,"cat","name1","legacy_event.phase","S","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",1000,7000,"cat","name1","debug.arg2","value2","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",2000,1000,"cat","name1","legacy_event.passthrough_utid","[NULL]",2
-"name1","[NULL]","[NULL]","[NULL]",2000,1000,"cat","name1","legacy_event.phase","S","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",3000,0,"cat","name1","debug.arg3","value3","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",3000,0,"cat","name1","debug.step","Step1","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",3000,0,"cat","name1","legacy_event.passthrough_utid","[NULL]",1
-"name1","[NULL]","[NULL]","[NULL]",3000,0,"cat","name1","legacy_event.phase","T","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",5000,0,"cat","name1","debug.arg4","value4","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",5000,0,"cat","name1","debug.step","Step2","[NULL]"
-"name1","[NULL]","[NULL]","[NULL]",5000,0,"cat","name1","legacy_event.passthrough_utid","[NULL]",1
-"name1","[NULL]","[NULL]","[NULL]",5000,0,"cat","name1","legacy_event.phase","p","[NULL]"
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 76bedbd..97f0804 100644
--- a/test/trace_processor/diff_tests/parser/track_event/tests.py
+++ b/test/trace_processor/diff_tests/parser/track_event/tests.py
@@ -196,7 +196,15 @@
   def test_track_event_typed_args_args(self):
     return DiffTestBlueprint(
         trace=Path('track_event_typed_args.textproto'),
-        query=Path('track_event_args_test.sql'),
+        query="""
+        SELECT
+          flat_key,
+          key,
+          int_value,
+          string_value
+        FROM args
+        ORDER BY key, display_value, arg_set_id, key ASC;
+        """,
         out=Path('track_event_typed_args_args.out'))
 
   # Track handling
@@ -222,7 +230,23 @@
         LEFT JOIN process thread_process ON thread.upid = thread_process.upid
         ORDER BY ts ASC;
         """,
-        out=Path('track_event_tracks_slices.out'))
+        out=Csv("""
+      "track","process","thread","thread_process","ts","dur","category","name"
+      "[NULL]","[NULL]","t1","p1",1000,0,"cat","event1_on_t1"
+      "[NULL]","[NULL]","t2","p1",2000,0,"cat","event1_on_t2"
+      "[NULL]","[NULL]","t2","p1",3000,0,"cat","event2_on_t2"
+      "[NULL]","p1","[NULL]","[NULL]",4000,0,"cat","event1_on_p1"
+      "async","p1","[NULL]","[NULL]",5000,0,"cat","event1_on_async"
+      "async2","p1","[NULL]","[NULL]",5100,100,"cat","event1_on_async2"
+      "[NULL]","[NULL]","t1","p1",6000,0,"cat","event3_on_t1"
+      "[NULL]","[NULL]","t3","p1",11000,0,"cat","event1_on_t3"
+      "[NULL]","p2","[NULL]","[NULL]",21000,0,"cat","event1_on_p2"
+      "[NULL]","[NULL]","t4","p2",22000,0,"cat","event1_on_t4"
+      "Default Track","[NULL]","[NULL]","[NULL]",30000,0,"cat","event1_on_t1"
+      "[NULL]","p2","[NULL]","[NULL]",31000,0,"cat","event2_on_p2"
+      "[NULL]","[NULL]","t4","p2",32000,0,"cat","event2_on_t4"
+      "event_and_track_async3","p1","[NULL]","[NULL]",40000,0,"cat","event_and_track_async3"
+        """))
 
   def test_track_event_tracks_processes(self):
     return DiffTestBlueprint(
@@ -393,7 +417,23 @@
         LEFT JOIN args ON slice.arg_set_id = args.arg_set_id
         ORDER BY slice.ts, args.id;
         """,
-        out=Path('legacy_async_event.out'))
+        out=Csv("""
+        "track","process","thread","thread_process","ts","dur","category","name","key","string_value","int_value"
+        "name1","[NULL]","[NULL]","[NULL]",1000,7000,"cat","name1","debug.arg1","value1","[NULL]"
+        "name1","[NULL]","[NULL]","[NULL]",1000,7000,"cat","name1","legacy_event.passthrough_utid","[NULL]",1
+        "name1","[NULL]","[NULL]","[NULL]",1000,7000,"cat","name1","legacy_event.phase","S","[NULL]"
+        "name1","[NULL]","[NULL]","[NULL]",1000,7000,"cat","name1","debug.arg2","value2","[NULL]"
+        "name1","[NULL]","[NULL]","[NULL]",2000,1000,"cat","name1","legacy_event.passthrough_utid","[NULL]",2
+        "name1","[NULL]","[NULL]","[NULL]",2000,1000,"cat","name1","legacy_event.phase","S","[NULL]"
+        "name1","[NULL]","[NULL]","[NULL]",3000,0,"cat","name1","debug.arg3","value3","[NULL]"
+        "name1","[NULL]","[NULL]","[NULL]",3000,0,"cat","name1","debug.step","Step1","[NULL]"
+        "name1","[NULL]","[NULL]","[NULL]",3000,0,"cat","name1","legacy_event.passthrough_utid","[NULL]",1
+        "name1","[NULL]","[NULL]","[NULL]",3000,0,"cat","name1","legacy_event.phase","T","[NULL]"
+        "name1","[NULL]","[NULL]","[NULL]",5000,0,"cat","name1","debug.arg4","value4","[NULL]"
+        "name1","[NULL]","[NULL]","[NULL]",5000,0,"cat","name1","debug.step","Step2","[NULL]"
+        "name1","[NULL]","[NULL]","[NULL]",5000,0,"cat","name1","legacy_event.passthrough_utid","[NULL]",1
+        "name1","[NULL]","[NULL]","[NULL]",5000,0,"cat","name1","legacy_event.phase","p","[NULL]"
+        """))
 
   # Legacy atrace
   def test_track_event_with_atrace(self):
@@ -458,8 +498,52 @@
   def test_track_event_merged_debug_annotations_args(self):
     return DiffTestBlueprint(
         trace=Path('track_event_merged_debug_annotations.textproto'),
-        query=Path('track_event_args_test.sql'),
-        out=Path('track_event_merged_debug_annotations_args.out'))
+        query="""
+        SELECT
+          flat_key,
+          key,
+          int_value,
+          string_value
+        FROM args
+        ORDER BY key, display_value, arg_set_id, key ASC;
+        """,
+        out=Csv('''
+          "flat_key","key","int_value","string_value"
+          "cookie","cookie",1234,"[NULL]"
+          "debug.debug1.key1","debug.debug1.key1",10,"[NULL]"
+          "debug.debug1.key2","debug.debug1.key2[0]",20,"[NULL]"
+          "debug.debug1.key2","debug.debug1.key2[1]",21,"[NULL]"
+          "debug.debug1.key2","debug.debug1.key2[2]",22,"[NULL]"
+          "debug.debug1.key2","debug.debug1.key2[3]",23,"[NULL]"
+          "debug.debug1.key3","debug.debug1.key3",30,"[NULL]"
+          "debug.debug2.key1","debug.debug2.key1",10,"[NULL]"
+          "debug.debug2.key2","debug.debug2.key2[0]",20,"[NULL]"
+          "debug.debug2.key2","debug.debug2.key2[1]",21,"[NULL]"
+          "debug.debug2.key2","debug.debug2.key2[2]",22,"[NULL]"
+          "debug.debug2.key2","debug.debug2.key2[3]",23,"[NULL]"
+          "debug.debug2.key3.key31","debug.debug2.key3.key31",31,"[NULL]"
+          "debug.debug2.key3.key32","debug.debug2.key3.key32",32,"[NULL]"
+          "debug.debug2.key4","debug.debug2.key4",40,"[NULL]"
+          "debug.debug3","debug.debug3",32,"[NULL]"
+          "debug.debug4.key1","debug.debug4.key1",10,"[NULL]"
+          "debug.debug4.key2","debug.debug4.key2[0]",20,"[NULL]"
+          "debug.debug4.key2","debug.debug4.key2[1]",21,"[NULL]"
+          "event.category","event.category","[NULL]","cat"
+          "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]"
+          "utid","utid",1,"[NULL]"
+          "utid","utid",2,"[NULL]"
+        '''))
 
   # Counters
   def test_track_event_counters_slices(self):
@@ -516,7 +600,27 @@
         LEFT JOIN process thread_process ON thread.upid = thread_process.upid
         ORDER BY ts ASC;
         """,
-        out=Path('track_event_counters_counters.out'))
+        out=Csv("""
+        "counter_name","process","thread","thread_process","unit","ts","value"
+        "thread_time","[NULL]","t1","Browser","ns",1000,1000000.000000
+        "thread_time","[NULL]","t1","Browser","ns",1100,1010000.000000
+        "thread_time","[NULL]","t1","Browser","ns",2000,2000000.000000
+        "thread_time","[NULL]","t1","Browser","ns",2000,2010000.000000
+        "thread_time","[NULL]","t1","Browser","ns",2200,2020000.000000
+        "thread_time","[NULL]","t1","Browser","ns",2200,2030000.000000
+        "MySizeCounter","[NULL]","[NULL]","[NULL]","bytes",3000,1024.000000
+        "MySizeCounter","[NULL]","[NULL]","[NULL]","bytes",3100,2048.000000
+        "thread_time","[NULL]","t1","Browser","ns",4000,2040000.000000
+        "MySizeCounter","[NULL]","[NULL]","[NULL]","bytes",4000,1024.000000
+        "thread_time","[NULL]","t4","Browser","[NULL]",4000,10000.000000
+        "thread_instruction_count","[NULL]","t4","Browser","[NULL]",4000,20.000000
+        "thread_time","[NULL]","t4","Browser","[NULL]",4100,15000.000000
+        "thread_instruction_count","[NULL]","t4","Browser","[NULL]",4100,25.000000
+        "MyDoubleCounter","[NULL]","[NULL]","[NULL]","[NULL]",4200,3.141593
+        "MyDoubleCounter","[NULL]","[NULL]","[NULL]","[NULL]",4300,0.500000
+        "MySizeCounter","[NULL]","[NULL]","[NULL]","bytes",4500,4096.000000
+        "MyDoubleCounter","[NULL]","[NULL]","[NULL]","[NULL]",4500,2.718280
+        """))
 
   # Clock handling
   def test_track_event_monotonic_trace_clock_slices(self):
@@ -589,7 +693,15 @@
   def test_track_event_chrome_histogram_sample_args(self):
     return DiffTestBlueprint(
         trace=Path('track_event_chrome_histogram_sample.textproto'),
-        query=Path('track_event_args_test.sql'),
+        query="""
+        SELECT
+          flat_key,
+          key,
+          int_value,
+          string_value
+        FROM args
+        ORDER BY key, display_value, arg_set_id, key ASC;
+        """,
         out=Path('track_event_chrome_histogram_sample_args.out'))
 
   # Flow events importing from proto
@@ -693,6 +805,34 @@
         13000,"slice4"
         """))
 
+  def test_track_event_tracks_ordering(self):
+    return DiffTestBlueprint(
+        trace=Path('track_event_tracks_ordering.textproto'),
+        query="""
+        SELECT
+          id,
+          parent_id,
+          EXTRACT_ARG(source_arg_set_id, 'child_ordering') AS ordering,
+          EXTRACT_ARG(source_arg_set_id, 'sibling_order_rank') AS rank
+        FROM track
+        """,
+        out=Csv("""
+        "id","parent_id","ordering","rank"
+        0,"[NULL]","explicit","[NULL]"
+        1,0,"[NULL]",-10
+        2,0,"[NULL]",-2
+        3,0,"[NULL]",1
+        4,0,"[NULL]",2
+        5,2,"[NULL]","[NULL]"
+        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):
     return DiffTestBlueprint(
         trace=Path('track_event_tracks.textproto'),
diff --git a/test/trace_processor/diff_tests/parser/track_event/track_event_args_test.sql b/test/trace_processor/diff_tests/parser/track_event/track_event_args_test.sql
deleted file mode 100644
index 7d8ced6..0000000
--- a/test/trace_processor/diff_tests/parser/track_event/track_event_args_test.sql
+++ /dev/null
@@ -1,16 +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 flat_key, key, int_value, string_value FROM args ORDER BY key, display_value, arg_set_id, key ASC;
diff --git a/test/trace_processor/diff_tests/parser/track_event/track_event_counters_counters.out b/test/trace_processor/diff_tests/parser/track_event/track_event_counters_counters.out
deleted file mode 100644
index 840ffe1..0000000
--- a/test/trace_processor/diff_tests/parser/track_event/track_event_counters_counters.out
+++ /dev/null
@@ -1,19 +0,0 @@
-"counter_name","process","thread","thread_process","unit","ts","value"
-"thread_time","[NULL]","t1","Browser","ns",1000,1000000.000000
-"thread_time","[NULL]","t1","Browser","ns",1100,1010000.000000
-"thread_time","[NULL]","t1","Browser","ns",2000,2000000.000000
-"thread_time","[NULL]","t1","Browser","ns",2000,2010000.000000
-"thread_time","[NULL]","t1","Browser","ns",2200,2020000.000000
-"thread_time","[NULL]","t1","Browser","ns",2200,2030000.000000
-"MySizeCounter","[NULL]","[NULL]","[NULL]","bytes",3000,1024.000000
-"MySizeCounter","[NULL]","[NULL]","[NULL]","bytes",3100,2048.000000
-"thread_time","[NULL]","t1","Browser","ns",4000,2040000.000000
-"MySizeCounter","[NULL]","[NULL]","[NULL]","bytes",4000,1024.000000
-"thread_time","[NULL]","t4","Browser","[NULL]",4000,10000.000000
-"thread_instruction_count","[NULL]","t4","Browser","[NULL]",4000,20.000000
-"thread_time","[NULL]","t4","Browser","[NULL]",4100,15000.000000
-"thread_instruction_count","[NULL]","t4","Browser","[NULL]",4100,25.000000
-"MyDoubleCounter","[NULL]","[NULL]","[NULL]","[NULL]",4200,3.141593
-"MyDoubleCounter","[NULL]","[NULL]","[NULL]","[NULL]",4300,0.500000
-"MySizeCounter","[NULL]","[NULL]","[NULL]","bytes",4500,4096.000000
-"MyDoubleCounter","[NULL]","[NULL]","[NULL]","[NULL]",4500,2.718280
diff --git a/test/trace_processor/diff_tests/parser/track_event/track_event_merged_debug_annotations_args.out b/test/trace_processor/diff_tests/parser/track_event/track_event_merged_debug_annotations_args.out
deleted file mode 100644
index 5536952b..0000000
--- a/test/trace_processor/diff_tests/parser/track_event/track_event_merged_debug_annotations_args.out
+++ /dev/null
@@ -1,31 +0,0 @@
-"flat_key","key","int_value","string_value"
-"debug.debug1.key1","debug.debug1.key1",10,"[NULL]"
-"debug.debug1.key2","debug.debug1.key2[0]",20,"[NULL]"
-"debug.debug1.key2","debug.debug1.key2[1]",21,"[NULL]"
-"debug.debug1.key2","debug.debug1.key2[2]",22,"[NULL]"
-"debug.debug1.key2","debug.debug1.key2[3]",23,"[NULL]"
-"debug.debug1.key3","debug.debug1.key3",30,"[NULL]"
-"debug.debug2.key1","debug.debug2.key1",10,"[NULL]"
-"debug.debug2.key2","debug.debug2.key2[0]",20,"[NULL]"
-"debug.debug2.key2","debug.debug2.key2[1]",21,"[NULL]"
-"debug.debug2.key2","debug.debug2.key2[2]",22,"[NULL]"
-"debug.debug2.key2","debug.debug2.key2[3]",23,"[NULL]"
-"debug.debug2.key3.key31","debug.debug2.key3.key31",31,"[NULL]"
-"debug.debug2.key3.key32","debug.debug2.key3.key32",32,"[NULL]"
-"debug.debug2.key4","debug.debug2.key4",40,"[NULL]"
-"debug.debug3","debug.debug3",32,"[NULL]"
-"debug.debug4.key1","debug.debug4.key1",10,"[NULL]"
-"debug.debug4.key2","debug.debug4.key2[0]",20,"[NULL]"
-"debug.debug4.key2","debug.debug4.key2[1]",21,"[NULL]"
-"event.category","event.category","[NULL]","cat"
-"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]"
-"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]"
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
new file mode 100644
index 0000000..892266b
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/track_event/track_event_tracks_ordering.textproto
@@ -0,0 +1,338 @@
+# Sequence 1 defaults to track for "t1".
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 0
+  incremental_state_cleared: true
+  first_packet_on_sequence: true
+  track_descriptor {
+    uuid: 1
+    parent_uuid: 10
+    thread {
+      pid: 5
+      tid: 1
+      thread_name: "t1"
+    }
+    sibling_order_rank: -10
+  }
+  trace_packet_defaults {
+    track_event_defaults {
+      track_uuid: 1
+    }
+  }
+}
+# Sequence 2 defaults to track for "t2".
+packet {
+  trusted_packet_sequence_id: 2
+  timestamp: 0
+  incremental_state_cleared: true
+  first_packet_on_sequence: true
+  track_descriptor {
+    uuid: 2
+    parent_uuid: 10
+    thread {
+      pid: 5
+      tid: 2
+      thread_name: "t2"
+    }
+    sibling_order_rank: -2
+  }
+  trace_packet_defaults {
+    track_event_defaults {
+      track_uuid: 2
+    }
+  }
+}
+# Both thread tracks are nested underneath this process track.
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 0
+  track_descriptor {
+    uuid: 10
+    process {
+      pid: 5
+      process_name: "p1"
+    }
+    child_ordering: 3
+    chrome_process {
+      host_app_package_name: "host_app"
+    }
+  }
+}
+# And we have an async track underneath the process too.
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 0
+  track_descriptor {
+    uuid: 11
+    parent_uuid: 10
+    name: "async"
+    sibling_order_rank: 1
+  }
+}
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 100
+  track_descriptor {
+    uuid: 12
+    parent_uuid: 10
+    name: "async2"
+    sibling_order_rank: 2
+  }
+}
+packet {
+  trusted_packet_sequence_id: 2
+  timestamp: 200
+  track_descriptor {
+    uuid: 12
+    parent_uuid: 10
+    name: "async2"
+  }
+}
+
+# Threads also can have child async tracks.
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 200
+  track_descriptor {
+    uuid: 14
+    parent_uuid: 2
+    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 {
+  trusted_packet_sequence_id: 3
+  timestamp: 10000
+  incremental_state_cleared: true
+  first_packet_on_sequence: true
+  track_descriptor {
+    uuid: 3
+    parent_uuid: 10
+    thread {
+      pid: 5
+      tid: 1
+      thread_name: "t3"
+    }
+  }
+}
+# Should appear on t3.
+packet {
+  trusted_packet_sequence_id: 3
+  timestamp: 11000
+  track_event {
+    track_uuid: 3
+    categories: "cat"
+    name: "event1_on_t3"
+    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 {
+  trusted_packet_sequence_id: 4
+  timestamp: 20000
+  incremental_state_cleared: true
+  track_descriptor {
+    uuid: 20
+    process {
+      pid: 5
+      process_name: "p2"
+    }
+  }
+}
+# 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
+  incremental_state_cleared: true
+  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_tracks_slices.out b/test/trace_processor/diff_tests/parser/track_event/track_event_tracks_slices.out
deleted file mode 100644
index d0186cc..0000000
--- a/test/trace_processor/diff_tests/parser/track_event/track_event_tracks_slices.out
+++ /dev/null
@@ -1,15 +0,0 @@
-"track","process","thread","thread_process","ts","dur","category","name"
-"[NULL]","[NULL]","t1","p1",1000,0,"cat","event1_on_t1"
-"[NULL]","[NULL]","t2","p1",2000,0,"cat","event1_on_t2"
-"[NULL]","[NULL]","t2","p1",3000,0,"cat","event2_on_t2"
-"[NULL]","p1","[NULL]","[NULL]",4000,0,"cat","event1_on_p1"
-"async","p1","[NULL]","[NULL]",5000,0,"cat","event1_on_async"
-"async2","p1","[NULL]","[NULL]",5100,100,"cat","event1_on_async2"
-"[NULL]","[NULL]","t1","p1",6000,0,"cat","event3_on_t1"
-"[NULL]","[NULL]","t3","p1",11000,0,"cat","event1_on_t3"
-"[NULL]","p2","[NULL]","[NULL]",21000,0,"cat","event1_on_p2"
-"[NULL]","[NULL]","t4","p2",22000,0,"cat","event1_on_t4"
-"Default Track","[NULL]","[NULL]","[NULL]",30000,0,"cat","event1_on_t1"
-"[NULL]","p2","[NULL]","[NULL]",31000,0,"cat","event2_on_p2"
-"[NULL]","[NULL]","t4","p2",32000,0,"cat","event2_on_t4"
-"event_and_track_async3","p1","[NULL]","[NULL]",40000,0,"cat","event_and_track_async3"
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 3083a2b..2e88593 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
@@ -38,3 +38,4 @@
 "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_args_test.sql b/test/trace_processor/diff_tests/parser/translated_args/chrome_args_test.sql
index 542df14..031604b 100644
--- a/test/trace_processor/diff_tests/parser/translated_args/chrome_args_test.sql
+++ b/test/trace_processor/diff_tests/parser/translated_args/chrome_args_test.sql
@@ -13,4 +13,6 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 --
-SELECT flat_key, key, int_value, string_value FROM args ORDER BY key, display_value, arg_set_id ASC;
+SELECT flat_key, key, int_value, string_value
+FROM args
+ORDER BY key, display_value, arg_set_id ASC;
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 7718306..efbb466 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
@@ -14,3 +14,4 @@
 "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 d61b9d3..fd17777 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
@@ -8,3 +8,4 @@
 "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 ece5310..ace9de6 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
@@ -13,3 +13,4 @@
 "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 3471f41..ea05157 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
@@ -18,3 +18,4 @@
 "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/zip/tests.py b/test/trace_processor/diff_tests/parser/zip/tests.py
index 660bdbe..4e46286 100644
--- a/test/trace_processor/diff_tests/parser/zip/tests.py
+++ b/test/trace_processor/diff_tests/parser/zip/tests.py
@@ -59,20 +59,37 @@
         "main,E"
         '''))
 
-  def test_tokenization_order(self):
+  def test_zip_tokenization_order(self):
     return DiffTestBlueprint(
         trace=DataPath('zip/perf_track_sym.zip'),
         query='''
         SELECT *
         FROM __intrinsic_trace_file
-        ORDER BY id
+        ORDER BY processing_order
         ''',
         out=Csv('''
-        "id","type","parent_id","name","size","trace_type"
-        0,"__intrinsic_trace_file","[NULL]","[NULL]",94651,"zip"
-        1,"__intrinsic_trace_file",0,"c.trace.pb",379760,"proto"
-        2,"__intrinsic_trace_file",0,"b.simpleperf.data",554911,"perf"
-        3,"__intrinsic_trace_file",0,"a.symbols.pb",186149,"symbols"
+        "id","type","parent_id","name","size","trace_type","processing_order"
+        0,"__intrinsic_trace_file","[NULL]","[NULL]",94651,"zip",0
+        3,"__intrinsic_trace_file",0,"c.trace.pb",379760,"proto",1
+        1,"__intrinsic_trace_file",0,"b.simpleperf.data",554911,"perf",2
+        2,"__intrinsic_trace_file",0,"a.symbols.pb",186149,"symbols",3
+        '''))
+
+  def test_tar_gz_tokenization_order(self):
+    return DiffTestBlueprint(
+        trace=DataPath('perf_track_sym.tar.gz'),
+        query='''
+        SELECT *
+        FROM __intrinsic_trace_file
+        ORDER BY processing_order
+        ''',
+        out=Csv('''
+        "id","type","parent_id","name","size","trace_type","processing_order"
+        0,"__intrinsic_trace_file","[NULL]","[NULL]",94091,"gzip",0
+        1,"__intrinsic_trace_file",0,"",1126400,"tar",1
+        4,"__intrinsic_trace_file",1,"/c.trace.pb",379760,"proto",2
+        3,"__intrinsic_trace_file",1,"/b.simpleperf.data",554911,"perf",3
+        2,"__intrinsic_trace_file",1,"/a.symbols.pb",186149,"symbols",4
         '''))
 
   # Make sure the logcat timestamps are correctly converted to trace ts. All
diff --git a/test/trace_processor/diff_tests/stdlib/android/desktop_mode_tests.py b/test/trace_processor/diff_tests/stdlib/android/desktop_mode_tests.py
new file mode 100644
index 0000000..6c16720
--- /dev/null
+++ b/test/trace_processor/diff_tests/stdlib/android/desktop_mode_tests.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import DataPath
+from python.generators.diff_tests.testing import Csv
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+class DesktopMode(TestSuite):
+
+  def test_android_desktop_mode_windows_statsd_events(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_desktop_mode/single_window_add_update_remove.pb'),
+        query="""
+          INCLUDE PERFETTO MODULE android.desktop_mode;
+          SELECT * FROM android_desktop_mode_windows;
+          """,
+        out=Csv("""
+        "raw_add_ts","raw_remove_ts","ts","dur","instance_id","uid"
+        1112172132337,1115098491388,1112172132337,2926359051,22,10211
+        """))
+
+  def test_android_desktop_mode_windows_statsd_events_multiple_windows(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_desktop_mode/multiple_windows_add_update_remove.pb'),
+        query="""
+          INCLUDE PERFETTO MODULE android.desktop_mode;
+          SELECT * FROM android_desktop_mode_windows;
+          """,
+        out=Csv("""
+        "raw_add_ts","raw_remove_ts","ts","dur","instance_id","uid"
+        1340951146935,1347096280320,1340951146935,6145133385,24,10211
+        1342507511641,1345461733688,1342507511641,2954222047,26,10183
+                """))
+
+  def test_android_desktop_mode_windows_statsd_events_add_no_remove(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_desktop_mode/single_window_add_update_no_remove.pb'),
+        query="""
+          INCLUDE PERFETTO MODULE android.desktop_mode;
+          SELECT * FROM android_desktop_mode_windows;
+          """,
+        out=Csv("""
+        "raw_add_ts","raw_remove_ts","ts","dur","instance_id","uid"
+        1552558346094,"[NULL]",1552558346094,1620521485,27,10211
+        """))
+
+  def test_android_desktop_mode_windows_statsd_events_no_add_update_remove(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_desktop_mode/single_window_no_add_update_remove.pb'),
+        query="""
+          INCLUDE PERFETTO MODULE android.desktop_mode;
+          SELECT * FROM android_desktop_mode_windows;
+          """,
+        out=Csv("""
+        "raw_add_ts","raw_remove_ts","ts","dur","instance_id","uid"
+        "[NULL]",1696520389866,1695387563286,1132826580,29,10211
+        """))
+
+  def test_android_desktop_mode_windows_statsd_events_only_update(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_desktop_mode/single_window_only_update.pb'),
+        query="""
+          INCLUDE PERFETTO MODULE android.desktop_mode;
+          SELECT * FROM android_desktop_mode_windows;
+          """,
+        out=Csv("""
+        "raw_add_ts","raw_remove_ts","ts","dur","instance_id","uid"
+        "[NULL]","[NULL]",1852548597746,3663403770,31,10211
+        """))
+
+  def test_android_desktop_mode_windows_statsd_events_multiple_windows_update_only(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_desktop_mode/multiple_window_only_update.pb'),
+        query="""
+          INCLUDE PERFETTO MODULE android.desktop_mode;
+          SELECT * FROM android_desktop_mode_windows;
+          """,
+        out=Csv("""
+        "raw_add_ts","raw_remove_ts","ts","dur","instance_id","uid"
+        "[NULL]","[NULL]",2137135290268,4737314089,33,10211
+        "[NULL]","[NULL]",2137135290268,4737314089,35,10183
+        """))
+
+  def test_android_desktop_mode_windows_statsd_events_multiple_windows_same_instance_new_session(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_desktop_mode/session_with_same_instance_id.pb'),
+        query="""
+          INCLUDE PERFETTO MODULE android.desktop_mode;
+          SELECT * FROM android_desktop_mode_windows;
+          """,
+        out=Csv("""
+        "raw_add_ts","raw_remove_ts","ts","dur","instance_id","uid"
+        8936818061228,8963638163943,8936818061228,26820102715,1000025,1110217
+        8966480744267,"[NULL]",8966480744267,3596089886,1000025,1110217
+        8966481546961,"[NULL]",8966481546961,3595287192,1000028,1110329
+        """))
+
diff --git a/test/trace_processor/diff_tests/stdlib/android/frames_tests.py b/test/trace_processor/diff_tests/stdlib/android/frames_tests.py
index a652129..fdf171a 100644
--- a/test/trace_processor/diff_tests/stdlib/android/frames_tests.py
+++ b/test/trace_processor/diff_tests/stdlib/android/frames_tests.py
@@ -45,22 +45,22 @@
         SELECT * FROM android_frames_choreographer_do_frame;
         """,
         out=Csv("""
-        "id","frame_id","ui_thread_utid","upid"
-        2,10,2,2
-        15,20,2,2
-        22,30,2,2
-        35,40,2,2
-        46,60,2,2
-        55,90,2,2
-        63,100,2,2
-        73,110,2,2
-        79,120,2,2
-        87,130,2,2
-        93,140,2,2
-        99,145,2,2
-        102,150,2,2
-        108,160,2,2
-        140,1000,2,2
+        "id","frame_id","ui_thread_utid","upid","ts"
+        2,10,2,2,0
+        15,20,2,2,20000000
+        22,30,2,2,30000000
+        35,40,2,2,40000000
+        46,60,2,2,70000000
+        55,90,2,2,100000000
+        63,100,2,2,200000000
+        73,110,2,2,300000000
+        79,120,2,2,400000000
+        87,130,2,2,550000000
+        93,140,2,2,608500000
+        99,145,2,2,655000000
+        102,150,2,2,700000000
+        108,160,2,2,800000000
+        140,1000,2,2,1100000000
         """))
 
   def test_android_frames_draw_frame(self):
@@ -101,24 +101,24 @@
         SELECT * FROM android_frames;
         """,
         out=Csv("""
-        "frame_id","ts","dur","do_frame_id","draw_frame_id","actual_frame_timeline_id","expected_frame_timeline_id","render_thread_utid","ui_thread_utid"
-        10,0,16000000,2,8,1,0,4,2
-        20,8000000,28000000,15,16,12,11,4,2
-        30,30000000,25000000,22,23,21,20,4,2
-        40,40000000,40000000,35,41,37,36,4,2
-        60,70000000,10000000,46,50,48,47,4,2
-        90,100000000,23000000,55,57,54,53,4,2
-        90,100000000,23000000,55,60,54,53,4,2
-        100,200000000,22000000,63,66,65,64,4,2
-        100,200000000,22000000,63,69,65,64,4,2
-        110,300000000,61000000,73,74,71,70,4,2
-        120,400000000,61000000,79,80,78,77,4,2
-        130,500000000,2000000,87,89,85,84,4,2
-        140,608600000,17000000,93,95,94,91,4,2
-        145,650000000,20000000,99,100,98,97,4,2
-        150,700500000,14500000,102,105,104,103,4,2
-        160,1070000000,16000000,108,109,132,107,4,2
-        1000,1100000000,500000000,140,146,138,137,4,2
+        "frame_id","ts","dur","do_frame_id","draw_frame_id","actual_frame_timeline_id","expected_frame_timeline_id","render_thread_utid","ui_thread_utid","actual_frame_timeline_count","expected_frame_timeline_count"
+        10,0,16000000,2,8,1,0,4,2,1,1
+        20,8000000,28000000,15,16,12,11,4,2,1,1
+        30,30000000,25000000,22,23,21,20,4,2,1,1
+        40,40000000,40000000,35,41,37,36,4,2,1,1
+        60,70000000,20000000,46,50,48,47,4,2,2,1
+        90,100000000,23000000,55,57,54,53,4,2,1,1
+        90,100000000,23000000,55,60,54,53,4,2,1,1
+        100,200000000,22000000,63,66,65,64,4,2,1,1
+        100,200000000,22000000,63,69,65,64,4,2,1,1
+        110,300000000,80000000,73,74,71,70,4,2,3,2
+        120,400000000,61000000,79,80,78,77,4,2,2,2
+        130,500000000,16000000,87,89,85,84,4,2,3,2
+        140,608600000,17000000,93,95,94,91,4,2,2,2
+        145,650000000,20000000,99,100,98,97,4,2,1,1
+        150,700500000,14500000,102,105,104,103,4,2,1,1
+        160,1070000000,16000000,108,109,132,107,4,2,1,2
+        1000,1100000000,500000000,140,146,138,137,4,2,1,1
         """))
 
   def test_android_first_frame_after(self):
@@ -145,22 +145,22 @@
         out=Csv("""
         "frame_id","overrun"
         10,0
-        20,-8000000
-        30,-5000000
-        40,-20000000
-        60,10000000
-        90,-3000000
-        100,-2000000
-        110,-41000000
-        120,-41000000
-        130,18000000
-        140,-5600000
+        20,8000000
+        30,5000000
+        40,20000000
+        60,-10000000
+        90,3000000
+        100,2000000
+        110,41000000
+        120,41000000
+        130,-18000000
+        140,5600000
         145,0
-        150,5000000
-        160,-266000000
+        150,-5000000
+        160,266000000
         190,0
-        200,-16000000
-        1000,-480000000
+        200,16000000
+        1000,480000000
         """))
 
   def test_android_app_vsync_delay_per_frame(self):
@@ -172,19 +172,22 @@
         SELECT * FROM android_app_vsync_delay_per_frame;
         """,
         out=Csv("""
-        "frame_id","app_vsync_delay","start_latency"
-        10,0,0
-        30,0,0
-        40,0,0
-        60,0,0
-        90,0,0
-        100,0,0
-        110,0,0
-        120,0,0
-        140,100000,8600000
-        150,500000,500000
-        160,270000000,270000000
-        1000,0,0
+        "frame_id","app_vsync_delay"
+        10,0
+        20,0
+        30,0
+        40,0
+        60,0
+        90,0
+        100,0
+        110,0
+        120,0
+        130,0
+        140,8600000
+        145,0
+        150,500000
+        160,270000000
+        1000,0
         """))
 
   def test_android_cpu_time_per_frame(self):
@@ -198,6 +201,7 @@
         out=Csv("""
         "frame_id","app_vsync_delay","do_frame_dur","draw_frame_dur","cpu_time"
         10,0,5000000,1000000,6000000
+        20,0,3000000,4000000,7000000
         30,0,3000000,19000000,22000000
         40,0,13000000,7000000,20000000
         60,0,10000000,9000000,19000000
@@ -205,7 +209,9 @@
         100,0,15000000,8000000,23000000
         110,0,15000000,2000000,17000000
         120,0,15000000,2000000,17000000
-        140,100000,1500000,17000000,18600000
+        130,0,5000000,4000000,9000000
+        140,8600000,1500000,17000000,27100000
+        145,0,20000000,3000000,23000000
         150,500000,2000000,13800000,16300000
         160,270000000,2000000,1000000,273000000
         1000,0,100000000,150000000,250000000
@@ -222,18 +228,18 @@
         out=Csv("""
         "frame_id","overrun","cpu_time","ui_time","was_jank","was_slow_frame","was_big_jank","was_huge_jank"
         10,0,6000000,5000000,"[NULL]","[NULL]","[NULL]","[NULL]"
-        20,-8000000,8000000,3000000,1,"[NULL]","[NULL]","[NULL]"
-        30,-5000000,22000000,3000000,1,1,"[NULL]","[NULL]"
-        40,-20000000,20000000,13000000,1,"[NULL]","[NULL]","[NULL]"
-        60,10000000,19000000,10000000,"[NULL]","[NULL]","[NULL]","[NULL]"
-        90,-3000000,23000000,15000000,1,1,"[NULL]","[NULL]"
-        100,-2000000,23000000,15000000,1,1,"[NULL]","[NULL]"
-        110,-41000000,17000000,15000000,1,"[NULL]","[NULL]","[NULL]"
-        120,-41000000,17000000,15000000,1,"[NULL]","[NULL]","[NULL]"
-        130,18000000,10000000,5000000,"[NULL]","[NULL]","[NULL]","[NULL]"
-        140,-5600000,18600000,1500000,1,"[NULL]","[NULL]","[NULL]"
-        145,0,25000000,20000000,"[NULL]",1,"[NULL]","[NULL]"
-        150,5000000,16300000,2000000,"[NULL]","[NULL]","[NULL]","[NULL]"
-        160,-266000000,273000000,2000000,1,1,1,1
-        1000,-480000000,250000000,100000000,1,1,1,1
+        20,8000000,7000000,3000000,1,"[NULL]","[NULL]","[NULL]"
+        30,5000000,22000000,3000000,1,1,"[NULL]","[NULL]"
+        40,20000000,20000000,13000000,1,"[NULL]","[NULL]","[NULL]"
+        60,-10000000,19000000,10000000,"[NULL]","[NULL]","[NULL]","[NULL]"
+        90,3000000,23000000,15000000,1,1,"[NULL]","[NULL]"
+        100,2000000,23000000,15000000,1,1,"[NULL]","[NULL]"
+        110,41000000,17000000,15000000,1,"[NULL]","[NULL]","[NULL]"
+        120,41000000,17000000,15000000,1,"[NULL]","[NULL]","[NULL]"
+        130,-18000000,9000000,5000000,"[NULL]","[NULL]","[NULL]","[NULL]"
+        140,5600000,27100000,1500000,1,1,"[NULL]","[NULL]"
+        145,0,23000000,20000000,"[NULL]",1,"[NULL]","[NULL]"
+        150,-5000000,16300000,2000000,"[NULL]","[NULL]","[NULL]","[NULL]"
+        160,266000000,273000000,2000000,1,1,1,1
+        1000,480000000,250000000,100000000,1,1,1,1
         """))
diff --git a/test/trace_processor/diff_tests/stdlib/android/heap_graph_tests.py b/test/trace_processor/diff_tests/stdlib/android/heap_graph_tests.py
index fe3ba2d..dfe5b2d 100644
--- a/test/trace_processor/diff_tests/stdlib/android/heap_graph_tests.py
+++ b/test/trace_processor/diff_tests/stdlib/android/heap_graph_tests.py
@@ -84,3 +84,20 @@
           10,2,"B",0,1,1000,1,1000
           10,2,"java.lang.String",1,2,10666,2,10666
         """))
+
+  def test_heap_graph_class_summary_tree(self):
+    return DiffTestBlueprint(
+        trace=Path('heap_graph_for_aggregation.textproto'),
+        query="""
+          INCLUDE PERFETTO MODULE android.memory.heap_graph.class_summary_tree;
+
+          SELECT name, self_count, self_size, cumulative_count, cumulative_size
+          FROM android_heap_graph_class_summary_tree
+          ORDER BY cumulative_size DESC;
+        """,
+        out=Csv("""
+          "name","self_count","self_size","cumulative_count","cumulative_size"
+          "A",2,200,4,11200
+          "java.lang.String",1,10000,1,10000
+          "B",1,1000,1,1000
+        """))
diff --git a/test/trace_processor/diff_tests/stdlib/android/heap_profile_tests.py b/test/trace_processor/diff_tests/stdlib/android/heap_profile_tests.py
new file mode 100644
index 0000000..bbdd809
--- /dev/null
+++ b/test/trace_processor/diff_tests/stdlib/android/heap_profile_tests.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import DataPath
+from python.generators.diff_tests.testing import Csv
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class HeapProfile(TestSuite):
+
+  def test_heap_profile_summary_tree(self):
+    return DiffTestBlueprint(
+        trace=DataPath('system-server-native-profile'),
+        query="""
+          INCLUDE PERFETTO MODULE android.memory.heap_profile.summary_tree;
+
+          SELECT
+            name,
+            self_size,
+            cumulative_size,
+            self_alloc_size,
+            cumulative_alloc_size
+          FROM android_heap_profile_summary_tree
+          ORDER BY cumulative_size DESC, name
+          LIMIT 10;
+        """,
+        out=Csv("""
+          "name","self_size","cumulative_size","self_alloc_size","cumulative_alloc_size"
+          "__pthread_start(void*)",0,84848,0,1084996
+          "__start_thread",0,84848,0,1084996
+          "art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)",0,57144,0,736946
+          "art::JValue art::InvokeVirtualOrInterfaceWithJValues<art::ArtMethod*>(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, art::ArtMethod*, jvalue const*)",0,57144,0,736946
+          "art::Thread::CreateCallback(void*)",0,57144,0,736946
+          "art_quick_invoke_stub",0,57144,0,736946
+          "android.os.HandlerThread.run",0,53048,0,197068
+          "com.android.server.UiThread.run",0,53048,0,197068
+          "android::AndroidRuntime::javaThreadShell(void*)",0,27704,0,348050
+          "(anonymous namespace)::nativeInitSensorEventQueue(_JNIEnv*, _jclass*, long, _jobject*, _jobject*, _jstring*, int)",0,26624,0,26624
+        """))
diff --git a/test/trace_processor/diff_tests/stdlib/android/startups_tests.py b/test/trace_processor/diff_tests/stdlib/android/startups_tests.py
index c4f5a2e..c44bca7 100644
--- a/test/trace_processor/diff_tests/stdlib/android/startups_tests.py
+++ b/test/trace_processor/diff_tests/stdlib/android/startups_tests.py
@@ -168,3 +168,35 @@
         "startup_id","time_to_initial_display","time_to_full_display","ttid_frame_id","ttfd_frame_id","upid"
         0,143980066,620815843,5873276,5873353,229
         """))
+
+  def test_android_startup_breakdown(self):
+    return DiffTestBlueprint(
+        trace=DataPath('api31_startup_cold.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.startup.startup_breakdowns;
+        SELECT
+          SUM(dur) AS dur,
+          reason
+          FROM android_startup_opinionated_breakdown
+          GROUP BY reason ORDER BY dur DESC;
+        """,
+        out=Csv("""
+        "dur","reason"
+        28663023,"choreographer_do_frame"
+        22564487,"binder"
+        22011252,"launch_delay"
+        16351925,"Running"
+        13212137,"activity_start"
+        10264635,"io"
+        6779947,"inflate"
+        6240207,"bind_application"
+        5214375,"R+"
+        3072397,"resources_manager_get_resources"
+        2722869,"D"
+        2574273,"open_dex_files_from_oat"
+        2392761,"S"
+        2353124,"activity_resume"
+        1325727,"R"
+        43698,"art_lock_contention"
+        5573,"verify_class"
+        """))
diff --git a/test/trace_processor/diff_tests/stdlib/android/tests.py b/test/trace_processor/diff_tests/stdlib/android/tests.py
index 7a649e1..1b204fc 100644
--- a/test/trace_processor/diff_tests/stdlib/android/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/android/tests.py
@@ -306,6 +306,28 @@
         13934,"D",11950576,1
       """))
 
+  def test_android_monitor_contention_chain_thread_state(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_monitor_contention_trace.atr'),
+        query="""
+      INCLUDE PERFETTO MODULE android.monitor_contention;
+      SELECT
+        *
+      FROM android_monitor_contention_chain_thread_state
+      WHERE id = 13934;
+      """,
+        out=Csv("""
+        "id","ts","dur","blocking_utid","blocked_function","state"
+        13934,1739927671503,141874,557,"[NULL]","R"
+        13934,1739927813377,69101,557,"[NULL]","Running"
+        13934,1739927882478,7649,557,"[NULL]","R+"
+        13934,1739927890127,3306,557,"[NULL]","Running"
+        13934,1739927893433,11950576,557,"blkdev_issue_flush","D"
+        13934,1739939844009,76306,557,"[NULL]","R"
+        13934,1739939920315,577554,557,"[NULL]","Running"
+        13934,1739940497869,82426,557,"[NULL]","R"
+      """))
+
   def test_monitor_contention_chain_extraction(self):
     return DiffTestBlueprint(
         trace=DataPath('android_monitor_contention_trace.atr'),
@@ -1302,3 +1324,117 @@
         "binder",4174605447
         "S",5144384456
         """))
+
+  def test_android_charging_states_output(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_job_scheduler.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.battery.charging_states;
+        SELECT ts, dur, charging_state FROM android_charging_states;
+      """,
+        out=Csv("""
+        "ts","dur","charging_state"
+        368604749651,59806073237,"Charging"
+      """))
+
+  def test_android_job_scheduler_states_output(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_job_scheduler.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.job_scheduler_states;
+        SELECT
+          id,
+          ts,
+          dur,
+          slice_id,
+          job_name || '_' || job_id AS job_name,
+          uid,
+          job_id,
+          package_name,
+          job_namespace,
+          effective_priority,
+          has_battery_not_low_constraint,
+          has_charging_constraint,
+          has_connectivity_constraint,
+          has_content_trigger_constraint,
+          has_deadline_constraint,
+          has_idle_constraint,
+          has_storage_not_low_constraint,
+          has_timing_delay_constraint,
+          is_prefetch,
+          is_requested_expedited_job,
+          is_running_as_expedited_job,
+          num_previous_attempts,
+          requested_priority,
+          standby_bucket,
+          is_periodic,
+          has_flex_constraint,
+          is_requested_as_user_initiated_job,
+          is_running_as_user_initiated_job,
+          deadline_ms,
+          job_start_latency_ms,
+          num_uncompleted_work_items,
+          proc_state,
+          internal_stop_reason,
+          public_stop_reason
+        FROM android_job_scheduler_states;
+      """,
+        out=Csv("""
+"id","ts","dur","slice_id","job_name","uid","job_id","package_name","job_namespace","effective_priority","has_battery_not_low_constraint","has_charging_constraint","has_connectivity_constraint","has_content_trigger_constraint","has_deadline_constraint","has_idle_constraint","has_storage_not_low_constraint","has_timing_delay_constraint","is_prefetch","is_requested_expedited_job","is_running_as_expedited_job","num_previous_attempts","requested_priority","standby_bucket","is_periodic","has_flex_constraint","is_requested_as_user_initiated_job","is_running_as_user_initiated_job","deadline_ms","job_start_latency_ms","num_uncompleted_work_items","proc_state","internal_stop_reason","public_stop_reason"
+1,377089754138,83200835,10,"@androidx.work.systemjobscheduler@com.android.providers.media.module/androidx.work.impl.background.systemjob.SystemJobService_-2746960329031286783",10090,-2746960329031286783,"com.android.providers.media.module","androidx.work.systemjobscheduler",400,1,0,0,0,0,0,0,0,0,0,0,0,400,"EXEMPTED",0,0,0,0,0,3,0,"PROCESS_STATE_PERSISTENT","INTERNAL_STOP_REASON_CANCELLED","STOP_REASON_CANCELLED_BY_APP"
+2,385507499374,111746552,17,"@androidx.work.systemjobscheduler@com.android.providers.media.module/androidx.work.impl.background.systemjob.SystemJobService_-2746960329031286782",10090,-2746960329031286782,"com.android.providers.media.module","androidx.work.systemjobscheduler",400,1,0,0,0,0,0,0,0,0,0,0,0,400,"EXEMPTED",0,0,0,0,0,6,0,"PROCESS_STATE_PERSISTENT","INTERNAL_STOP_REASON_SUCCESSFUL_FINISH","STOP_REASON_UNDEFINED"
+3,416753734715,129444346,53,"@androidx.work.systemjobscheduler@com.android.providers.media.module/androidx.work.impl.background.systemjob.SystemJobService_-2746960329031286781",10090,-2746960329031286781,"com.android.providers.media.module","androidx.work.systemjobscheduler",400,1,0,0,0,0,0,0,0,0,0,0,0,400,"EXEMPTED",0,0,0,0,0,5,0,"PROCESS_STATE_PERSISTENT","INTERNAL_STOP_REASON_SUCCESSFUL_FINISH","STOP_REASON_UNDEFINED"
+4,422530232411,86735906,59,"@androidx.work.systemjobscheduler@com.android.providers.media.module/androidx.work.impl.background.systemjob.SystemJobService_-2746960329031286780",10090,-2746960329031286780,"com.android.providers.media.module","androidx.work.systemjobscheduler",400,1,0,0,0,0,0,0,0,0,0,0,0,400,"EXEMPTED",0,0,0,0,0,3,0,"PROCESS_STATE_PERSISTENT","INTERNAL_STOP_REASON_SUCCESSFUL_FINISH","STOP_REASON_UNDEFINED"
+      """))
+
+  def test_android_job_scheduler_with_screen_charging_output(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_job_scheduler.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.job_scheduler_states;
+        SELECT
+          ts,
+          dur,
+          slice_id,
+          job_name,
+          uid,
+          job_id,
+          job_dur,
+          package_name,
+          job_namespace,
+          charging_state,
+          screen_state,
+          effective_priority,
+          has_battery_not_low_constraint,
+          has_charging_constraint,
+          has_connectivity_constraint,
+          has_content_trigger_constraint,
+          has_deadline_constraint,
+          has_idle_constraint,
+          has_storage_not_low_constraint,
+          has_timing_delay_constraint,
+          is_prefetch,
+          is_requested_expedited_job,
+          is_running_as_expedited_job,
+          num_previous_attempts,
+          requested_priority,
+          standby_bucket,
+          is_periodic,
+          has_flex_constraint,
+          is_requested_as_user_initiated_job,
+          is_running_as_user_initiated_job,
+          deadline_ms,
+          job_start_latency_ms,
+          num_uncompleted_work_items,
+          proc_state,
+          internal_stop_reason,
+          public_stop_reason
+        from android_job_scheduler_with_screen_charging_states;
+      """,
+        out=Csv("""
+        "ts","dur","slice_id","job_name","uid","job_id","job_dur","package_name","job_namespace","charging_state","screen_state","effective_priority","has_battery_not_low_constraint","has_charging_constraint","has_connectivity_constraint","has_content_trigger_constraint","has_deadline_constraint","has_idle_constraint","has_storage_not_low_constraint","has_timing_delay_constraint","is_prefetch","is_requested_expedited_job","is_running_as_expedited_job","num_previous_attempts","requested_priority","standby_bucket","is_periodic","has_flex_constraint","is_requested_as_user_initiated_job","is_running_as_user_initiated_job","deadline_ms","job_start_latency_ms","num_uncompleted_work_items","proc_state","internal_stop_reason","public_stop_reason"
+377089754138,83200835,10,"@androidx.work.systemjobscheduler@com.android.providers.media.module/androidx.work.impl.background.systemjob.SystemJobService_-2746960329031286783",10090,-2746960329031286783,83200835,"com.android.providers.media.module","androidx.work.systemjobscheduler","Charging","Unknown",400,1,0,0,0,0,0,0,0,0,0,0,0,400,"EXEMPTED",0,0,0,0,0,3,0,"PROCESS_STATE_PERSISTENT","INTERNAL_STOP_REASON_CANCELLED","STOP_REASON_CANCELLED_BY_APP"
+385507499374,111746552,17,"@androidx.work.systemjobscheduler@com.android.providers.media.module/androidx.work.impl.background.systemjob.SystemJobService_-2746960329031286782",10090,-2746960329031286782,111746552,"com.android.providers.media.module","androidx.work.systemjobscheduler","Charging","Unknown",400,1,0,0,0,0,0,0,0,0,0,0,0,400,"EXEMPTED",0,0,0,0,0,6,0,"PROCESS_STATE_PERSISTENT","INTERNAL_STOP_REASON_SUCCESSFUL_FINISH","STOP_REASON_UNDEFINED"
+416753734715,129444346,53,"@androidx.work.systemjobscheduler@com.android.providers.media.module/androidx.work.impl.background.systemjob.SystemJobService_-2746960329031286781",10090,-2746960329031286781,129444346,"com.android.providers.media.module","androidx.work.systemjobscheduler","Charging","Unknown",400,1,0,0,0,0,0,0,0,0,0,0,0,400,"EXEMPTED",0,0,0,0,0,5,0,"PROCESS_STATE_PERSISTENT","INTERNAL_STOP_REASON_SUCCESSFUL_FINISH","STOP_REASON_UNDEFINED"
+422530232411,86735906,59,"@androidx.work.systemjobscheduler@com.android.providers.media.module/androidx.work.impl.background.systemjob.SystemJobService_-2746960329031286780",10090,-2746960329031286780,86735906,"com.android.providers.media.module","androidx.work.systemjobscheduler","Charging","Unknown",400,1,0,0,0,0,0,0,0,0,0,0,0,400,"EXEMPTED",0,0,0,0,0,3,0,"PROCESS_STATE_PERSISTENT","INTERNAL_STOP_REASON_SUCCESSFUL_FINISH","STOP_REASON_UNDEFINED"
+      """))
diff --git a/test/trace_processor/diff_tests/stdlib/chrome/tests.py b/test/trace_processor/diff_tests/stdlib/chrome/tests.py
index d2afd6a..c7ce840 100755
--- a/test/trace_processor/diff_tests/stdlib/chrome/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/chrome/tests.py
@@ -149,6 +149,19 @@
         694052857814984,6063770000,"iteration-10",10,"208.0","96.2"
         """))
 
+  def test_speedometer_2_1_renderer_main_utid(self):
+    return DiffTestBlueprint(
+        trace=DataPath('speedometer_21.perfetto_trace.gz'),
+        query="""
+        INCLUDE PERFETTO MODULE chrome.speedometer;
+
+        SELECT chrome_speedometer_renderer_main_utid();
+        """,
+        out=Csv("""
+        "chrome_speedometer_renderer_main_utid()"
+        4
+        """))
+
   def test_speedometer_3_score(self):
     return DiffTestBlueprint(
         trace=DataPath('speedometer_3.perfetto_trace.gz'),
@@ -189,6 +202,19 @@
         372470568756,4301553000,"iteration-9",9,"96.9","10.3"
         """))
 
+  def test_speedometer_3_renderer_main_utid(self):
+    return DiffTestBlueprint(
+        trace=DataPath('speedometer_3.perfetto_trace.gz'),
+        query="""
+        INCLUDE PERFETTO MODULE chrome.speedometer;
+
+        SELECT chrome_speedometer_renderer_main_utid();
+        """,
+        out=Csv("""
+        "chrome_speedometer_renderer_main_utid()"
+        2
+        """))
+
   # CPU power ups
   def test_cpu_powerups(self):
     return DiffTestBlueprint(
@@ -203,3 +229,119 @@
         703,2
         708,2
         """))
+
+  def test_chrome_graphics_pipeline_surface_frame_steps(self):
+    return DiffTestBlueprint(
+        trace=DataPath('scroll_m131.pftrace'),
+        query="""
+        INCLUDE PERFETTO MODULE chrome.graphics_pipeline;
+
+        SELECT
+          id,
+          ts,
+          dur,
+          step,
+          surface_frame_trace_id,
+          utid
+        FROM chrome_graphics_pipeline_surface_frame_steps
+        ORDER BY ts
+        LIMIT 10;
+        """,
+        out=Csv("""
+        "id","ts","dur","step","surface_frame_trace_id","utid"
+        209,1292552020392633,142000,"STEP_ISSUE_BEGIN_FRAME",1407387768455321,6
+        210,1292552020907210,1264000,"STEP_RECEIVE_BEGIN_FRAME",1407387768455321,4
+        259,1292552026179210,1550000,"STEP_GENERATE_COMPOSITOR_FRAME",1407387768455321,4
+        264,1292552026586210,924000,"STEP_SUBMIT_COMPOSITOR_FRAME",1407387768455321,4
+        265,1292552027255633,791000,"STEP_RECEIVE_COMPOSITOR_FRAME",1407387768455321,6
+        268,1292552028200633,122000,"STEP_ISSUE_BEGIN_FRAME",4294967439,6
+        269,1292552028581257,1772000,"STEP_GENERATE_COMPOSITOR_FRAME",4294967439,1
+        276,1292552030185257,164000,"STEP_SUBMIT_COMPOSITOR_FRAME",4294967439,1
+        277,1292552030600633,195000,"STEP_RECEIVE_COMPOSITOR_FRAME",4294967439,6
+        302,1292552032277633,178000,"STEP_ISSUE_BEGIN_FRAME",1407387768455322,6
+        """))
+
+  def test_chrome_graphics_pipeline_display_frame_steps(self):
+    return DiffTestBlueprint(
+      trace=DataPath('scroll_m131.pftrace'),
+      query="""
+      INCLUDE PERFETTO MODULE chrome.graphics_pipeline;
+
+      SELECT
+        id,
+        ts,
+        dur,
+        step,
+        display_trace_id,
+        utid
+      FROM chrome_graphics_pipeline_display_frame_steps
+      ORDER BY ts
+      LIMIT 10;
+      """,
+      out=Csv("""
+      "id","ts","dur","step","display_trace_id","utid"
+      279,1292552030930633,1263000,"STEP_DRAW_AND_SWAP",65565,6
+      285,1292552031240633,143000,"STEP_SURFACE_AGGREGATION",65565,6
+      299,1292552032042633,68000,"STEP_SEND_BUFFER_SWAP",65565,6
+      319,1292552033751131,667000,"STEP_BUFFER_SWAP_POST_SUBMIT",65565,7
+      337,1292552036240633,2033000,"STEP_DRAW_AND_SWAP",65566,6
+      341,1292552036520633,873000,"STEP_SURFACE_AGGREGATION",65566,6
+      359,1292552038113633,75000,"STEP_SEND_BUFFER_SWAP",65566,6
+      376,1292552039773131,458000,"STEP_BUFFER_SWAP_POST_SUBMIT",65566,7
+      394,1292552043191131,48000,"STEP_FINISH_BUFFER_SWAP",65565,7
+      397,1292552043253633,75000,"STEP_SWAP_BUFFERS_ACK",65565,6
+      """))
+
+  def test_chrome_graphics_pipeline_aggregated_frames(self):
+    return DiffTestBlueprint(
+      trace=DataPath('scroll_m131.pftrace'),
+      query="""
+      INCLUDE PERFETTO MODULE chrome.graphics_pipeline;
+
+      SELECT
+        display_trace_id,
+        surface_frame_trace_id
+      FROM chrome_graphics_pipeline_aggregated_frames
+      ORDER BY display_trace_id, surface_frame_trace_id
+      LIMIT 10;
+      """,
+      out=Csv("""
+      "display_trace_id","surface_frame_trace_id"
+      65565,4294967439
+      65565,1407387768455321
+      65566,4294967440
+      65566,1407387768455322
+      65567,4294967441
+      65567,1407387768455323
+      65568,4294967442
+      65568,1407387768455324
+      65569,4294967443
+      65569,1407387768455325
+      """))
+
+  def test_chrome_graphics_pipeline_inputs_to_surface_frames(self):
+    return DiffTestBlueprint(
+      trace=DataPath('scroll_m131.pftrace'),
+      query="""
+      INCLUDE PERFETTO MODULE chrome.graphics_pipeline;
+
+      SELECT
+        surface_frame_trace_id,
+        latency_id
+      FROM chrome_graphics_pipeline_inputs_to_surface_frames
+      ORDER BY surface_frame_trace_id, latency_id
+      LIMIT 10;
+      """,
+      out=Csv("""
+      "surface_frame_trace_id","latency_id"
+      1407387768455321,-2143831735395279174
+      1407387768455321,-2143831735395279169
+      1407387768455322,-2143831735395279191
+      1407387768455323,-2143831735395279278
+      1407387768455324,-2143831735395279270
+      1407387768455325,-2143831735395279284
+      1407387768455326,-2143831735395279244
+      1407387768455327,-2143831735395279233
+      1407387768455328,-2143831735395279258
+      1407387768455329,-2143831735395279255
+      """))
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 fcb98a7..083354a 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
@@ -307,3 +307,172 @@
         "BrowserMainToRendererCompositor","[NULL]",22.250000,50230,"GESTURE_SCROLL_UPDATE"
         "SubmitCompositorFrameToPresentationCompositorFrame","BufferReadyToLatch",22.267000,50517,"GESTURE_SCROLL_UPDATE"
         """))
+
+  def test_chrome_event_latencies(self):
+    return DiffTestBlueprint(
+        trace=DataPath('chrome_input_with_frame_view_new.pftrace'),
+        query="""
+        INCLUDE PERFETTO MODULE chrome.event_latency;
+
+        SELECT
+          id,
+          name,
+          ts,
+          dur,
+          scroll_update_id,
+          is_presented,
+          event_type,
+          track_id,
+          vsync_interval_ms,
+          is_janky_scrolled_frame,
+          buffer_available_timestamp,
+          buffer_ready_timestamp,
+          latch_timestamp,
+          swap_end_timestamp,
+          presentation_timestamp
+        FROM chrome_event_latencies
+        WHERE
+          (
+            event_type = 'GESTURE_SCROLL_UPDATE'
+            OR event_type = 'INERTIAL_GESTURE_SCROLL_UPDATE')
+          AND is_presented
+        ORDER BY id
+        LIMIT 10;
+        """,
+        out=Csv("""
+        "id","name","ts","dur","scroll_update_id","is_presented","event_type","track_id","vsync_interval_ms","is_janky_scrolled_frame","buffer_available_timestamp","buffer_ready_timestamp","latch_timestamp","swap_end_timestamp","presentation_timestamp"
+        69,"EventLatency",4488833086777189,49497000,10,1,"GESTURE_SCROLL_UPDATE",14,11.111000,0,4488833114547189,4488833114874189,4488833119872189,4488833126765189,4488833136274189
+        431,"EventLatency",4488833114107189,33292000,14,1,"INERTIAL_GESTURE_SCROLL_UPDATE",27,11.111000,0,"[NULL]",4488833122361189,4488833137159189,4488833138924189,4488833147399189
+        480,"EventLatency",4488833125213189,33267000,15,1,"INERTIAL_GESTURE_SCROLL_UPDATE",29,11.111000,0,4488833131905189,4488833132275189,4488833148524189,4488833150809189,4488833158480189
+        531,"EventLatency",4488833142387189,27234000,16,1,"INERTIAL_GESTURE_SCROLL_UPDATE",32,11.111000,0,4488833147322189,4488833147657189,4488833159447189,4488833161654189,4488833169621189
+        581,"EventLatency",4488833153584189,27225000,17,1,"INERTIAL_GESTURE_SCROLL_UPDATE",34,11.111000,0,4488833158111189,4488833158333189,4488833170433189,4488833171562189,4488833180809189
+        630,"EventLatency",4488833164707189,27227000,18,1,"INERTIAL_GESTURE_SCROLL_UPDATE",36,11.111000,0,4488833169215189,4488833169529189,4488833181700189,4488833183880189,4488833191934189
+        679,"EventLatency",4488833175814189,27113000,19,1,"INERTIAL_GESTURE_SCROLL_UPDATE",38,11.111000,0,4488833180589189,4488833180876189,4488833192722189,4488833194160189,4488833202927189
+        728,"EventLatency",4488833186929189,38217000,20,1,"INERTIAL_GESTURE_SCROLL_UPDATE",40,11.111000,1,4488833201398189,4488833201459189,4488833215357189,4488833217727189,4488833225146189
+        772,"EventLatency",4488833198068189,38185000,21,1,"INERTIAL_GESTURE_SCROLL_UPDATE",42,11.111000,0,4488833211744189,4488833212097189,4488833226028189,4488833227246189,4488833236253189
+        819,"EventLatency",4488833209202189,38170000,22,1,"INERTIAL_GESTURE_SCROLL_UPDATE",43,11.111000,0,4488833223115189,4488833223308189,4488833237115189,4488833238196189,4488833247372189
+        """))
+
+  # A trace from M131 (ToT as of adding this test) has the necessary
+  # events/arguments.
+  def test_chrome_input_pipeline_steps(self):
+        return DiffTestBlueprint(
+        trace=DataPath('scroll_m131.pftrace'),
+        query="""
+        INCLUDE PERFETTO MODULE chrome.input;
+
+        SELECT latency_id,
+          input_type,
+          GROUP_CONCAT(step) AS steps
+        FROM chrome_input_pipeline_steps
+        GROUP BY latency_id
+        ORDER by input_type
+        LIMIT 20
+        """,
+        out=Csv("""
+        "latency_id","input_type","steps"
+        -2143831735395279846,"GESTURE_FLING_CANCEL_EVENT","STEP_SEND_INPUT_EVENT_UI"
+        -2143831735395279570,"GESTURE_FLING_CANCEL_EVENT","STEP_SEND_INPUT_EVENT_UI"
+        -2143831735395279037,"GESTURE_FLING_CANCEL_EVENT","STEP_SEND_INPUT_EVENT_UI"
+        -2143831735395280234,"GESTURE_FLING_START_EVENT","STEP_SEND_INPUT_EVENT_UI"
+        -2143831735395279756,"GESTURE_FLING_START_EVENT","STEP_SEND_INPUT_EVENT_UI"
+        -2143831735395279516,"GESTURE_FLING_START_EVENT","STEP_SEND_INPUT_EVENT_UI"
+        -2143831735395278975,"GESTURE_FLING_START_EVENT","STEP_SEND_INPUT_EVENT_UI"
+        -2143831735395280167,"GESTURE_SCROLL_BEGIN_EVENT","STEP_SEND_INPUT_EVENT_UI,STEP_HANDLE_INPUT_EVENT_IMPL,STEP_DID_HANDLE_INPUT_AND_OVERSCROLL,STEP_GESTURE_EVENT_HANDLED"
+        -2143831735395279816,"GESTURE_SCROLL_BEGIN_EVENT","STEP_SEND_INPUT_EVENT_UI,STEP_HANDLE_INPUT_EVENT_IMPL,STEP_DID_HANDLE_INPUT_AND_OVERSCROLL,STEP_GESTURE_EVENT_HANDLED"
+        -2143831735395279175,"GESTURE_SCROLL_BEGIN_EVENT","STEP_SEND_INPUT_EVENT_UI,STEP_HANDLE_INPUT_EVENT_IMPL,STEP_DID_HANDLE_INPUT_AND_OVERSCROLL,STEP_GESTURE_EVENT_HANDLED"
+        -2143831735395279004,"GESTURE_SCROLL_BEGIN_EVENT","STEP_SEND_INPUT_EVENT_UI,STEP_HANDLE_INPUT_EVENT_IMPL,STEP_DID_HANDLE_INPUT_AND_OVERSCROLL,STEP_GESTURE_EVENT_HANDLED"
+        -2143831735395280198,"GESTURE_SCROLL_END_EVENT","STEP_SEND_INPUT_EVENT_UI,STEP_GESTURE_EVENT_HANDLED,STEP_HANDLE_INPUT_EVENT_IMPL,STEP_DID_HANDLE_INPUT_AND_OVERSCROLL"
+        -2143831735395279762,"GESTURE_SCROLL_END_EVENT","STEP_SEND_INPUT_EVENT_UI,STEP_GESTURE_EVENT_HANDLED,STEP_HANDLE_INPUT_EVENT_IMPL,STEP_DID_HANDLE_INPUT_AND_OVERSCROLL"
+        -2143831735395279584,"GESTURE_SCROLL_END_EVENT","STEP_SEND_INPUT_EVENT_UI,STEP_GESTURE_EVENT_HANDLED,STEP_HANDLE_INPUT_EVENT_IMPL,STEP_DID_HANDLE_INPUT_AND_OVERSCROLL"
+        -2143831735395279038,"GESTURE_SCROLL_END_EVENT","STEP_SEND_INPUT_EVENT_UI,STEP_GESTURE_EVENT_HANDLED,STEP_HANDLE_INPUT_EVENT_IMPL,STEP_DID_HANDLE_INPUT_AND_OVERSCROLL"
+        -2143831735395280256,"GESTURE_SCROLL_UPDATE_EVENT","STEP_SEND_INPUT_EVENT_UI,STEP_HANDLE_INPUT_EVENT_IMPL,STEP_DID_HANDLE_INPUT_AND_OVERSCROLL,STEP_GESTURE_EVENT_HANDLED"
+        -2143831735395280254,"GESTURE_SCROLL_UPDATE_EVENT","STEP_SEND_INPUT_EVENT_UI,STEP_HANDLE_INPUT_EVENT_IMPL,STEP_DID_HANDLE_INPUT_AND_OVERSCROLL,STEP_GESTURE_EVENT_HANDLED"
+        -2143831735395280250,"GESTURE_SCROLL_UPDATE_EVENT","STEP_SEND_INPUT_EVENT_UI,STEP_HANDLE_INPUT_EVENT_IMPL,STEP_DID_HANDLE_INPUT_AND_OVERSCROLL,STEP_GESTURE_EVENT_HANDLED"
+        -2143831735395280248,"GESTURE_SCROLL_UPDATE_EVENT","STEP_SEND_INPUT_EVENT_UI,STEP_HANDLE_INPUT_EVENT_IMPL,STEP_DID_HANDLE_INPUT_AND_OVERSCROLL,STEP_GESTURE_EVENT_HANDLED"
+        -2143831735395280246,"GESTURE_SCROLL_UPDATE_EVENT","STEP_SEND_INPUT_EVENT_UI,STEP_HANDLE_INPUT_EVENT_IMPL,STEP_DID_HANDLE_INPUT_AND_OVERSCROLL,STEP_GESTURE_EVENT_HANDLED"
+        """))
+
+  def test_task_start_time(self):
+    return DiffTestBlueprint(
+        trace=DataPath('scroll_m131.pftrace'),
+        query="""
+        INCLUDE PERFETTO MODULE chrome.input;
+
+        SELECT
+          latency_id,
+          step,
+          task_start_time_ts
+        FROM chrome_input_pipeline_steps
+        ORDER BY latency_id
+        LIMIT 10;
+        """,
+        # STEP_SEND_INPUT_EVENT_UI does not run in a task,
+        # so its task_start_time_ts will be NULL.
+        out=Csv("""
+        "latency_id","step","task_start_time_ts"
+        -2143831735395280256,"STEP_SEND_INPUT_EVENT_UI","[NULL]"
+        -2143831735395280256,"STEP_HANDLE_INPUT_EVENT_IMPL",1292554143003210
+        -2143831735395280256,"STEP_DID_HANDLE_INPUT_AND_OVERSCROLL",1292554153539210
+        -2143831735395280256,"STEP_GESTURE_EVENT_HANDLED",1292554154651257
+        -2143831735395280254,"STEP_SEND_INPUT_EVENT_UI","[NULL]"
+        -2143831735395280254,"STEP_HANDLE_INPUT_EVENT_IMPL",1292554155188210
+        -2143831735395280254,"STEP_DID_HANDLE_INPUT_AND_OVERSCROLL",1292554164359210
+        -2143831735395280254,"STEP_GESTURE_EVENT_HANDLED",1292554165141257
+        -2143831735395280250,"STEP_SEND_INPUT_EVENT_UI","[NULL]"
+        -2143831735395280250,"STEP_HANDLE_INPUT_EVENT_IMPL",1292554131865210
+        """))
+
+  def test_chrome_coalesced_inputs(self):
+        return DiffTestBlueprint(
+        trace=DataPath('scroll_m131.pftrace'),
+        query="""
+        INCLUDE PERFETTO MODULE chrome.input;
+
+        SELECT
+          coalesced_latency_id,
+          presented_latency_id
+        FROM chrome_coalesced_inputs
+        ORDER BY coalesced_latency_id
+        LIMIT 10
+        """,
+        out=Csv("""
+        "coalesced_latency_id","presented_latency_id"
+        -2143831735395280239,-2143831735395280239
+        -2143831735395280183,-2143831735395280179
+        -2143831735395280179,-2143831735395280179
+        -2143831735395280166,-2143831735395280166
+        -2143831735395280158,-2143831735395280153
+        -2143831735395280153,-2143831735395280153
+        -2143831735395280150,-2143831735395280146
+        -2143831735395280146,-2143831735395280146
+        -2143831735395280144,-2143831735395280139
+        -2143831735395280139,-2143831735395280139
+        """))
+
+  def test_chrome_touch_move_to_scroll_update(self):
+        return DiffTestBlueprint(
+        trace=DataPath('scroll_m131.pftrace'),
+        query="""
+        INCLUDE PERFETTO MODULE chrome.input;
+
+        SELECT
+          touch_move_latency_id,
+          scroll_update_latency_id
+        FROM chrome_touch_move_to_scroll_update
+        ORDER BY touch_move_latency_id
+        LIMIT 10
+        """,
+        out=Csv("""
+        "touch_move_latency_id","scroll_update_latency_id"
+        -2143831735395280236,-2143831735395280239
+        -2143831735395280189,-2143831735395280179
+        -2143831735395280181,-2143831735395280139
+        -2143831735395280177,-2143831735395280183
+        -2143831735395280163,-2143831735395280166
+        -2143831735395280160,-2143831735395280158
+        -2143831735395280155,-2143831735395280153
+        -2143831735395280152,-2143831735395280150
+        -2143831735395280148,-2143831735395280146
+        -2143831735395280142,-2143831735395280132
+        """))
diff --git a/test/trace_processor/diff_tests/stdlib/common/tests.py b/test/trace_processor/diff_tests/stdlib/common/tests.py
deleted file mode 100644
index d103320..0000000
--- a/test/trace_processor/diff_tests/stdlib/common/tests.py
+++ /dev/null
@@ -1,134 +0,0 @@
-#!/usr/bin/env python3
-# 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 a
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from python.generators.diff_tests.testing import Path, DataPath, Metric
-from python.generators.diff_tests.testing import Csv, Json, TextProto
-from python.generators.diff_tests.testing import DiffTestBlueprint
-from python.generators.diff_tests.testing import TestSuite
-
-
-class StdlibCommon(TestSuite):
-
-  def test_spans_overlapping_dur_intersect_edge(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(0, 2, 1, 2) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        1
-        """))
-
-  def test_spans_overlapping_dur_intersect_edge_reversed(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(1, 2, 0, 2) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        1
-        """))
-
-  def test_spans_overlapping_dur_intersect_all(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(0, 3, 1, 1) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        1
-        """))
-
-  def test_spans_overlapping_dur_intersect_all_reversed(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(1, 1, 0, 3) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        1
-        """))
-
-  def test_spans_overlapping_dur_no_intersect(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(0, 1, 2, 1) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        0
-        """))
-
-  def test_spans_overlapping_dur_no_intersect_reversed(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(2, 1, 0, 1) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        0
-        """))
-
-  def test_spans_overlapping_dur_negative_dur(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(0, -1, 0, 1) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        0
-        """))
-
-  def test_spans_overlapping_dur_negative_dur_reversed(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(0, 1, 0, -1) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        0
-        """))
diff --git a/test/trace_processor/diff_tests/stdlib/counters/tests.py b/test/trace_processor/diff_tests/stdlib/counters/tests.py
index d3ade3e..1bf51b4 100644
--- a/test/trace_processor/diff_tests/stdlib/counters/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/counters/tests.py
@@ -27,24 +27,20 @@
         query="""
         INCLUDE PERFETTO MODULE counters.intervals;
 
-        WITH
-          foo AS (
-            SELECT 0 AS id, 0 AS ts, 10 AS value, 1 AS track_id
-            UNION ALL
-            SELECT 1 AS id, 0 AS ts, 10 AS value, 2 AS track_id
-            UNION ALL
-            SELECT 2 AS id, 10 AS ts, 10 AS value, 1 AS track_id
-            UNION ALL
-            SELECT 3 AS id, 10 AS ts, 20 AS value, 2 AS track_id
-            UNION ALL
-            SELECT 4 AS id, 20 AS ts, 30 AS value, 1 AS track_id
+          WITH data(id, ts, value, track_id) AS (
+            VALUES
+            (0, 0, 10, 1),
+            (1, 0, 10, 2),
+            (2, 10, 10, 1),
+            (3, 10, 20, 2),
+            (4, 20, 30, 1)
           )
-        SELECT * FROM counter_leading_intervals !(foo);
+          SELECT * FROM counter_leading_intervals!(data);
         """,
         out=Csv("""
         "id","ts","dur","track_id","value","next_value","delta_value"
-        0,0,20,1,10,30,"[NULL]"
-        4,20,19980,1,30,"[NULL]",20
-        1,0,10,2,10,20,"[NULL]"
-        3,10,19990,2,20,"[NULL]",10
+        0,0,20,1,10.000000,30.000000,"[NULL]"
+        4,20,19980,1,30.000000,"[NULL]",20.000000
+        1,0,10,2,10.000000,20.000000,"[NULL]"
+        3,10,19990,2,20.000000,"[NULL]",10.000000
         """))
diff --git a/test/trace_processor/diff_tests/stdlib/intervals/tests.py b/test/trace_processor/diff_tests/stdlib/intervals/tests.py
index 76810bf..2712599 100644
--- a/test/trace_processor/diff_tests/stdlib/intervals/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/intervals/tests.py
@@ -119,290 +119,71 @@
         9,1,1,1
         """))
 
-  def test_simple_ii_operator(self):
+  def test_intervals_flatten_by_intersection(self):
     return DiffTestBlueprint(
         trace=TextProto(""),
         query="""
+        INCLUDE PERFETTO MODULE intervals.overlap;
 
-        CREATE PERFETTO TABLE A AS
-          WITH data(id, ts, ts_end, c0, c1) AS (
-            VALUES
-            (0, 1, 7, 10, 3)
-          )
-          SELECT * FROM data;
-
-        CREATE PERFETTO TABLE B AS
-          WITH data(id, ts, ts_end, c0, c2) AS (
-            VALUES
-            (0, 0, 2, 10, 100),
-            (1, 3, 5, 10, 200),
-            (2, 6, 8, 20, 300)
-          )
-          SELECT * FROM data;
-
-        SELECT a.id AS a_id, b.id AS b_id
-        FROM __intrinsic_ii_with_interval_tree('A', 'c0, c1') a
-        JOIN __intrinsic_ii_with_interval_tree('B', 'c0, c2') b
-        WHERE a.ts < b.ts_end AND a.ts_end > b.ts
-        """,
-        out=Csv("""
-        "a_id","b_id"
-        0,1
-        0,0
-        0,2
-        """))
-
-  def test_ii_operator_big(self):
-    return DiffTestBlueprint(
-        trace=DataPath('example_android_trace_30s.pb'),
-        query="""
-        CREATE PERFETTO TABLE big_foo AS
-        SELECT
-          id,
-          ts,
-          ts+dur AS ts_end
-        FROM sched
-        WHERE dur != -1
-        ORDER BY ts;
-
-        CREATE PERFETTO TABLE small_foo AS
-        SELECT
-        id * 10 AS id,
-        ts + 1000 AS ts,
-        ts_end + 1000 AS ts_end
-        FROM big_foo
-        LIMIT 10
-        OFFSET 5;
-
-        CREATE PERFETTO TABLE res AS
-        SELECT a.id AS a_id, b.id AS b_id
-        FROM __intrinsic_ii_with_interval_tree('small_foo', '') a
-        JOIN __intrinsic_ii_with_interval_tree('big_foo', '') b
-        WHERE a.ts < b.ts_end AND a.ts_end > b.ts;
-
-        SELECT * FROM res
-        ORDER BY a_id, b_id
-        LIMIT 10;
-        """,
-        out=Csv("""
-        "a_id","b_id"
-        50,1
-        50,5
-        50,6
-        60,1
-        60,6
-        60,7
-        60,8
-        70,1
-        70,6
-        70,7
-        """))
-
-  def test_ii_with_ii_operator(self):
-    return DiffTestBlueprint(
-        trace=DataPath('example_android_trace_30s.pb'),
-        query="""
-        INCLUDE PERFETTO MODULE intervals.intersect;
-
-        CREATE PERFETTO TABLE big_foo AS
-        SELECT
-          ts,
-          ts + dur as ts_end,
-          id * 10 AS id
-        FROM sched
-        WHERE utid == 1 AND dur > 0;
-
-        CREATE PERFETTO TABLE small_foo AS
-        SELECT
-        ts + 1000 AS ts,
-        ts + dur + 1000 AS ts_end,
-        id
-        FROM sched
-        WHERE utid == 1 AND dur > 0;
-
-        CREATE PERFETTO TABLE small_foo_for_ii AS
-        SELECT id, ts, ts_end - ts AS dur
-        FROM small_foo;
-
-        CREATE PERFETTO TABLE big_foo_for_ii AS
-        SELECT id, ts, ts_end - ts AS dur
-        FROM big_foo;
-
-        CREATE PERFETTO TABLE both AS
-        SELECT
-          id_0,
-          id_1,
-          cat,
-          count() AS c,
-          MAX(ts) AS max_ts, MAX(dur) AS max_dur
-        FROM (
-          SELECT a.id AS id_0, b.id AS id_1, 0 AS ts, 0 AS dur, "it" AS cat
-          FROM __intrinsic_ii_with_interval_tree('big_foo', '') a
-          JOIN __intrinsic_ii_with_interval_tree('small_foo', '') b
-          WHERE a.ts < b.ts_end AND a.ts_end > b.ts
-          UNION
-          SELECT id_0, id_1, ts, dur, "ii" AS cat
-          FROM _interval_intersect!((big_foo_for_ii, small_foo_for_ii), ())
-          WHERE dur != 0
+        CREATE PERFETTO TABLE foo AS
+        WITH roots_data (id, ts, dur, utid) AS (
+          VALUES
+            (0, 0, 9, 0),
+            (0, 0, 9, 1),
+            (1, 9, 1, 2)
+        ), children_data (id, parent_id, ts, dur, utid) AS (
+          VALUES
+            (2, 0, 1, 3, 0),
+            (3, 0, 5, 1, 0),
+            (4, 0, 6, 1, 0),
+            (5, 0, 7, 0, 0),
+            (6, 0, 7, 1, 0),
+            (7, 2, 2, 1, 0)
         )
-          GROUP BY id_0, id_1;
+        SELECT *
+        FROM _intervals_merge_root_and_children_by_intersection!(roots_data, children_data, utid);
 
-        SELECT
-          SUM(c) FILTER (WHERE c == 2) AS good,
-          SUM(c) FILTER (WHERE c != 2) AS bad
-        FROM both;
+        SELECT ts, dur, id, root_id FROM _intervals_flatten!(foo) ORDER BY ts;
         """,
         out=Csv("""
-          "good","bad"
-          314,"[NULL]"
+        "ts","dur","id","root_id"
+        0,1,0,0
+        1,1,2,0
+        2,1,7,0
+        3,1,2,0
+        4,1,0,0
+        5,1,3,0
+        6,1,4,0
+        7,1,6,0
+        8,1,0,0
         """))
 
-  def test_ii_operator_partitioned_big(self):
+  def test_intervals_flatten_by_intersection_no_matching_key(self):
     return DiffTestBlueprint(
-        trace=DataPath('example_android_trace_30s.pb'),
+        trace=TextProto(""),
         query="""
-        INCLUDE PERFETTO MODULE intervals.intersect;
+        INCLUDE PERFETTO MODULE intervals.overlap;
 
-        CREATE PERFETTO TABLE big_foo AS
-        SELECT
-          ts,
-          ts + dur as ts_end,
-          id * 10 AS id,
-          cpu AS c0
-        FROM sched
-        WHERE dur != -1;
-
-        CREATE PERFETTO TABLE small_foo AS
-        SELECT
-          ts + 1000 AS ts,
-          ts + dur + 1000 AS ts_end,
-          id,
-          cpu AS c0
-        FROM sched
-        WHERE dur != -1;
-
-        CREATE PERFETTO TABLE res AS
-        SELECT a.id AS a_id, b.id AS b_id
-        FROM __intrinsic_ii_with_interval_tree('small_foo', 'c0') a
-        JOIN __intrinsic_ii_with_interval_tree('big_foo', 'c0') b
-        USING (c0)
-        WHERE a.ts < b.ts_end AND a.ts_end > b.ts;
-
-        SELECT * FROM res
-        ORDER BY a_id, b_id
-        LIMIT 10;
-        """,
-        out=Csv("""
-        "a_id","b_id"
-        0,0
-        0,10
-        1,10
-        1,430
-        2,20
-        2,30
-        3,30
-        3,40
-        4,40
-        4,50
-        """))
-
-  def test_compare_ii_operator_with_span_join(self):
-    return DiffTestBlueprint(
-        trace=DataPath('example_android_trace_30s.pb'),
-        query="""
-        INCLUDE PERFETTO MODULE intervals.intersect;
-
-        CREATE PERFETTO TABLE big_foo AS
-        SELECT
-          ts,
-          ts + dur as ts_end,
-          id * 10 AS id,
-          cpu AS c0
-        FROM sched
-        WHERE dur != -1;
-
-        CREATE PERFETTO TABLE small_foo AS
-        SELECT
-          ts + 1000 AS ts,
-          ts + dur + 1000 AS ts_end,
-          id,
-          cpu AS c0
-        FROM sched
-        WHERE dur != -1;
-
-        CREATE PERFETTO TABLE small_foo_for_sj AS
-        SELECT
-          id AS small_id,
-          ts,
-          ts_end - ts AS dur,
-          c0
-        FROM small_foo
-        WHERE dur != 0;
-
-        CREATE PERFETTO TABLE big_foo_for_sj AS
-        SELECT
-          id AS big_id,
-          ts,
-          ts_end - ts AS dur,
-          c0
-        FROM big_foo
-        WHERE dur != 0;
-
-        CREATE VIRTUAL TABLE sj_res
-        USING SPAN_JOIN(
-          small_foo_for_sj PARTITIONED c0,
-          big_foo_for_sj PARTITIONED c0);
-
-        CREATE PERFETTO TABLE both AS
-        SELECT
-          id_0,
-          id_1,
-          cat,
-          count() AS c,
-          MAX(ts) AS max_ts, MAX(dur) AS max_dur
-        FROM (
-          SELECT a.id AS id_0, b.id AS id_1, 0 AS ts, 0 AS dur, "it" AS cat
-          FROM __intrinsic_ii_with_interval_tree('big_foo', 'c0') a
-          JOIN __intrinsic_ii_with_interval_tree('small_foo', 'c0') b
-          USING (c0)
-          WHERE a.ts < b.ts_end AND a.ts_end > b.ts
-          UNION
-          SELECT big_id AS id_0, small_id AS id_1, ts, dur, "sj" AS cat FROM sj_res
+        CREATE PERFETTO TABLE foo AS
+        WITH roots_data (id, ts, dur, utid) AS (
+          VALUES
+            (0, 0, 9, 1),
+            (0, 0, 9, 2),
+            (1, 9, 1, 3)
+        ), children_data (id, parent_id, ts, dur, utid) AS (
+          VALUES
+            (2, 0, 1, 3, 0),
+            (3, 0, 5, 1, 0),
+            (4, 0, 6, 1, 0),
+            (5, 0, 7, 0, 0),
+            (6, 0, 7, 1, 0),
+            (7, 2, 2, 1, 0)
         )
-          GROUP BY id_0, id_1;
+        SELECT *
+        FROM _intervals_merge_root_and_children_by_intersection!(roots_data, children_data, utid);
 
-        SELECT
-          SUM(c) FILTER (WHERE c == 2) AS good,
-          SUM(c) FILTER (WHERE c != 2) AS bad
-        FROM both;
+        SELECT ts, dur, id, root_id FROM _intervals_flatten!(foo) ORDER BY ts;
         """,
         out=Csv("""
-          "good","bad"
-          1538288,"[NULL]"
-        """))
-
-  def test_ii_operator_wrong_partition(self):
-    return DiffTestBlueprint(
-        trace=TextProto(''),
-        query="""
-        CREATE PERFETTO TABLE A
-        AS
-        WITH x(id, ts, ts_end, c0) AS (VALUES(1, 1, 2, 1), (2, 3, 4, 2))
-        SELECT * FROM x;
-
-        CREATE PERFETTO TABLE B
-        AS
-        WITH x(id, ts, ts_end, c0) AS (VALUES(1, 5, 6, 3))
-        SELECT * FROM x;
-
-        SELECT
-        a.id AS a_id,
-        b.id AS b_id
-        FROM __intrinsic_ii_with_interval_tree('A', 'c0') a
-        JOIN __intrinsic_ii_with_interval_tree('B', 'c0') b
-        USING (c0)
-        WHERE a.ts < b.ts_end AND a.ts_end > b.ts;
-        """,
-        out=Csv("""
-        "a_id","b_id"
+        "ts","dur","id","root_id"
         """))
diff --git a/test/trace_processor/diff_tests/stdlib/linux/cpu.py b/test/trace_processor/diff_tests/stdlib/linux/cpu.py
index 6dfe8bd..1c38b5b 100644
--- a/test/trace_processor/diff_tests/stdlib/linux/cpu.py
+++ b/test/trace_processor/diff_tests/stdlib/linux/cpu.py
@@ -122,6 +122,46 @@
         92000000000,0.000009,0.000071
         """))
 
+  def test_cpu_cycles(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_postboot_unlock.pftrace'),
+        query=("""
+             INCLUDE PERFETTO MODULE linux.cpu.utilization.system;
+
+             SELECT
+              millicycles,
+              megacycles,
+              runtime,
+              min_freq,
+              max_freq,
+              avg_freq
+             FROM cpu_cycles;
+             """),
+        out=Csv("""
+        "millicycles","megacycles","runtime","min_freq","max_freq","avg_freq"
+        36093928491870,36093,17131594098,500000,2850000,2112132
+            """))
+
+  def test_cpu_cycles_in_interval(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_cpu_eos.pb'),
+        query=("""
+             INCLUDE PERFETTO MODULE linux.cpu.utilization.system;
+
+             SELECT
+              millicycles,
+              megacycles,
+              runtime,
+              min_freq,
+              max_freq,
+              avg_freq
+             FROM cpu_cycles_in_interval(TRACE_START(), TRACE_DUR() / 10);
+             """),
+        out=Csv("""
+          "millicycles","megacycles","runtime","min_freq","max_freq","avg_freq"
+          31636287288,31,76193077,614400,1708800,415998
+            """))
+
   def test_cpu_cycles_per_cpu(self):
     return DiffTestBlueprint(
         trace=DataPath('android_postboot_unlock.pftrace'),
@@ -129,7 +169,13 @@
              INCLUDE PERFETTO MODULE linux.cpu.utilization.system;
 
              SELECT
-               *
+              cpu,
+              millicycles,
+              megacycles,
+              runtime,
+              min_freq,
+              max_freq,
+              avg_freq
              FROM cpu_cycles_per_cpu;
              """),
         out=Csv("""
@@ -144,6 +190,30 @@
           7,4594701918170,4594,1719270548,500000,2850000,2685290
             """))
 
+  def test_cpu_cycles_per_cpu_in_interval(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_cpu_eos.pb'),
+        query=("""
+             INCLUDE PERFETTO MODULE linux.cpu.utilization.system;
+
+             SELECT
+              cpu,
+              millicycles,
+              megacycles,
+              runtime,
+              min_freq,
+              max_freq,
+              avg_freq
+             FROM cpu_cycles_per_cpu_in_interval(TRACE_START(), TRACE_DUR() / 10);
+             """),
+        out=Csv("""
+          "cpu","millicycles","megacycles","runtime","min_freq","max_freq","avg_freq"
+          0,27811901835,27,50296201,614400,1708800,554220
+          1,2893791427,2,4709947,614400,614400,615831
+          2,177750720,0,3718178,864000,864000,47885
+          3,752843306,0,17468751,614400,864000,43128
+            """))
+
   def test_cpu_cycles_per_thread(self):
     return DiffTestBlueprint(
         trace=DataPath('android_cpu_eos.pb'),
@@ -151,17 +221,54 @@
              INCLUDE PERFETTO MODULE linux.cpu.utilization.thread;
 
              SELECT
-               AVG(millicycles) AS millicycles,
-               AVG(megacycles) AS megacycles,
-               AVG(runtime) AS runtime,
-               AVG(min_freq) AS min_freq,
-               AVG(max_freq) AS max_freq,
-               AVG(avg_freq) AS avg_freq
-             FROM cpu_cycles_per_thread;
+              utid,
+              millicycles,
+              megacycles,
+              runtime,
+              min_freq,
+              max_freq,
+              avg_freq
+             FROM cpu_cycles_per_thread
+             WHERE utid < 10
              """),
         out=Csv("""
-            "millicycles","megacycles","runtime","min_freq","max_freq","avg_freq"
-            25048302186.035053,24.624742,16080173.697531,1402708.453608,1648468.453608,1582627.707216
+        "utid","millicycles","megacycles","runtime","min_freq","max_freq","avg_freq"
+        1,39042295612,39,28747861,614400,1708800,1359695
+        2,286312857,0,167552,1708800,1708800,1714448
+        8,124651656403,124,99592232,614400,1708800,1255974
+            """))
+
+  def test_cpu_cycles_per_thread_in_interval(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_cpu_eos.pb'),
+        query=("""
+             INCLUDE PERFETTO MODULE linux.cpu.utilization.thread;
+
+             SELECT
+              utid,
+              millicycles,
+              megacycles,
+              runtime,
+              min_freq,
+              max_freq,
+              avg_freq
+             FROM cpu_cycles_per_thread_in_interval(TRACE_START(), TRACE_DUR() / 10)
+             WHERE utid < 100
+             """),
+        out=Csv("""
+            "utid","millicycles","megacycles","runtime","min_freq","max_freq","avg_freq"
+            1,1226879384,1,1996874,614400,614400,614669
+            14,1247778191,1,2446930,614400,614400,513911
+            15,1407232193,1,2384063,614400,614400,593768
+            16,505278870,0,1142238,614400,614400,444396
+            30,29888102,0,48646,614400,614400,622668
+            37,"[NULL]","[NULL]",222814,"[NULL]","[NULL]","[NULL]"
+            38,"[NULL]","[NULL]",2915520,"[NULL]","[NULL]","[NULL]"
+            45,"[NULL]","[NULL]",2744688,"[NULL]","[NULL]","[NULL]"
+            54,"[NULL]","[NULL]",8614114,"[NULL]","[NULL]","[NULL]"
+            61,151616101,0,246771,614400,614400,618841
+            62,58740000,0,8307552,1708800,1708800,7071
+            92,243675648,0,962397,864000,864000,255157
             """))
 
   def test_cpu_cycles_per_process(self):
@@ -171,17 +278,104 @@
              INCLUDE PERFETTO MODULE linux.cpu.utilization.process;
 
              SELECT
-               AVG(millicycles) AS millicycles,
-               AVG(megacycles) AS megacycles,
-               AVG(runtime) AS runtime,
-               AVG(min_freq) AS min_freq,
-               AVG(max_freq) AS max_freq,
-               AVG(avg_freq) AS avg_freq
-             FROM cpu_cycles_per_process;
+              upid,
+              millicycles,
+              megacycles,
+              runtime,
+              min_freq,
+              max_freq,
+              avg_freq
+             FROM cpu_cycles_per_process
+             WHERE upid < 10
              """),
         out=Csv("""
-            "millicycles","megacycles","runtime","min_freq","max_freq","avg_freq"
-            83208401098.424652,82.753425,53163023.244898,1189742.465753,1683945.205479,1534667.547945
+        "upid","millicycles","megacycles","runtime","min_freq","max_freq","avg_freq"
+        1,79550724630,79,56977346,614400,1708800,1398005
+        2,286312857,0,167552,1708800,1708800,1714448
+        8,124651656403,124,99592232,614400,1708800,1255974
+            """))
+
+  def test_cpu_cycles_per_process_in_interval(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_cpu_eos.pb'),
+        query=("""
+             INCLUDE PERFETTO MODULE linux.cpu.utilization.process;
+
+             SELECT
+              upid,
+              millicycles,
+              megacycles,
+              runtime,
+              min_freq,
+              max_freq,
+              avg_freq
+             FROM cpu_cycles_per_process_in_interval(TRACE_START(), TRACE_DUR() / 10)
+             WHERE upid < 30;
+             """),
+        out=Csv("""
+          "upid","millicycles","megacycles","runtime","min_freq","max_freq","avg_freq"
+          1,2163648305,2,3521563,614400,614400,614672
+          14,1247778191,1,2446930,614400,614400,513911
+          15,1407232193,1,2384063,614400,614400,593768
+          16,505278870,0,1142238,614400,614400,444396
+            """))
+
+  def test_cpu_cycles_per_thread_slice(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_postboot_unlock.pftrace'),
+        query=("""
+             INCLUDE PERFETTO MODULE linux.cpu.utilization.slice;
+
+             SELECT
+              id,
+              utid,
+              millicycles,
+              megacycles
+             FROM cpu_cycles_per_thread_slice
+             WHERE millicycles IS NOT NULL
+             LIMIT 10
+             """),
+        out=Csv("""
+        "id","utid","millicycles","megacycles"
+        125,6,6375728,0
+        126,6,8699728,0
+        128,6,5565648,0
+        129,6,5565648,0
+        214,6,7132688,0
+        270,6,662972400,0
+        271,6,58483872,0
+        274,6,571785696,0
+        277,6,206411922,0
+        278,6,190908162,0
+            """))
+
+  def test_cpu_cycles_per_thread_slice(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_postboot_unlock.pftrace'),
+        query=("""
+             INCLUDE PERFETTO MODULE linux.cpu.utilization.slice;
+
+             SELECT
+              id,
+              utid,
+              millicycles,
+              megacycles
+             FROM cpu_cycles_per_thread_slice_in_interval(TRACE_START(), TRACE_DUR() / 10)
+             WHERE millicycles IS NOT NULL
+             LIMIT 10
+             """),
+        out=Csv("""
+        "id","utid","millicycles","megacycles"
+        110,17,13022368,0
+        121,17,9618704,0
+        125,6,6375728,0
+        126,6,8699728,0
+        128,6,5565648,0
+        129,6,5565648,0
+        146,24,6916224,0
+        151,26,5296064,0
+        203,17,150060016,0
+        214,6,7132688,0
             """))
 
   # Test CPU frequency counter grouping.
@@ -323,3 +517,58 @@
          "cpu","state","count","dur","avg_dur","idle_percent"
          0,2,2,2000000,1000000,40.000000
          """))
+
+  def test_linux_cpu_idle_time_in_state(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          sys_stats {
+            cpuidle_state {
+              cpu_id: 0
+              cpuidle_state_entry {
+                state: "C8"
+                duration_us: 1000000
+              }
+            }
+          }
+          timestamp: 200000000000
+          trusted_packet_sequence_id: 2
+        }
+        packet {
+          sys_stats {
+            cpuidle_state {
+              cpu_id: 0
+              cpuidle_state_entry {
+                state: "C8"
+                duration_us: 1000100
+              }
+            }
+          }
+          timestamp: 200001000000
+          trusted_packet_sequence_id: 2
+        }
+        packet {
+          sys_stats {
+            cpuidle_state {
+              cpu_id: 0
+              cpuidle_state_entry {
+                state: "C8"
+                duration_us: 1000200
+              }
+            }
+          }
+          timestamp: 200002000000
+          trusted_packet_sequence_id: 2
+        }
+         """),
+        query="""
+         INCLUDE PERFETTO MODULE linux.cpu.idle_time_in_state;
+         SELECT * FROM cpu_idle_time_in_state_counters;
+         """,
+        out=Csv("""
+         "ts","state_name","idle_percentage","total_residency","time_slice"
+          200001000000,"cpuidle.C8",10.000000,100.000000,1000
+          200002000000,"cpuidle.C8",10.000000,100.000000,1000
+          200001000000,"cpuidle.C0",90.000000,900.000000,1000
+          200002000000,"cpuidle.C0",90.000000,900.000000,1000
+         """))
diff --git a/test/trace_processor/diff_tests/stdlib/linux/tests.py b/test/trace_processor/diff_tests/stdlib/linux/tests.py
index 5be22eb..20fc769 100644
--- a/test/trace_processor/diff_tests/stdlib/linux/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/linux/tests.py
@@ -44,4 +44,37 @@
         18,45,2379,2379,"csf_kcpu_0","csf_kcpu_0"
         12,47,247,247,"decon0_kthread","decon0_kthread"
         65,48,159,159,"spi0","spi0"
-            """))
\ No newline at end of file
+            """))
+
+  # Tests that DSU devfreq counters are working properly
+  def test_dsu_devfreq(self):
+    return DiffTestBlueprint(
+        trace=DataPath('wattson_tk4_pcmark.pb'),
+        query=("""
+            INCLUDE PERFETTO MODULE linux.devfreq;
+            SELECT id, ts, dur, dsu_freq FROM linux_devfreq_dsu_counter
+            LIMIT 20
+            """),
+        out=Csv("""
+            "id","ts","dur","dsu_freq"
+            61,4106584783742,11482788,610000
+            166,4106596266530,8108602,1197000
+            212,4106604375132,21453410,610000
+            487,4106625828542,39427368,820000
+            1130,4106665255910,3264242,610000
+            1173,4106668520152,16966105,820000
+            1391,4106685486257,10596883,970000
+            1584,4106696083140,10051636,610000
+            1868,4106706134776,14058960,820000
+            2136,4106720193736,116719238,610000
+            4388,4106836912974,8285848,1197000
+            4583,4106845198822,16518433,820000
+            5006,4106861717255,9357503,1328000
+            5238,4106871074758,27228760,1197000
+            5963,4106898303518,16581706,820000
+            6498,4106914885224,9954142,1197000
+            6763,4106924839366,9024780,970000
+            7061,4106933864146,26264160,820000
+            7637,4106960128306,11008505,970000
+            7880,4106971136811,9282511,1197000
+            """))
diff --git a/test/trace_processor/diff_tests/stdlib/metasql/column_list.py b/test/trace_processor/diff_tests/stdlib/metasql/column_list.py
deleted file mode 100644
index 5c4fe6c..0000000
--- a/test/trace_processor/diff_tests/stdlib/metasql/column_list.py
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env python3
-# Copyright (C) 2024 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License a
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from python.generators.diff_tests.testing import Csv, TextProto
-from python.generators.diff_tests.testing import DiffTestBlueprint
-from python.generators.diff_tests.testing import TestSuite
-
-
-class ColumnListTests(TestSuite):
-
-  def test_column_list_result_columns(self):
-    return DiffTestBlueprint(
-        trace=TextProto(''),
-        query="""
-        INCLUDE PERFETTO MODULE metasql.column_list;
-
-        WITH data(foo, bar) AS (
-          VALUES (0, 1)
-        )
-        SELECT _metasql_unparenthesize_column_list!((foo, bar))
-        FROM data
-        """,
-        out=Csv("""
-        "foo","bar"
-        0,1
-        """))
diff --git a/test/trace_processor/diff_tests/stdlib/metasql/table_list.py b/test/trace_processor/diff_tests/stdlib/metasql/table_list.py
deleted file mode 100644
index 503f84c..0000000
--- a/test/trace_processor/diff_tests/stdlib/metasql/table_list.py
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/usr/bin/env python3
-# Copyright (C) 2024 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License a
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from python.generators.diff_tests.testing import Csv, TextProto
-from python.generators.diff_tests.testing import DiffTestBlueprint
-from python.generators.diff_tests.testing import TestSuite
-
-
-class TableListTests(TestSuite):
-
-  def test_table_list_result_columns(self):
-    return DiffTestBlueprint(
-        trace=TextProto(''),
-        query="""
-        INCLUDE PERFETTO MODULE metasql.table_list;
-
-        CREATE PERFETTO MACRO mac(t TableOrSubquery)
-        RETURNS TableOrSubquery AS
-        (SELECT * FROM $t);
-
-        WITH foo AS (
-          SELECT 0 AS a
-        ),
-        bar AS (
-          SELECT 1 AS b
-        ),
-        baz AS (
-          SELECT 2 AS c
-        )
-        SELECT a + b + c
-        FROM _metasql_map_join_table_list!((foo, bar, baz), mac);
-        """,
-        out=Csv("""
-        "a + b + c"
-        3
-        """))
-
-  def test_table_list_with_capture(self):
-    return DiffTestBlueprint(
-        trace=TextProto(''),
-        query="""
-        INCLUDE PERFETTO MODULE metasql.table_list;
-
-        CREATE PERFETTO MACRO mac(t TableOrSubquery, x Expr)
-        RETURNS TableOrSubquery AS
-        (SELECT *, $x AS bla FROM $t);
-
-        WITH foo AS (
-          SELECT 0 AS a
-        ),
-        bar AS (
-          SELECT 1 AS b
-        ),
-        baz AS (
-          SELECT 2 AS c
-        )
-        SELECT
-          a + b + c
-        FROM _metasql_map_join_table_list_with_capture!(
-          (foo, bar, baz),
-          mac,
-          (3)
-        );
-        """,
-        out=Csv("""
-        "a + b + c"
-        3
-        """))
diff --git a/test/trace_processor/diff_tests/stdlib/slices/tests.py b/test/trace_processor/diff_tests/stdlib/slices/tests.py
index 5555c88..88b430f 100644
--- a/test/trace_processor/diff_tests/stdlib/slices/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/slices/tests.py
@@ -104,6 +104,26 @@
         "NestedThreadSlice",6,1,1
       """))
 
+  def test_slice_remove_nulls_and_reparent(self):
+    return DiffTestBlueprint(
+        trace=Path('trace.py'),
+        query="""
+        INCLUDE PERFETTO MODULE slices.hierarchy;
+
+        SELECT id, parent_id, name, depth
+        FROM _slice_remove_nulls_and_reparent!(
+          (SELECT id, parent_id, depth, IIF(name = 'ProcessSlice', NULL, name) AS name
+          FROM slice),
+          name
+        ) LIMIT 10;
+      """,
+        out=Csv("""
+        "id","parent_id","name","depth"
+        0,"[NULL]","AsyncSlice",0
+        2,"[NULL]","ThreadSlice",0
+        3,2,"NestedThreadSlice",0
+      """))
+
   # Common functions
 
   def test_slice_flattened(self):
@@ -140,7 +160,7 @@
         query="""
         INCLUDE PERFETTO MODULE slices.cpu_time;
 
-        SELECT *
+        SELECT id, cpu_time
         FROM thread_slice_cpu_time
         LIMIT 10;
         """,
@@ -156,4 +176,28 @@
         7,33333
         8,46926
         9,17865
-        """))
\ No newline at end of file
+        """))
+
+  def test_thread_slice_time_in_state(self):
+    return DiffTestBlueprint(
+        trace=DataPath('example_android_trace_30s.pb'),
+        query="""
+        INCLUDE PERFETTO MODULE slices.time_in_state;
+
+        SELECT id, name, state, io_wait, blocked_function, dur
+        FROM thread_slice_time_in_state
+        LIMIT 10;
+        """,
+        out=Csv("""
+          "id","name","state","io_wait","blocked_function","dur"
+          0,"Deoptimization JIT inline cache","Running","[NULL]","[NULL]",178646
+          1,"Deoptimization JIT inline cache","Running","[NULL]","[NULL]",119740
+          2,"Lock contention on thread list lock (owner tid: 0)","Running","[NULL]","[NULL]",58073
+          3,"Lock contention on thread list lock (owner tid: 0)","Running","[NULL]","[NULL]",98698
+          3,"Lock contention on thread list lock (owner tid: 0)","S","[NULL]","[NULL]",56302
+          4,"monitor contention with owner InputReader (1421) at void com.android.server.power.PowerManagerService.acquireWakeLockInternal(android.os.IBinder, int, java.lang.String, java.lang.String, android.os.WorkSource, java.lang.String, int, int)(PowerManagerService.java:1018) waiters=0 blocking from void com.android.server.power.PowerManagerService.handleSandman()(PowerManagerService.java:2280)","Running","[NULL]","[NULL]",121979
+          4,"monitor contention with owner InputReader (1421) at void com.android.server.power.PowerManagerService.acquireWakeLockInternal(android.os.IBinder, int, java.lang.String, java.lang.String, android.os.WorkSource, java.lang.String, int, int)(PowerManagerService.java:1018) waiters=0 blocking from void com.android.server.power.PowerManagerService.handleSandman()(PowerManagerService.java:2280)","S","[NULL]","[NULL]",51198
+          5,"monitor contention with owner main (1204) at void com.android.server.am.ActivityManagerService.onWakefulnessChanged(int)(ActivityManagerService.java:7244) waiters=0 blocking from void com.android.server.am.ActivityManagerService$3.handleMessage(android.os.Message)(ActivityManagerService.java:1704)","Running","[NULL]","[NULL]",45000
+          5,"monitor contention with owner main (1204) at void com.android.server.am.ActivityManagerService.onWakefulnessChanged(int)(ActivityManagerService.java:7244) waiters=0 blocking from void com.android.server.am.ActivityManagerService$3.handleMessage(android.os.Message)(ActivityManagerService.java:1704)","S","[NULL]","[NULL]",20164377
+          6,"monitor contention with owner main (1204) at void com.android.server.am.ActivityManagerService.onWakefulnessChanged(int)(ActivityManagerService.java:7244) waiters=1 blocking from com.android.server.wm.ActivityTaskManagerInternal$SleepToken com.android.server.am.ActivityTaskManagerService.acquireSleepToken(java.lang.String, int)(ActivityTaskManagerService.java:5048)","Running","[NULL]","[NULL]",35104
+        """))
diff --git a/test/trace_processor/diff_tests/stdlib/timestamps/tests.py b/test/trace_processor/diff_tests/stdlib/timestamps/tests.py
index a029ce5..d5cc34b 100644
--- a/test/trace_processor/diff_tests/stdlib/timestamps/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/timestamps/tests.py
@@ -22,86 +22,60 @@
 
 class Timestamps(TestSuite):
 
-  def test_ns(self):
+  def test_to_time(self):
     return DiffTestBlueprint(
         trace=TextProto(""),
         query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT ns(4) as result;
+        INCLUDE PERFETTO MODULE time.conversion;
+
+        WITH data(unit, time) AS (
+          VALUES
+            ('ns', time_to_ns(cast_int!(1e14))),
+            ('us', time_to_us(cast_int!(1e14))),
+            ('ms', time_to_ms(cast_int!(1e14))),
+            ('s', time_to_s(cast_int!(1e14))),
+            ('min', time_to_min(cast_int!(1e14))),
+            ('h', time_to_hours(cast_int!(1e14))),
+            ('days', time_to_days(cast_int!(1e14)))
+        )
+        SELECT * FROM data
       """,
         out=Csv("""
-        "result"
-        4
+        "unit","time"
+        "ns",100000000000000
+        "us",100000000000
+        "ms",100000000
+        "s",100000
+        "min",1666
+        "h",27
+        "days",1
       """))
 
-  def test_us(self):
+  def test_from_time(self):
     return DiffTestBlueprint(
         trace=TextProto(""),
         query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT us(4) as result;
-      """,
-        out=Csv("""
-        "result"
-        4000
-      """))
+        INCLUDE PERFETTO MODULE time.conversion;
 
-  def test_ms(self):
-    return DiffTestBlueprint(
-        trace=TextProto(""),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT ms(4) as result;
+        WITH data(unit, time) AS (
+          VALUES
+            ('ns', time_from_ns(1)),
+            ('us', time_from_us(1)),
+            ('ms', time_from_ms(1)),
+            ('s', time_from_s(1)),
+            ('min', time_from_min(1)),
+            ('h', time_from_hours(1)),
+            ('days', time_from_days(1))
+        )
+        SELECT * FROM data
       """,
         out=Csv("""
-        "result"
-        4000000
-      """))
-
-  def test_seconds(self):
-    return DiffTestBlueprint(
-        trace=TextProto(""),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT seconds(4) as result;
-      """,
-        out=Csv("""
-        "result"
-        4000000000
-      """))
-
-  def test_minutes(self):
-    return DiffTestBlueprint(
-        trace=TextProto(""),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT minutes(1) as result;
-      """,
-        out=Csv("""
-        "result"
-        60000000000
-      """))
-
-  def test_hours(self):
-    return DiffTestBlueprint(
-        trace=TextProto(""),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT hours(1) as result;
-      """,
-        out=Csv("""
-        "result"
-        3600000000000
-      """))
-
-  def test_days(self):
-    return DiffTestBlueprint(
-        trace=TextProto(""),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT days(1) as result;
-      """,
-        out=Csv("""
-        "result"
-        86400000000000
-      """))
+        "unit","time"
+        "ns",1
+        "us",1000
+        "ms",1000000
+        "s",1000000000
+        "min",60000000000
+        "h",3600000000000
+        "days",86400000000000
+      """))
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/stdlib/viz/tests.py b/test/trace_processor/diff_tests/stdlib/viz/tests.py
new file mode 100644
index 0000000..0b986bb
--- /dev/null
+++ b/test/trace_processor/diff_tests/stdlib/viz/tests.py
@@ -0,0 +1,459 @@
+#!/usr/bin/env python3
+# 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 a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import Path, DataPath, Metric
+from python.generators.diff_tests.testing import Csv, Json, TextProto
+from python.generators.diff_tests.testing import DiffTestBlueprint, TraceInjector
+from python.generators.diff_tests.testing import TestSuite
+
+
+class Viz(TestSuite):
+  chronological_trace = TextProto(r"""
+        packet {
+          track_descriptor {
+            uuid: 1
+            name: "Root Chronological"
+            child_ordering: 2
+          }
+        }
+        packet {
+          track_descriptor {
+            uuid: 11
+            name: "A"
+            parent_uuid: 1
+          }
+        }
+        packet {
+          timestamp: 220
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_BEGIN
+            track_uuid: 11
+            name: "A1"
+          }
+        }
+        packet {
+          timestamp: 230
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_END
+            track_uuid: 11
+          }
+        }
+        packet {
+          track_descriptor {
+            uuid: 12
+            name: "B"
+            parent_uuid: 1
+          }
+        }
+        packet {
+          timestamp: 210
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_BEGIN
+            track_uuid: 12
+            name: "B"
+          }
+        }
+        packet {
+          timestamp: 240
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_END
+            track_uuid: 12
+          }
+        }
+        """)
+
+  explicit_trace = TextProto(r"""
+        packet {
+          track_descriptor {
+            uuid: 2
+            name: "Root Explicit"
+            child_ordering: 3
+          }
+        }
+        packet {
+          track_descriptor {
+            uuid: 110
+            name: "B"
+            parent_uuid: 2
+            sibling_order_rank: 1
+          }
+        }
+        packet {
+          track_descriptor {
+            uuid: 120
+            name: "A"
+            parent_uuid: 2
+            sibling_order_rank: 100
+          }
+        }
+        packet {
+          track_descriptor {
+            uuid: 130
+            name: "C"
+            parent_uuid: 2
+            sibling_order_rank: -100
+          }
+        }
+        packet {
+          timestamp: 220
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_BEGIN
+            track_uuid: 110
+            name: "1"
+          }
+        }
+        packet {
+          timestamp: 230
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_END
+            track_uuid: 110
+          }
+        }
+        packet {
+          timestamp: 230
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_BEGIN
+            track_uuid: 120
+            name: "2"
+          }
+        }
+        packet {
+          timestamp: 240
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_END
+            track_uuid: 120
+          }
+        }
+        packet {
+          timestamp: 225
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_BEGIN
+            track_uuid: 130
+            name: "3"
+          }
+        }
+        packet {
+          timestamp: 235
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_END
+            track_uuid: 130
+          }
+        }
+          """)
+
+  lexicographic_trace = TextProto(r"""
+        packet {
+          track_descriptor {
+            uuid: 3
+            name: "Root Lexicographic"
+            child_ordering: 1
+          }
+        }
+        packet {
+          track_descriptor {
+            uuid: 1100
+            name: "B"
+            parent_uuid: 3
+          }
+        }
+        packet {
+          track_descriptor {
+            uuid: 1200
+            name: "A"
+            parent_uuid: 3
+          }
+        }
+        packet {
+          track_descriptor {
+            uuid: 1300
+            name: "C"
+            parent_uuid: 3
+          }
+        }
+        packet {
+          timestamp: 220
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_BEGIN
+            track_uuid: 1100
+            name: "A1"
+          }
+        }
+        packet {
+          timestamp: 230
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_END
+            track_uuid: 1100
+          }
+        }
+        packet {
+          timestamp: 210
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_BEGIN
+            track_uuid: 1200
+            name: "B1"
+          }
+        }
+        packet {
+          timestamp: 300
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_END
+            track_uuid: 1200
+          }
+        }
+        packet {
+          timestamp: 350
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_BEGIN
+            track_uuid: 1300
+            name: "C1"
+          }
+        }
+        packet {
+          timestamp: 400
+          trusted_packet_sequence_id: 3903809
+          track_event {
+            type: TYPE_SLICE_END
+            track_uuid: 1300
+          }
+        }
+        """)
+
+  all_ordering_trace = TextProto(f"""{chronological_trace.contents}
+      {explicit_trace.contents}
+      {lexicographic_trace.contents}""")
+
+  def test_track_event_tracks_chronological(self):
+    return DiffTestBlueprint(
+        trace=self.chronological_trace,
+        query="""
+        SELECT
+          id,
+          parent_id,
+          EXTRACT_ARG(source_arg_set_id, 'child_ordering') AS ordering,
+          EXTRACT_ARG(source_arg_set_id, 'sibling_order_rank') AS rank
+        FROM track;
+        """,
+        out=Csv("""
+        "id","parent_id","ordering","rank"
+        0,"[NULL]","chronological","[NULL]"
+        1,0,"[NULL]","[NULL]"
+        2,0,"[NULL]","[NULL]"
+        """))
+
+  def test_all_tracks_ordered_chronological(self):
+    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;
+        """,
+        out=Csv("""
+        "id","order_id"
+        1,2
+        2,1
+        """))
+
+  def test_track_event_tracks_explicit(self):
+    return DiffTestBlueprint(
+        trace=self.explicit_trace,
+        query="""
+        SELECT
+          id,
+          parent_id,
+          EXTRACT_ARG(source_arg_set_id, 'child_ordering') AS ordering,
+          EXTRACT_ARG(source_arg_set_id, 'sibling_order_rank') AS rank
+        FROM track;
+        """,
+        out=Csv("""
+        "id","parent_id","ordering","rank"
+        0,"[NULL]","explicit","[NULL]"
+        1,0,"[NULL]",1
+        2,0,"[NULL]",100
+        3,0,"[NULL]",-100
+        """))
+
+  def test_all_tracks_ordered_explicit(self):
+    return DiffTestBlueprint(
+        trace=self.explicit_trace,
+        query="""
+        INCLUDE PERFETTO MODULE viz.summary.tracks;
+        SELECT id, order_id
+        FROM _track_event_tracks_ordered
+        ORDER BY id;
+        """,
+        out=Csv("""
+        "id","order_id"
+        1,2
+        2,3
+        3,1
+        """))
+
+  def test_track_event_tracks_lexicographic(self):
+    return DiffTestBlueprint(
+        trace=self.lexicographic_trace,
+        query="""
+        SELECT
+          id,
+          parent_id,
+          name,
+          EXTRACT_ARG(source_arg_set_id, 'child_ordering') AS ordering,
+          EXTRACT_ARG(source_arg_set_id, 'sibling_order_rank') AS rank
+        FROM track;
+        """,
+        out=Csv("""
+        "id","parent_id","name","ordering","rank"
+        0,"[NULL]","Root Lexicographic","lexicographic","[NULL]"
+        1,0,"B","[NULL]","[NULL]"
+        2,0,"A","[NULL]","[NULL]"
+        3,0,"C","[NULL]","[NULL]"
+        """))
+
+  def test_all_tracks_ordered_lexicographic(self):
+    return DiffTestBlueprint(
+        trace=self.lexicographic_trace,
+        query="""
+        INCLUDE PERFETTO MODULE viz.summary.tracks;
+        SELECT id, order_id
+        FROM _track_event_tracks_ordered
+        ORDER BY id;
+        """,
+        out=Csv("""
+        "id","order_id"
+        1,2
+        2,1
+        3,3
+        """))
+
+  def test_track_event_tracks_all_orderings(self):
+    return DiffTestBlueprint(
+        trace=self.all_ordering_trace,
+        query="""
+        SELECT
+          id,
+          parent_id,
+          name,
+          EXTRACT_ARG(source_arg_set_id, 'child_ordering') AS ordering,
+          EXTRACT_ARG(source_arg_set_id, 'sibling_order_rank') AS rank
+        FROM track
+        ORDER BY parent_id, id;
+        """,
+        out=Csv("""
+        "id","parent_id","name","ordering","rank"
+        0,"[NULL]","Root Chronological","chronological","[NULL]"
+        3,"[NULL]","Root Lexicographic","lexicographic","[NULL]"
+        5,"[NULL]","Root Explicit","explicit","[NULL]"
+        1,0,"A","[NULL]","[NULL]"
+        2,0,"B","[NULL]","[NULL]"
+        4,3,"A","[NULL]","[NULL]"
+        7,3,"B","[NULL]","[NULL]"
+        10,3,"C","[NULL]","[NULL]"
+        6,5,"B","[NULL]",1
+        8,5,"C","[NULL]",-100
+        9,5,"A","[NULL]",100
+        """))
+
+  def test_all_tracks_ordered_all_ordering(self):
+    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)
+        ORDER BY parent_id, id
+        """,
+        out=Csv("""
+        "id","parent_id","order_id"
+        1,0,2
+        2,0,1
+        4,3,1
+        7,3,2
+        10,3,3
+        6,5,2
+        8,5,1
+        9,5,3
+        """))
+
+  def test_sanity_ordering_tracks(self):
+    return DiffTestBlueprint(
+        trace=Path('track_event_tracks_ordering.textproto'),
+        query="""
+        SELECT
+          id,
+          parent_id,
+          name,
+          EXTRACT_ARG(source_arg_set_id, 'child_ordering') AS ordering,
+          EXTRACT_ARG(source_arg_set_id, 'sibling_order_rank') AS rank
+        FROM track
+        ORDER BY parent_id, id;
+        """,
+        out=Csv("""
+          "id","parent_id","name","ordering","rank"
+          0,"[NULL]","explicit_parent","explicit",-10
+          4,"[NULL]","chronological_parent","chronological","[NULL]"
+          9,"[NULL]","lexicographic_parent","lexicographic","[NULL]"
+          1,0,"explicit_child:no z-index","[NULL]","[NULL]"
+          2,0,"explicit_child:5 z-index","[NULL]",5
+          3,0,"explicit_child:-5 z-index","[NULL]",-5
+          8,0,"explicit_child:-5 z-index","[NULL]",-5
+          5,4,"chrono","[NULL]","[NULL]"
+          6,4,"chrono2","[NULL]","[NULL]"
+          7,4,"chrono1","[NULL]","[NULL]"
+          10,9,"[NULL]","[NULL]","[NULL]"
+          11,9,"a","[NULL]","[NULL]"
+          12,9,"b","[NULL]","[NULL]"
+          13,9,"ab","[NULL]","[NULL]"
+        """))
+
+  def test_sanity_ordering(self):
+    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
+        ORDER BY id;
+        """,
+        out=Csv("""
+        "id","order_id"
+        1,3
+        2,4
+        3,1
+        5,1
+        6,2
+        7,3
+        8,2
+        10,1
+        11,2
+        12,4
+        13,3
+        """))
diff --git a/test/trace_processor/diff_tests/stdlib/viz/track_event_tracks_ordering.textproto b/test/trace_processor/diff_tests/stdlib/viz/track_event_tracks_ordering.textproto
new file mode 100644
index 0000000..90f3b05
--- /dev/null
+++ b/test/trace_processor/diff_tests/stdlib/viz/track_event_tracks_ordering.textproto
@@ -0,0 +1,163 @@
+# Explicit tracks.
+
+## Parent
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 0
+  incremental_state_cleared: true
+  first_packet_on_sequence: true
+  track_descriptor {
+    uuid: 100
+    child_ordering: 3
+    name: "explicit_parent"
+    sibling_order_rank: -10
+  }
+  trace_packet_defaults {
+    track_event_defaults {
+      track_uuid: 1
+    }
+  }
+}
+
+## Children
+packet {
+  trusted_packet_sequence_id: 2
+  timestamp: 0
+  incremental_state_cleared: true
+  first_packet_on_sequence: true
+  track_descriptor {
+    uuid: 2
+    parent_uuid: 100
+    name: "explicit_child:no z-index"
+  }
+  trace_packet_defaults {
+    track_event_defaults {
+      track_uuid: 2
+    }
+  }
+}
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 0
+  track_descriptor {
+    uuid: 3
+    parent_uuid: 100
+    name: "explicit_child:5 z-index"
+    sibling_order_rank: 5
+  }
+}
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 0
+  track_descriptor {
+    uuid: 4
+    parent_uuid: 100
+    name: "explicit_child:-5 z-index"
+    sibling_order_rank: -5
+  }
+}
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 100
+  track_descriptor {
+    uuid: 5
+    parent_uuid: 100
+    name: "explicit_child:-5 z-index"
+    sibling_order_rank: -5
+  }
+}
+
+# Lexicographic tracks.
+
+## Parent
+packet {
+  trusted_packet_sequence_id: 2
+  timestamp: 200
+  track_descriptor {
+    uuid: 200
+    child_ordering: 1
+    name: "lexicographic_parent"
+  }
+}
+
+## Children
+
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 200
+  track_descriptor {
+    uuid: 6
+    parent_uuid: 200
+  }
+}
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 1000
+  track_descriptor {
+    uuid: 7
+    parent_uuid: 200
+    name: "a"
+  }
+}
+packet {
+  trusted_packet_sequence_id: 2
+  timestamp: 2000
+  track_descriptor {
+    uuid: 8
+    parent_uuid: 200
+    name: "b"
+  }
+}
+# Should appear on overridden track "t2".
+packet {
+  trusted_packet_sequence_id: 2
+  timestamp: 2000
+  track_descriptor {
+    uuid: 9
+    parent_uuid: 200
+    name: "ab"
+  }
+}
+
+# Chronological tracks.
+
+## Parent
+packet {
+  trusted_packet_sequence_id: 2
+  timestamp: 1000
+  track_descriptor {
+    uuid: 300
+    child_ordering: 2
+    name: "chronological_parent"
+  }
+}
+
+## Children
+
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 0
+  track_descriptor {
+    uuid: 10
+    parent_uuid: 300
+    name: "chrono"
+  }
+}
+packet {
+  trusted_packet_sequence_id: 2
+  timestamp: 10
+  track_descriptor {
+    uuid: 11
+    parent_uuid: 300
+    name: "chrono1"
+  }
+}
+packet {
+  trusted_packet_sequence_id: 2
+  timestamp: 5
+  track_descriptor {
+    uuid: 12
+    parent_uuid: 300
+    name: "chrono2"
+  }
+}
diff --git a/test/trace_processor/diff_tests/stdlib/wattson/tests.py b/test/trace_processor/diff_tests/stdlib/wattson/tests.py
index 0b1a678..b5c9cae 100644
--- a/test/trace_processor/diff_tests/stdlib/wattson/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/wattson/tests.py
@@ -50,24 +50,24 @@
         "ts","dur","l3_hit_count","l3_miss_count","freq_0","idle_0","freq_1","idle_1","freq_2","idle_2","freq_3","idle_3","freq_4","idle_4","freq_5","idle_5","freq_6","idle_6","freq_7","idle_7","suspended"
         370103436540,339437,"[NULL]","[NULL]",738000,-1,738000,1,738000,-1,738000,-1,400000,-1,400000,1,1106000,1,1106000,1,0
         370103419857,16683,"[NULL]","[NULL]",738000,-1,738000,1,738000,1,738000,-1,400000,-1,400000,1,1106000,1,1106000,1,0
-        370103413314,6543,"[NULL]","[NULL]",738000,1,738000,1,738000,1,738000,-1,400000,-1,400000,1,1106000,1,1106000,1,0
-        370103079729,333585,"[NULL]","[NULL]",738000,1,738000,1,738000,1,738000,-1,400000,-1,400000,1,1106000,-1,1106000,1,0
-        370103037378,42351,"[NULL]","[NULL]",738000,1,738000,1,738000,1,738000,-1,400000,1,400000,1,1106000,-1,1106000,1,0
-        370102869076,168302,"[NULL]","[NULL]",738000,1,738000,1,738000,1,738000,-1,400000,1,400000,1,1106000,-1,1106000,-1,0
-        370102832862,36214,"[NULL]","[NULL]",738000,1,738000,1,738000,-1,738000,-1,400000,1,400000,1,1106000,-1,1106000,-1,0
+        370103213314,206543,"[NULL]","[NULL]",738000,1,738000,1,738000,1,738000,-1,400000,-1,400000,1,1106000,1,1106000,1,0
+        370103079729,133585,"[NULL]","[NULL]",738000,1,738000,1,738000,1,738000,-1,400000,-1,400000,1,1106000,-1,1106000,1,0
+        370102869076,210653,"[NULL]","[NULL]",738000,1,738000,1,738000,1,738000,-1,400000,1,400000,1,1106000,-1,1106000,1,0
+        370102837378,31698,"[NULL]","[NULL]",738000,1,738000,1,738000,-1,738000,-1,400000,1,400000,1,1106000,-1,1106000,1,0
+        370102832862,4516,"[NULL]","[NULL]",738000,1,738000,1,738000,-1,738000,-1,400000,1,400000,1,1106000,-1,1106000,-1,0
         370102831844,1018,"[NULL]","[NULL]",738000,1,738000,1,738000,-1,738000,-1,400000,1,400000,1,1106000,-1,984000,-1,0
         370102819475,12369,"[NULL]","[NULL]",738000,1,738000,1,738000,-1,738000,-1,400000,1,400000,1,984000,-1,984000,-1,0
         370102816586,1098,"[NULL]","[NULL]",738000,1,574000,1,574000,-1,574000,-1,400000,1,400000,1,984000,-1,984000,-1,0
         370102669043,147543,"[NULL]","[NULL]",574000,1,574000,1,574000,-1,574000,-1,400000,1,400000,1,984000,-1,984000,-1,0
-        370102244564,424479,"[NULL]","[NULL]",574000,1,574000,1,574000,-1,574000,1,400000,1,400000,1,984000,-1,984000,-1,0
-        370100810360,1434204,"[NULL]","[NULL]",574000,1,574000,1,574000,-1,574000,1,400000,1,400000,1,984000,1,984000,-1,0
+        370102044564,624479,"[NULL]","[NULL]",574000,1,574000,1,574000,-1,574000,1,400000,1,400000,1,984000,-1,984000,-1,0
+        370100810360,1234204,"[NULL]","[NULL]",574000,1,574000,1,574000,-1,574000,1,400000,1,400000,1,984000,1,984000,-1,0
         370100731096,79264,"[NULL]","[NULL]",574000,1,574000,1,574000,-1,574000,-1,400000,1,400000,1,984000,1,984000,-1,0
         370100411312,319784,"[NULL]","[NULL]",574000,1,574000,1,574000,1,574000,-1,400000,1,400000,1,984000,1,984000,-1,0
         370100224219,187093,"[NULL]","[NULL]",574000,-1,574000,1,574000,1,574000,-1,400000,1,400000,1,984000,1,984000,-1,0
         370100171729,52490,"[NULL]","[NULL]",574000,1,574000,1,574000,1,574000,-1,400000,1,400000,1,984000,1,984000,-1,0
-        370096612858,3558871,"[NULL]","[NULL]",574000,1,574000,1,574000,1,574000,1,400000,1,400000,1,984000,1,984000,-1,0
-        370096452775,160083,"[NULL]","[NULL]",574000,1,574000,1,574000,1,574000,1,400000,1,400000,1,984000,-1,984000,-1,0
-        370096347307,105468,"[NULL]","[NULL]",574000,-1,574000,1,574000,1,574000,1,400000,1,400000,1,984000,-1,984000,-1,0
+        370096452775,3718954,"[NULL]","[NULL]",574000,1,574000,1,574000,1,574000,1,400000,1,400000,1,984000,1,984000,-1,0
+        370096412858,39917,"[NULL]","[NULL]",574000,-1,574000,1,574000,1,574000,1,400000,1,400000,1,984000,1,984000,-1,0
+        370096347307,65551,"[NULL]","[NULL]",574000,-1,574000,1,574000,1,574000,1,400000,1,400000,1,984000,-1,984000,-1,0
             """))
 
   # Test fixup of deep idle offset and time marker window.
@@ -88,26 +88,26 @@
                                                        "time_window_intersect"),
         out=Csv("""
             "duration","l3_hit_count","l3_miss_count","freq_0","idle_0","freq_1","idle_1","freq_2","idle_2","freq_3","idle_3","freq_4","idle_4","freq_5","idle_5","freq_6","idle_6","freq_7","idle_7","suspended"
-            58982760,2787779,1229222,574000,0,574000,1,574000,1,574000,0,553000,0,553000,0,500000,1,500000,0,0
+            59232508,2796301,1232977,574000,0,574000,1,574000,1,574000,0,553000,0,553000,0,500000,1,500000,0,0
             50664181,2364802,1133322,574000,0,574000,1,574000,1,574000,0,553000,1,553000,0,500000,0,500000,0,0
-            41743544,2013295,917107,574000,0,574000,0,574000,1,574000,1,553000,0,553000,0,500000,0,500000,1,0
-            33801135,1479851,684489,300000,0,300000,0,300000,1,300000,1,400000,0,400000,0,500000,0,500000,1,0
+            41917186,2020898,920691,574000,0,574000,0,574000,1,574000,1,553000,0,553000,0,500000,0,500000,1,0
+            33778317,1478303,683731,300000,0,300000,0,300000,1,300000,1,400000,0,400000,0,500000,0,500000,1,0
             32703489,1428203,690001,300000,0,300000,0,300000,1,300000,0,400000,0,400000,0,500000,0,500000,0,0
-            29062133,1597555,719900,574000,0,574000,0,574000,1,574000,0,553000,1,553000,0,500000,1,500000,0,0
+            28770906,1588177,715673,574000,0,574000,0,574000,1,574000,0,553000,1,553000,0,500000,1,500000,0,0
             28310872,1211262,566873,300000,0,300000,1,300000,1,300000,0,400000,0,400000,0,500000,0,500000,0,0
             26754474,1224826,569901,300000,0,300000,1,300000,0,300000,0,400000,1,400000,0,500000,0,500000,0,0
-            25107621,1059311,473161,300000,0,300000,1,300000,1,300000,0,400000,0,400000,0,500000,1,500000,0,0
+            24816645,1047517,467614,300000,0,300000,1,300000,1,300000,0,400000,0,400000,0,500000,1,500000,0,0
+            24251986,984546,417947,300000,0,300000,0,300000,1,300000,0,400000,1,400000,0,500000,1,500000,0,0
             23771603,987803,450930,300000,0,300000,1,300000,1,300000,0,400000,1,400000,0,500000,0,500000,0,0
-            23721891,963220,409196,300000,0,300000,0,300000,1,300000,0,400000,1,400000,0,500000,1,500000,0,0
             22988523,984240,473025,300000,0,300000,0,300000,1,300000,0,400000,0,400000,0,500000,0,500000,1,0
-            21790436,987511,449348,300000,0,300000,1,300000,1,300000,0,400000,0,400000,0,500000,0,500000,1,0
-            21673975,1034856,445803,574000,0,574000,0,574000,1,574000,0,553000,0,553000,0,500000,0,500000,1,0
+            22057168,998933,453689,300000,0,300000,1,300000,1,300000,0,400000,0,400000,0,500000,0,500000,1,0
+            21663200,1034424,445500,574000,0,574000,0,574000,1,574000,0,553000,0,553000,0,500000,0,500000,1,0
             20665650,974100,442861,300000,0,300000,0,300000,1,300000,0,400000,0,400000,1,500000,0,500000,0,0
-            18024891,823424,339250,300000,0,300000,1,300000,0,300000,0,400000,0,400000,0,500000,1,500000,0,0
-            17669272,826030,346995,574000,0,574000,0,574000,0,574000,0,553000,1,553000,0,500000,1,500000,0,0
-            16774291,762738,348469,574000,0,574000,1,574000,1,574000,0,553000,0,553000,0,500000,0,500000,1,0
+            18224891,834959,345078,300000,0,300000,1,300000,0,300000,0,400000,0,400000,0,500000,1,500000,0,0
+            17469272,816735,342795,574000,0,574000,0,574000,0,574000,0,553000,1,553000,0,500000,1,500000,0,0
+            16560058,754170,344777,574000,0,574000,1,574000,1,574000,0,553000,0,553000,0,500000,0,500000,1,0
             16191449,689792,316923,300000,0,300000,0,300000,1,300000,0,400000,1,400000,0,500000,0,500000,0,0
-            15918895,742531,325426,574000,0,574000,1,574000,0,574000,1,553000,0,553000,0,500000,1,500000,0,0
+            16008137,748321,327736,574000,0,574000,1,574000,0,574000,1,553000,0,553000,0,500000,1,500000,0,0
             """))
 
   # Test on Raven for checking system states and the DSU PMU counts.
@@ -119,26 +119,26 @@
                    "SYSTEM_STATE_TABLE", "wattson_system_states")),
         out=Csv("""
             "duration","l3_hit_count","l3_miss_count","freq_0","idle_0","freq_1","idle_1","freq_2","idle_2","freq_3","idle_3","freq_4","idle_4","freq_5","idle_5","freq_6","idle_6","freq_7","idle_7","suspended"
-            1279130692,1318302,419159,300000,1,300000,1,300000,1,300000,1,400000,1,400000,1,500000,1,500000,1,0
-            165854009,118369,42037,300000,-1,300000,1,300000,1,300000,1,400000,1,400000,1,500000,1,500000,1,0
-            121156343,5846554,2343180,574000,0,574000,1,574000,0,574000,0,553000,0,553000,0,500000,1,500000,1,0
-            72876331,133532,57759,300000,1,300000,1,300000,-1,300000,1,400000,1,400000,1,500000,1,500000,1,0
-            71060865,69029,22056,300000,1,300000,-1,300000,1,300000,1,400000,1,400000,1,500000,1,500000,1,0
-            64501262,276098,309757,300000,1,300000,1,300000,1,300000,1,400000,1,400000,1,500000,-1,500000,1,0
-            58982760,2787779,1229222,574000,0,574000,1,574000,1,574000,0,553000,0,553000,0,500000,1,500000,0,0
-            51127388,50724,18075,300000,1,300000,1,300000,1,300000,-1,400000,1,400000,1,500000,1,500000,1,0
+            1280071578,1319309,419083,300000,1,300000,1,300000,1,300000,1,400000,1,400000,1,500000,1,500000,1,0
+            165833778,118250,42072,300000,-1,300000,1,300000,1,300000,1,400000,1,400000,1,500000,1,500000,1,0
+            121848767,5879527,2358273,574000,0,574000,1,574000,0,574000,0,553000,0,553000,0,500000,1,500000,1,0
+            72914132,134731,58480,300000,1,300000,1,300000,-1,300000,1,400000,1,400000,1,500000,1,500000,1,0
+            70723657,68341,22021,300000,1,300000,-1,300000,1,300000,1,400000,1,400000,1,500000,1,500000,1,0
+            64738046,275953,309822,300000,1,300000,1,300000,1,300000,1,400000,1,400000,1,500000,-1,500000,1,0
+            59232508,2796301,1232977,574000,0,574000,1,574000,1,574000,0,553000,0,553000,0,500000,1,500000,0,0
+            50960835,50577,17976,300000,1,300000,1,300000,1,300000,-1,400000,1,400000,1,500000,1,500000,1,0
             50664181,2364802,1133322,574000,0,574000,1,574000,1,574000,0,553000,1,553000,0,500000,0,500000,0,0
-            49948122,2216740,934893,300000,0,300000,1,300000,0,300000,0,400000,0,400000,0,500000,1,500000,1,0
-            41743544,2013295,917107,574000,0,574000,0,574000,1,574000,1,553000,0,553000,0,500000,0,500000,1,0
-            40606558,"[NULL]","[NULL]",1401000,1,1401000,1,1401000,1,1401000,1,400000,1,400000,1,2802000,1,2802000,1,0
-            39887541,14272,1252,300000,0,300000,1,300000,1,300000,1,400000,1,400000,1,500000,1,500000,1,0
+            49614333,2201254,928640,300000,0,300000,1,300000,0,300000,0,400000,0,400000,0,500000,1,500000,1,0
+            41917186,2020898,920691,574000,0,574000,0,574000,1,574000,1,553000,0,553000,0,500000,0,500000,1,0
+            40469221,"[NULL]","[NULL]",1401000,1,1401000,1,1401000,1,1401000,1,400000,1,400000,1,2802000,1,2802000,1,0
+            40265209,14021,1245,300000,0,300000,1,300000,1,300000,1,400000,1,400000,1,500000,1,500000,1,0
             38159789,1428203,690001,300000,0,300000,0,300000,1,300000,0,400000,0,400000,0,500000,0,500000,0,0
-            33801135,1479851,684489,300000,0,300000,0,300000,1,300000,1,400000,0,400000,0,500000,0,500000,1,0
-            31543574,34702,18036,300000,1,300000,1,300000,1,300000,1,400000,-1,400000,1,500000,1,500000,1,0
-            31470669,163778,200331,1098000,1,1098000,1,1098000,1,1098000,1,400000,1,400000,1,500000,1,500000,-1,0
-            30993650,39579,48376,300000,1,300000,1,300000,1,300000,1,400000,1,400000,1,500000,1,500000,-1,0
-            30144287,1396131,585581,574000,0,574000,1,574000,0,574000,0,553000,0,553000,0,500000,1,500000,0,0
-            30009881,"[NULL]","[NULL]",1328000,1,1328000,1,1328000,1,1328000,1,2253000,1,2253000,1,500000,1,500000,1,0
+            33778317,1478303,683731,300000,0,300000,0,300000,1,300000,1,400000,0,400000,0,500000,0,500000,1,0
+            31421773,34528,17983,300000,1,300000,1,300000,1,300000,1,400000,-1,400000,1,500000,1,500000,1,0
+            31137678,162530,198792,1098000,1,1098000,1,1098000,1,1098000,1,400000,1,400000,1,500000,1,500000,-1,0
+            30271091,38946,48402,300000,1,300000,1,300000,1,300000,1,400000,1,400000,1,500000,1,500000,-1,0
+            30209881,"[NULL]","[NULL]",1328000,1,1328000,1,1328000,1,1328000,1,2253000,1,2253000,1,500000,1,500000,1,0
+            30118849,1394832,585081,574000,0,574000,1,574000,0,574000,0,553000,0,553000,0,500000,1,500000,0,0
             """))
 
   # Test on eos to check that suspend states are being calculated appropriately.
@@ -162,24 +162,24 @@
             "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
-            6933558399,1708800,-1,1708800,-1,1708800,-1,1708800,-1,0
+            6972220533,1708800,-1,1708800,-1,1708800,-1,1708800,-1,0
             1649400745,614400,0,614400,0,614400,0,614400,0,0
-            1199187488,614400,-1,614400,1,614400,1,614400,1,0
+            1206977074,614400,-1,614400,1,614400,1,614400,1,0
             945900007,1708800,0,1708800,0,1708800,0,1708800,0,0
-            936351409,1363200,0,1363200,0,1363200,0,1363200,1,0
-            708490325,1708800,0,1708800,0,1708800,0,1708800,1,0
+            943703078,1363200,0,1363200,0,1363200,0,1363200,1,0
+            736663600,1708800,0,1708800,0,1708800,0,1708800,1,0
             706695995,1708800,1,1708800,1,1708800,1,1708800,1,0
             656873956,1363200,1,1363200,1,1363200,1,1363200,1,0
             633440914,1363200,0,1363200,0,1363200,0,1363200,0,0
-            627708654,1708800,-1,1708800,0,1708800,0,1708800,0,0
-            620315547,1708800,-1,1708800,1,1708800,1,1708800,1,0
-            578173274,1708800,-1,1708800,0,1708800,0,1708800,-1,0
-            530967964,1708800,-1,1708800,-1,1708800,0,1708800,-1,0
-            516281990,1708800,0,1708800,0,1708800,0,1708800,-1,0
-            473910837,1363200,-1,1363200,0,1363200,0,1363200,1,0
-            461831724,1708800,0,1708800,-1,1708800,0,1708800,0,0
-            402233299,1708800,-1,1708800,-1,1708800,-1,1708800,1,0
+            627957352,1708800,-1,1708800,0,1708800,0,1708800,0,0
+            615611076,1708800,-1,1708800,1,1708800,1,1708800,1,0
+            575584212,1708800,-1,1708800,0,1708800,0,1708800,-1,0
+            527581753,1708800,-1,1708800,-1,1708800,0,1708800,-1,0
+            488107828,1708800,0,1708800,0,1708800,0,1708800,-1,0
+            474912603,1363200,-1,1363200,0,1363200,0,1363200,1,0
+            461943392,1708800,0,1708800,-1,1708800,0,1708800,0,0
             375051979,864000,1,864000,1,864000,1,864000,1,0
+            371458882,1363200,-1,1363200,0,1363200,0,1363200,-1,0
             """))
 
   # Test that the device name can be extracted from the trace's metadata.
@@ -200,7 +200,7 @@
     return DiffTestBlueprint(
         trace=DataPath('wattson_dsu_pmu.pb'),
         query=("""
-            INCLUDE PERFETTO MODULE wattson.curves.ungrouped;
+            INCLUDE PERFETTO MODULE wattson.curves.estimates;
               select * from _w_independent_cpus_calc
               WHERE ts > 359661672577
               ORDER by ts ASC
@@ -225,7 +225,7 @@
     return DiffTestBlueprint(
         trace=DataPath('wattson_dsu_pmu.pb'),
         query=("""
-            INCLUDE PERFETTO MODULE wattson.curves.ungrouped;
+            INCLUDE PERFETTO MODULE wattson.curves.estimates;
               select * from _system_state_curves
               ORDER by ts ASC
               LIMIT 5
@@ -244,7 +244,7 @@
     return DiffTestBlueprint(
         trace=DataPath('wattson_dsu_pmu.pb'),
         query=("""
-            INCLUDE PERFETTO MODULE wattson.curves.ungrouped;
+            INCLUDE PERFETTO MODULE wattson.curves.estimates;
               select * from _system_state_curves
               WHERE ts > 359661672577
               ORDER by ts ASC
@@ -264,7 +264,7 @@
     return DiffTestBlueprint(
         trace=DataPath('wattson_dsu_pmu.pb'),
         query=("""
-            INCLUDE PERFETTO MODULE wattson.curves.ungrouped;
+            INCLUDE PERFETTO MODULE wattson.curves.estimates;
               select * from _system_state_mw
               WHERE ts > 359661672577
               ORDER by ts ASC
@@ -289,7 +289,7 @@
     return DiffTestBlueprint(
         trace=DataPath('wattson_eos_suspend.pb'),
         query=("""
-            INCLUDE PERFETTO MODULE wattson.curves.ungrouped;
+            INCLUDE PERFETTO MODULE wattson.curves.estimates;
               select * from _system_state_curves
               WHERE ts > 24790009884888
               ORDER by ts ASC
@@ -297,60 +297,158 @@
             """),
         out=Csv("""
             "ts","dur","cpu0_curve","cpu1_curve","cpu2_curve","cpu3_curve","cpu4_curve","cpu5_curve","cpu6_curve","cpu7_curve","static_curve","l3_hit_value","l3_miss_value"
-            24790009886451,21406,39.690000,39.690000,39.690000,39.690000,0.000000,0.000000,0.000000,0.000000,18.390000,"[NULL]","[NULL]"
             24790009907857,2784616769,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0,0
-            24792794524626,654584,39.690000,39.690000,39.690000,39.690000,0.000000,0.000000,0.000000,0.000000,18.390000,"[NULL]","[NULL]"
-            24792795179210,38854,39.690000,39.690000,39.690000,39.690000,0.000000,0.000000,0.000000,0.000000,18.390000,"[NULL]","[NULL]"
-            24792795218064,164583,39.690000,39.690000,39.690000,39.690000,0.000000,0.000000,0.000000,0.000000,18.390000,"[NULL]","[NULL]"
-            """))
-
-  # Tests that device curve table is being looked up correctly
-  def test_wattson_device_curve_per_policy(self):
-    return DiffTestBlueprint(
-        trace=DataPath('wattson_dsu_pmu.pb'),
-        query=("""
-            INCLUDE PERFETTO MODULE wattson.curves.grouped;
-              select * from wattson_estimate_per_component
-              WHERE ts > 359661672577
-              ORDER by ts ASC
-              LIMIT 10
-            """),
-        out=Csv("""
-            "ts","dur","l3","little_cpus","mid_cpus","big_cpus"
-            359661672578,75521,18051.005200,49.300000,550.550000,2064.320000
-            359661748099,2254517,840849.917600,49.440000,47.000000,2064.320000
-            359664003674,11596,2770.713600,1031.260000,1054.100000,3885.780000
-            359664015270,4720,1127.359000,1031.260000,1054.100000,2064.320000
-            359664019990,18921,4522.446400,1031.260000,550.550000,2064.320000
-            359664038911,8871,2120.319000,785.770000,550.550000,2064.320000
-            359664047782,1343,320.839600,540.280000,550.550000,2064.320000
-            359664049491,1383,514.276100,254.130000,47.000000,2064.320000
-            359664050874,2409912,898807.333300,49.440000,47.000000,2064.320000
-            359666460786,13754,3286.709200,49.300000,550.550000,2064.320000
+            24792794524626,424063,39.690000,39.690000,39.690000,39.690000,0.000000,0.000000,0.000000,0.000000,18.390000,"[NULL]","[NULL]"
+            24792794948689,205625,39.690000,39.690000,39.690000,39.690000,0.000000,0.000000,0.000000,0.000000,18.390000,"[NULL]","[NULL]"
+            24792795154314,19531,39.690000,39.690000,39.690000,39.690000,0.000000,0.000000,0.000000,0.000000,18.390000,"[NULL]","[NULL]"
+            24792795173845,50781,39.690000,39.690000,0.000000,39.690000,0.000000,0.000000,0.000000,0.000000,18.390000,"[NULL]","[NULL]"
             """))
 
   # Tests that total calculations are correct
-  def test_wattson_total_raven_calc(self):
-    return DiffTestBlueprint(
-        trace=DataPath('wattson_dsu_pmu.pb'),
-        query=("""
-            INCLUDE PERFETTO MODULE wattson.curves.grouped;
-               select * from _wattson_entire_trace
-            """),
-        out=Csv("""
-            "total_l3","total_little_cpus","total_mid_cpus","total_big_cpus","total"
-            500.120000,661.980000,370.730000,1490.970000,3023.800000
-            """))
-
-  # Tests that total calculations are correct
-  def test_wattson_total_eos_calc(self):
+  def test_wattson_idle_attribution(self):
     return DiffTestBlueprint(
         trace=DataPath('wattson_eos_suspend.pb'),
         query=("""
-            INCLUDE PERFETTO MODULE wattson.curves.grouped;
-               select * from _wattson_entire_trace
+            INCLUDE PERFETTO MODULE wattson.curves.idle_attribution;
+            SELECT
+              SUM(estimated_mw * dur) / 1000000000 as idle_transition_cost_mws,
+              utid,
+              upid
+            FROM _idle_transition_cost
+            GROUP BY utid
+            ORDER BY idle_transition_cost_mws DESC
+            LIMIT 20
             """),
         out=Csv("""
-            "total_l3","total_little_cpus","total_mid_cpus","total_big_cpus","total"
-            0.000000,2603.100000,0.000000,0.000000,2603.100000
+            "idle_transition_cost_mws","utid","upid"
+            18.291706,10,10
+            6.929671,73,73
+            5.366740,146,146
+            4.596243,457,457
+            4.488426,515,137
+            4.178230,1262,401
+            3.907947,694,353
+            3.580991,169,169
+            3.575903,11,11
+            3.270123,147,147
+            3.251823,396,396
+            3.156336,486,486
+            2.998187,727,356
+            2.945958,606,326
+            2.899400,464,464
+            2.781249,29,29
+            2.567914,1270,401
+            2.446651,471,471
+            2.434878,172,172
+            2.256320,414,414
+            """))
+
+  # Tests that DSU devfreq calculations are merged correctly
+  def test_wattson_dsu_devfreq(self):
+    return DiffTestBlueprint(
+        trace=DataPath('wattson_tk4_pcmark.pb'),
+        query=("""
+            INCLUDE PERFETTO MODULE wattson.curves.w_dsu_dependence;
+            SELECT * FROM _cpu_curves
+            WHERE ts > 4108586775197
+            LIMIT 20
+            """),
+        out=Csv("""
+            "ts","dur","freq_0","idle_0","freq_1","idle_1","freq_2","idle_2","freq_3","idle_3","cpu4_curve","cpu5_curve","cpu6_curve","cpu7_curve","l3_hit_count","l3_miss_count","no_static","all_cpu_deep_idle"
+            4108586789603,35685,1950000,0,1950000,-1,1950000,-1,1950000,-1,674.240000,674.240000,674.240000,3327.560000,14718,5837,-1,-1
+            4108586825288,30843,1950000,-1,1950000,-1,1950000,-1,1950000,-1,674.240000,674.240000,674.240000,3327.560000,12721,5045,-1,-1
+            4108586856131,13387,1950000,-1,1950000,-1,1950000,-1,1950000,-1,674.240000,674.240000,674.240000,99.470000,5521,2189,-1,-1
+            4108586869518,22542,1950000,-1,1950000,-1,1950000,-1,1950000,-1,674.240000,674.240000,674.240000,3327.560000,9297,3687,-1,-1
+            4108586892060,2482,1950000,-1,1950000,-1,1950000,-1,1950000,0,674.240000,674.240000,674.240000,3327.560000,1023,406,-1,-1
+            4108586894542,68563,1950000,-1,1950000,-1,1950000,-1,1950000,-1,674.240000,674.240000,674.240000,3327.560000,28279,11216,-1,-1
+            4108586963105,59652,1950000,-1,1950000,-1,1950000,-1,1950000,0,674.240000,674.240000,674.240000,3327.560000,24603,9758,-1,-1
+            4108587022757,3743,1950000,0,1950000,-1,1950000,-1,1950000,0,674.240000,674.240000,674.240000,3327.560000,1543,612,-1,-1
+            4108587026500,15992,1950000,-1,1950000,-1,1950000,-1,1950000,0,674.240000,674.240000,674.240000,3327.560000,6595,2616,-1,-1
+            4108587042492,15625,1950000,-1,1950000,-1,1950000,-1,1950000,0,674.240000,674.240000,674.240000,99.470000,6444,2556,-1,-1
+            4108587058117,8138,1950000,-1,1950000,-1,1950000,-1,1950000,0,674.240000,674.240000,674.240000,3327.560000,3356,1331,-1,-1
+            4108587066255,80566,1950000,-1,1950000,-1,1950000,-1,1950000,-1,674.240000,674.240000,674.240000,3327.560000,33229,13179,-1,-1
+            4108587146821,19572,1950000,-1,1950000,-1,1950000,-1,1950000,-1,674.240000,674.240000,674.240000,99.470000,8072,3201,-1,-1
+            4108587166393,219116,1950000,-1,1950000,-1,1950000,-1,1950000,-1,674.240000,674.240000,674.240000,3327.560000,90375,35845,-1,-1
+            4108587385509,81991,1950000,-1,1950000,0,1950000,-1,1950000,-1,674.240000,674.240000,674.240000,3327.560000,33817,13413,-1,-1
+            4108587467500,90413,1950000,-1,1950000,0,1950000,0,1950000,-1,674.240000,674.240000,674.240000,3327.560000,37291,14790,-1,-1
+            4108587557913,92896,1950000,0,1950000,0,1950000,0,1950000,-1,674.240000,674.240000,674.240000,3327.560000,38315,15196,-1,-1
+            4108587650809,95296,1950000,-1,1950000,0,1950000,0,1950000,-1,674.240000,674.240000,674.240000,3327.560000,39305,15589,-1,-1
+            4108587746105,12451,1950000,0,1950000,0,1950000,0,1950000,-1,674.240000,674.240000,674.240000,3327.560000,5135,2036,-1,-1
+            4108587758556,28524,1950000,0,1950000,0,1950000,-1,1950000,-1,674.240000,674.240000,674.240000,3327.560000,11764,4666,-1,-1
+            """))
+
+  # Tests that DSU devfreq calculations are merged correctly
+  def test_wattson_dsu_devfreq_system_state(self):
+    return DiffTestBlueprint(
+        trace=DataPath('wattson_tk4_pcmark.pb'),
+        query=("""
+            INCLUDE PERFETTO MODULE wattson.curves.estimates;
+            SELECT * FROM _system_state_mw
+            WHERE ts > 4108586775197
+            LIMIT 20
+            """),
+        out=Csv("""
+            "ts","dur","cpu0_mw","cpu1_mw","cpu2_mw","cpu3_mw","cpu4_mw","cpu5_mw","cpu6_mw","cpu7_mw","dsu_scu_mw"
+            4108586789603,35685,2.670000,205.600000,205.600000,205.600000,674.240000,674.240000,674.240000,3327.560000,1166.695271
+            4108586825288,30843,205.600000,205.600000,205.600000,205.600000,674.240000,674.240000,674.240000,3327.560000,1166.698554
+            4108586856131,13387,205.600000,205.600000,205.600000,205.600000,674.240000,674.240000,674.240000,99.470000,1166.545753
+            4108586869518,22542,205.600000,205.600000,205.600000,205.600000,674.240000,674.240000,674.240000,3327.560000,1166.655587
+            4108586892060,2482,205.600000,205.600000,205.600000,2.670000,674.240000,674.240000,674.240000,3327.560000,1166.164641
+            4108586894542,68563,205.600000,205.600000,205.600000,205.600000,674.240000,674.240000,674.240000,3327.560000,1166.746124
+            4108586963105,59652,205.600000,205.600000,205.600000,2.670000,674.240000,674.240000,674.240000,3327.560000,1166.716706
+            4108587022757,3743,2.670000,205.600000,205.600000,2.670000,674.240000,674.240000,674.240000,3327.560000,1166.170321
+            4108587026500,15992,205.600000,205.600000,205.600000,2.670000,674.240000,674.240000,674.240000,3327.560000,1166.620056
+            4108587042492,15625,205.600000,205.600000,205.600000,2.670000,674.240000,674.240000,674.240000,99.470000,1166.668234
+            4108587058117,8138,205.600000,205.600000,205.600000,2.670000,674.240000,674.240000,674.240000,3327.560000,1166.555033
+            4108587066255,80566,205.600000,205.600000,205.600000,205.600000,674.240000,674.240000,674.240000,3327.560000,1166.717766
+            4108587146821,19572,205.600000,205.600000,205.600000,205.600000,674.240000,674.240000,674.240000,99.470000,1166.626795
+            4108587166393,219116,205.600000,205.600000,205.600000,205.600000,674.240000,674.240000,674.240000,3327.560000,1166.750356
+            4108587385509,81991,205.600000,2.670000,205.600000,205.600000,674.240000,674.240000,674.240000,3327.560000,1166.743880
+            4108587467500,90413,205.600000,2.670000,2.670000,205.600000,674.240000,674.240000,674.240000,3327.560000,1166.736713
+            4108587557913,92896,2.670000,2.670000,2.670000,205.600000,674.240000,674.240000,674.240000,3327.560000,1166.730805
+            4108587650809,95296,205.600000,2.670000,2.670000,205.600000,674.240000,674.240000,674.240000,3327.560000,1166.740927
+            4108587746105,12451,2.670000,2.670000,2.670000,205.600000,674.240000,674.240000,674.240000,3327.560000,1166.556475
+            4108587758556,28524,2.670000,2.670000,205.600000,205.600000,674.240000,674.240000,674.240000,3327.560000,1166.680924
+            """))
+
+  def test_wattson_time_window_api(self):
+    return DiffTestBlueprint(
+        trace=DataPath('wattson_dsu_pmu.pb'),
+        query="""
+        INCLUDE PERFETTO MODULE wattson.curves.estimates;
+
+        SELECT
+          cpu0_mw,
+          cpu1_mw,
+          cpu2_mw,
+          cpu3_mw,
+          cpu4_mw,
+          cpu5_mw,
+          cpu6_mw,
+          cpu7_mw,
+          dsu_scu_mw
+        FROM _windowed_system_state_mw(362426061658, 5067704349)
+        """,
+        out=Csv("""
+            "cpu0_mw","cpu1_mw","cpu2_mw","cpu3_mw","cpu4_mw","cpu5_mw","cpu6_mw","cpu7_mw","dsu_scu_mw"
+            13.025673,6.270190,5.448549,8.796540,8.937174,10.717942,29.482823,30.239208,26.121213
+            """))
+
+  # Tests that suspend calculations are correct on 8 CPU device where suspend
+  # indication comes from "syscore" command
+  def test_wattson_syscore_suspend(self):
+    return DiffTestBlueprint(
+        trace=DataPath('wattson_syscore_suspend.pb'),
+        query=("""
+            INCLUDE PERFETTO MODULE wattson.curves.estimates;
+            SELECT ts, dur, cpu0_id, cpu1_id, cpu2_id, cpu3_id, suspended
+            FROM _stats_cpu0123_suspend
+            WHERE suspended
+            """),
+        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
             """))
diff --git a/test/trace_processor/diff_tests/syntax/include_tests.py b/test/trace_processor/diff_tests/syntax/include_tests.py
index 6c4d88a..6316ae6 100644
--- a/test/trace_processor/diff_tests/syntax/include_tests.py
+++ b/test/trace_processor/diff_tests/syntax/include_tests.py
@@ -23,121 +23,40 @@
 
   def test_import(self):
     return DiffTestBlueprint(
-        trace=TextProto(r"""
-        packet {
-          ftrace_events {
-            cpu: 1
-            event {
-              timestamp: 1000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.data_conn|13\n"
-              }
-            }
-            event {
-              timestamp: 4000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.data_conn|20\n"
-              }
-            }
-            event {
-              timestamp: 1000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.audio|1\n"
-              }
-            }
-          }
-        }
-        """),
+        trace=TextProto(''),
         query="""
-        SELECT IMPORT('common.timestamps');
+        SELECT IMPORT('time.conversion');
 
-        SELECT TRACE_START();
+        SELECT 1 AS x;
         """,
         out=Csv("""
-        "TRACE_START()"
-        1000
+        "x"
+        1
         """))
 
   def test_include_perfetto_module(self):
     return DiffTestBlueprint(
-        trace=TextProto(r"""
-        packet {
-          ftrace_events {
-            cpu: 1
-            event {
-              timestamp: 1000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.data_conn|13\n"
-              }
-            }
-            event {
-              timestamp: 4000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.data_conn|20\n"
-              }
-            }
-            event {
-              timestamp: 1000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.audio|1\n"
-              }
-            }
-          }
-        }
-        """),
+        trace=TextProto(''),
         query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
+        INCLUDE PERFETTO MODULE time.conversion;
 
-        SELECT TRACE_START();
+        SELECT time_to_ns(1) AS x
         """,
         out=Csv("""
-        "TRACE_START()"
-        1000
+        "x"
+        1
         """))
 
   def test_include_and_import(self):
     return DiffTestBlueprint(
-        trace=TextProto(r"""
-        packet {
-          ftrace_events {
-            cpu: 1
-            event {
-              timestamp: 1000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.data_conn|13\n"
-              }
-            }
-            event {
-              timestamp: 4000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.data_conn|20\n"
-              }
-            }
-            event {
-              timestamp: 1000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.audio|1\n"
-              }
-            }
-          }
-        }
-        """),
+        trace=TextProto(''),
         query="""
-        SELECT IMPORT('common.timestamps');
-        INCLUDE PERFETTO MODULE common.timestamps;
+        SELECT IMPORT('time.conversion');
+        INCLUDE PERFETTO MODULE time.conversion;
 
-        SELECT TRACE_START();
+        SELECT 1 AS x
         """,
         out=Csv("""
-        "TRACE_START()"
-        1000
+        "x"
+        1
         """))
diff --git a/test/trace_processor/diff_tests/syntax/table_tests.py b/test/trace_processor/diff_tests/syntax/table_tests.py
index a90e71c..fb2cbc2 100644
--- a/test/trace_processor/diff_tests/syntax/table_tests.py
+++ b/test/trace_processor/diff_tests/syntax/table_tests.py
@@ -412,3 +412,159 @@
         "MAX(id)"
         20745
         """))
+
+  def test_winscope_proto_to_args_with_defaults_with_nested_fields(self):
+    return DiffTestBlueprint(
+        trace=Path('../parser/android/surfaceflinger_layers.textproto'),
+        query="""
+        SELECT flat_key, key, int_value, string_value, real_value FROM __intrinsic_winscope_proto_to_args_with_defaults('surfaceflinger_layer') AS sfl
+        ORDER BY sfl.base64_proto_id, key
+        LIMIT 95
+        """,
+        out=Csv("""
+        "flat_key","key","int_value","string_value","real_value"
+        "active_buffer","active_buffer","[NULL]","[NULL]","[NULL]"
+        "app_id","app_id",0,"[NULL]","[NULL]"
+        "background_blur_radius","background_blur_radius",0,"[NULL]","[NULL]"
+        "barrier_layer","barrier_layer","[NULL]","[NULL]","[NULL]"
+        "blur_regions","blur_regions","[NULL]","[NULL]","[NULL]"
+        "bounds.bottom","bounds.bottom","[NULL]","[NULL]",24000.000000
+        "bounds.left","bounds.left","[NULL]","[NULL]",-10800.000000
+        "bounds.right","bounds.right","[NULL]","[NULL]",10800.000000
+        "bounds.top","bounds.top","[NULL]","[NULL]",-24000.000000
+        "buffer_transform","buffer_transform","[NULL]","[NULL]","[NULL]"
+        "children","children[0]",4,"[NULL]","[NULL]"
+        "children","children[1]",35,"[NULL]","[NULL]"
+        "children","children[2]",43,"[NULL]","[NULL]"
+        "children","children[3]",45,"[NULL]","[NULL]"
+        "children","children[4]",44,"[NULL]","[NULL]"
+        "children","children[5]",77,"[NULL]","[NULL]"
+        "children","children[6]",87,"[NULL]","[NULL]"
+        "color.a","color.a","[NULL]","[NULL]",1.000000
+        "color.b","color.b","[NULL]","[NULL]",-1.000000
+        "color.g","color.g","[NULL]","[NULL]",-1.000000
+        "color.r","color.r","[NULL]","[NULL]",-1.000000
+        "color_transform","color_transform","[NULL]","[NULL]","[NULL]"
+        "corner_radius","corner_radius","[NULL]","[NULL]",0.000000
+        "corner_radius_crop","corner_radius_crop","[NULL]","[NULL]","[NULL]"
+        "crop.bottom","crop.bottom",-1,"[NULL]","[NULL]"
+        "crop.left","crop.left",0,"[NULL]","[NULL]"
+        "crop.right","crop.right",-1,"[NULL]","[NULL]"
+        "crop.top","crop.top",0,"[NULL]","[NULL]"
+        "curr_frame","curr_frame",0,"[NULL]","[NULL]"
+        "damage_region","damage_region","[NULL]","[NULL]","[NULL]"
+        "dataspace","dataspace","[NULL]","BT709 sRGB Full range","[NULL]"
+        "destination_frame.bottom","destination_frame.bottom",-1,"[NULL]","[NULL]"
+        "destination_frame.left","destination_frame.left",0,"[NULL]","[NULL]"
+        "destination_frame.right","destination_frame.right",-1,"[NULL]","[NULL]"
+        "destination_frame.top","destination_frame.top",0,"[NULL]","[NULL]"
+        "effective_scaling_mode","effective_scaling_mode",0,"[NULL]","[NULL]"
+        "effective_transform","effective_transform","[NULL]","[NULL]","[NULL]"
+        "final_crop","final_crop","[NULL]","[NULL]","[NULL]"
+        "flags","flags",2,"[NULL]","[NULL]"
+        "hwc_composition_type","hwc_composition_type","[NULL]","HWC_TYPE_UNSPECIFIED","[NULL]"
+        "hwc_crop","hwc_crop","[NULL]","[NULL]","[NULL]"
+        "hwc_frame","hwc_frame","[NULL]","[NULL]","[NULL]"
+        "hwc_transform","hwc_transform",0,"[NULL]","[NULL]"
+        "id","id",3,"[NULL]","[NULL]"
+        "input_window_info","input_window_info","[NULL]","[NULL]","[NULL]"
+        "invalidate","invalidate",1,"[NULL]","[NULL]"
+        "is_opaque","is_opaque",0,"[NULL]","[NULL]"
+        "is_protected","is_protected",0,"[NULL]","[NULL]"
+        "is_relative_of","is_relative_of",0,"[NULL]","[NULL]"
+        "is_trusted_overlay","is_trusted_overlay",0,"[NULL]","[NULL]"
+        "layer_stack","layer_stack",0,"[NULL]","[NULL]"
+        "metadata","metadata","[NULL]","[NULL]","[NULL]"
+        "name","name","[NULL]","Display 0 name=\"Built-in Screen\"#3","[NULL]"
+        "original_id","original_id",0,"[NULL]","[NULL]"
+        "owner_uid","owner_uid",1000,"[NULL]","[NULL]"
+        "parent","parent",0,"[NULL]","[NULL]"
+        "pixel_format","pixel_format","[NULL]","Unknown/None","[NULL]"
+        "position","position","[NULL]","[NULL]","[NULL]"
+        "queued_frames","queued_frames",0,"[NULL]","[NULL]"
+        "refresh_pending","refresh_pending",0,"[NULL]","[NULL]"
+        "relatives","relatives","[NULL]","[NULL]","[NULL]"
+        "requested_color.a","requested_color.a","[NULL]","[NULL]",1.000000
+        "requested_color.b","requested_color.b","[NULL]","[NULL]",-1.000000
+        "requested_color.g","requested_color.g","[NULL]","[NULL]",-1.000000
+        "requested_color.r","requested_color.r","[NULL]","[NULL]",-1.000000
+        "requested_corner_radius","requested_corner_radius","[NULL]","[NULL]",0.000000
+        "requested_position","requested_position","[NULL]","[NULL]","[NULL]"
+        "requested_transform.dsdx","requested_transform.dsdx","[NULL]","[NULL]",0.000000
+        "requested_transform.dsdy","requested_transform.dsdy","[NULL]","[NULL]",0.000000
+        "requested_transform.dtdx","requested_transform.dtdx","[NULL]","[NULL]",0.000000
+        "requested_transform.dtdy","requested_transform.dtdy","[NULL]","[NULL]",0.000000
+        "requested_transform.type","requested_transform.type",0,"[NULL]","[NULL]"
+        "screen_bounds.bottom","screen_bounds.bottom","[NULL]","[NULL]",24000.000000
+        "screen_bounds.left","screen_bounds.left","[NULL]","[NULL]",-10800.000000
+        "screen_bounds.right","screen_bounds.right","[NULL]","[NULL]",10800.000000
+        "screen_bounds.top","screen_bounds.top","[NULL]","[NULL]",-24000.000000
+        "shadow_radius","shadow_radius","[NULL]","[NULL]",0.000000
+        "size","size","[NULL]","[NULL]","[NULL]"
+        "source_bounds.bottom","source_bounds.bottom","[NULL]","[NULL]",24000.000000
+        "source_bounds.left","source_bounds.left","[NULL]","[NULL]",-10800.000000
+        "source_bounds.right","source_bounds.right","[NULL]","[NULL]",10800.000000
+        "source_bounds.top","source_bounds.top","[NULL]","[NULL]",-24000.000000
+        "transform.dsdx","transform.dsdx","[NULL]","[NULL]",0.000000
+        "transform.dsdy","transform.dsdy","[NULL]","[NULL]",0.000000
+        "transform.dtdx","transform.dtdx","[NULL]","[NULL]",0.000000
+        "transform.dtdy","transform.dtdy","[NULL]","[NULL]",0.000000
+        "transform.type","transform.type",0,"[NULL]","[NULL]"
+        "transparent_region","transparent_region","[NULL]","[NULL]","[NULL]"
+        "trusted_overlay","trusted_overlay","[NULL]","UNSET","[NULL]"
+        "type","type","[NULL]","[NULL]","[NULL]"
+        "visible_region","visible_region","[NULL]","[NULL]","[NULL]"
+        "window_type","window_type",0,"[NULL]","[NULL]"
+        "z","z",0,"[NULL]","[NULL]"
+        "z_order_relative_of","z_order_relative_of",0,"[NULL]","[NULL]"
+        "active_buffer","active_buffer","[NULL]","[NULL]","[NULL]"
+        """))
+
+  def test_winscope_proto_to_args_with_defaults_with_repeated_fields(self):
+    return DiffTestBlueprint(
+        trace=Path('../parser/android/surfaceflinger_layers.textproto'),
+        query="""
+        SELECT flat_key, key, int_value, string_value, real_value FROM __intrinsic_winscope_proto_to_args_with_defaults('surfaceflinger_layers_snapshot') AS sfs
+        WHERE key != "hwc_blob"
+        ORDER BY sfs.base64_proto_id DESC, key ASC
+        LIMIT 36
+        """,
+        out=Csv("""
+        "flat_key","key","int_value","string_value","real_value"
+        "displays.dpi_x","displays[0].dpi_x","[NULL]","[NULL]",0.000000
+        "displays.dpi_y","displays[0].dpi_y","[NULL]","[NULL]",0.000000
+        "displays.id","displays[0].id",4619827677550801152,"[NULL]","[NULL]"
+        "displays.is_virtual","displays[0].is_virtual",0,"[NULL]","[NULL]"
+        "displays.layer_stack","displays[0].layer_stack",0,"[NULL]","[NULL]"
+        "displays.layer_stack_space_rect.bottom","displays[0].layer_stack_space_rect.bottom",2400,"[NULL]","[NULL]"
+        "displays.layer_stack_space_rect.left","displays[0].layer_stack_space_rect.left",0,"[NULL]","[NULL]"
+        "displays.layer_stack_space_rect.right","displays[0].layer_stack_space_rect.right",1080,"[NULL]","[NULL]"
+        "displays.layer_stack_space_rect.top","displays[0].layer_stack_space_rect.top",0,"[NULL]","[NULL]"
+        "displays.name","displays[0].name","[NULL]","Common Panel","[NULL]"
+        "displays.size.h","displays[0].size.h",2400,"[NULL]","[NULL]"
+        "displays.size.w","displays[0].size.w",1080,"[NULL]","[NULL]"
+        "displays.transform.dsdx","displays[0].transform.dsdx","[NULL]","[NULL]",0.000000
+        "displays.transform.dsdy","displays[0].transform.dsdy","[NULL]","[NULL]",0.000000
+        "displays.transform.dtdx","displays[0].transform.dtdx","[NULL]","[NULL]",0.000000
+        "displays.transform.dtdy","displays[0].transform.dtdy","[NULL]","[NULL]",0.000000
+        "displays.transform.type","displays[0].transform.type",0,"[NULL]","[NULL]"
+        "displays.dpi_x","displays[1].dpi_x","[NULL]","[NULL]",0.000000
+        "displays.dpi_y","displays[1].dpi_y","[NULL]","[NULL]",0.000000
+        "displays.id","displays[1].id",4619827677550801153,"[NULL]","[NULL]"
+        "displays.is_virtual","displays[1].is_virtual",0,"[NULL]","[NULL]"
+        "displays.layer_stack","displays[1].layer_stack",0,"[NULL]","[NULL]"
+        "displays.layer_stack_space_rect.bottom","displays[1].layer_stack_space_rect.bottom",2400,"[NULL]","[NULL]"
+        "displays.layer_stack_space_rect.left","displays[1].layer_stack_space_rect.left",0,"[NULL]","[NULL]"
+        "displays.layer_stack_space_rect.right","displays[1].layer_stack_space_rect.right",1080,"[NULL]","[NULL]"
+        "displays.layer_stack_space_rect.top","displays[1].layer_stack_space_rect.top",0,"[NULL]","[NULL]"
+        "displays.name","displays[1].name","[NULL]","Common Panel","[NULL]"
+        "displays.size.h","displays[1].size.h",2400,"[NULL]","[NULL]"
+        "displays.size.w","displays[1].size.w",1080,"[NULL]","[NULL]"
+        "displays.transform","displays[1].transform","[NULL]","[NULL]","[NULL]"
+        "elapsed_realtime_nanos","elapsed_realtime_nanos",2749500341063,"[NULL]","[NULL]"
+        "excludes_composition_state","excludes_composition_state",0,"[NULL]","[NULL]"
+        "missed_entries","missed_entries",0,"[NULL]","[NULL]"
+        "vsync_id","vsync_id",24767,"[NULL]","[NULL]"
+        "where","where","[NULL]","bufferLatched","[NULL]"
+        "displays.dpi_x","displays[0].dpi_x","[NULL]","[NULL]",0.000000
+        """))
diff --git a/test/trace_processor/diff_tests/tables/trace_metadata.json.out b/test/trace_processor/diff_tests/tables/trace_metadata.json.out
index 26214e6..9bf43c0 100644
--- a/test/trace_processor/diff_tests/tables/trace_metadata.json.out
+++ b/test/trace_processor/diff_tests/tables/trace_metadata.json.out
@@ -4,6 +4,9 @@
     "trace_uuid": "00000000-0000-0000-e77f-20a2204c2a49",
     "trace_size_bytes": 6365447,
     "trace_config_pbtxt": "buffers {\n  size_kb: 32768\n  fill_policy: UNSPECIFIED\n}\ndata_sources {\n  config {\n    name: \"linux.ftrace\"\n    target_buffer: 0\n    trace_duration_ms: 0\n    tracing_session_id: 0\n    ftrace_config {\n      ftrace_events: \"print\"\n      ftrace_events: \"sched_switch\"\n      ftrace_events: \"rss_stat\"\n      ftrace_events: \"ion_heap_shrink\"\n      ftrace_events: \"ion_heap_grow\"\n      atrace_categories: \"am\"\n      atrace_categories: \"dalvik\"\n      buffer_size_kb: 0\n      drain_period_ms: 0\n    }\n    chrome_config {\n      trace_config: \"\"\n    }\n    inode_file_config {\n      scan_interval_ms: 0\n      scan_delay_ms: 0\n      scan_batch_size: 0\n      do_not_scan: false\n    }\n    process_stats_config {\n      scan_all_processes_on_start: false\n      record_thread_names: false\n      proc_stats_poll_ms: 0\n    }\n    sys_stats_config {\n      meminfo_period_ms: 0\n      vmstat_period_ms: 0\n      stat_period_ms: 0\n    }\n    heapprofd_config {\n      sampling_interval_bytes: 0\n      all: false\n      continuous_dump_config {\n        dump_phase_ms: 0\n        dump_interval_ms: 0\n      }\n    }\n    legacy_config: \"\"\n  }\n}\ndata_sources {\n  config {\n    name: \"linux.process_stats\"\n    target_buffer: 0\n    trace_duration_ms: 0\n    tracing_session_id: 0\n    ftrace_config {\n      buffer_size_kb: 0\n      drain_period_ms: 0\n    }\n    chrome_config {\n      trace_config: \"\"\n    }\n    inode_file_config {\n      scan_interval_ms: 0\n      scan_delay_ms: 0\n      scan_batch_size: 0\n      do_not_scan: false\n    }\n    process_stats_config {\n      scan_all_processes_on_start: false\n      record_thread_names: false\n      proc_stats_poll_ms: 100\n    }\n    sys_stats_config {\n      meminfo_period_ms: 0\n      vmstat_period_ms: 0\n      stat_period_ms: 0\n    }\n    heapprofd_config {\n      sampling_interval_bytes: 0\n      all: false\n      continuous_dump_config {\n        dump_phase_ms: 0\n        dump_interval_ms: 0\n      }\n    }\n    legacy_config: \"\"\n  }\n}\ndata_sources {\n  config {\n    name: \"linux.sys_stats\"\n    target_buffer: 0\n    trace_duration_ms: 0\n    tracing_session_id: 0\n    ftrace_config {\n      buffer_size_kb: 0\n      drain_period_ms: 0\n    }\n    chrome_config {\n      trace_config: \"\"\n    }\n    inode_file_config {\n      scan_interval_ms: 0\n      scan_delay_ms: 0\n      scan_batch_size: 0\n      do_not_scan: false\n    }\n    process_stats_config {\n      scan_all_processes_on_start: false\n      record_thread_names: false\n      proc_stats_poll_ms: 0\n    }\n    sys_stats_config {\n      meminfo_period_ms: 50\n      meminfo_counters: MEMINFO_MEM_AVAILABLE\n      meminfo_counters: MEMINFO_SWAP_CACHED\n      meminfo_counters: MEMINFO_ACTIVE\n      meminfo_counters: MEMINFO_INACTIVE\n      vmstat_period_ms: 0\n      stat_period_ms: 0\n    }\n    heapprofd_config {\n      sampling_interval_bytes: 0\n      all: false\n      continuous_dump_config {\n        dump_phase_ms: 0\n        dump_interval_ms: 0\n      }\n    }\n    legacy_config: \"\"\n  }\n}\nduration_ms: 10000\nenable_extra_guardrails: false\nlockdown_mode: LOCKDOWN_UNCHANGED\nstatsd_metadata {\n  triggering_alert_id: 0\n  triggering_config_uid: 0\n  triggering_config_id: 0\n}\nwrite_into_file: false\nfile_write_period_ms: 0\nmax_file_size_bytes: 0\nguardrail_overrides {\n  max_upload_per_day_bytes: 0\n}\ndeferred_start: false",
-    "sched_duration_ns": 9452761359
+    "sched_duration_ns": 9452761359,
+    "suspend_count": 0,
+    "data_loss_count": 0,
+    "error_count": 1
   }
 }
diff --git a/test/vts/Android.bp b/test/vts/Android.bp
index f65011e..8a726e5 100644
--- a/test/vts/Android.bp
+++ b/test/vts/Android.bp
@@ -1,4 +1,5 @@
 package {
+    default_team: "trendy_team_perfetto",
     // See: http://go/android-license-faq
     // A large-scale-change added 'default_applicable_licenses' to import
     // all of the 'license_kinds' from "external_perfetto_license"
diff --git a/tools/check_sql_metrics.py b/tools/check_sql_metrics.py
index cd82c6d..4d9810f 100755
--- a/tools/check_sql_metrics.py
+++ b/tools/check_sql_metrics.py
@@ -127,7 +127,7 @@
       sql,
       path.split(ROOT_DIR)[1],
       metrics_sources.split(ROOT_DIR)[1], CREATE_TABLE_ALLOWLIST)
-  errors += check_banned_create_view_as(sql, path.split(ROOT_DIR)[1])
+  errors += check_banned_create_view_as(sql)
   for name, [line, type] in create_table_view_dir.items():
     if name not in drop_table_view_dir:
       errors.append(f'Missing DROP before CREATE {type.upper()} "{name}"\n'
@@ -142,7 +142,7 @@
       errors.append(f'DROP type doesnt match CREATE {type.upper()} "{name}"\n'
                     f'Offending file: {path}\n')
 
-  errors += check_banned_words(sql, path)
+  errors += check_banned_words(sql)
   return errors
 
 
diff --git a/tools/check_sql_modules.py b/tools/check_sql_modules.py
index 7765697..ed8ddc3 100755
--- a/tools/check_sql_modules.py
+++ b/tools/check_sql_modules.py
@@ -25,11 +25,12 @@
 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 sys.path.append(os.path.join(ROOT_DIR))
 
-from python.generators.sql_processing.docs_parse import ParsedFile
+from python.generators.sql_processing.docs_parse import ParsedModule
 from python.generators.sql_processing.docs_parse import parse_file
 from python.generators.sql_processing.utils import check_banned_create_table_as
 from python.generators.sql_processing.utils import check_banned_create_view_as
 from python.generators.sql_processing.utils import check_banned_words
+from python.generators.sql_processing.utils import check_banned_drop
 from python.generators.sql_processing.utils import check_banned_include_all
 
 
@@ -51,8 +52,7 @@
       help='Filter the name of the modules to check (regex syntax)')
 
   args = parser.parse_args()
-  errors = []
-  modules: List[Tuple[str, str, ParsedFile]] = []
+  modules: List[Tuple[str, str, ParsedModule]] = []
   for root, _, files in os.walk(args.stdlib_sources, topdown=True):
     for f in files:
       path = os.path.join(root, f)
@@ -79,40 +79,67 @@
         obj_count = len(parsed.functions) + len(parsed.table_functions) + len(
             parsed.table_views) + len(parsed.macros)
         print(
-            f"""Parsing '{rel_path}' ({obj_count} objects, {len(parsed.errors)} errors)
-- {len(parsed.functions)} functions + {len(parsed.table_functions)} table functions,
-- {len(parsed.table_views)} tables/views,
-- {len(parsed.macros)} macros.""")
+            f"Parsing '{rel_path}' ({obj_count} objects, "
+            f"{len(parsed.errors)} errors) - "
+            f"{len(parsed.functions)} functions, "
+            f"{len(parsed.table_functions)} table functions, "
+            f"{len(parsed.table_views)} tables/views, "
+            f"{len(parsed.macros)} macros.")
 
+  all_errors = 0
   for path, sql, parsed in modules:
+    errors = []
     lines = [l.strip() for l in sql.split('\n')]
     for line in lines:
       if line.startswith('--'):
         continue
-      if 'RUN_METRIC' in line:
-        errors.append(f"RUN_METRIC is banned in standard library.\n"
-                      f"Offending file: {path}\n")
-      if 'include perfetto module common.' in line.casefold():
-        errors.append(
-            f"Common module has been deprecated in the standard library.\n"
-            f"Offending file: {path}\n")
+      if 'run_metric' in line.casefold():
+        errors.append("RUN_METRIC is banned in standard library.")
       if 'insert into' in line.casefold():
-        errors.append(f"INSERT INTO table is not allowed in standard library.\n"
-                      f"Offending file: {path}\n")
+        errors.append("INSERT INTO table is not allowed in standard library.")
 
-    errors += parsed.errors
-    errors += check_banned_words(sql, path)
-    errors += check_banned_create_table_as(
-        sql,
-        path.split(ROOT_DIR)[1],
-        args.stdlib_sources.split(ROOT_DIR)[1])
-    errors += check_banned_create_view_as(sql, path.split(ROOT_DIR)[1])
-    errors += check_banned_include_all(sql, path.split(ROOT_DIR)[1])
+    # Validate includes
+    package = parsed.package_name
+    for include in parsed.includes:
+      package = package.lower()
+      include_package = include.package.lower()
 
-  if errors:
-    sys.stderr.write("\n".join(errors))
-    sys.stderr.write("\n")
-  return 0 if not errors else 1
+      if (include_package == "common"):
+        errors.append(
+            "Common module has been deprecated in the standard library. "
+            "Please check `slices.with_context` for a replacement for "
+            "`common.slices` and `time.conversion` for replacement for "
+            "`common.timestamps`")
+
+      if (package != "viz" and include_package == "viz"):
+        errors.append("No modules can depend on 'viz' outside 'viz' package.")
+
+      if (package == "chrome" and include_package == "android"):
+        errors.append(
+            f"Modules from package 'chrome' can't include '{include.module}' "
+            f"from package 'android'")
+
+      if (package == "android" and include_package == "chrome"):
+        errors.append(
+            f"Modules from package 'android' can't include '{include.module}' "
+            f"from package 'chrome'")
+
+    errors += [
+        *parsed.errors, *check_banned_words(sql),
+        *check_banned_create_table_as(sql), *check_banned_create_view_as(sql),
+        *check_banned_include_all(sql), *check_banned_drop(sql)
+    ]
+
+    if errors:
+      sys.stderr.write(
+          f"\nFound {len(errors)} errors in file '{path.split(ROOT_DIR)[1]}':\n- "
+      )
+      sys.stderr.write("\n- ".join(errors))
+      sys.stderr.write("\n\n")
+
+    all_errors += len(errors)
+
+  return 0 if not all_errors else 1
 
 
 if __name__ == "__main__":
diff --git a/tools/cpu_profile b/tools/cpu_profile
index 0f09f49..5339b16 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 v47.0
+# This file has been generated by: tools/roll-prebuilts v48.1
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        9481408,
+        9041560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/traceconv',
     'sha256':
-        'b6819bb922438d585e816646c4d60e43bfa823d5f3f499bd8efcaccd26a9009c',
+        'cec2da5cb771a4812d0b2d15604d5023954d28e0af12e87313da2ab70d26b970',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -58,11 +58,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8852520,
+        8375512,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/traceconv',
     'sha256':
-        '9e9eac795578f6ed76128127d8a81c1ce5620b3d47f2c5e23c1f7f2ebbff9bea',
+        '64e200a58ea9c9f366e1071dd274d0023d1fd14043f75dbba3fe0cc138ff5fc7',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -72,11 +72,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        9538432,
+        9134136,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/traceconv',
     'sha256':
-        '01881a82050f36b8db427c741ce236360bb86548e6a6c9445b2477c6150de05b',
+        '87b87e1778367c1e3b99fc77439a28b4911125d2751f9909fd1b51f6bd60b6f4',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -86,11 +86,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7286504,
+        6753020,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/traceconv',
     'sha256':
-        'bc700f945c78c4a65a60bdb499c6c59e671c5173f45f42976fd0661396b72c16',
+        '804c4e13aca5798731056952d9cb0c6ee58795c03477c69514ccd39703060812',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -100,11 +100,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        9168408,
+        8740064,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/traceconv',
     'sha256':
-        'd0192785a56c5088811a175ab083bb599aab5fd594a3248057380038f0b2b5c2',
+        '0d781886531d11e1d573a1ec5e06376ef139bb479eec38c16c8735821c35b895',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -114,55 +114,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        7322024,
+        6792280,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/traceconv',
     'sha256':
-        '77583ed2eefe75796a3fe2a7149d6e234c643d4f83690f732367442bbb259812'
+        '7d91e4133184a3722a25488edd3692c5a195148eba56621014311d3f85d3fc15'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        9123224,
+        8677992,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/traceconv',
     'sha256':
-        'cd93333b3d56f41949066688308a23fdd3fbbf367b03aceff711d8102e696702'
+        'c03c4a901ed23f1e20a12c98ce4556353a62bddcd260fb4d797cd29ff6c49a05'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        9868752,
+        9503704,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/traceconv',
     'sha256':
-        '9b2921ba776ad99bf2ef0d28ff6919dea5ed726a3c61a81159b9d99b39dd7b6d'
+        '704e58a7249de56aadec64d4c0d83bab0821d2c4fd77114a9b71705ff4224539'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        9382824,
+        8964488,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/traceconv',
     'sha256':
-        'c1fdbcb3c2cd15838468c3cc0ed8131a073f220c133384139aa57c4c05b2d34b'
+        'e4f07836fc2a5fb7cd997a9acc4183af7a06997d1e73aac71021af5114b921bc'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        9209856,
+        8763904,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/traceconv.exe',
     'sha256':
-        'ddef23109550784b6069b57bbac1ee627a1ab09086fdd72950965e866cfba536',
+        '084670ac28ed59a9642782a30e051735c1b7474b8cd569b9bc94c305af68290e',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/diff_test_trace_processor.py b/tools/diff_test_trace_processor.py
index a4d3261..181039f 100755
--- a/tools/diff_test_trace_processor.py
+++ b/tools/diff_test_trace_processor.py
@@ -60,6 +60,8 @@
       action='store_true',
       help='Update the expected output file with the actual result')
   parser.add_argument(
+      '--quiet', action='store_true', help='Only print if the test failed.')
+  parser.add_argument(
       '--no-colors', action='store_true', help='Print without coloring')
   parser.add_argument(
       'trace_processor', type=str, help='location of trace processor binary')
@@ -80,7 +82,8 @@
 
   test_runner = DiffTestsRunner(args.name_filter, args.trace_processor,
                                 args.trace_descriptor, args.no_colors,
-                                args.override_sql_module, args.test_dir)
+                                args.override_sql_module, args.test_dir,
+                                args.quiet)
   sys.stderr.write(f"[==========] Running {len(test_runner.tests)} tests.\n")
 
   results = test_runner.run_all_tests(args.metrics_descriptor,
diff --git a/tools/download_changed_screenshots.py b/tools/download_changed_screenshots.py
index 539da22..cc0ec4b 100755
--- a/tools/download_changed_screenshots.py
+++ b/tools/download_changed_screenshots.py
@@ -14,41 +14,67 @@
 # limitations under the License.
 
 import argparse
-import sys
+import base64
+import json
+import os
+import io
+import re
+import zipfile
 import urllib.request
 from os import path
 
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
 
 def get_artifact_url(run, name):
   return f'https://storage.googleapis.com/perfetto-ci-artifacts/{run}/ui-test-artifacts/{name}'
 
 
 def main():
+  os.chdir(ROOT_DIR)
   parser = argparse.ArgumentParser()
-  parser.add_argument('run', metavar='RUN', help='CI run identifier')
+  parser.add_argument(
+      'run',
+      metavar='RUN',
+      help='CI run identifier, e.g. ' +
+      '\'20240923144821--cls-3258215-15--ui-clang-x86_64-release\'')
   args = parser.parse_args()
-
-  with urllib.request.urlopen(get_artifact_url(args.run, 'report.txt')) as resp:
+  url = get_artifact_url(args.run, 'index.html')
+  with urllib.request.urlopen(url) as resp:
     handle_report(resp.read().decode('utf-8'), args.run)
 
 
+def sanitize(name):
+  return re.sub('[ _]', '-', name)
+
+
 def handle_report(report: str, run: str):
-  for line in report.split('\n'):
-    if len(line) == 0:
-      continue
+  m = re.findall(
+      r'playwrightReportBase64 = "data:application/zip;base64,([^"]+)"', report)
+  bin = base64.b64decode(m[0])
+  z = zipfile.ZipFile(io.BytesIO(bin))
+  report = json.loads(z.open('report.json').read().decode())
+  pngs = {}
+  for f in report['files']:
+    test_file = f['fileName'].removeprefix('test/')
+    for t in f['tests']:
+      title = sanitize(t['title'])
+      for r in t['results']:
+        for a in r['attachments']:
+          png_name = sanitize(a['name'])
+          if not png_name.endswith('-actual.png'):
+            continue
+          path = 'test/data/ui-screenshots/%s/%s/%s' % (
+              test_file, title, png_name.replace('-actual', ''))
+          pngs[path] = a['path']
 
-    parts = line.split(';')
-    if len(parts) != 2:
-      print('Erroneous report line!')
-      sys.exit(1)
+  for local_path, remote_path in pngs.items():
+    url = get_artifact_url(run, remote_path)
+    print(f'Downloading {local_path} from {url}')
+    urllib.request.urlretrieve(url, local_path)
 
-    screenshot_name = parts[0]
-    url = get_artifact_url(run, screenshot_name)
-    output_path = path.join('test', 'data', 'ui-screenshots', screenshot_name)
-    print(f'Downloading {url}')
-    urllib.request.urlretrieve(url, output_path)
   print('Done. Now run:')
-  print('./tools/test_data upload')
+  print('./tools/test_data upload  (or status)')
 
 
 if __name__ == "__main__":
diff --git a/tools/fix_include_guards b/tools/fix_include_guards
index 1848eda..8780ac9 100755
--- a/tools/fix_include_guards
+++ b/tools/fix_include_guards
@@ -22,6 +22,11 @@
 from codecs import open
 from compat import xrange
 
+EXCLUDED_FILES = [
+  'src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.h',
+  'src/trace_processor/perfetto_sql/grammar/perfettosql_grammar.h',
+]
+
 
 def fix_guards(fpath, checkonly):
   with open(fpath, 'r', encoding='utf-8') as f:
@@ -74,6 +79,8 @@
         if not name.endswith('.h'):
           continue
         fpath = os.path.join(root, name)
+        if fpath in EXCLUDED_FILES:
+          continue
         num_files_changed += fix_guards(fpath, checkonly)
   if checkonly:
     return 0 if num_files_changed == 0 else 1
diff --git a/tools/gen_amalgamated_sql.py b/tools/gen_amalgamated_sql.py
index 17fbe1e..ac7a530 100755
--- a/tools/gen_amalgamated_sql.py
+++ b/tools/gen_amalgamated_sql.py
@@ -66,8 +66,10 @@
 
 
 def filename_to_variable(filename: str):
-  return "k" + "".join(
-      [x.capitalize() for x in filename.replace(os.path.sep, '_').split("_")])
+  return "k" + "".join([
+      x.capitalize()
+      for x in filename.replace(os.path.sep, '_').replace('-', '_').split("_")
+  ])
 
 
 def main():
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index 9dbeac7..f5386dc 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -139,11 +139,17 @@
         ]
     },
     'config': {
-        'types': ['filegroup'],
+        'types': ['lite', 'filegroup'],
         'targets': [
             '//protos/perfetto/config:source_set',
         ]
     },
+    'metrics': {
+        'types': ['python'],
+        'targets': [
+            '//protos/perfetto/metrics:source_set',
+        ]
+    },
 }
 
 needs_libfts = [
@@ -382,6 +388,16 @@
     module.shared_libs.add('libz')
 
 
+def enable_expat(module):
+  if module.type == 'cc_binary_host':
+    module.static_libs.add('libexpat')
+  elif module.host_supported:
+    module.android.shared_libs.add('libexpat')
+    module.host.static_libs.add('libexpat')
+  else:
+    module.shared_libs.add('libexpat')
+
+
 def enable_uapi_headers(module):
   module.include_dirs.add('bionic/libc/kernel')
 
@@ -417,6 +433,8 @@
         enable_sqlite,
     '//gn:zlib':
         enable_zlib,
+    '//gn:expat':
+        enable_expat,
     '//gn:bionic_kernel_uapi_headers':
         enable_uapi_headers,
     '//src/profiling/memory:bionic_libc_platform_headers_on_android':
@@ -998,6 +1016,13 @@
       module.proto = {'type': 'lite', 'canonical_path_from_root': False}
       module.srcs = module_sources
       blueprint.add_module(module)
+    elif type == 'python':
+      name = label_to_module_name(module_name) + '_python_protos'
+      module = Module('python_library_host', name, name)
+      module.comment = f'''GN: [{', '.join(target_names)}]'''
+      module.proto = {'canonical_path_from_root': False}
+      module.srcs = module_sources
+      blueprint.add_module(module)
     else:
       raise Error('Unhandled proto group type: {}'.format(group.type))
 
diff --git a/tools/gen_bazel b/tools/gen_bazel
index ec48704..a30f5ca 100755
--- a/tools/gen_bazel
+++ b/tools/gen_bazel
@@ -78,7 +78,6 @@
 public_targets = [
     '//:libperfetto_client_experimental',
     '//src/perfetto_cmd:perfetto',
-    '//src/shared_lib:libperfetto_c',
     '//src/traced/probes:traced_probes',
     '//src/traced/service:traced',
     '//src/trace_processor:trace_processor_shell',
@@ -87,6 +86,11 @@
     '//src/traceconv:libpprofbuilder',
 ]
 
+# These targets will be exported with visibility only to our allowlist.
+allowlist_public_targets = [
+    '//src/shared_lib:libperfetto_c',
+]
+
 # These targets are required by internal build rules but don't need to be
 # exported publicly.
 default_targets = [
@@ -100,7 +104,7 @@
     '//src/tools/proto_merger:proto_merger',
     '//src/trace_processor/rpc:trace_processor_rpc',
     '//test:client_api_example',
-] + public_targets
+] + public_targets + allowlist_public_targets
 
 # Proto target groups which will be made public.
 proto_groups = {
@@ -144,6 +148,7 @@
 external_deps = {
     '//gn:default_deps': [],
     '//gn:base_platform': ['PERFETTO_CONFIG.deps.base_platform'],
+    '//gn:expat': ['PERFETTO_CONFIG.deps.expat'],
     '//gn:jsoncpp': ['PERFETTO_CONFIG.deps.jsoncpp'],
     '//gn:linenoise': ['PERFETTO_CONFIG.deps.linenoise'],
     '//gn:protobuf_full': ['PERFETTO_CONFIG.deps.protobuf_full'],
@@ -173,6 +178,7 @@
     '//python:experimental_slice_breakdown_bin',
     '//python:trace_processor_table_generator',
     '//python:trace_processor_py_example',
+    '//python:sql_processing',
 ]
 
 # Internal equivalents for third-party Python libraries.
@@ -711,8 +717,14 @@
   else:
     label.srcs = raw_srcs
 
-  if gn_target.name in public_targets:
+  is_public = gn_target.name in public_targets
+  is_public_for_allowlist = gn_target.name in allowlist_public_targets
+  if is_public and is_public_for_allowlist:
+    raise Error('Target %s in both public_targets and allowlist_public_targets', gn.target.name)
+  elif is_public:
     label.visibility = ['//visibility:public']
+  elif is_public_for_allowlist:
+    label.visibility = ALLOWLIST_PUBLIC_VISIBILITY
 
   if win_target:
     label.win_srcs = list(set(label.srcs) & {s[2:] for s in win_target.sources | win_target.inputs})
diff --git a/tools/gen_cc_proto_descriptor.py b/tools/gen_cc_proto_descriptor.py
index 0c2dc66..c1fddbe 100755
--- a/tools/gen_cc_proto_descriptor.py
+++ b/tools/gen_cc_proto_descriptor.py
@@ -24,7 +24,7 @@
 import textwrap
 
 
-def write_cpp_header(gendir, target, descriptor_bytes):
+def write_cpp_header(gendir, target, namespace, descriptor_bytes):
   _, target_name = os.path.split(target)
 
   proto_name = target_name[:-len('.descriptor.h')].title().replace("_", "")
@@ -66,12 +66,12 @@
 #include <stdint.h>
 #include <array>
 
-namespace perfetto {{
+namespace {namespace} {{
 
-constexpr std::array<uint8_t, {size}> k{proto_name}Descriptor{{
+inline constexpr std::array<uint8_t, {size}> k{proto_name}Descriptor{{
 {binary}}};
 
-}}  // namespace perfetto
+}}  // namespace {namespace}
 
 #endif  // {include_guard}
 """.format(
@@ -79,6 +79,7 @@
         size=len(descriptor_bytes),
         binary=binary,
         include_guard=include_guard,
+        namespace=namespace,
     ).encode())
 
 
@@ -86,12 +87,13 @@
   parser = argparse.ArgumentParser()
   parser.add_argument('--cpp_out', required=True)
   parser.add_argument('--gen_dir', default='')
+  parser.add_argument('--namespace', default='perfetto')
   parser.add_argument('descriptor')
   args = parser.parse_args()
 
   with open(args.descriptor, 'rb') as fdescriptor:
     s = fdescriptor.read()
-    write_cpp_header(args.gen_dir, args.cpp_out, s)
+    write_cpp_header(args.gen_dir, args.cpp_out, args.namespace, s)
 
   return 0
 
diff --git a/tools/gen_clickhouse_bigtrace_protos.py b/tools/gen_clickhouse_bigtrace_protos.py
new file mode 100755
index 0000000..36a5173
--- /dev/null
+++ b/tools/gen_clickhouse_bigtrace_protos.py
@@ -0,0 +1,63 @@
+#!/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 subprocess
+import os
+"""
+Compile the gRPC python code for Clickhouse for Bigtrace
+and modify the include paths to point to the correct file paths
+
+"""
+
+
+def main():
+  subprocess.run([
+      "python3",
+      "-m",
+      "grpc_tools.protoc",
+      "-I.",
+      "--python_out=python/perfetto/bigtrace_clickhouse",
+      "--pyi_out=python/perfetto/bigtrace_clickhouse",
+      "protos/perfetto/bigtrace/orchestrator.proto",
+      "protos/perfetto/trace_processor/trace_processor.proto",
+      "protos/perfetto/common/descriptor.proto",
+      "protos/perfetto/trace_processor/metatrace_categories.proto",
+  ])
+  subprocess.run([
+      "python3",
+      "-m",
+      "grpc_tools.protoc",
+      "-I.",
+      "--python_out=python/perfetto/bigtrace_clickhouse",
+      "--pyi_out=python/perfetto/bigtrace_clickhouse",
+      "--grpc_python_out=python/perfetto/bigtrace_clickhouse",
+      "protos/perfetto/bigtrace/orchestrator.proto",
+  ])
+  subprocess.run([
+      "sed",
+      "-i",
+      "-e",
+      "s/protos\.perfetto/\./",
+      "python/perfetto/bigtrace_clickhouse/protos/perfetto/bigtrace/orchestrator_pb2_grpc.py",
+      "python/perfetto/bigtrace_clickhouse/protos/perfetto/bigtrace/orchestrator_pb2.py",
+      "python/perfetto/bigtrace_clickhouse/protos/perfetto/bigtrace/orchestrator_pb2.pyi",
+      "python/perfetto/bigtrace_clickhouse/protos/perfetto/trace_processor/trace_processor_pb2.py",
+      "python/perfetto/bigtrace_clickhouse/protos/perfetto/trace_processor/trace_processor_pb2.pyi",
+  ])
+  return 0
+
+
+if __name__ == "__main__":
+  main()
diff --git a/tools/gen_stdlib_docs_json.py b/tools/gen_stdlib_docs_json.py
index 8ae6044..5edfd8f 100755
--- a/tools/gen_stdlib_docs_json.py
+++ b/tools/gen_stdlib_docs_json.py
@@ -26,10 +26,15 @@
 from python.generators.sql_processing.docs_parse import parse_file
 
 
+def _summary_desc(s: str) -> str:
+  return s.split('. ')[0].replace('\n', ' ')
+
+
 def main():
   parser = argparse.ArgumentParser()
   parser.add_argument('--json-out', required=True)
   parser.add_argument('--input-list-file')
+  parser.add_argument('--minify')
   parser.add_argument('sql_files', nargs='*')
   args = parser.parse_args()
 
@@ -64,11 +69,11 @@
 
       sql_outputs[relpath] = f.read()
 
-  modules = defaultdict(list)
+  packages = defaultdict(list)
   # Add documentation from each file
   for path, sql in sql_outputs.items():
-    module_name = path.split("/")[0]
-    import_key = path.split(".sql")[0].replace("/", ".")
+    package_name = path.split("/")[0]
+    module_name = path.split(".sql")[0].replace("/", ".")
 
     docs = parse_file(path, sql)
 
@@ -81,69 +86,81 @@
         print(e)
       return 1
 
-    file_dict = {
-        'import_key':
-            import_key,
-        'imports': [{
-            'name': table.name,
-            'desc': table.desc,
-            'summary_desc': table.desc.split('\n\n')[0].replace('\n', ' '),
-            'type': table.type,
-            'cols': {
-                col_name: {
-                    'type': col.type,
-                    'desc': col.description,
-                } for (col_name, col) in table.cols.items()
-            },
+    module_dict = {
+        'module_name':
+            module_name,
+        'data_objects': [{
+            'name':
+                table.name,
+            'desc':
+                table.desc,
+            'summary_desc':
+                _summary_desc(table.desc),
+            'type':
+                table.type,
+            'cols': [{
+                'name': col_name,
+                'type': col.type,
+                'desc': col.description
+            } for (col_name, col) in table.cols.items()]
         } for table in docs.table_views],
         'functions': [{
             'name': function.name,
             'desc': function.desc,
-            'summary_desc': function.desc.split('\n\n')[0].replace('\n', ' '),
-            'args': {
-                arg_name: {
-                    'type': arg.type,
-                    'desc': arg.description,
-                } for (arg_name, arg) in function.args.items()
-            },
+            'summary_desc': _summary_desc(function.desc),
+            'args': [{
+                'name': arg_name,
+                'type': arg.type,
+                'desc': arg.description,
+            } for (arg_name, arg) in function.args.items()],
             'return_type': function.return_type,
             'return_desc': function.return_desc,
         } for function in docs.functions],
         'table_functions': [{
-            'name': function.name,
-            'desc': function.desc,
-            'summary_desc': function.desc.split('\n\n')[0].replace('\n', ' '),
-            'args': {
-                arg_name: {
-                    'type': arg.type,
-                    'desc': arg.description,
-                } for (arg_name, arg) in function.args.items()
-            },
-            'cols': {
-                col_name: {
-                    'type': col.type,
-                    'desc': col.description,
-                } for (col_name, col) in function.cols.items()
-            },
+            'name':
+                function.name,
+            'desc':
+                function.desc,
+            'summary_desc':
+                _summary_desc(function.desc),
+            'args': [{
+                'name': arg_name,
+                'type': arg.type,
+                'desc': arg.description,
+            } for (arg_name, arg) in function.args.items()],
+            'cols': [{
+                'name': col_name,
+                'type': col.type,
+                'desc': col.description
+            } for (col_name, col) in function.cols.items()]
         } for function in docs.table_functions],
         'macros': [{
-            'name': macro.name,
-            'desc': macro.desc,
-            'summary_desc': macro.desc.split('\n\n')[0].replace('\n', ' '),
-            'return_desc': macro.return_desc,
-            'return_type': macro.return_type,
-            'args': {
-                arg_name: {
-                    'type': arg.type,
-                    'desc': arg.description,
-                } for (arg_name, arg) in macro.args.items()
-            },
+            'name':
+                macro.name,
+            'desc':
+                macro.desc,
+            'summary_desc':
+                _summary_desc(macro.desc),
+            'return_desc':
+                macro.return_desc,
+            'return_type':
+                macro.return_type,
+            'args': [{
+                'name': arg_name,
+                'type': arg.type,
+                'desc': arg.description,
+            } for (arg_name, arg) in macro.args.items()],
         } for macro in docs.macros],
     }
-    modules[module_name].append(file_dict)
+    packages[package_name].append(module_dict)
+
+  packages_list = [{
+      "name": name,
+      "modules": modules
+  } for name, modules in packages.items()]
 
   with open(args.json_out, 'w+') as f:
-    json.dump(modules, f, indent=4)
+    json.dump(packages_list, f, indent=None if args.minify else 4)
 
   return 0
 
diff --git a/tools/gen_tp_table_docs.py b/tools/gen_tp_table_docs.py
index 04339bf..c69ab58 100755
--- a/tools/gen_tp_table_docs.py
+++ b/tools/gen_tp_table_docs.py
@@ -110,6 +110,13 @@
   table_docs = []
   for parsed in util.parse_tables_from_modules(modules):
     table = parsed.table
+
+    # If there is no non-intrinsic alias for the table, don't
+    # include the table in the docs.
+    name = util.public_sql_name(table)
+    if name.startswith('__intrinsic_') or name.startswith('experimental_'):
+      continue
+
     doc = table.tabledoc
     assert doc
     cols = (
@@ -117,7 +124,7 @@
         for c in parsed.columns
         if not c.is_ancestor)
     table_docs.append({
-        'name': util.public_sql_name(table),
+        'name': name,
         'cppClassName': table.class_name,
         'defMacro': table.class_name,
         'comment': '\n'.join(l.strip() for l in doc.doc.splitlines()),
diff --git a/tools/gen_ui_imports b/tools/gen_ui_imports
index 36b3825..4b3947f 100755
--- a/tools/gen_ui_imports
+++ b/tools/gen_ui_imports
@@ -23,13 +23,13 @@
 
 This generates code like:
 
-import {pluginRegistry} from '../common/plugins';
-
 import {plugin as fooPlugin} from '../plugins/foo_plugin';
 import {plugin as barPlugin} from '../plugins/bar_plugin';
 
-pluginRegistry.register(fooPlugin);
-pluginRegistry.register(barPlugin);
+export default [
+  fooPlugin,
+  barPlugin,
+];
 """
 
 from __future__ import print_function
@@ -49,9 +49,15 @@
   return first + ''.join(x.title() for x in rest)
 
 
+def is_plugin_dir(dir):
+  # Ensure plugins contain a file called index.ts. This avoids the issue empty
+  # dirs are detected as plugins.
+  return os.path.isdir(dir) and os.path.exists(os.path.join(dir, 'index.ts'))
+
+
 def gen_imports(input_dir, output_path):
   paths = [os.path.join(input_dir, p) for p in os.listdir(input_dir)]
-  paths = [p for p in paths if os.path.isdir(p)]
+  paths = [p for p in paths if is_plugin_dir(p)]
   paths.sort()
 
   output_dir = os.path.dirname(output_path)
@@ -60,17 +66,20 @@
   imports = []
   registrations = []
   for path in paths:
+    # Get out if 
     rel_path = os.path.relpath(path, output_dir)
     snake_name = os.path.basename(path)
     camel_name = to_camel_case(snake_name)
-    imports.append(f"import {{plugin as {camel_name}}} from '{rel_path}';")
-    registrations.append(f"pluginRegistry.register({camel_name});")
+    imports.append(f"import {camel_name} from '{rel_path}';")
+    registrations.append(camel_name)
 
-  header = f"import {{pluginRegistry}} from '{rel_plugins_path}';"
   import_text = '\n'.join(imports)
-  registration_text = '\n'.join(registrations)
+  registration_text = 'export default [\n'
+  for camel_name in registrations:
+    registration_text += f"  {camel_name},\n"
+  registration_text += '];\n'
 
-  expected = f"{header}\n\n{import_text}\n\n{registration_text}\n"
+  expected = f"{import_text}\n\n{registration_text}"
 
   with open(output_path, 'w') as f:
     f.write(expected)
diff --git a/tools/gn_utils.py b/tools/gn_utils.py
index 9e32c91..eb1a5c7 100644
--- a/tools/gn_utils.py
+++ b/tools/gn_utils.py
@@ -444,7 +444,8 @@
       target.proto_plugin = proto_target_type
       target.proto_paths.update(self.get_proto_paths(proto_desc))
       target.proto_exports.update(self.get_proto_exports(proto_desc))
-      target.sources.update(proto_desc.get('sources', []))
+      target.sources.update(
+          self.get_proto_sources(proto_target_type, proto_desc))
       assert (all(x.endswith('.proto') for x in target.sources))
     elif target.type == 'source_set':
       self.source_sets[gn_target_name] = target
@@ -536,6 +537,12 @@
     metadata = proto_desc.get('metadata', {})
     return metadata.get('proto_import_dirs', [])
 
+  def get_proto_sources(self, proto_target_type, proto_desc):
+    if proto_target_type == 'source_set':
+      metadata = proto_desc.get('metadata', {})
+      return metadata.get('proto_library_sources', [])
+    return proto_desc.get('sources', [])
+
   def get_proto_target_type(
       self, target: Target) -> Tuple[Optional[str], Optional[Dict]]:
     """ Checks if the target is a proto library and return the plugin.
diff --git a/tools/heap_profile b/tools/heap_profile
index e94d216..df14a90 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 v47.0
+# This file has been generated by: tools/roll-prebuilts v48.1
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        9481408,
+        9041560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/traceconv',
     'sha256':
-        'b6819bb922438d585e816646c4d60e43bfa823d5f3f499bd8efcaccd26a9009c',
+        'cec2da5cb771a4812d0b2d15604d5023954d28e0af12e87313da2ab70d26b970',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -55,11 +55,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8852520,
+        8375512,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/traceconv',
     'sha256':
-        '9e9eac795578f6ed76128127d8a81c1ce5620b3d47f2c5e23c1f7f2ebbff9bea',
+        '64e200a58ea9c9f366e1071dd274d0023d1fd14043f75dbba3fe0cc138ff5fc7',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -69,11 +69,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        9538432,
+        9134136,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/traceconv',
     'sha256':
-        '01881a82050f36b8db427c741ce236360bb86548e6a6c9445b2477c6150de05b',
+        '87b87e1778367c1e3b99fc77439a28b4911125d2751f9909fd1b51f6bd60b6f4',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -83,11 +83,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7286504,
+        6753020,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/traceconv',
     'sha256':
-        'bc700f945c78c4a65a60bdb499c6c59e671c5173f45f42976fd0661396b72c16',
+        '804c4e13aca5798731056952d9cb0c6ee58795c03477c69514ccd39703060812',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -97,11 +97,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        9168408,
+        8740064,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/traceconv',
     'sha256':
-        'd0192785a56c5088811a175ab083bb599aab5fd594a3248057380038f0b2b5c2',
+        '0d781886531d11e1d573a1ec5e06376ef139bb479eec38c16c8735821c35b895',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -111,55 +111,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        7322024,
+        6792280,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/traceconv',
     'sha256':
-        '77583ed2eefe75796a3fe2a7149d6e234c643d4f83690f732367442bbb259812'
+        '7d91e4133184a3722a25488edd3692c5a195148eba56621014311d3f85d3fc15'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        9123224,
+        8677992,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/traceconv',
     'sha256':
-        'cd93333b3d56f41949066688308a23fdd3fbbf367b03aceff711d8102e696702'
+        'c03c4a901ed23f1e20a12c98ce4556353a62bddcd260fb4d797cd29ff6c49a05'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        9868752,
+        9503704,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/traceconv',
     'sha256':
-        '9b2921ba776ad99bf2ef0d28ff6919dea5ed726a3c61a81159b9d99b39dd7b6d'
+        '704e58a7249de56aadec64d4c0d83bab0821d2c4fd77114a9b71705ff4224539'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        9382824,
+        8964488,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/traceconv',
     'sha256':
-        'c1fdbcb3c2cd15838468c3cc0ed8131a073f220c133384139aa57c4c05b2d34b'
+        'e4f07836fc2a5fb7cd997a9acc4183af7a06997d1e73aac71021af5114b921bc'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        9209856,
+        8763904,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/traceconv.exe',
     'sha256':
-        'ddef23109550784b6069b57bbac1ee627a1ab09086fdd72950965e866cfba536',
+        '084670ac28ed59a9642782a30e051735c1b7474b8cd569b9bc94c305af68290e',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/install-build-deps b/tools/install-build-deps
index 3367a7a..7b2b8d7 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -257,6 +257,15 @@
         'all',
         'all'),
 
+    # Libexpat for Instruments XML import.
+    # If updating the version, also update bazel/deps.bzl.
+    Dependency(
+        'buildtools/expat/src',
+        'https://chromium.googlesource.com/external/github.com/libexpat/libexpat.git',
+        'fa75b96546c069d17b8f80d91e0f4ef0cde3790d',  # refs/tags/upstream/R_2_6_2.
+        'all',
+        'all'),
+
     # Archive with only the demangling sources from llvm-project.
     # See tools/repackage_llvm_demangler.sh on how to update this.
     # File suffix is the git reference to the commit at which we rearchived the
@@ -426,11 +435,17 @@
 ]
 
 # Dependencies to build gRPC.
-GRPC_DEPS = [
+BIGTRACE_DEPS = [
     Dependency(
         'buildtools/grpc/src',
         'https://chromium.googlesource.com/external/github.com/grpc/grpc.git',
         '4795c5e69b25e8c767b498bea784da0ef8c96fd5', 'all', 'all', True),
+    Dependency(
+      'buildtools/cpp-httplib',
+      'https://github.com/yhirose/cpp-httplib.git',
+      '6c3e8482f7b4e3b307bb42afbb85fd8771da86b8',
+      'all', 'all', True
+    )
 ]
 
 # Sysroots required to cross-compile Linux targets (linux-arm{,64}).
@@ -707,8 +722,9 @@
     deps += BUILD_DEPS_LINUX_CROSS_SYSROOTS
   if args.ui:
     deps += UI_DEPS
+  # TODO(b/360084012) Change the arg name to bigtrace
   if args.grpc:
-    deps += GRPC_DEPS
+    deps += BIGTRACE_DEPS
   deps_updated = False
   nodejs_updated = False
 
diff --git a/tools/java_heap_dump b/tools/java_heap_dump
index 36e6428..379a771 100755
--- a/tools/java_heap_dump
+++ b/tools/java_heap_dump
@@ -237,7 +237,7 @@
       "--buffer-size",
       help="Buffer size in memory that store the whole java heap graph. N(kb|mb|gb)",
       type=str,
-      default="100024kb")
+      default="256mb")
   parser.add_argument(
       "-c",
       "--continuous-dump",
diff --git a/tools/measure_tp_performance.py b/tools/measure_tp_performance.py
index 8014c39..4e51d6a 100755
--- a/tools/measure_tp_performance.py
+++ b/tools/measure_tp_performance.py
@@ -32,6 +32,8 @@
   tp_args = [os.path.join(args.out, 'trace_processor_shell'), args.trace_file]
   if not args.ftrace_raw:
     tp_args.append('--no-ftrace-raw')
+  tp_args.append('--dev')
+  tp_args.append('--dev-flag drop-after-sort=true')
   tp = subprocess.Popen(
       tp_args,
       stdin=subprocess.PIPE,
@@ -123,7 +125,6 @@
 def only_sort_run(args):
   env = {
       'TRACE_PROCESSOR_NO_MMAP': '1',
-      'TRACE_PROCESSOR_SORT_ONLY': '1',
   }
   (tp, fail, time) = run_tp_until_ingestion(args, env)
 
diff --git a/tools/record_android_trace b/tools/record_android_trace
index 8f0d20e..203b636 100755
--- a/tools/record_android_trace
+++ b/tools/record_android_trace
@@ -25,6 +25,7 @@
 import os
 import re
 import shutil
+import signal
 import socketserver
 import subprocess
 import sys
@@ -33,18 +34,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
-# This file has been generated by: tools/roll-prebuilts v47.0
+# This file has been generated by: tools/roll-prebuilts v48.1
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1597456,
+        1613864,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/tracebox',
     'sha256':
-        '1e4b56533ad59e8131473ae6d4204356288a7b7a92241e303ab9865842d36c1d',
+        'dfb1a3affe905d2e7d1f82bc4dda46b1fda6db054d60ae87c3215dd529b77fee',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -54,11 +55,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1475640,
+        1492184,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/tracebox',
     'sha256':
-        '8eae02034fa45581bd7262d1e3095616cc4f9a06a1bc0345cb5cae1277d8b4e4',
+        '4a492a629dd1f13f3146c4b8267c0b163afba8cef1d49e0c00c48bb727496066',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -68,11 +69,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2351336,
+        2380040,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/tracebox',
     'sha256':
-        '0a533702f1ddf80998aaf3e95ce2ee8b154bfcf010c87bb740be6d04ac2e7380',
+        'd70b284e8c28858fd539ae61ca59764d7f9fd6232073c304926e892fe75e692a',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -82,11 +83,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1433188,
+        1450708,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/tracebox',
     'sha256':
-        'd346f0ef77211230dd1f61284badb8edf4736852d446b36bb3d3e52a195934e4',
+        '178fa6a1a9bc80f72d81938d40fe201c25c595ffaff7e030d59c2af09dfcc06c',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -96,11 +97,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2245088,
+        2269816,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/tracebox',
     'sha256':
-        '7899b352ead70894a0cce25cd47db81229804daa168c9b18760003ae2068d3b0',
+        '42c64f9807756aaa08a2bfa13e9e4828c193a6b90ba1329408873c3ebf5adf3f',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -110,44 +111,44 @@
     'file_name':
         'tracebox',
     'file_size':
-        1323304,
+        1333336,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/tracebox',
     'sha256':
-        '727bfbab060aeaf8e97bdef45f318d28c9e7452f91a7135311aff81f72a02fe7'
+        '93a78d2c42e3c00f117e2f155326383f69c891281ed693a39d87b8cb54ca4e19'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        2101880,
+        2115984,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/tracebox',
     'sha256':
-        'ca9f2bbcc6fda0f8b2915e7c6b3d113a0a0ec256da14edcdb3ae4ffe69b4f2cb'
+        '508248a9e47ab605fd742efb700391d7267b68b586199a93e13e6ca14b72fe3d'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        2282928,
+        2302960,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/tracebox',
     'sha256':
-        'ffddf5dcdbe72419a610e7218908a96352b1a6b4fa27cd333aeab34f80a47fc1'
+        '63d20a69c4e0c291329d7917e640fa0d4f146c344e79988e87393b1431d594b1'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'tracebox',
     'file_size':
-        2131400,
+        2147880,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/tracebox',
     'sha256':
-        'defba9ba1730c2583da87326096448cd7445271254392cd8f250e2fde0b54456'
+        'c0ea1d5fd6d020e4c2b45d4d45cdd0c44ae63cd755d69260a3e5d2bacd3cbd6a'
 }]
 
 # ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
@@ -502,6 +503,18 @@
   return args
 
 
+class SignalException(Exception):
+  pass
+
+
+def signal_handler(sig, frame):
+  raise SignalException('Received signal ' + str(sig))
+
+
+signal.signal(signal.SIGINT, signal_handler)
+signal.signal(signal.SIGTERM, signal_handler)
+
+
 def start_trace(args, print_log=True):
   perfetto_cmd = 'perfetto'
   device_dir = '/data/misc/perfetto-traces/'
@@ -663,7 +676,7 @@
         prt('Too many unrecoverable ADB failures, bailing out', ANSI.RED)
         sys.exit(1)
       time.sleep(2)
-    except KeyboardInterrupt:
+    except (KeyboardInterrupt, SignalException):
       sig = 'TERM' if ctrl_c_count == 0 else 'KILL'
       ctrl_c_count += 1
       if print_log:
diff --git a/tools/setup_minikube_cluster.sh b/tools/setup_minikube_cluster.sh
index ae7d19e..7bde652 100755
--- a/tools/setup_minikube_cluster.sh
+++ b/tools/setup_minikube_cluster.sh
@@ -15,7 +15,7 @@
 
 set -e
 
-cd src/bigtrace
+cd infra/bigtrace/docker
 
 minikube start
 eval $(minikube docker-env)
diff --git a/tools/test_data b/tools/test_data
index c638fa2..1442ae9 100755
--- a/tools/test_data
+++ b/tools/test_data
@@ -129,9 +129,9 @@
   if args.dry_run:
     return 0
   if not args.quiet:
-    print('About to upload %d files:' % len(files_to_upload))
     print('\n'.join(relpath(f.path) for f in files_to_upload))
     print('')
+    print('About to upload %d files' % len(files_to_upload))
     input('Press a key to continue or CTRL-C to abort')
 
   def upload_one_file(fs):
@@ -150,6 +150,27 @@
   return 0
 
 
+def cmd_clean(dir):
+  all_files = list_files(dir, scan_new_files=True)
+  files_to_clean = []
+  for fs in ThreadPool(args.jobs).imap_unordered(get_file_status, all_files):
+    if fs.status in (FS_NEW_FILE, FS_MODIFIED):
+      files_to_clean.append(fs.path)
+  if len(files_to_clean) == 0:
+    if not args.quiet:
+      print('No modified or new files require cleaning')
+    return 0
+  if args.dry_run:
+    return 0
+  if not args.quiet:
+    print('\n'.join(relpath(f) for f in files_to_clean))
+    print('')
+    print('About to remove %d files' % len(files_to_clean))
+    input('Press a key to continue or CTRL-C to abort')
+  list(map(os.remove, files_to_clean))
+  return 0
+
+
 def cmd_download(dir, overwrite_locally_modified=False):
   files_to_download = []
   modified = []
@@ -237,7 +258,7 @@
   parser.add_argument('--quiet', '-q', action='store_true')
   parser.add_argument('--verbose', '-v', action='store_true')
   parser.add_argument('--ignore-new', action='store_true')
-  parser.add_argument('cmd', choices=['status', 'download', 'upload'])
+  parser.add_argument('cmd', choices=['status', 'download', 'upload', 'clean'])
   global args
   args = parser.parse_args()
   logging.basicConfig(
@@ -250,6 +271,8 @@
     return cmd_download(args.dir, overwrite_locally_modified=args.overwrite)
   if args.cmd == 'upload':
     return cmd_upload(args.dir)
+  if args.cmd == 'clean':
+    return cmd_clean(args.dir)
   print('Unknown command: %s' % args.cmd)
 
 
diff --git a/tools/trace_processor b/tools/trace_processor
index d651a13..29e6186 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 v47.0
+# This file has been generated by: tools/roll-prebuilts v48.1
 TRACE_PROCESSOR_SHELL_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        10209056,
+        9949656,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/trace_processor_shell',
     'sha256':
-        '203c4c7a3621ee7c60a3d558613216427aa0f7245dc34fbe27e03cbcaf15cbd7',
+        'e9dcf95aaa02f8c00a724f0ff34ba3a454c717beb9900cf9fd97ab142b362452',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -51,11 +51,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9518360,
+        9223224,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/trace_processor_shell',
     'sha256':
-        '02130db81f477e795f0fb33e5183eed6d9350057346d730fe30aac5a6443d9c1',
+        '9a0541a0f52f95bfcb8dc88d94bc4494c660d95eefc40fc946ab43d995051ff7',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        10363488,
+        10142800,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/trace_processor_shell',
     'sha256':
-        '832425c3c7934904d1e0ec1721beb51423de7dbcf399a899973f2b6b464603fa',
+        '18c8730b52f8ee1d9e202031527435b6b2e3149fbd9b1046b2e77d18f06aa337',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        7682608,
+        7329432,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/trace_processor_shell',
     'sha256':
-        '0d5e41279051326311b178c73289d6027493bdd8627f537e538aa39a6f74af81',
+        '0558040998666576e1063d6d626b8aa9e354f18d73d225240f043b3c9236befa',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9949744,
+        9703384,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/trace_processor_shell',
     'sha256':
-        '2a9e5f6ee3d9a0d6007fc5503a9358629d7b3881233ee6fbe157edaa0f5a3b1b',
+        'eeb95cc54358df08375ffae4862c043a6737902179ce8e0408984004c32cf93c',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -107,55 +107,55 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        7716332,
+        7367412,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/trace_processor_shell',
     'sha256':
-        'a3a1f49448e72c368748cb6ec0cb1f63ba4fe5598ff08118053dac68916b9433'
+        'd29b1e6aee52ceff24c072f56c7be7795d0fa29f3596e2633fafa60782384718'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9861544,
+        9598784,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/trace_processor_shell',
     'sha256':
-        '49c9f94802986b9cada8ffab9ec911f21a416966a7c0b2acc3e467f03892ec56'
+        '06e80c562c0043cca9225ade3c961a081bcc7435660117d5a6db26b815d0b9ca'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        10805720,
+        10625488,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x86/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/trace_processor_shell',
     'sha256':
-        'b188a7d95533a26b9eadcc5233d9fcc8552f94c0c7224a7f39f5e6eaebd7e981'
+        '2a576fb397da14d0dabcfa97f5eeec15b4dc55df009308f75a5fdf9de8a9b0dd'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        10150016,
+        9915664,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/trace_processor_shell',
     'sha256':
-        'ff83eb7f53fc91d42c8756bb752fd70ed1f03b40a9daba99a6843c391bc8ff66'
+        'a30be9f09b53110394e87af4d6b41ae24cd74d9a3f97ac1cc4d6ae2057ac6977'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'trace_processor_shell.exe',
     'file_size':
-        10187264,
+        9922560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/windows-amd64/trace_processor_shell.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/trace_processor_shell.exe',
     'sha256':
-        'f9b39c21a99f412697b4bf59f7046f80482c9f07dc3507c2d448dda02915aa14',
+        'd41639844a6c36dbaa195d91e9c356f2172d924c70a1bfed5432c407f857f009',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/tracebox b/tools/tracebox
index b3ed784..9389051 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 v47.0
+# This file has been generated by: tools/roll-prebuilts v48.1
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1597456,
+        1613864,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/tracebox',
     'sha256':
-        '1e4b56533ad59e8131473ae6d4204356288a7b7a92241e303ab9865842d36c1d',
+        'dfb1a3affe905d2e7d1f82bc4dda46b1fda6db054d60ae87c3215dd529b77fee',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -51,11 +51,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1475640,
+        1492184,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/tracebox',
     'sha256':
-        '8eae02034fa45581bd7262d1e3095616cc4f9a06a1bc0345cb5cae1277d8b4e4',
+        '4a492a629dd1f13f3146c4b8267c0b163afba8cef1d49e0c00c48bb727496066',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2351336,
+        2380040,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/tracebox',
     'sha256':
-        '0a533702f1ddf80998aaf3e95ce2ee8b154bfcf010c87bb740be6d04ac2e7380',
+        'd70b284e8c28858fd539ae61ca59764d7f9fd6232073c304926e892fe75e692a',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1433188,
+        1450708,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/tracebox',
     'sha256':
-        'd346f0ef77211230dd1f61284badb8edf4736852d446b36bb3d3e52a195934e4',
+        '178fa6a1a9bc80f72d81938d40fe201c25c595ffaff7e030d59c2af09dfcc06c',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2245088,
+        2269816,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/tracebox',
     'sha256':
-        '7899b352ead70894a0cce25cd47db81229804daa168c9b18760003ae2068d3b0',
+        '42c64f9807756aaa08a2bfa13e9e4828c193a6b90ba1329408873c3ebf5adf3f',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -107,44 +107,44 @@
     'file_name':
         'tracebox',
     'file_size':
-        1323304,
+        1333336,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/tracebox',
     'sha256':
-        '727bfbab060aeaf8e97bdef45f318d28c9e7452f91a7135311aff81f72a02fe7'
+        '93a78d2c42e3c00f117e2f155326383f69c891281ed693a39d87b8cb54ca4e19'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        2101880,
+        2115984,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/tracebox',
     'sha256':
-        'ca9f2bbcc6fda0f8b2915e7c6b3d113a0a0ec256da14edcdb3ae4ffe69b4f2cb'
+        '508248a9e47ab605fd742efb700391d7267b68b586199a93e13e6ca14b72fe3d'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        2282928,
+        2302960,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/tracebox',
     'sha256':
-        'ffddf5dcdbe72419a610e7218908a96352b1a6b4fa27cd333aeab34f80a47fc1'
+        '63d20a69c4e0c291329d7917e640fa0d4f146c344e79988e87393b1431d594b1'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'tracebox',
     'file_size':
-        2131400,
+        2147880,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/tracebox',
     'sha256':
-        'defba9ba1730c2583da87326096448cd7445271254392cd8f250e2fde0b54456'
+        'c0ea1d5fd6d020e4c2b45d4d45cdd0c44ae63cd755d69260a3e5d2bacd3cbd6a'
 }]
 
 # ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
diff --git a/tools/traceconv b/tools/traceconv
index dca907c..55805b0 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 v47.0
+# This file has been generated by: tools/roll-prebuilts v48.1
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        9481408,
+        9041560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/traceconv',
     'sha256':
-        'b6819bb922438d585e816646c4d60e43bfa823d5f3f499bd8efcaccd26a9009c',
+        'cec2da5cb771a4812d0b2d15604d5023954d28e0af12e87313da2ab70d26b970',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -51,11 +51,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8852520,
+        8375512,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/traceconv',
     'sha256':
-        '9e9eac795578f6ed76128127d8a81c1ce5620b3d47f2c5e23c1f7f2ebbff9bea',
+        '64e200a58ea9c9f366e1071dd274d0023d1fd14043f75dbba3fe0cc138ff5fc7',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        9538432,
+        9134136,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/traceconv',
     'sha256':
-        '01881a82050f36b8db427c741ce236360bb86548e6a6c9445b2477c6150de05b',
+        '87b87e1778367c1e3b99fc77439a28b4911125d2751f9909fd1b51f6bd60b6f4',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7286504,
+        6753020,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/traceconv',
     'sha256':
-        'bc700f945c78c4a65a60bdb499c6c59e671c5173f45f42976fd0661396b72c16',
+        '804c4e13aca5798731056952d9cb0c6ee58795c03477c69514ccd39703060812',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        9168408,
+        8740064,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/traceconv',
     'sha256':
-        'd0192785a56c5088811a175ab083bb599aab5fd594a3248057380038f0b2b5c2',
+        '0d781886531d11e1d573a1ec5e06376ef139bb479eec38c16c8735821c35b895',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -107,55 +107,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        7322024,
+        6792280,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/traceconv',
     'sha256':
-        '77583ed2eefe75796a3fe2a7149d6e234c643d4f83690f732367442bbb259812'
+        '7d91e4133184a3722a25488edd3692c5a195148eba56621014311d3f85d3fc15'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        9123224,
+        8677992,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/traceconv',
     'sha256':
-        'cd93333b3d56f41949066688308a23fdd3fbbf367b03aceff711d8102e696702'
+        'c03c4a901ed23f1e20a12c98ce4556353a62bddcd260fb4d797cd29ff6c49a05'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        9868752,
+        9503704,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/traceconv',
     'sha256':
-        '9b2921ba776ad99bf2ef0d28ff6919dea5ed726a3c61a81159b9d99b39dd7b6d'
+        '704e58a7249de56aadec64d4c0d83bab0821d2c4fd77114a9b71705ff4224539'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        9382824,
+        8964488,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/traceconv',
     'sha256':
-        'c1fdbcb3c2cd15838468c3cc0ed8131a073f220c133384139aa57c4c05b2d34b'
+        'e4f07836fc2a5fb7cd997a9acc4183af7a06997d1e73aac71021af5114b921bc'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        9209856,
+        8763904,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v47.0/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/traceconv.exe',
     'sha256':
-        'ddef23109550784b6069b57bbac1ee627a1ab09086fdd72950965e866cfba536',
+        '084670ac28ed59a9642782a30e051735c1b7474b8cd569b9bc94c305af68290e',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/update_sql_parsers.py b/tools/update_sql_parsers.py
new file mode 100755
index 0000000..137d95d
--- /dev/null
+++ b/tools/update_sql_parsers.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+# 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.
+
+import argparse
+import os
+import subprocess
+import shutil
+import sys
+import tempfile
+
+GRAMMAR_FOOTER = '''
+%token SPACE ILLEGAL.
+'''
+
+KEYWORDHASH_HEADER = '''
+#include "src/trace_processor/perfetto_sql/grammar/perfettosql_keywordhash_helper.h"
+'''
+
+KEYWORD_END = '''
+  { "WITHOUT",          "TK_WITHOUT",      ALWAYS,           1      },
+};'''
+
+KEYWORD_END_REPLACE = '''
+  { "WITHOUT",          "TK_WITHOUT",      ALWAYS,           1      },
+  { "PERFETTO",         "TK_PERFETTO",     ALWAYS,           1      },
+  { "MACRO",            "TK_MACRO",        ALWAYS,           1      },
+  { "INCLUDE",          "TK_INCLUDE",      ALWAYS,           1      },
+  { "MODULE",           "TK_MODULE",       ALWAYS,           1      },
+  { "RETURNS",          "TK_RETURNS",      ALWAYS,           1      },
+  { "FUNCTION",         "TK_FUNCTION",     ALWAYS,           1      },
+};'''
+
+
+def copy_tokenizer(args: argparse.Namespace):
+  shutil.copy(args.sqlite_tokenize, args.sqlite_tokenize_out)
+
+  with open(args.sqlite_tokenize_out, 'r+', encoding='utf-8') as fp:
+    res: str = fp.read()
+    idx = res.find('/*\n** Run the parser on the given SQL string.')
+    assert idx != -1
+    res = res[0:idx]
+    res = res.replace(
+        '#include "sqliteInt.h"',
+        '#include "src/trace_processor/perfetto_sql/tokenizer/tokenize_internal_helper.h"',
+    )
+    res = res.replace('#include "keywordhash.h"\n', '')
+    fp.seek(0)
+    fp.write(res)
+    fp.truncate()
+
+
+def main():
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      '--lemon', default=os.path.normpath('buildtools/sqlite_src/tool/lemon.c'))
+  parser.add_argument(
+      '--mkkeywordhash',
+      default=os.path.normpath('buildtools/sqlite_src/tool/mkkeywordhash.c'))
+  parser.add_argument(
+      '--lemon-template',
+      default=os.path.normpath('buildtools/sqlite_src/tool/lempar.c'))
+  parser.add_argument(
+      '--clang', default=os.path.normpath('buildtools/linux64/clang/bin/clang'))
+  parser.add_argument(
+      '--preprocessor-grammar',
+      default=os.path.normpath(
+          'src/trace_processor/perfetto_sql/preprocessor/preprocessor_grammar.y'
+      ),
+  )
+  parser.add_argument(
+      '--sqlite-grammar',
+      default=os.path.normpath('buildtools/sqlite_src/src/parse.y'),
+  )
+  parser.add_argument(
+      '--perfettosql-grammar-include',
+      default=os.path.normpath(
+          'src/trace_processor/perfetto_sql/grammar/perfettosql_include.y'),
+  )
+  parser.add_argument(
+      '--grammar-out',
+      default=os.path.join(
+          os.path.normpath('src/trace_processor/perfetto_sql/grammar/')),
+  )
+  parser.add_argument(
+      '--sqlite-tokenize',
+      default=os.path.normpath('buildtools/sqlite_src/src/tokenize.c'),
+  )
+  parser.add_argument(
+      '--sqlite-tokenize-out',
+      default=os.path.join(
+          os.path.normpath(
+              'src/trace_processor/perfetto_sql/tokenizer/tokenize_internal.c')
+      ),
+  )
+  args = parser.parse_args()
+
+  with tempfile.TemporaryDirectory() as tmp:
+    # Preprocessor grammar
+    subprocess.check_call([
+        args.clang,
+        os.path.join(args.lemon), '-o',
+        os.path.join(tmp, 'lemon')
+    ])
+    shutil.copy(args.lemon_template, tmp)
+    subprocess.check_call([
+        os.path.join(tmp, 'lemon'),
+        args.preprocessor_grammar,
+        '-q',
+        '-l',
+        '-s',
+    ])
+
+    # PerfettoSQL keywords
+    keywordhash_tmp = os.path.join(tmp, 'mkkeywordhash.c')
+    shutil.copy(args.mkkeywordhash, keywordhash_tmp)
+
+    with open(keywordhash_tmp, "r+") as fp:
+      keyword_source = fp.read()
+      assert keyword_source.find(KEYWORD_END) != -1
+      fp.seek(0)
+      fp.write(keyword_source.replace(KEYWORD_END, KEYWORD_END_REPLACE))
+      fp.truncate()
+
+    subprocess.check_call([
+        args.clang,
+        os.path.join(keywordhash_tmp), '-o',
+        os.path.join(tmp, 'mkkeywordhash')
+    ])
+    keywordhash_res = subprocess.check_output(
+        [os.path.join(tmp, 'mkkeywordhash')]).decode()
+
+    with open(os.path.join(args.grammar_out, "perfettosql_keywordhash.h"),
+              "w") as g:
+      idx = keywordhash_res.find('#define SQLITE_N_KEYWORD')
+      assert idx != -1
+      keywordhash_res = keywordhash_res[0:idx]
+      g.write(KEYWORDHASH_HEADER)
+      g.write(keywordhash_res)
+
+    # PerfettoSQL grammar
+    sqlite_grammar = subprocess.check_output([
+        os.path.join(tmp, 'lemon'),
+        args.sqlite_grammar,
+        '-g',
+    ]).decode()
+    with open(os.path.join(args.grammar_out, "perfettosql_grammar.y"),
+              "w") as g:
+      with open(args.perfettosql_grammar_include, 'r') as i:
+        g.write(i.read())
+      g.write(sqlite_grammar)
+      g.write(GRAMMAR_FOOTER)
+    subprocess.check_call([
+        os.path.join(tmp, 'lemon'),
+        os.path.join(args.grammar_out, "perfettosql_grammar.y"),
+        '-q',
+        '-l',
+        '-s',
+    ])
+
+  copy_tokenizer(args)
+
+  return 0
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/ui/.gitignore b/ui/.gitignore
index b4e1aa5..1d0a09a 100644
--- a/ui/.gitignore
+++ b/ui/.gitignore
@@ -1,3 +1,4 @@
 /node_modules/
 /out
 /src/gen
+/playwright/.cache/
diff --git a/ui/build.js b/ui/build.js
index 2df02c3..1079dff 100644
--- a/ui/build.js
+++ b/ui/build.js
@@ -122,10 +122,6 @@
   {r: /buildtools\/catapult_trace_viewer\/(.+(js|html))/, f: copyAssets},
   {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss},
   {r: /ui\/src\/chrome_extension\/.*/, f: copyExtensionAssets},
-  {
-    r: /ui\/src\/test\/diff_viewer\/(.+[.](?:html|js))/,
-    f: copyUiTestArtifactsAssets,
-  },
   {r: /.*\/dist\/.+\/(?!manifest\.json).*/, f: genServiceWorkerManifestJson},
   {r: /.*\/dist\/.*[.](js|html|css|wasm)$/, f: notifyLiveServer},
 ];
@@ -152,7 +148,6 @@
   parser.add_argument('--no-build', '-n', {action: 'store_true'});
   parser.add_argument('--no-wasm', '-W', {action: 'store_true'});
   parser.add_argument('--run-unittests', '-t', {action: 'store_true'});
-  parser.add_argument('--run-integrationtests', '-T', {action: 'store_true'});
   parser.add_argument('--debug', '-d', {action: 'store_true'});
   parser.add_argument('--bigtrace', {action: 'store_true'});
   parser.add_argument('--open-perfetto-trace', {action: 'store_true'});
@@ -255,13 +250,13 @@
     buildWasm(args.no_wasm);
     scanDir('ui/src/assets');
     scanDir('ui/src/chrome_extension');
-    scanDir('ui/src/test/diff_viewer');
     scanDir('buildtools/typefaces');
     scanDir('buildtools/catapult_trace_viewer');
     generateImports('ui/src/core_plugins', 'all_core_plugins.ts');
     generateImports('ui/src/plugins', 'all_plugins.ts');
     compileProtos();
     genVersion();
+    generateStdlibDocs();
 
     const tsProjects = [
       'ui',
@@ -316,9 +311,6 @@
   if (args.run_unittests) {
     runTests('jest.unittest.config.js');
   }
-  if (args.run_integrationtests) {
-    runTests('jest.integrationtest.config.js');
-  }
 }
 
 // -----------
@@ -462,6 +454,25 @@
   addTask(exec, [cmd, args]);
 }
 
+function generateStdlibDocs() {
+  const cmd = pjoin(ROOT_DIR, 'tools/gen_stdlib_docs_json.py');
+  const stdlibDir = pjoin(ROOT_DIR, 'src/trace_processor/perfetto_sql/stdlib');
+
+  const stdlibFiles =
+    listFilesRecursive(stdlibDir)
+    .filter((filePath) => path.extname(filePath) === '.sql');
+
+  addTask(exec, [
+    cmd,
+    [
+      '--json-out',
+      pjoin(cfg.outDistDir, 'stdlib_docs.json'),
+      '--minify',
+      ...stdlibFiles,
+    ],
+  ]);
+}
+
 function updateSymlinks() {
   // /ui/out -> /out/ui.
   mklink(cfg.outUiDir, pjoin(ROOT_DIR, 'ui/out'));
@@ -575,9 +586,10 @@
 }
 
 function startServer() {
+  const host = cfg.httpServerListenHost == '127.0.0.1' ? 'localhost' : cfg.httpServerListenHost;
   console.log(
       'Starting HTTP server on',
-      `http://${cfg.httpServerListenHost}:${cfg.httpServerListenPort}`);
+      `http://${host}:${cfg.httpServerListenPort}`);
   http.createServer(function(req, res) {
         console.debug(req.method, req.url);
         let uri = req.url.split('?', 1)[0];
@@ -831,6 +843,18 @@
   }
 }
 
+// Recursively build a list of files in a given directory and return a list of
+// file paths, similar to `find -type f`.
+function listFilesRecursive(dir) {
+  const fileList = [];
+
+  walk(dir, (filePath) => {
+    fileList.push(filePath);
+  });
+
+  return fileList;
+}
+
 function ensureDir(dirPath, clean) {
   const exists = fs.existsSync(dirPath);
   if (exists && clean) {
diff --git a/ui/config/integrationtest_env.js b/ui/config/integrationtest_env.js
deleted file mode 100644
index 0108278..0000000
--- a/ui/config/integrationtest_env.js
+++ /dev/null
@@ -1,65 +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.
-
-const NodeEnvironment = require('jest-environment-node').default;
-const puppeteer = require('puppeteer');
-
-module.exports = class IntegrationtestEnvironment extends NodeEnvironment {
-  constructor(config) {
-    super(config);
-  }
-
-  async setup() {
-    await super.setup();
-    const headless = process.env.PERFETTO_UI_TESTS_INTERACTIVE !== '1';
-    if (headless) {
-      console.log('Starting Perfetto UI tests in headless mode.');
-      console.log(
-          'Pass --interactive to run-integrationtests or set ' +
-          'PERFETTO_UI_TESTS_INTERACTIVE=1 to inspect the behavior ' +
-          'in a visible Chrome window');
-    }
-    this.global.__BROWSER__ = await puppeteer.launch({
-      args: [
-        '--window-size=1920,1080',
-        '--disable-accelerated-2d-canvas',
-        '--disable-gpu',
-        '--no-sandbox',  // Disable sandbox to run in Docker.
-        '--disable-setuid-sandbox',
-        '--font-render-hinting=none',
-        '--enable-benchmarking',  // Disable finch and other sources of non
-                                  // determinism.
-      ],
-
-      // This is so screenshot in --interactive and headless mode match. The
-      // scrollbars are never part of the screenshot, but without this cmdline
-      // switch, in headless mode we don't get any blank space (as if it was
-      // overflow:hidden) and that changes the layout of the page.
-      ignoreDefaultArgs: ['--hide-scrollbars'],
-
-      headless: headless,
-    });
-  }
-
-  async teardown() {
-    if (this.global.__BROWSER__) {
-      await this.global.__BROWSER__.close();
-    }
-    await super.teardown();
-  }
-
-  runScript(script) {
-    return super.runScript(script);
-  }
-};
diff --git a/ui/config/integrationtest_setup.js b/ui/config/integrationtest_setup.js
deleted file mode 100644
index 7dc1873..0000000
--- a/ui/config/integrationtest_setup.js
+++ /dev/null
@@ -1,56 +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.
-
-const path = require('path');
-const http = require('http');
-const childProcess = require('child_process');
-
-module.exports = async function() {
-  // Start the local HTTP server.
-  const ROOT_DIR = path.dirname(path.dirname(__dirname));
-  const node = path.join(ROOT_DIR, 'ui', 'node');
-  const args = [
-    path.join(ROOT_DIR, 'ui', 'build.js'),
-    '--serve',
-    '--no-build',
-    '--out=.',
-  ];
-  const spwOpts = {stdio: ['ignore', 'inherit', 'inherit']};
-  const srvProc = childProcess.spawn(node, args, spwOpts);
-  global.__DEV_SERVER__ = srvProc;
-
-  // Wait for the HTTP server to be ready.
-  let attempts = 10;
-  for (; attempts > 0; attempts--) {
-    await new Promise((r) => setTimeout(r, 1000));
-    try {
-      await new Promise((resolve, reject) => {
-        const req = http.request('http://127.0.0.1:10000/frontend_bundle.js');
-        req.end();
-        req.on('error', (err) => reject(err));
-        req.on('finish', () => resolve());
-      });
-      break;
-    } catch (err) {
-      console.error('Waiting for HTTP server to come up', err.message);
-    }
-  }
-  if (attempts === 0) {
-    throw new Error('HTTP server didn\'t come up');
-  }
-  if (srvProc.exitCode !== null) {
-    throw new Error(
-        `The dev server unexpectedly exited, code=${srvProc.exitCode}`);
-  }
-};
diff --git a/ui/config/integrationtest_teardown.js b/ui/config/integrationtest_teardown.js
deleted file mode 100644
index 4cfbc84..0000000
--- a/ui/config/integrationtest_teardown.js
+++ /dev/null
@@ -1,24 +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.
-
-module.exports = async function() {
-  const proc = global.__DEV_SERVER__;
-  // Kill the HTTP server.
-  proc.kill();
-  for (;;) {
-    if (proc.exitCode !== null || proc.killed) break;
-    console.log('Waiting for dev server termination');
-    await new Promise((r) => setTimeout(r, 1000));
-  }
-};
diff --git a/ui/config/jest.integrationtest.config.js b/ui/config/jest.integrationtest.config.js
deleted file mode 100644
index 9f58c3d..0000000
--- a/ui/config/jest.integrationtest.config.js
+++ /dev/null
@@ -1,21 +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.
-
-module.exports = {
-  transform: {},
-  testRegex: '.*_integrationtest.js$',
-  globalSetup: __dirname + '/integrationtest_setup.js',
-  globalTeardown: __dirname + '/integrationtest_teardown.js',
-  testEnvironment: __dirname + '/integrationtest_env.js',
-};
diff --git a/ui/config/rollup.config.js b/ui/config/rollup.config.js
index daec62e..6984d91 100644
--- a/ui/config/rollup.config.js
+++ b/ui/config/rollup.config.js
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-
-const {uglify} = require('rollup-plugin-uglify')
+const {uglify} = require('rollup-plugin-uglify');
 const commonjs = require('@rollup/plugin-commonjs');
 const nodeResolve = require('@rollup/plugin-node-resolve');
 const path = require('path');
@@ -61,12 +60,16 @@
       sourcemaps(),
     ].concat(maybeUglify()),
     onwarn: function (warning, warn) {
-      // Ignore circular dependency warnings coming from third party code.
-      if (
-        warning.code === 'CIRCULAR_DEPENDENCY' &&
-        warning.message.includes('node_modules')
-      ) {
-        return;
+      if (warning.code === 'CIRCULAR_DEPENDENCY') {
+        // Ignore circular dependency warnings coming from third party code.
+        if (warning.message.includes('node_modules')) {
+          return;
+        }
+
+        // Treat all other circular dependency warnings as errors.
+        throw new Error(
+          `Circular dependency detected in ${warning.importer}:\n\n  ${warning.cycle.join('\n  ')}`,
+        );
       }
 
       // Call the default warning handler for all remaining warnings.
diff --git a/ui/package.json b/ui/package.json
index 8a0a289..600775f 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -31,6 +31,7 @@
     "devtools-protocol": "0.0.1319565",
     "esbuild": "^0.21.5",
     "events": "^3.3.0",
+    "fzf": "^0.5.2",
     "hsluv": "^0.1.0",
     "immer": "^10.1.1",
     "jsbn-rsa": "^1.0.4",
@@ -48,10 +49,10 @@
   "devDependencies": {
     "@eslint/eslintrc": "^3.1.0",
     "@eslint/js": "^9.6.0",
+    "@playwright/test": "^1.47.0",
     "@rollup/plugin-commonjs": "^26.0.1",
     "@rollup/plugin-node-resolve": "^15.2.3",
     "@types/jest": "^29.5.12",
-    "@types/pixelmatch": "^5.2.6",
     "@typescript-eslint/eslint-plugin": "^7.14.1",
     "@typescript-eslint/parser": "^7.14.1",
     "dingusjs": "^0.0.3",
@@ -64,7 +65,6 @@
     "jest-canvas-mock": "^2.5.2",
     "jest-environment-jsdom": "^29.7.0",
     "jest-localstorage-mock": "^2.4.26",
-    "pixelmatch": "^5.3.0",
     "pngjs": "^7.0.0",
     "prettier": "^3.3.2",
     "puppeteer": "^22.12.1",
diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts
new file mode 100644
index 0000000..4f9a8c3
--- /dev/null
+++ b/ui/playwright.config.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 {defineConfig} from '@playwright/test';
+import * as os from 'os';
+
+const isMac = os.platform() === 'darwin';
+const isCi = Boolean(process.env.CI);
+const outDir = process.env.OUT_DIR ?? '../out/ui';
+
+// Installed by test/ci/ui_tests.sh
+const ciChromePath = '/ci/ramdisk/chrome/opt/google/chrome/google-chrome';
+
+export default defineConfig({
+  testDir: './src',
+  snapshotDir: '../test/data/ui-screenshots',
+  snapshotPathTemplate: '{snapshotDir}/{testFileName}/{testName}/{arg}{ext}',
+  outputDir: `${outDir}/ui-test-results`,
+  fullyParallel: true,
+  retries: isCi ? 2 : 0, // Retry only in CI
+  workers: isCi ? 1 : undefined, // No parallelism in CI.
+  reporter: [
+    [
+      'html',
+      {
+        outputFolder: `${outDir}/ui-test-artifacts`,
+        open: isCi ? 'never' : 'on-failure',
+      },
+    ],
+  ],
+
+  expect: {
+    timeout: 5000,
+    toHaveScreenshot: {
+      // Rendering is not 100% identical on Mac. Be more tolerant.
+      maxDiffPixelRatio: isMac ? 0.05 : undefined,
+    },
+  },
+
+  use: {
+    baseURL: 'http://127.0.0.1:10000',
+    trace: 'off',
+  },
+
+  projects: [
+    {
+      name: 'chromium',
+      use: {
+        headless: true,
+        viewport: {width: 1920, height: 1080},
+        launchOptions: {
+          executablePath: isCi ? ciChromePath : undefined,
+          args: [
+            '--disable-accelerated-2d-canvas',
+            '--disable-font-subpixel-positioning',
+            '--disable-gpu',
+            '--disable-lcd-text',
+            '--font-render-hinting=none',
+            '--force-device-scale-factor=1',
+            '--hide-scrollbars',
+            '--enable-skia-renderer',
+            '--js-flags=--random-seed=1',
+          ],
+        },
+        ignoreHTTPSErrors: true,
+        trace: 'off',
+        screenshot: 'on',
+        channel: 'chrome',
+        video: 'off',
+      },
+    },
+  ],
+
+  webServer: {
+    command: './run-dev-server ' + (process.env.DEV_SERVER_ARGS ?? ''),
+    url: 'http://127.0.0.1:10000',
+    reuseExistingServer: true,
+    timeout: 5 * 60 * 1000,
+    stdout: 'pipe',
+  },
+});
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 1feed7a..636e138 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -77,6 +77,9 @@
   events:
     specifier: ^3.3.0
     version: 3.3.0
+  fzf:
+    specifier: ^0.5.2
+    version: 0.5.2
   hsluv:
     specifier: ^0.1.0
     version: 0.1.0
@@ -124,6 +127,9 @@
   '@eslint/js':
     specifier: ^9.6.0
     version: 9.6.0
+  '@playwright/test':
+    specifier: ^1.47.0
+    version: 1.47.0
   '@rollup/plugin-commonjs':
     specifier: ^26.0.1
     version: 26.0.1(rollup@2.79.1)
@@ -133,9 +139,6 @@
   '@types/jest':
     specifier: ^29.5.12
     version: 29.5.12
-  '@types/pixelmatch':
-    specifier: ^5.2.6
-    version: 5.2.6
   '@typescript-eslint/eslint-plugin':
     specifier: ^7.14.1
     version: 7.14.1(@typescript-eslint/parser@7.14.1)(eslint@9.6.0)(typescript@5.5.2)
@@ -172,9 +175,6 @@
   jest-localstorage-mock:
     specifier: ^2.4.26
     version: 2.4.26
-  pixelmatch:
-    specifier: ^5.3.0
-    version: 5.3.0
   pngjs:
     specifier: ^7.0.0
     version: 7.0.0
@@ -1454,6 +1454,14 @@
     engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
     dev: true
 
+  /@playwright/test@1.47.0:
+    resolution: {integrity: sha512-SgAdlSwYVpToI4e/IH19IHHWvoijAYH5hu2MWSXptRypLSnzj51PcGD+rsOXFayde4P9ZLi+loXVwArg6IUkCA==}
+    engines: {node: '>=18'}
+    hasBin: true
+    dependencies:
+      playwright: 1.47.0
+    dev: true
+
   /@popperjs/core@2.11.8:
     resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
     dev: false
@@ -1753,12 +1761,6 @@
     resolution: {integrity: sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==}
     dev: false
 
-  /@types/pixelmatch@5.2.6:
-    resolution: {integrity: sha512-wC83uexE5KGuUODn6zkm9gMzTwdY5L0chiK+VrKcDfEjzxh1uadlWTvOmAbCpnM9zx/Ww3f8uKlYQVnO/TrqVg==}
-    dependencies:
-      '@types/node': 20.14.9
-    dev: true
-
   /@types/pngjs@6.0.5:
     resolution: {integrity: sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==}
     dependencies:
@@ -3240,6 +3242,14 @@
   /fs.realpath@1.0.0:
     resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
 
+  /fsevents@2.3.2:
+    resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
   /fsevents@2.3.3:
     resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -3251,6 +3261,10 @@
   /function-bind@1.1.1:
     resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
 
+  /fzf@0.5.2:
+    resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==}
+    dev: false
+
   /gensync@1.0.0-beta.2:
     resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
     engines: {node: '>=6.9.0'}
@@ -4802,13 +4816,6 @@
     engines: {node: '>= 6'}
     dev: true
 
-  /pixelmatch@5.3.0:
-    resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==}
-    hasBin: true
-    dependencies:
-      pngjs: 6.0.0
-    dev: true
-
   /pkg-dir@4.2.0:
     resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
     engines: {node: '>=8'}
@@ -4816,9 +4823,20 @@
       find-up: 4.1.0
     dev: true
 
-  /pngjs@6.0.0:
-    resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==}
-    engines: {node: '>=12.13.0'}
+  /playwright-core@1.47.0:
+    resolution: {integrity: sha512-1DyHT8OqkcfCkYUD9zzUTfg7EfTd+6a8MkD/NWOvjo0u/SCNd5YmY/lJwFvUZOxJbWNds+ei7ic2+R/cRz/PDg==}
+    engines: {node: '>=18'}
+    hasBin: true
+    dev: true
+
+  /playwright@1.47.0:
+    resolution: {integrity: sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww==}
+    engines: {node: '>=18'}
+    hasBin: true
+    dependencies:
+      playwright-core: 1.47.0
+    optionalDependencies:
+      fsevents: 2.3.2
     dev: true
 
   /pngjs@7.0.0:
diff --git a/ui/release/channels.json b/ui/release/channels.json
index 75a3d6b..c2c76c2 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -2,11 +2,11 @@
   "channels": [
     {
       "name": "stable",
-      "rev": "206f403988fb603720111dcabb14f38f6ebc1a54"
+      "rev": "1b47763032cf6112de5efdacdf46eee6ec77611b"
     },
     {
       "name": "canary",
-      "rev": "f098f373db79c554c96aaecb68997a798ee4e86f"
+      "rev": "4817ff8af4289f905c36a8a1ba6a583afc569af4"
     },
     {
       "name": "autopush",
diff --git a/ui/run-integrationtests b/ui/run-integrationtests
index abcbf4a..3118e18 100755
--- a/ui/run-integrationtests
+++ b/ui/run-integrationtests
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env python3
 # Copyright (C) 2021 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,6 +13,49 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-UI_DIR="$(cd -P ${BASH_SOURCE[0]%/*}; pwd)"
+import argparse
+import os
+import sys
 
-$UI_DIR/node $UI_DIR/build.js --run-integrationtests "$@"
+UI_DIR = os.path.dirname(__file__)
+
+
+def main():
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      '--interactive',
+      '-i',
+      action='store_true',
+      help='Run in interactive mode')
+  parser.add_argument(
+      '--rebaseline', '-r', action='store_true', help='Rebaseline screenshots')
+  parser.add_argument('--out', help='out directory')
+  parser.add_argument('--no-build', action='store_true')
+  parser.add_argument('filters', nargs='*')
+  args = parser.parse_args()
+
+  cmd = ['./pnpm', 'exec', 'playwright', 'test']
+  if args.interactive:
+    if args.rebaseline:
+      print('--interactive and --rebaseline are mutually exclusive')
+      return 1
+    cmd += ['--ui']
+  elif args.rebaseline:
+    cmd += ['--update-snapshots']
+  cmd += args.filters
+
+  env = dict(os.environ.items())
+  dev_server_args = []
+  if args.out:
+    out_rel_path = os.path.relpath(args.out, UI_DIR)
+    env['OUT_DIR'] = out_rel_path
+    dev_server_args += ['--out', out_rel_path]
+  if args.no_build:
+    dev_server_args += ['--no-build']
+  env['DEV_SERVER_ARGS'] = ' '.join(dev_server_args)
+  os.chdir(UI_DIR)
+  os.execve(cmd[0], cmd, env)
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 26a03a0..14f3fd7 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -27,8 +27,6 @@
   --selection-fill-color: #8398e64d;
   --overview-timeline-non-visible-color: #c8c8c8cc;
   --details-content-height: 280px;
-  --collapsed-background: hsla(190, 49%, 97%, 1);
-  --expanded-background: hsl(215, 22%, 19%);
 }
 
 @mixin transition($time: 0.1s) {
@@ -157,24 +155,6 @@
   grid-area: page;
 }
 
-.alerts {
-  grid-area: alerts;
-  background-color: #f2f2f2;
-  > div {
-    font-family: "Roboto", sans-serif;
-    font-weight: 400;
-    letter-spacing: 0.25px;
-    padding: 1rem;
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    button {
-      width: 24px;
-      height: 24px;
-    }
-  }
-}
-
 @mixin table-font-size {
   font-size: 14px;
   line-height: 18px;
@@ -195,6 +175,10 @@
 
   thead {
     font-weight: normal;
+
+    td.reorderable-cell {
+      cursor: grab;
+    }
   }
 
   tr:hover td {
@@ -215,6 +199,10 @@
       // density a little bit.
       font-size: 16px;
     }
+
+    has-left-border {
+      border-left: 1px solid rgba(60, 76, 92, 0.4);
+    }
   }
 }
 
@@ -233,9 +221,6 @@
     border-left: 1px solid $table-border-color;
     padding-left: 6px;
   }
-  thead td.reorderable-cell {
-    cursor: grab;
-  }
   .disabled {
     cursor: default;
   }
diff --git a/ui/src/assets/panel_container.scss b/ui/src/assets/panel_container.scss
index 32a4ce5..0500294 100644
--- a/ui/src/assets/panel_container.scss
+++ b/ui/src/assets/panel_container.scss
@@ -28,13 +28,5 @@
       inset: 0; // Shorthand for [top, left, right, bottom]: 0
       pointer-events: none; // Make this overlay invisible to pointer events
     }
-
-    .pf-panel {
-      &.pf-sticky {
-        position: sticky;
-        top: 0;
-        z-index: 1;
-      }
-    }
   }
 }
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index fdf03d2..3e04c42 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -16,7 +16,6 @@
 @import "common";
 @import "panel_container";
 @import "viewer_page";
-@import "track_panel";
 @import "home_page";
 @import "query_page";
 @import "metrics_page";
@@ -37,6 +36,7 @@
 @import "widgets/button";
 @import "widgets/callout";
 @import "widgets/checkbox";
+@import "widgets/chip";
 @import "widgets/details_shell";
 @import "widgets/editor";
 @import "widgets/empty_state";
@@ -45,7 +45,9 @@
 @import "widgets/form";
 @import "widgets/grid_layout";
 @import "widgets/hotkey";
+@import "widgets/icon";
 @import "widgets/menu";
+@import "widgets/middle_ellipsis";
 @import "widgets/multiselect";
 @import "widgets/popup";
 @import "widgets/section";
@@ -57,6 +59,7 @@
 @import "widgets/text_input";
 @import "widgets/text_paragraph";
 @import "widgets/timestamp";
+@import "widgets/track_widget";
 @import "widgets/tree";
 @import "widgets/treetable";
 @import "widgets/vega_view";
diff --git a/ui/src/assets/plugins_page.scss b/ui/src/assets/plugins_page.scss
index 0312323..69288f3 100644
--- a/ui/src/assets/plugins_page.scss
+++ b/ui/src/assets/plugins_page.scss
@@ -31,6 +31,11 @@
     font-size: 28px;
   }
 
+  .restart_needed {
+    margin: 16px 0;
+    color: #ef6c00;
+  }
+
   .pf-plugins-grid {
     display: inline-grid;
     grid-template-columns: repeat(6, auto);
diff --git a/ui/src/assets/query_page.scss b/ui/src/assets/query_page.scss
index f87281c..2903240 100644
--- a/ui/src/assets/query_page.scss
+++ b/ui/src/assets/query_page.scss
@@ -20,6 +20,16 @@
     height: 0;
     min-height: 3rem;
     overflow-y: auto;
-    resize: vertical;
+    position: relative;
+
+    .resize-handler {
+      display: block;
+      width: 100%;
+      height: 5px;
+      z-index: 2;
+      position: absolute;
+      bottom: 0;
+      cursor: row-resize;
+    }
   }
 }
diff --git a/ui/src/assets/topbar.scss b/ui/src/assets/topbar.scss
index d3ae7ef..2cce56f 100644
--- a/ui/src/assets/topbar.scss
+++ b/ui/src/assets/topbar.scss
@@ -236,21 +236,6 @@
   font-family: "Roboto Condensed", sans-serif;
 }
 
-.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;
-  box-shadow: 1px 3px 15px rgba(23, 32, 44, 0.3);
-}
-
 .hint-text {
   padding-bottom: 5px;
 }
diff --git a/ui/src/assets/track_panel.scss b/ui/src/assets/track_panel.scss
deleted file mode 100644
index dc44a9f..0000000
--- a/ui/src/assets/track_panel.scss
+++ /dev/null
@@ -1,272 +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";
-
-@mixin track_shell_title() {
-  font-size: 14px;
-  max-height: 30px;
-  overflow: hidden;
-  text-align: left;
-  overflow-wrap: break-word;
-  font-family: "Roboto Condensed", sans-serif;
-  font-weight: 300;
-  line-break: anywhere;
-}
-
-.track-content.pf-track-content-error {
-  // Necessary trig because we have a 45deg stripes
-  $pattern-density: 1px * math.sqrt(2);
-  $pattern-col: #ddd;
-
-  // box-shadow: inset 0 0 0 5px red;
-  background: repeating-linear-gradient(
-    -45deg,
-    $pattern-col,
-    $pattern-col $pattern-density,
-    white $pattern-density,
-    white $pattern-density * 2
-  );
-}
-
-.track {
-  display: grid;
-  grid-template-columns: auto 1fr;
-  grid-template-rows: 1fr 0;
-  container-type: size;
-
-  &::after {
-    display: block;
-    content: "";
-    grid-column: 1 / span 2;
-    border-top: 1px solid var(--track-border-color);
-    margin-top: -1px;
-    z-index: 2;
-  }
-
-  .track-shell {
-    @include transition();
-    cursor: grab;
-    width: var(--track-shell-width);
-    border-right: 1px solid #c7d0db;
-
-    .track-menubar {
-      position: sticky;
-      top: 0;
-      display: grid;
-      padding-block: 6px;
-      padding-left: 10px;
-      padding-right: 2px;
-      grid-template-areas: "title buttons";
-      grid-template-columns: 1fr auto;
-    }
-
-    .pf-visible-on-hover {
-      visibility: hidden;
-      &.pf-active {
-        visibility: visible;
-      }
-    }
-
-    &:hover .pf-visible-on-hover {
-      visibility: visible;
-    }
-
-    &.drag {
-      background-color: #eee;
-      box-shadow: 0 4px 12px -4px #999 inset;
-    }
-    &.drop-before {
-      box-shadow: 0 4px 2px -1px hsl(213, 40%, 50%) inset;
-    }
-    &.drop-after {
-      box-shadow: 0 -4px 2px -1px hsl(213, 40%, 50%) inset;
-    }
-
-    &.selected {
-      background-color: #ebeef9;
-    }
-
-    .chip {
-      background-color: #bed6ff;
-      border-radius: $pf-border-radius;
-      font-size: smaller;
-      padding: 0 0.1rem;
-      margin-left: 1ch;
-      white-space: nowrap;
-    }
-
-    h1 {
-      grid-area: title;
-      color: hsl(213, 22%, 30%);
-      @include track_shell_title();
-    }
-    .track-buttons {
-      grid-area: buttons;
-      display: flex;
-      height: 100%;
-      align-items: center;
-      font-size: 18px;
-    }
-
-    &.flash {
-      background-color: #ffe263;
-    }
-  }
-}
-
-.track-group-panel {
-  display: grid;
-  grid-template-columns: auto 1fr;
-  grid-template-rows: 1fr;
-  height: 40px;
-
-  .shell {
-    border-right: 1px solid transparent;
-    padding-right: 2px;
-  }
-
-  &::after {
-    display: block;
-    content: "";
-    grid-column: 1 / span 2;
-    border-top: 1px solid var(--track-border-color);
-    margin-top: -1px;
-  }
-  &[collapsed="true"] {
-    background-color: var(--collapsed-background);
-    .shell {
-      border-right: 1px solid #c7d0db;
-    }
-    .track-button {
-      color: rgb(60, 86, 136);
-    }
-  }
-  &[collapsed="false"] {
-    background-color: var(--expanded-background);
-    color: white;
-    font-weight: bold;
-    .shell.flash {
-      color: #121212;
-    }
-    .track-button {
-      color: white;
-    }
-    span.chip {
-      color: #121212;
-    }
-  }
-  .shell {
-    padding-left: 10px;
-    display: grid;
-    grid-template-areas: "fold-button title buttons";
-    grid-template-columns: 28px 1fr auto;
-    align-items: center;
-    line-height: 1;
-    width: var(--track-shell-width);
-    min-height: 40px;
-
-    .track-title {
-      user-select: text;
-    }
-
-    .track-subtitle {
-      font-size: 0.6rem;
-      font-weight: normal;
-      overflow: hidden;
-      white-space: nowrap;
-      text-overflow: ellipsis;
-      // Maximum width according to grid-template-columns value for .shell
-      width: calc(var(--track-shell-width) - 56px);
-    }
-
-    .chip {
-      background-color: #bed6ff;
-      border-radius: $pf-border-radius;
-      font-size: smaller;
-      padding: 0 0.1rem;
-      margin-left: 1ch;
-      white-space: nowrap;
-    }
-
-    .title-wrapper {
-      grid-area: title;
-      overflow: hidden;
-    }
-    h1 {
-      @include track_shell_title();
-    }
-    .fold-button {
-      grid-area: fold-button;
-    }
-
-    .track-buttons {
-      grid-area: buttons;
-      display: flex;
-      height: 100%;
-      align-items: center;
-      font-size: 18px;
-    }
-
-    &.pf-clickable {
-      cursor: pointer;
-    }
-    &:hover {
-      .fold-button {
-        color: hsl(45, 100%, 48%);
-      }
-    }
-    &.flash {
-      background-color: #ffe263;
-    }
-    &.selected {
-      background-color: #ebeef9;
-    }
-  }
-  .track-content {
-    display: grid;
-    span {
-      @include track_shell_title();
-      align-self: center;
-    }
-  }
-}
-
-.pf-track-details-dropdown {
-  max-width: 400px;
-}
-
-.pf-panel-group {
-  .track-shell {
-    .track-menubar {
-      top: 40px;
-    }
-  }
-}
-
-// If the track is short, center the track titlebar vertically
-@container (height < 26px) {
-  .track {
-    .track-shell {
-      display: flex;
-      flex-direction: column;
-      align-items: stretch;
-      justify-content: center;
-
-      .track-menubar {
-        padding-block: 0px;
-      }
-    }
-  }
-}
diff --git a/ui/src/assets/viewer_page.scss b/ui/src/assets/viewer_page.scss
index 73d82a6..56a8e5b 100644
--- a/ui/src/assets/viewer_page.scss
+++ b/ui/src/assets/viewer_page.scss
@@ -94,4 +94,27 @@
   .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;
+    box-shadow: 1px 3px 15px rgba(23, 32, 44, 0.3);
+  }
+}
+
+.pf-track-crash-popup {
+  font-family: $pf-font;
+  max-width: 300px;
+  display: flex;
+  flex-direction: column;
+  row-gap: 6px;
 }
diff --git a/ui/src/assets/widgets/anchor.scss b/ui/src/assets/widgets/anchor.scss
index 2894928..60aa5a0 100644
--- a/ui/src/assets/widgets/anchor.scss
+++ b/ui/src/assets/widgets/anchor.scss
@@ -28,9 +28,7 @@
     background $pf-anim-timing;
 
   & > .material-icons {
-    // For some reason, floating this icon results in the most pleasing vertical
-    // alignment.
-    float: right;
+    vertical-align: bottom;
     margin: 0 0 0 0px;
     font-size: inherit;
     line-height: inherit;
diff --git a/ui/src/assets/widgets/chip.scss b/ui/src/assets/widgets/chip.scss
new file mode 100644
index 0000000..3ee1704
--- /dev/null
+++ b/ui/src/assets/widgets/chip.scss
@@ -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 "theme";
+
+.pf-chip {
+  font-family: $pf-font;
+  line-height: 1;
+  user-select: none;
+  border-radius: $pf-border-radius;
+  padding: 2px 8px;
+  white-space: nowrap;
+  min-width: max-content;
+  border: solid 1px $pf-minimal-border;
+
+  &.pf-rounded {
+    border-radius: 100px;
+  }
+
+  & > .pf-left-icon {
+    float: left;
+    margin-right: 6px; // Make some room between the icon and label
+  }
+
+  & > .pf-right-icon {
+    float: right;
+    margin-left: 6px; // Make some room between the icon and label
+  }
+
+  & > .material-icons,
+  & > .material-icons-filled {
+    font-size: inherit;
+    line-height: inherit;
+  }
+
+  &:focus-visible {
+    @include focus;
+  }
+
+  background: $pf-minimal-background;
+  color: inherit;
+
+  &[disabled] {
+    color: $pf-minimal-foreground-disabled;
+    background: $pf-minimal-background-disabled;
+    cursor: not-allowed;
+  }
+
+  // Remove default background in minimal mode, showing only the text
+  &.pf-intent-primary {
+    color: $pf-primary-foreground;
+    background: $pf-primary-background;
+    border: solid 1px $pf-primary-border;
+
+    &[disabled] {
+      background: $pf-primary-background-disabled;
+      color: $pf-primary-foreground-disabled;
+      box-shadow: none;
+      cursor: not-allowed;
+    }
+  }
+
+  // Reduce padding when compact
+  &.pf-compact {
+    padding: 0px 4px;
+    & > .pf-left-icon {
+      margin-right: 2px;
+    }
+
+    & > .pf-right-icon {
+      margin-left: 2px;
+    }
+  }
+
+  // Reduce padding when we are icon-only
+  &.pf-icon-only {
+    & > .pf-left-icon {
+      margin: 0;
+    }
+
+    padding: 4px;
+
+    &.pf-compact {
+      padding: 0;
+    }
+  }
+}
+
+.pf-chip-bar {
+  display: flex;
+  flex-direction: row;
+  gap: 2px;
+}
diff --git a/ui/src/assets/widgets/editor.scss b/ui/src/assets/widgets/editor.scss
index 44ee524..46f39aa 100644
--- a/ui/src/assets/widgets/editor.scss
+++ b/ui/src/assets/widgets/editor.scss
@@ -19,5 +19,10 @@
 
   .cm-editor {
     height: 100%;
+
+    .cm-scroller {
+      font-family: var(--monospace-font);
+      font-size: 13px;
+    }
   }
 }
diff --git a/ui/src/assets/widgets/icon.scss b/ui/src/assets/widgets/icon.scss
new file mode 100644
index 0000000..0512405
--- /dev/null
+++ b/ui/src/assets/widgets/icon.scss
@@ -0,0 +1,24 @@
+// 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 "../typefaces";
+
+.pf-icon {
+  @include icon;
+  &.pf-filled {
+    @include icon-filled;
+  }
+  line-height: 1;
+  font-size: inherit;
+}
diff --git a/ui/src/assets/widgets/middle_ellipsis.scss b/ui/src/assets/widgets/middle_ellipsis.scss
new file mode 100644
index 0000000..8709f68
--- /dev/null
+++ b/ui/src/assets/widgets/middle_ellipsis.scss
@@ -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.
+
+.pf-middle-ellipsis {
+  display: flex;
+  flex-direction: row;
+  overflow: hidden;
+
+  .pf-middle-ellipsis-left {
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+
+  .pf-middle-ellipsis-right {
+    overflow: hidden;
+    white-space: nowrap;
+    flex-shrink: 0;
+  }
+
+  .pf-middle-ellipsis-extras {
+    overflow: hidden;
+    white-space: nowrap;
+    flex-shrink: 0;
+  }
+}
diff --git a/ui/src/assets/widgets/popup.scss b/ui/src/assets/widgets/popup.scss
index 6d26981..6d18d47 100644
--- a/ui/src/assets/widgets/popup.scss
+++ b/ui/src/assets/widgets/popup.scss
@@ -21,6 +21,13 @@
   // When width = 0 it can cause layout issues in popup content, so we give this
   // element some width manually
   width: 100%;
+
+  // Move the portal to the top of the page. This appears to fix issues where
+  // popups can sometimes be rendered below the rest of the page momentarily,
+  // causing the whole page to judder up and down while popper.js sorts out the
+  // positioning.
+  // TODO(stevegolton): There is probably a better way to fix this issue.
+  top: 0;
 }
 
 .pf-popup {
diff --git a/ui/src/assets/widgets/theme.scss b/ui/src/assets/widgets/theme.scss
index cdb451b..7b298c6 100644
--- a/ui/src/assets/widgets/theme.scss
+++ b/ui/src/assets/widgets/theme.scss
@@ -28,6 +28,7 @@
 
 $pf-primary-foreground: #fff;
 $pf-primary-foreground-disabled: #aaa;
+$pf-primary-border: #31466f;
 $pf-primary-background: #3d5688;
 $pf-primary-background-hover: #4966a2;
 $pf-primary-background-active: #243e71;
@@ -35,6 +36,7 @@
 
 $pf-minimal-foreground: #19212b;
 $pf-minimal-foreground-disabled: #aaa;
+$pf-minimal-border: #aaa;
 $pf-minimal-background: none;
 $pf-minimal-background-hover: #0001;
 $pf-minimal-background-active: #0002;
diff --git a/ui/src/assets/widgets/track_widget.scss b/ui/src/assets/widgets/track_widget.scss
new file mode 100644
index 0000000..6b45ef9
--- /dev/null
+++ b/ui/src/assets/widgets/track_widget.scss
@@ -0,0 +1,165 @@
+// 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: 1fr auto; // title, buttons
+      padding: 1px 2px;
+      gap: 2px;
+
+      h1.pf-track-title {
+        // Override h1 formatting
+        font-size: inherit;
+        margin: inherit;
+
+        display: flex;
+        flex-direction: row;
+        gap: 2px;
+        overflow: hidden;
+        margin-left: 2px;
+
+        // The chevron icon used is very thin - take some empty space out of
+        // left & right
+        .pf-icon {
+          margin-inline: -0.2em;
+        }
+
+        .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%);
+        }
+      }
+
+      .pf-track-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);
+
+    &.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/array_buffer_builder.ts b/ui/src/base/array_buffer_builder.ts
index 551f07f..4dfa188 100644
--- a/ui/src/base/array_buffer_builder.ts
+++ b/ui/src/base/array_buffer_builder.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import {length as utf8Len, write as utf8Write} from '@protobufjs/utf8';
-
 import {assertTrue} from '../base/logging';
 import {isString} from '../base/object_utils';
 
diff --git a/ui/src/base/assets.ts b/ui/src/base/assets.ts
new file mode 100644
index 0000000..7eee18e
--- /dev/null
+++ b/ui/src/base/assets.ts
@@ -0,0 +1,33 @@
+// 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 {getServingRoot} from './http_utils';
+
+let rootUrl = '';
+
+/**
+ * This function must be called once while bootstrapping in a direct script
+ * context (i.e. not a promise or callback). Typically frontend/index.ts.
+ */
+export function initAssets() {
+  rootUrl = getServingRoot();
+}
+
+/**
+ * Returns the absolute url of an asset.
+ * assetSrc('assets/image.jpg') -> '/v123-deadbef/assets/image.png';
+ */
+export function assetSrc(relPath: string) {
+  return rootUrl + relPath;
+}
diff --git a/ui/src/base/canvas/bezier_arrow.ts b/ui/src/base/canvas/bezier_arrow.ts
new file mode 100644
index 0000000..c5f0a60
--- /dev/null
+++ b/ui/src/base/canvas/bezier_arrow.ts
@@ -0,0 +1,207 @@
+// 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 {Point2D, Vector2D} from '../geom';
+import {assertUnreachable} from '../logging';
+
+export type CardinalDirection = 'north' | 'south' | 'east' | 'west';
+
+export type ArrowHeadOrientation =
+  | CardinalDirection
+  | 'auto_vertical' // Either north or south depending on the location of the other end of the arrow
+  | 'auto_horizontal' // Either east or west depending on the location of the other end of the arrow
+  | 'auto'; // Choose the closest cardinal direction depending on the location of the other end of the arrow
+
+export type ArrowHeadShape = 'none' | 'triangle' | 'circle';
+
+export interface ArrowHeadStyle {
+  orientation: ArrowHeadOrientation;
+  shape: ArrowHeadShape;
+  size?: number;
+}
+
+/**
+ * Renders an curved arrow using a bezier curve.
+ *
+ * This arrow is comprised of a line and the arrow caps are filled shapes, so
+ * the arrow's colour and width will be dictated by the current canvas
+ * strokeStyle, lineWidth, and fillStyle, so adjust these accordingly before
+ * calling this function.
+ *
+ * @param ctx - The canvas to draw on.
+ * @param start - Start point of the arrow.
+ * @param end - End point of the arrow.
+ * @param controlPointOffset - The distance in pixels of the control points from
+ * the start and end points, in the direction of the start and end orientation
+ * values above.
+ * @param startStyle - The style of the start of the arrow.
+ * @param endStyle - The style of the end of the arrow.
+ */
+export function drawBezierArrow(
+  ctx: CanvasRenderingContext2D,
+  start: Point2D,
+  end: Point2D,
+  controlPointOffset: number = 30,
+  startStyle: ArrowHeadStyle = {
+    shape: 'none',
+    orientation: 'auto',
+  },
+  endStyle: ArrowHeadStyle = {
+    shape: 'none',
+    orientation: 'auto',
+  },
+): void {
+  const startOri = getOri(start, end, startStyle.orientation);
+  const endOri = getOri(end, start, endStyle.orientation);
+
+  const startRetreat = drawArrowEnd(ctx, start, startOri, startStyle);
+  const endRetreat = drawArrowEnd(ctx, end, endOri, endStyle);
+
+  const startRetreatVec = orientationToUnitVector(startOri).scale(startRetreat);
+  const endRetreatVec = orientationToUnitVector(endOri).scale(endRetreat);
+
+  const startVec = new Vector2D(start).add(startRetreatVec);
+  const endVec = new Vector2D(end).add(endRetreatVec);
+
+  const startOffset =
+    orientationToUnitVector(startOri).scale(controlPointOffset);
+  const endOffset = orientationToUnitVector(endOri).scale(controlPointOffset);
+
+  const cp1 = startVec.add(startOffset);
+  const cp2 = endVec.add(endOffset);
+
+  ctx.beginPath();
+  ctx.moveTo(start.x, start.y);
+  ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y);
+  ctx.stroke();
+}
+
+function getOri(
+  pos: Point2D,
+  other: Point2D,
+  ori: ArrowHeadOrientation,
+): CardinalDirection {
+  switch (ori) {
+    case 'auto_vertical':
+      return other.y > pos.y ? 'south' : 'north';
+    case 'auto_horizontal':
+      return other.x > pos.x ? 'east' : 'west';
+    case 'auto':
+      const verticalDelta = Math.abs(other.y - pos.y);
+      const horizontalDelta = Math.abs(other.x - pos.x);
+      if (verticalDelta > horizontalDelta) {
+        return other.y > pos.y ? 'south' : 'north';
+      } else {
+        return other.x > pos.x ? 'east' : 'west';
+      }
+    default:
+      return ori;
+  }
+}
+
+function drawArrowEnd(
+  ctx: CanvasRenderingContext2D,
+  pos: Point2D,
+  orientation: CardinalDirection,
+  style: ArrowHeadStyle,
+): number {
+  switch (style.shape) {
+    case 'triangle':
+      const size = style.size ?? 5;
+      drawTriangle(ctx, pos, orientation, size);
+      return size;
+    case 'circle':
+      drawCircle(ctx, pos, style.size ?? 3);
+      return 0;
+    case 'none':
+      return 0;
+    default:
+      assertUnreachable(style.shape);
+  }
+}
+
+function orientationToAngle(orientation: CardinalDirection): number {
+  switch (orientation) {
+    case 'north':
+      return 0;
+    case 'east':
+      return Math.PI / 2;
+    case 'south':
+      return Math.PI;
+    case 'west':
+      return (3 * Math.PI) / 2;
+    default:
+      assertUnreachable(orientation);
+  }
+}
+
+function orientationToUnitVector(orientation: CardinalDirection): Vector2D {
+  switch (orientation) {
+    case 'north':
+      return new Vector2D({x: 0, y: -1});
+    case 'east':
+      return new Vector2D({x: 1, y: 0});
+    case 'south':
+      return new Vector2D({x: 0, y: 1});
+    case 'west':
+      return new Vector2D({x: -1, y: 0});
+    default:
+      assertUnreachable(orientation);
+  }
+}
+
+function drawTriangle(
+  ctx: CanvasRenderingContext2D,
+  pos: Point2D,
+  orientation: CardinalDirection,
+  size: number,
+) {
+  // Calculate the transformed coordinates directly
+  const angle = orientationToAngle(orientation);
+  const cosAngle = Math.cos(angle);
+  const sinAngle = Math.sin(angle);
+
+  const transformedPoints = [
+    {x: 0, y: 0},
+    {x: -1, y: -1},
+    {x: 1, y: -1},
+  ].map((point) => {
+    const scaledX = point.x * size;
+    const scaledY = point.y * size;
+    const rotatedX = scaledX * cosAngle - scaledY * sinAngle;
+    const rotatedY = scaledX * sinAngle + scaledY * cosAngle;
+    return {
+      x: rotatedX + pos.x,
+      y: rotatedY + pos.y,
+    };
+  });
+
+  ctx.beginPath();
+  ctx.moveTo(transformedPoints[0].x, transformedPoints[0].y);
+  ctx.lineTo(transformedPoints[1].x, transformedPoints[1].y);
+  ctx.lineTo(transformedPoints[2].x, transformedPoints[2].y);
+  ctx.closePath();
+  ctx.fill();
+}
+
+function drawCircle(
+  ctx: CanvasRenderingContext2D,
+  pos: Point2D,
+  radius: number,
+) {
+  ctx.beginPath();
+  ctx.arc(pos.x, pos.y, radius, 0, 2 * Math.PI);
+  ctx.closePath();
+  ctx.fill();
+}
diff --git a/ui/src/base/canvas_utils.ts b/ui/src/base/canvas_utils.ts
new file mode 100644
index 0000000..8ec410c
--- /dev/null
+++ b/ui/src/base/canvas_utils.ts
@@ -0,0 +1,198 @@
+// Copyright (C) 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
+//
+//      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 {Size2D, Point2D} from './geom';
+import {isString} from './object_utils';
+
+export function drawDoubleHeadedArrow(
+  ctx: CanvasRenderingContext2D,
+  x: number,
+  y: number,
+  length: number,
+  showArrowHeads: boolean,
+  width = 2,
+  color = 'black',
+) {
+  ctx.beginPath();
+  ctx.lineWidth = width;
+  ctx.lineCap = 'round';
+  ctx.strokeStyle = color;
+  ctx.moveTo(x, y);
+  ctx.lineTo(x + length, y);
+  ctx.stroke();
+  ctx.closePath();
+  // Arrowheads on the each end of the line.
+  if (showArrowHeads) {
+    ctx.beginPath();
+    ctx.moveTo(x + length - 8, y - 4);
+    ctx.lineTo(x + length, y);
+    ctx.lineTo(x + length - 8, y + 4);
+    ctx.stroke();
+    ctx.closePath();
+    ctx.beginPath();
+    ctx.moveTo(x + 8, y - 4);
+    ctx.lineTo(x, y);
+    ctx.lineTo(x + 8, y + 4);
+    ctx.stroke();
+    ctx.closePath();
+  }
+}
+
+export function drawIncompleteSlice(
+  ctx: CanvasRenderingContext2D,
+  x: number,
+  y: number,
+  width: number,
+  height: number,
+  showGradient: boolean = true,
+) {
+  if (width <= 0 || height <= 0) {
+    return;
+  }
+  ctx.beginPath();
+  const triangleSize = height / 4;
+  ctx.moveTo(x, y);
+  ctx.lineTo(x + width, y);
+  ctx.lineTo(x + width - 3, y + triangleSize * 0.5);
+  ctx.lineTo(x + width, y + triangleSize);
+  ctx.lineTo(x + width - 3, y + triangleSize * 1.5);
+  ctx.lineTo(x + width, y + 2 * triangleSize);
+  ctx.lineTo(x + width - 3, y + triangleSize * 2.5);
+  ctx.lineTo(x + width, y + 3 * triangleSize);
+  ctx.lineTo(x + width - 3, y + triangleSize * 3.5);
+  ctx.lineTo(x + width, y + 4 * triangleSize);
+  ctx.lineTo(x, y + height);
+
+  const fillStyle = ctx.fillStyle;
+  if (isString(fillStyle)) {
+    if (showGradient) {
+      const gradient = ctx.createLinearGradient(x, y, x + width, y + height);
+      gradient.addColorStop(0.66, fillStyle);
+      gradient.addColorStop(1, '#FFFFFF');
+      ctx.fillStyle = gradient;
+    }
+  } else {
+    throw new Error(
+      `drawIncompleteSlice() expects fillStyle to be a simple color not ${fillStyle}`,
+    );
+  }
+
+  ctx.fill();
+  ctx.fillStyle = fillStyle;
+}
+
+export function drawTrackHoverTooltip(
+  ctx: CanvasRenderingContext2D,
+  pos: Point2D,
+  trackSize: Size2D,
+  text: string,
+  text2?: string,
+) {
+  ctx.font = '10px Roboto Condensed';
+  ctx.textBaseline = 'middle';
+  ctx.textAlign = 'left';
+
+  // TODO(hjd): Avoid measuring text all the time (just use monospace?)
+  const textMetrics = ctx.measureText(text);
+  const text2Metrics = ctx.measureText(text2 ?? '');
+
+  // Padding on each side of the box containing the tooltip:
+  const paddingPx = 4;
+
+  // Figure out the width of the tool tip box:
+  let width = Math.max(textMetrics.width, text2Metrics.width);
+  width += paddingPx * 2;
+
+  // and the height:
+  let height = 0;
+  height += textMetrics.fontBoundingBoxAscent;
+  height += textMetrics.fontBoundingBoxDescent;
+  if (text2 !== undefined) {
+    height += text2Metrics.fontBoundingBoxAscent;
+    height += text2Metrics.fontBoundingBoxDescent;
+  }
+  height += paddingPx * 2;
+
+  let x = pos.x;
+  let y = pos.y;
+
+  // Move box to the top right of the mouse:
+  x += 10;
+  y -= 10;
+
+  // Ensure the box is on screen:
+  const endPx = trackSize.width;
+  if (x + width > endPx) {
+    x -= x + width - endPx;
+  }
+  if (y < 0) {
+    y = 0;
+  }
+  if (y + height > trackSize.height) {
+    y -= y + height - trackSize.height;
+  }
+
+  // Draw everything:
+  ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
+  ctx.fillRect(x, y, width, height);
+
+  ctx.fillStyle = 'hsl(200, 50%, 40%)';
+  ctx.fillText(
+    text,
+    x + paddingPx,
+    y + paddingPx + textMetrics.fontBoundingBoxAscent,
+  );
+  if (text2 !== undefined) {
+    const yOffsetPx =
+      textMetrics.fontBoundingBoxAscent +
+      textMetrics.fontBoundingBoxDescent +
+      text2Metrics.fontBoundingBoxAscent;
+    ctx.fillText(text2, x + paddingPx, y + paddingPx + yOffsetPx);
+  }
+}
+
+export function canvasClip(
+  ctx: CanvasRenderingContext2D,
+  x: number,
+  y: number,
+  w: number,
+  h: number,
+): void {
+  ctx.beginPath();
+  ctx.rect(x, y, w, h);
+  ctx.clip();
+}
+
+/**
+ * Save the state of the canvas, returning a disposable which restores the state
+ * when disposed.
+ *
+ * Allows using the |using| keyword to automatically restore the canvas state.
+ * @param ctx - The canvas context to save the state of.
+ * @returns A disposable.
+ *
+ * @example
+ * {
+ *   using const _ = canvasSave(ctx);
+ *   ctx.translate(123, 456); // Manipulate the canvas state
+ * } // ctx.restore() is automatically called when the _ falls out of scope
+ */
+export function canvasSave(ctx: CanvasRenderingContext2D): Disposable {
+  ctx.save();
+  return {
+    [Symbol.dispose](): void {
+      ctx.restore();
+    },
+  };
+}
diff --git a/ui/src/base/disposable_stack.ts b/ui/src/base/disposable_stack.ts
index 8023c0f..89c110d 100644
--- a/ui/src/base/disposable_stack.ts
+++ b/ui/src/base/disposable_stack.ts
@@ -127,7 +127,15 @@
       if (res === undefined) {
         break;
       }
+      const timerId = setTimeout(() => {
+        throw new Error(
+          'asyncDispose timed out. This might be due to a Disposable ' +
+            'resource  trying to issue cleanup queries on trace unload, ' +
+            'while the Wasm module was already destroyed ',
+        );
+      }, 10_000);
       await res[Symbol.asyncDispose]();
+      clearTimeout(timerId);
     }
   }
 
diff --git a/ui/src/base/dom_utils.ts b/ui/src/base/dom_utils.ts
index 3c39e9c..ab58c90 100644
--- a/ui/src/base/dom_utils.ts
+++ b/ui/src/base/dom_utils.ts
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {Vector2D} from './geom';
+
 // Check whether a DOM element contains another, or whether they're the same
 export function isOrContains(container: Element, target: Element): boolean {
   return container === target || container.contains(target);
@@ -68,17 +70,17 @@
 // Similar to |offsetX|, |offsetY| but for |currentTarget| rather than |target|.
 // If the event has no currentTarget or it is not an element, offsetX & offsetY
 // are returned instead.
-export function currentTargetOffset(e: MouseEvent): {x: number; y: number} {
+export function currentTargetOffset(e: MouseEvent): Vector2D {
   if (e.currentTarget === e.target) {
-    return {x: e.offsetX, y: e.offsetY};
+    return new Vector2D({x: e.offsetX, y: e.offsetY});
   }
 
   if (e.currentTarget && e.currentTarget instanceof Element) {
     const rect = e.currentTarget.getBoundingClientRect();
     const offsetX = e.clientX - rect.left;
     const offsetY = e.clientY - rect.top;
-    return {x: offsetX, y: offsetY};
+    return new Vector2D({x: offsetX, y: offsetY});
   }
 
-  return {x: e.offsetX, y: e.offsetY};
+  return new Vector2D({x: e.offsetX, y: e.offsetY});
 }
diff --git a/ui/src/frontend/drag_gesture_handler.ts b/ui/src/base/drag_gesture_handler.ts
similarity index 100%
rename from ui/src/frontend/drag_gesture_handler.ts
rename to ui/src/base/drag_gesture_handler.ts
diff --git a/ui/src/base/fuzzy.ts b/ui/src/base/fuzzy.ts
index 7bc00a1..7a67a6c 100644
--- a/ui/src/base/fuzzy.ts
+++ b/ui/src/base/fuzzy.ts
@@ -12,6 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {Fzf} from 'fzf';
+import {SyncOptionsTuple} from 'fzf/dist/types/finders';
+
 export interface FuzzySegment {
   matching: boolean;
   value: string;
@@ -26,36 +29,32 @@
 
 // Finds approx matching in arbitrary lists of items.
 export class FuzzyFinder<T> {
-  private items: T[];
-  private keyLookup: KeyLookup<T>;
-
+  private readonly fzf: Fzf<ReadonlyArray<T>>;
   // Because we operate on arbitrary lists, a key lookup function is required to
   // so we know which part of the list is to be be searched. It should return
   // the relevant search string for each item.
-  constructor(items: ArrayLike<T>, keyLookup: KeyLookup<T>) {
-    this.items = Array.from(items);
-    this.keyLookup = keyLookup;
+  constructor(
+    items: ReadonlyArray<T>,
+    private readonly keyLookup: KeyLookup<T>,
+  ) {
+    // NOTE(stevegolton): This type assertion because FZF appears to be very
+    // fussy about its input types.
+    const options = [{selector: keyLookup}] as SyncOptionsTuple<T>;
+    this.fzf = new Fzf<ReadonlyArray<T>>(items, ...options);
   }
 
   // Return a list of items that match any of the search terms.
-  find(...searchTerms: string[]): FuzzyResult<T>[] {
-    const result: FuzzyResult<T>[] = [];
-
-    for (const item of this.items) {
-      const key = this.keyLookup(item);
-      for (const searchTerm of searchTerms) {
-        const indicies: number[] = new Array(searchTerm.length);
-        if (match(searchTerm, key, indicies)) {
-          const segments = indiciesToSegments(indicies, key);
-          result.push({item, segments});
-
-          // Don't try to match any more...
-          break;
-        }
-      }
-    }
-
-    return result;
+  find(searchTerm: string): FuzzyResult<T>[] {
+    return this.fzf.find(searchTerm).map((m) => {
+      const normalisedTerm = this.keyLookup(m.item);
+      return {
+        item: m.item,
+        segments: indiciesToSegments(
+          Array.from(m.positions).sort((a, b) => a - b),
+          normalisedTerm,
+        ),
+      };
+    });
   }
 }
 
diff --git a/ui/src/base/fuzzy_unittest.ts b/ui/src/base/fuzzy_unittest.ts
index aa3fdd3..5d195bc 100644
--- a/ui/src/base/fuzzy_unittest.ts
+++ b/ui/src/base/fuzzy_unittest.ts
@@ -15,7 +15,7 @@
 import {FuzzyFinder, fuzzyMatch} from './fuzzy';
 
 describe('FuzzyFinder', () => {
-  const items = ['aaa', 'aba', 'zzz', 'c z d z e', 'Foo', 'ababc'];
+  const items = ['aaa', 'aba', 'zzz', 'c z d z e', 'CAPS', 'ababc'];
   const finder = new FuzzyFinder(items, (x) => x);
 
   it('finds all for empty search term', () => {
@@ -26,7 +26,7 @@
       {item: 'aba', segments: [{matching: false, value: 'aba'}]},
       {item: 'zzz', segments: [{matching: false, value: 'zzz'}]},
       {item: 'c z d z e', segments: [{matching: false, value: 'c z d z e'}]},
-      {item: 'Foo', segments: [{matching: false, value: 'Foo'}]},
+      {item: 'CAPS', segments: [{matching: false, value: 'CAPS'}]},
       {item: 'ababc', segments: [{matching: false, value: 'ababc'}]},
     ]);
   });
@@ -91,11 +91,11 @@
     );
   });
 
-  it('finds case insensitive match', () => {
-    const result = finder.find('foO');
+  it('finds caps match when search term is in lower case', () => {
+    const result = finder.find('caps');
     expect(result).toEqual(
       expect.arrayContaining([
-        {item: 'Foo', segments: [{matching: true, value: 'Foo'}]},
+        {item: 'CAPS', segments: [{matching: true, value: 'CAPS'}]},
       ]),
     );
   });
@@ -115,29 +115,6 @@
       ]),
     );
   });
-
-  it('match multiple', () => {
-    const result = finder.find('abc', 'c z d');
-    expect(result).toEqual(
-      expect.arrayContaining([
-        {
-          item: 'ababc',
-          segments: [
-            {matching: true, value: 'ab'},
-            {matching: false, value: 'ab'},
-            {matching: true, value: 'c'},
-          ],
-        },
-        {
-          item: 'c z d z e',
-          segments: [
-            {matching: true, value: 'c z d'},
-            {matching: false, value: ' z e'},
-          ],
-        },
-      ]),
-    );
-  });
 });
 
 test('fuzzyMatch', () => {
diff --git a/ui/src/base/gcs_uploader.ts b/ui/src/base/gcs_uploader.ts
new file mode 100644
index 0000000..b2f2bd5
--- /dev/null
+++ b/ui/src/base/gcs_uploader.ts
@@ -0,0 +1,209 @@
+// 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.
+
+import {defer} from './deferred';
+import {Time} from './time';
+
+export const BUCKET_NAME = 'perfetto-ui-data';
+export const MIME_JSON = 'application/json; charset=utf-8';
+export const MIME_BINARY = 'application/octet-stream';
+
+export interface GcsUploaderArgs {
+  /**
+   * The mime-type to use for the upload. If undefined uses
+   * application/octet-stream.
+   */
+  mimeType?: string;
+
+  /**
+   * The name to use for the uploaded file. By default it uses a hash of
+   * the passed data/blob and uses content-addressing.
+   */
+  fileName?: string;
+
+  /** An optional callback that is invoked upon upload progress (or failure) */
+  onProgress?: (uploader: GcsUploader) => void;
+}
+
+/**
+ * A utility class to handle uploads of possibly large files to
+ * Google Cloud Storage.
+ * It returns immediately if the file exists already
+ */
+export class GcsUploader {
+  state: 'UPLOADING' | 'UPLOADED' | 'ERROR' = 'UPLOADING';
+  error = '';
+  totalSize = 0;
+  uploadedSize = 0;
+  uploadedUrl = '';
+  uploadedFileName = '';
+
+  private args: GcsUploaderArgs;
+  private onProgress: (_: GcsUploader) => void;
+  private req: XMLHttpRequest;
+  private donePromise = defer<void>();
+  private startTime = performance.now();
+
+  constructor(data: Blob | ArrayBuffer | string, args: GcsUploaderArgs) {
+    this.args = args;
+    this.onProgress = args.onProgress ?? ((_: GcsUploader) => {});
+    this.req = new XMLHttpRequest();
+    this.start(data);
+  }
+
+  async start(data: Blob | ArrayBuffer | string) {
+    let fname = this.args.fileName;
+    if (fname === undefined) {
+      // If the file name is unspecified, hash the contents.
+      if (data instanceof Blob) {
+        fname = await hashFileStreaming(data);
+      } else {
+        fname = await sha1(data);
+      }
+    }
+    this.uploadedFileName = fname;
+    this.uploadedUrl = `https://storage.googleapis.com/${BUCKET_NAME}/${fname}`;
+
+    // Check if the file has been uploaded already. If so, skip.
+    const res = await fetch(
+      `https://www.googleapis.com/storage/v1/b/${BUCKET_NAME}/o/${fname}`,
+    );
+    if (res.status === 200) {
+      console.log(
+        `Skipping upload of ${this.uploadedUrl} because it exists already`,
+      );
+      this.state = 'UPLOADED';
+      this.donePromise.resolve();
+      return;
+    }
+
+    const reqUrl =
+      'https://www.googleapis.com/upload/storage/v1/b/' +
+      `${BUCKET_NAME}/o?uploadType=media` +
+      `&name=${fname}&predefinedAcl=publicRead`;
+    this.req.onabort = (e: ProgressEvent) => this.onRpcEvent(e);
+    this.req.onerror = (e: ProgressEvent) => this.onRpcEvent(e);
+    this.req.upload.onprogress = (e: ProgressEvent) => this.onRpcEvent(e);
+    this.req.onloadend = (e: ProgressEvent) => this.onRpcEvent(e);
+    this.req.open('POST', reqUrl, /* async= */ true);
+    const mimeType = this.args.mimeType ?? MIME_BINARY;
+    this.req.setRequestHeader('Content-Type', mimeType);
+    this.req.send(data);
+  }
+
+  waitForCompletion(): Promise<void> {
+    return this.donePromise;
+  }
+
+  abort() {
+    if (this.state === 'UPLOADING') {
+      this.req.abort();
+    }
+  }
+
+  getEtaString() {
+    let str = `${Math.ceil((100 * this.uploadedSize) / this.totalSize)}%`;
+    str += ` (${(this.uploadedSize / 1e6).toFixed(2)} MB)`;
+    const elapsed = (performance.now() - this.startTime) / 1000;
+    const rate = this.uploadedSize / elapsed;
+    const etaSecs = Math.round((this.totalSize - this.uploadedSize) / rate);
+    str += ' - ETA: ' + Time.toTimecode(Time.fromSeconds(etaSecs)).dhhmmss;
+    return str;
+  }
+
+  private onRpcEvent(e: ProgressEvent) {
+    let done = false;
+    switch (e.type) {
+      case 'progress':
+        this.uploadedSize = e.loaded;
+        this.totalSize = e.total;
+        break;
+      case 'abort':
+        this.state = 'ERROR';
+        this.error = 'Upload aborted';
+        break;
+      case 'error':
+        this.state = 'ERROR';
+        this.error = `${this.req.status} - ${this.req.statusText}`;
+        break;
+      case 'loadend':
+        done = true;
+        if (this.req.status === 200) {
+          this.state = 'UPLOADED';
+        } else if (this.state === 'UPLOADING') {
+          this.state = 'ERROR';
+          this.error = `${this.req.status} - ${this.req.statusText}`;
+        }
+        break;
+      default:
+        return;
+    }
+    this.onProgress(this);
+    if (done) {
+      this.donePromise.resolve();
+    }
+  }
+}
+
+/**
+ * Computes the SHA-1 of a string or ArrayBuffer(View)
+ * @param data a string or ArrayBuffer to hash.
+ */
+async function sha1(data: string | ArrayBuffer): Promise<string> {
+  let buffer: ArrayBuffer;
+  if (typeof data === 'string') {
+    buffer = new TextEncoder().encode(data);
+  } else {
+    buffer = data;
+  }
+  const digest = await crypto.subtle.digest('SHA-1', buffer);
+  return digestToHex(digest);
+}
+
+/**
+ * Converts a hash for the given file in streaming mode, without loading the
+ * whole file into memory. The result is "a" SHA-1 but is not the same of
+ * `shasum -a 1 file`. The reason for this is that the crypto APIs support
+ * only one-shot digest computation and lack the usual update() + digest()
+ * chunked API. So we end up computing a SHA-1 of the concatenation of the
+ * SHA-1 of each chunk.
+ * Speed: ~800 MB/s on a M2 Macbook Air 2023.
+ * @param file The file to hash.
+ * @returns A hex-encoded string containing the hash of the file.
+ */
+async function hashFileStreaming(file: Blob): Promise<string> {
+  const CHUNK_SIZE = 32 * 1024 * 1024; // 32MB
+  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
+  let chunkDigests = '';
+
+  for (let i = 0; i < totalChunks; i++) {
+    const start = i * CHUNK_SIZE;
+    const end = Math.min(start + CHUNK_SIZE, file.size);
+    const chunk = await file.slice(start, end).arrayBuffer();
+    const digest = await crypto.subtle.digest('SHA-1', chunk);
+    chunkDigests += digestToHex(digest);
+  }
+  return sha1(chunkDigests);
+}
+
+/**
+ * Converts the return value of crypto.digest() to a hex string.
+ * @param digest an array of bytes containing the digest
+ * @returns hex-encoded string of the digest.
+ */
+function digestToHex(digest: ArrayBuffer): string {
+  return Array.from(new Uint8Array(digest))
+    .map((x) => x.toString(16).padStart(2, '0'))
+    .join('');
+}
diff --git a/ui/src/base/geom.ts b/ui/src/base/geom.ts
index 5a20023..da50cc5 100644
--- a/ui/src/base/geom.ts
+++ b/ui/src/base/geom.ts
@@ -12,78 +12,206 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-export interface Rect {
-  readonly left: number;
-  readonly top: number;
-  readonly right: number;
-  readonly bottom: number;
-}
+// This library provides interfaces and classes for handling 2D geometry
+// operations.
 
-export interface Size {
-  readonly width: number;
-  readonly height: number;
-}
-
-export interface Vector {
+/**
+ * Interface representing a point in 2D space.
+ */
+export interface Point2D {
   readonly x: number;
   readonly y: number;
 }
 
-export function intersectRects(a: Rect, b: Rect): Rect {
-  return {
-    top: Math.max(a.top, b.top),
-    left: Math.max(a.left, b.left),
-    bottom: Math.min(a.bottom, b.bottom),
-    right: Math.min(a.right, b.right),
-  };
-}
+/**
+ * Class representing a 2D vector with methods for vector operations.
+ *
+ * Note: This class is immutable in TypeScript (not enforced at runtime). Any
+ * method that modifies the vector returns a new instance, leaving the original
+ * unchanged.
+ */
+export class Vector2D implements Point2D {
+  readonly x: number;
+  readonly y: number;
 
-export function expandRect(r: Rect, amount: number): Rect {
-  return {
-    top: r.top - amount,
-    left: r.left - amount,
-    bottom: r.bottom + amount,
-    right: r.right + amount,
-  };
-}
+  constructor({x, y}: Point2D) {
+    this.x = x;
+    this.y = y;
+  }
 
-export function rebaseRect(r: Rect, x: number, y: number): Rect {
-  return {
-    left: r.left - x,
-    right: r.right - x,
-    top: r.top - y,
-    bottom: r.bottom - y,
-  };
-}
+  /**
+   * Adds the given point to this vector and returns a new vector.
+   *
+   * @param point - The point to add.
+   * @returns A new Vector2D instance representing the result.
+   */
+  add(point: Point2D): Vector2D {
+    return new Vector2D({x: this.x + point.x, y: this.y + point.y});
+  }
 
-export function rectSize(r: Rect): Size {
-  return {
-    width: r.right - r.left,
-    height: r.bottom - r.top,
-  };
+  /**
+   * Subtracts the given point from this vector and returns a new vector.
+   *
+   * @param point - The point to subtract.
+   * @returns A new Vector2D instance representing the result.
+   */
+  sub(point: Point2D): Vector2D {
+    return new Vector2D({x: this.x - point.x, y: this.y - point.y});
+  }
+
+  /**
+   * Scales this vector by the given scalar and returns a new vector.
+   *
+   * @param scalar - The scalar value to multiply the vector by.
+   * @returns A new Vector2D instance representing the scaled vector.
+   */
+  scale(scalar: number): Vector2D {
+    return new Vector2D({x: this.x * scalar, y: this.y * scalar});
+  }
+
+  /**
+   * Computes the Manhattan distance, which is the sum of the absolute values of
+   * the x and y components of the vector. This represents the distance
+   * travelled along axes at right angles (grid-based distance).
+   */
+  get manhattanDistance(): number {
+    return Math.abs(this.x) + Math.abs(this.y);
+  }
+
+  /**
+   * Computes the Euclidean magnitude (or length) of the vector. This is the
+   * straight-line distance from the origin (0, 0) to the point (x, y) in 2D
+   * space.
+   */
+  get magnitude(): number {
+    return Math.sqrt(this.x * this.x + this.y * this.y);
+  }
 }
 
 /**
- * Return true if rect a contains rect b.
- *
- * @param a A rect.
- * @param b Another rect.
- * @returns True if rect a contains rect b, false otherwise.
+ * Interface representing the vertical bounds of an object (top and bottom).
  */
-export function containsRect(a: Rect, b: Rect): boolean {
-  return !(
-    b.top < a.top ||
-    b.bottom > a.bottom ||
-    b.left < a.left ||
-    b.right > a.right
-  );
+export interface VerticalBounds {
+  readonly top: number;
+  readonly bottom: number;
 }
 
-export function translateRect(a: Rect, b: Vector): Rect {
-  return {
-    top: a.top + b.y,
-    left: a.left + b.x,
-    bottom: a.bottom + b.y,
-    right: a.right + b.x,
-  };
+/**
+ * Interface representing the horizontal bounds of an object (left and right).
+ */
+export interface HorizontalBounds {
+  readonly left: number;
+  readonly right: number;
+}
+
+/**
+ * Interface combining vertical and horizontal bounds to describe a 2D bounding
+ * box.
+ */
+export interface Bounds2D extends VerticalBounds, HorizontalBounds {}
+
+/**
+ * Interface representing the size of a 2D object.
+ */
+export interface Size2D {
+  readonly width: number;
+  readonly height: number;
+}
+
+/**
+ * Class representing a 2D rectangle, implementing bounds and size interfaces.
+ */
+export class Rect2D implements Bounds2D, Size2D {
+  readonly left: number;
+  readonly top: number;
+  readonly right: number;
+  readonly bottom: number;
+  readonly width: number;
+  readonly height: number;
+
+  constructor({left, top, right, bottom}: Bounds2D) {
+    this.left = left;
+    this.top = top;
+    this.right = right;
+    this.bottom = bottom;
+    this.width = right - left;
+    this.height = bottom - top;
+  }
+
+  /**
+   * Returns a new rectangle representing the intersection with another
+   * rectangle.
+   *
+   * @param bounds - The bounds of the other rectangle to intersect with.
+   * @returns A new Rect2D instance representing the intersected rectangle.
+   */
+  intersect(bounds: Bounds2D): Rect2D {
+    return new Rect2D({
+      top: Math.max(this.top, bounds.top),
+      left: Math.max(this.left, bounds.left),
+      bottom: Math.min(this.bottom, bounds.bottom),
+      right: Math.min(this.right, bounds.right),
+    });
+  }
+
+  /**
+   * Expands the rectangle by the given amount on all sides and returns a new
+   * rectangle.
+   *
+   * @param amount - The amount to expand the rectangle by.
+   * @returns A new Rect2D instance representing the expanded rectangle.
+   */
+  expand(amount: number): Rect2D {
+    return new Rect2D({
+      top: this.top - amount,
+      left: this.left - amount,
+      bottom: this.bottom + amount,
+      right: this.right + amount,
+    });
+  }
+
+  /**
+   * Reframes the rectangle by shifting its origin by the given point.
+   *
+   * @param point - The point by which to shift the origin.
+   * @returns A new Rect2D instance representing the reframed rectangle.
+   */
+  reframe(point: Point2D): Rect2D {
+    return new Rect2D({
+      left: this.left - point.x,
+      right: this.right - point.x,
+      top: this.top - point.y,
+      bottom: this.bottom - point.y,
+    });
+  }
+
+  /**
+   * Checks if this rectangle fully contains another set of bounds.
+   *
+   * @param bounds - The bounds to check containment for.
+   * @returns True if this rectangle contains the given bounds, false otherwise.
+   */
+  contains(bounds: Bounds2D): boolean {
+    return !(
+      bounds.top < this.top ||
+      bounds.bottom > this.bottom ||
+      bounds.left < this.left ||
+      bounds.right > this.right
+    );
+  }
+
+  /**
+   * Translates the rectangle by the given point and returns a new rectangle.
+   *
+   * @param point - The point by which to translate the rectangle.
+   * @returns A new Rect2D instance representing the translated rectangle.
+   */
+  translate(point: Point2D): Rect2D {
+    return new Rect2D({
+      top: this.top + point.y,
+      left: this.left + point.x,
+      bottom: this.bottom + point.y,
+      right: this.right + point.x,
+    });
+  }
 }
diff --git a/ui/src/base/geom_unittest.ts b/ui/src/base/geom_unittest.ts
index 628d053..cb7d804 100644
--- a/ui/src/base/geom_unittest.ts
+++ b/ui/src/base/geom_unittest.ts
@@ -12,41 +12,72 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {intersectRects, expandRect, rebaseRect, rectSize, Rect} from './geom';
+import {Vector2D, Rect2D, Bounds2D} from './geom';
 
-describe('intersectRects', () => {
-  it('should correctly intersect two overlapping rects', () => {
-    const a: Rect = {left: 1, top: 1, right: 4, bottom: 4};
-    const b: Rect = {left: 2, top: 2, right: 5, bottom: 5};
-    const result = intersectRects(a, b);
-    expect(result).toEqual({left: 2, top: 2, right: 4, bottom: 4});
+describe('Vector2D', () => {
+  test('add', () => {
+    const vector1 = new Vector2D({x: 1, y: 2});
+    const vector2 = new Vector2D({x: 3, y: 4});
+    const result = vector1.add(vector2);
+    expect(result.x).toBe(4);
+    expect(result.y).toBe(6);
   });
-  // Note: Non-overlapping rects are not supported and thus not tested
-});
 
-describe('expandRect', () => {
-  it('should correctly expand a rect by a given amount', () => {
-    const rect: Rect = {left: 1, top: 1, right: 3, bottom: 3};
-    const amount = 1;
-    const result = expandRect(rect, amount);
-    expect(result).toEqual({left: 0, top: 0, right: 4, bottom: 4});
+  test('sub', () => {
+    const vector1 = new Vector2D({x: 5, y: 7});
+    const vector2 = new Vector2D({x: 2, y: 3});
+    const result = vector1.sub(vector2);
+    expect(result.x).toBe(3);
+    expect(result.y).toBe(4);
+  });
+
+  test('scale', () => {
+    const vector = new Vector2D({x: 2, y: 3});
+    const result = vector.scale(2);
+    expect(result.x).toBe(4);
+    expect(result.y).toBe(6);
   });
 });
 
-describe('rebaseRect', () => {
-  it('should correctly rebase a rect', () => {
-    const rect: Rect = {left: 2, top: 2, right: 5, bottom: 5};
-    const x = 1;
-    const y = 1;
-    const result = rebaseRect(rect, x, y);
-    expect(result).toEqual({left: 1, top: 1, right: 4, bottom: 4});
+describe('Rect2D', () => {
+  test('intersect', () => {
+    const a = new Rect2D({left: 1, top: 1, right: 4, bottom: 4});
+    const b = {left: 2, top: 2, right: 5, bottom: 5};
+    const result = a.intersect(b);
+    expect(result).toMatchObject({left: 2, top: 2, right: 4, bottom: 4});
+    // Note: Non-overlapping rects are UB and thus not tested
+    // TODO(stevegolton): Work out what to do here.
   });
-});
 
-describe('rectSize', () => {
-  it('should correctly calculate the size of a rect', () => {
-    const rect: Rect = {left: 1, top: 1, right: 4, bottom: 3};
-    const result = rectSize(rect);
-    expect(result).toEqual({width: 3, height: 2});
+  test('expand', () => {
+    const rect = new Rect2D({left: 1, top: 1, right: 3, bottom: 3});
+    const result = rect.expand(1);
+    expect(result).toMatchObject({left: 0, top: 0, right: 4, bottom: 4});
+  });
+
+  test('reframe', () => {
+    const rect = new Rect2D({left: 2, top: 2, right: 5, bottom: 5});
+    const result = rect.reframe({x: 1, y: 1});
+    expect(result).toMatchObject({left: 1, top: 1, right: 4, bottom: 4});
+  });
+
+  test('size', () => {
+    const rect = new Rect2D({left: 1, top: 1, right: 4, bottom: 3});
+    expect(rect).toMatchObject({width: 3, height: 2});
+  });
+
+  it('translate', () => {
+    const rect = new Rect2D({left: 2, top: 2, right: 5, bottom: 5});
+    const result = rect.translate({x: 3, y: 4});
+    expect(result).toMatchObject({left: 5, top: 6, right: 8, bottom: 9});
+  });
+
+  it('contains', () => {
+    const outerRect = new Rect2D({left: 0, top: 0, right: 10, bottom: 10});
+    const innerRect: Bounds2D = {left: 2, top: 2, right: 8, bottom: 8};
+    expect(outerRect.contains(innerRect)).toBe(true);
+
+    const nonContainedRect: Bounds2D = {left: 2, top: 2, right: 12, bottom: 8};
+    expect(outerRect.contains(nonContainedRect)).toBe(false);
   });
 });
diff --git a/ui/src/core/hash.ts b/ui/src/base/hash.ts
similarity index 100%
rename from ui/src/core/hash.ts
rename to ui/src/base/hash.ts
diff --git a/ui/src/base/high_precision_time.ts b/ui/src/base/high_precision_time.ts
new file mode 100644
index 0000000..f205c83
--- /dev/null
+++ b/ui/src/base/high_precision_time.ts
@@ -0,0 +1,241 @@
+// 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 {assertUnreachable} from './logging';
+import {Time, time} from './time';
+
+export type RoundMode = 'round' | 'floor' | 'ceil';
+
+/**
+ * Represents a time value in trace processor's time units, which is capable of
+ * representing a time with at least 64 bit integer precision and 53 bits of
+ * fractional precision.
+ *
+ * This class is immutable - any methods that modify this time will return a new
+ * copy containing instead.
+ */
+export class HighPrecisionTime {
+  // This is the high precision time representing 0
+  static readonly ZERO = new HighPrecisionTime(Time.fromRaw(0n));
+
+  // time value == |integral| + |fractional|
+  // |fractional| is kept in the range 0 <= x < 1 to avoid losing precision
+  readonly integral: time;
+  readonly fractional: number;
+
+  /**
+   * Constructs a HighPrecisionTime object.
+   *
+   * @param integral The integer part of the time value.
+   * @param fractional The fractional part of the time value.
+   */
+  constructor(integral: time, fractional: number = 0) {
+    // Normalize |fractional| to the range 0.0 <= x < 1.0
+    const fractionalFloor = Math.floor(fractional);
+    this.integral = (integral + BigInt(fractionalFloor)) as time;
+    this.fractional = fractional - fractionalFloor;
+  }
+
+  /**
+   * Converts to an integer time value.
+   *
+   * @param round How to round ('round', 'floor', or 'ceil').
+   */
+  toTime(round: RoundMode = 'floor'): time {
+    switch (round) {
+      case 'round':
+        return Time.fromRaw(
+          this.integral + BigInt(Math.round(this.fractional)),
+        );
+      case 'floor':
+        return Time.fromRaw(this.integral);
+      case 'ceil':
+        return Time.fromRaw(this.integral + BigInt(Math.ceil(this.fractional)));
+      default:
+        assertUnreachable(round);
+    }
+  }
+
+  /**
+   * Converts to a JavaScript number. Precision loss should be expected when
+   * integral values are large.
+   */
+  toNumber(): number {
+    return Number(this.integral) + this.fractional;
+  }
+
+  /**
+   * Adds another HighPrecisionTime to this one and returns the result.
+   *
+   * @param time A HighPrecisionTime object to add.
+   */
+  add(time: HighPrecisionTime): HighPrecisionTime {
+    return new HighPrecisionTime(
+      Time.add(this.integral, time.integral),
+      this.fractional + time.fractional,
+    );
+  }
+
+  /**
+   * Adds an integer time value to this HighPrecisionTime and returns the result.
+   *
+   * @param t A time value to add.
+   */
+  addTime(t: time): HighPrecisionTime {
+    return new HighPrecisionTime(Time.add(this.integral, t), this.fractional);
+  }
+
+  /**
+   * Adds a floating point time value to this one and returns the result.
+   *
+   * @param n A floating point value to add.
+   */
+  addNumber(n: number): HighPrecisionTime {
+    return new HighPrecisionTime(this.integral, this.fractional + n);
+  }
+
+  /**
+   * Subtracts another HighPrecisionTime from this one and returns the result.
+   *
+   * @param time A HighPrecisionTime object to subtract.
+   */
+  sub(time: HighPrecisionTime): HighPrecisionTime {
+    return new HighPrecisionTime(
+      Time.sub(this.integral, time.integral),
+      this.fractional - time.fractional,
+    );
+  }
+
+  /**
+   * Subtract an integer time value from this HighPrecisionTime and returns the
+   * result.
+   *
+   * @param t A time value to subtract.
+   */
+  subTime(t: time): HighPrecisionTime {
+    return new HighPrecisionTime(Time.sub(this.integral, t), this.fractional);
+  }
+
+  /**
+   * Subtracts a floating point time value from this one and returns the result.
+   *
+   * @param n A floating point value to subtract.
+   */
+  subNumber(n: number): HighPrecisionTime {
+    return new HighPrecisionTime(this.integral, this.fractional - n);
+  }
+
+  /**
+   * Checks if this HighPrecisionTime is approximately equal to another, within
+   * a given epsilon.
+   *
+   * @param other A HighPrecisionTime object to compare.
+   * @param epsilon The tolerance for equality check.
+   */
+  equals(other: HighPrecisionTime, epsilon: number = 1e-6): boolean {
+    return Math.abs(this.sub(other).toNumber()) < epsilon;
+  }
+
+  /**
+   * Checks if this time value is within the range defined by [start, end).
+   *
+   * @param start The start of the time range (inclusive).
+   * @param end The end of the time range (exclusive).
+   */
+  containedWithin(start: time, end: time): boolean {
+    return this.integral >= start && this.integral < end;
+  }
+
+  /**
+   * Checks if this HighPrecisionTime is less than a given time.
+   *
+   * @param t A time value.
+   */
+  lt(t: time): boolean {
+    return this.integral < t;
+  }
+
+  /**
+   * Checks if this HighPrecisionTime is less than or equal to a given time.
+   *
+   * @param t A time value.
+   */
+  lte(t: time): boolean {
+    return (
+      this.integral < t ||
+      (this.integral === t && Math.abs(this.fractional - 0.0) < Number.EPSILON)
+    );
+  }
+
+  /**
+   * Checks if this HighPrecisionTime is greater than a given time.
+   *
+   * @param t A time value.
+   */
+  gt(t: time): boolean {
+    return (
+      this.integral > t ||
+      (this.integral === t && Math.abs(this.fractional - 0.0) > Number.EPSILON)
+    );
+  }
+
+  /**
+   * Checks if this HighPrecisionTime is greater than or equal to a given time.
+   *
+   * @param t A time value.
+   */
+  gte(t: time): boolean {
+    return this.integral >= t;
+  }
+
+  /**
+   * Clamps this HighPrecisionTime to be within the specified range.
+   *
+   * @param lower The lower bound of the range.
+   * @param upper The upper bound of the range.
+   */
+  clamp(lower: time, upper: time): HighPrecisionTime {
+    if (this.integral < lower) {
+      return new HighPrecisionTime(lower);
+    } else if (this.integral >= upper) {
+      return new HighPrecisionTime(upper);
+    } else {
+      return this;
+    }
+  }
+
+  /**
+   * Returns the absolute value of this HighPrecisionTime.
+   */
+  abs(): HighPrecisionTime {
+    if (this.integral >= 0n) {
+      return this;
+    }
+    const newIntegral = Time.fromRaw(-this.integral);
+    const newFractional = -this.fractional;
+    return new HighPrecisionTime(newIntegral, newFractional);
+  }
+
+  /**
+   * Converts this HighPrecisionTime to a string representation.
+   */
+  toString(): string {
+    const fractionalAsString = this.fractional.toString();
+    if (fractionalAsString === '0') {
+      return this.integral.toString();
+    } else {
+      return `${this.integral}${fractionalAsString.substring(1)}`;
+    }
+  }
+}
diff --git a/ui/src/base/high_precision_time_span.ts b/ui/src/base/high_precision_time_span.ts
new file mode 100644
index 0000000..0bb6e75
--- /dev/null
+++ b/ui/src/base/high_precision_time_span.ts
@@ -0,0 +1,215 @@
+// 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 {TimeSpan, time} from './time';
+import {HighPrecisionTime} from './high_precision_time';
+
+/**
+ * Represents a time span using a high precision time value to represent the
+ * start of the span, and a number to represent the duration of the span.
+ */
+export class HighPrecisionTimeSpan {
+  static readonly ZERO = new HighPrecisionTimeSpan(HighPrecisionTime.ZERO, 0);
+
+  readonly start: HighPrecisionTime;
+  readonly duration: number;
+
+  constructor(start: HighPrecisionTime, duration: number) {
+    this.start = start;
+    this.duration = duration;
+  }
+
+  /**
+   * Create a new span from integral start and end points.
+   *
+   * @param start The start of the span.
+   * @param end The end of the span.
+   */
+  static fromTime(start: time, end: time): HighPrecisionTimeSpan {
+    return new HighPrecisionTimeSpan(
+      new HighPrecisionTime(start),
+      Number(end - start),
+    );
+  }
+
+  /**
+   * The center point of the span.
+   */
+  get midpoint(): HighPrecisionTime {
+    return this.start.addNumber(this.duration / 2);
+  }
+
+  /**
+   * The end of the span.
+   */
+  get end(): HighPrecisionTime {
+    return this.start.addNumber(this.duration);
+  }
+
+  /**
+   * Checks if this span exactly equals another.
+   */
+  equals(other: HighPrecisionTimeSpan): boolean {
+    return this.start.equals(other.start) && this.duration === other.duration;
+  }
+
+  /**
+   * Create a new span with the same duration but the start point moved through
+   * time by some amount of time.
+   */
+  translate(time: number): HighPrecisionTimeSpan {
+    return new HighPrecisionTimeSpan(this.start.addNumber(time), this.duration);
+  }
+
+  /**
+   * Create a new span with the the start of the span moved backward and the end
+   * of the span moved forward by a certain amount of time.
+   */
+  pad(time: number): HighPrecisionTimeSpan {
+    return new HighPrecisionTimeSpan(
+      this.start.subNumber(time),
+      this.duration + 2 * time,
+    );
+  }
+
+  /**
+   * Create a new span which is zoomed in or out centered on a specific point.
+   *
+   * @param ratio The scaling ratio, the new duration will be the current
+   * duration * ratio.
+   * @param center The center point as a normalized value between 0 and 1 where
+   * 0 is the start of the time window and 1 is the end.
+   * @param minDur Don't allow the time span to become shorter than this.
+   */
+  scale(ratio: number, center: number, minDur: number): HighPrecisionTimeSpan {
+    const currentDuration = this.duration;
+    const newDuration = Math.max(currentDuration * ratio, minDur);
+    // Delta between new and old duration
+    // +ve if new duration is shorter than old duration
+    const durationDeltaNanos = currentDuration - newDuration;
+    // If offset is 0, don't move the start at all
+    // If offset if 1, move the start by the amount the duration has changed
+    // If new duration is shorter - move start to right
+    // If new duration is longer - move start to left
+    const start = this.start.addNumber(durationDeltaNanos * center);
+    return new HighPrecisionTimeSpan(start, newDuration);
+  }
+
+  /**
+   * Create a new span that represents the intersection of this span with
+   * another.
+   *
+   * If the two spans do not overlap at all, the empty span is returned.
+   *
+   * @param start THe start of the other span.
+   * @param end The end of the other span.
+   */
+  intersect(start: time, end: time): HighPrecisionTimeSpan {
+    if (!this.overlaps(start, end)) {
+      return HighPrecisionTimeSpan.ZERO;
+    }
+    const newStart = this.start.clamp(start, end);
+    const newEnd = this.end.clamp(start, end);
+    const newDuration = newEnd.sub(newStart).toNumber();
+    return new HighPrecisionTimeSpan(newStart, newDuration);
+  }
+
+  /**
+   * Create a new timespan which fits within the specified bounds, preserving
+   * its duration if possible.
+   *
+   * This function moves the timespan forwards or backwards in time while
+   * keeping its duration unchanged, so that it fits entirely within the range
+   * defined by `start` and `end`.
+   *
+   * If the specified bounds are smaller than the current timespan's duration, a
+   * new timespan matching the bounds is returned.
+   *
+   * @param start The start of the bounds within which the timespan should fit.
+   * @param end The end of the bounds within which the timespan should fit.
+   *
+   * @example
+   * // assume `timespan` is defined as: [5, 8)
+   * timespan.fitWithin(10n, 20n); // -> [10, 13)
+   * timespan.fitWithin(-10n, -5n); // -> [-8, -5)
+   * timespan.fitWithin(1n, 2n); // -> [1, 2)
+   */
+  fitWithin(start: time, end: time): HighPrecisionTimeSpan {
+    if (this.duration > Number(end - start)) {
+      // Current span is greater than the limits
+      return HighPrecisionTimeSpan.fromTime(start, end);
+    }
+    if (this.start.integral < start) {
+      // Current span starts before limits
+      return new HighPrecisionTimeSpan(
+        new HighPrecisionTime(start),
+        this.duration,
+      );
+    }
+    if (this.end.gt(end)) {
+      // Current span ends after limits
+      return new HighPrecisionTimeSpan(
+        new HighPrecisionTime(end).subNumber(this.duration),
+        this.duration,
+      );
+    }
+    return this;
+  }
+
+  /**
+   * Clamp duration to some minimum value. The start remains the same, just the
+   * duration is changed.
+   */
+  clampDuration(minDuration: number): HighPrecisionTimeSpan {
+    if (this.duration < minDuration) {
+      return new HighPrecisionTimeSpan(this.start, minDuration);
+    } else {
+      return this;
+    }
+  }
+
+  /**
+   * Checks whether this span completely contains a time instant.
+   */
+  contains(t: time): boolean {
+    return this.start.lte(t) && this.end.gt(t);
+  }
+
+  /**
+   * Checks whether this span entirely contains another span.
+   *
+   * @param start The start of the span to check.
+   * @param end The end of the span to check.
+   */
+  containsSpan(start: time, end: time): boolean {
+    return this.start.lte(start) && this.end.gte(end);
+  }
+
+  /**
+   * Checks if this span overlaps at all with another.
+   *
+   * @param start The start of the span to check.
+   * @param end The end of the span to check.
+   */
+  overlaps(start: time, end: time): boolean {
+    return !(this.start.gte(end) || this.end.lte(start));
+  }
+
+  /**
+   * Get the span of integer intervals values that overlap this span.
+   */
+  toTimeSpan(): TimeSpan {
+    return new TimeSpan(this.start.toTime('floor'), this.end.toTime('ceil'));
+  }
+}
diff --git a/ui/src/base/high_precision_time_span_unittest.ts b/ui/src/base/high_precision_time_span_unittest.ts
new file mode 100644
index 0000000..537d173
--- /dev/null
+++ b/ui/src/base/high_precision_time_span_unittest.ts
@@ -0,0 +1,165 @@
+// 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 {HighPrecisionTime as HPTime} from './high_precision_time';
+import {HighPrecisionTimeSpan as HPTimeSpan} from './high_precision_time_span';
+import {Time} from './time';
+
+const t = Time.fromRaw;
+
+// Quick 'n' dirty function to convert a string to a HPtime
+// Used to make tests more readable
+// E.g. '1.3' -> {base: 1, offset: 0.3}
+// E.g. '-0.3' -> {base: -1, offset: 0.7}
+function hptime(time: string): HPTime {
+  const array = time.split('.');
+  if (array.length > 2) throw new Error(`Bad time format ${time}`);
+  const [base, fractions] = array;
+  const negative = time.startsWith('-');
+  const numBase = BigInt(base);
+
+  if (fractions) {
+    const numFractions = Number(`0.${fractions}`);
+    if (negative) {
+      return new HPTime(t(numBase - 1n), 1.0 - numFractions);
+    } else {
+      return new HPTime(t(numBase), numFractions);
+    }
+  } else {
+    return new HPTime(t(numBase));
+  }
+}
+
+describe('HighPrecisionTimeSpan', () => {
+  it('can be constructed from integer time', () => {
+    const span = HPTimeSpan.fromTime(t(10n), t(20n));
+    expect(span.start.integral).toEqual(10n);
+    expect(span.start.fractional).toBeCloseTo(0);
+    expect(span.duration).toBeCloseTo(10);
+  });
+
+  test('end', () => {
+    const span = HPTimeSpan.fromTime(t(10n), t(20n));
+    expect(span.end.integral).toEqual(20n);
+    expect(span.end.fractional).toBeCloseTo(0);
+  });
+
+  test('midpoint', () => {
+    const span = HPTimeSpan.fromTime(t(10n), t(20n));
+    expect(span.midpoint.integral).toEqual(15n);
+    expect(span.midpoint.fractional).toBeCloseTo(0);
+  });
+
+  test('translate', () => {
+    const span = HPTimeSpan.fromTime(t(10n), t(20n));
+    expect(span.translate(10).start.integral).toEqual(20n);
+    expect(span.translate(10).start.fractional).toEqual(0);
+    expect(span.translate(10).duration).toBeCloseTo(10);
+  });
+
+  test('pad', () => {
+    const span = HPTimeSpan.fromTime(t(10n), t(20n));
+    expect(span.pad(10).start.integral).toEqual(0n);
+    expect(span.pad(10).start.fractional).toEqual(0);
+    expect(span.pad(10).duration).toBeCloseTo(30);
+  });
+
+  test('scale', () => {
+    const span = HPTimeSpan.fromTime(t(10n), t(20n));
+    const zoomed = span.scale(2, 0.5, 0);
+    expect(zoomed.start.integral).toEqual(5n);
+    expect(zoomed.start.fractional).toBeCloseTo(0);
+    expect(zoomed.duration).toBeCloseTo(20);
+  });
+
+  test('intersect', () => {
+    const span = new HPTimeSpan(hptime('5'), 3);
+
+    let result = span.intersect(t(7n), t(10n));
+    expect(result.start.integral).toBe(7n);
+    expect(result.start.fractional).toBeCloseTo(0);
+    expect(result.duration).toBeCloseTo(1);
+
+    result = span.intersect(t(1n), t(6n));
+    expect(result.start.integral).toBe(5n);
+    expect(result.start.fractional).toBeCloseTo(0);
+    expect(result.duration).toBeCloseTo(1);
+
+    // Non overlapping time spans should return 0
+    result = span.intersect(t(100n), t(200n));
+    expect(result.start.integral).toBe(0n);
+    expect(result.start.fractional).toBeCloseTo(0);
+    expect(result.duration).toBeCloseTo(0);
+  });
+
+  test('fitWithin', () => {
+    const span = new HPTimeSpan(hptime('5'), 3);
+
+    let result = span.fitWithin(t(10n), t(20n));
+    expect(result.start.integral).toBe(10n);
+    expect(result.start.fractional).toBeCloseTo(0);
+    expect(result.duration).toBeCloseTo(3);
+
+    result = span.fitWithin(t(-10n), t(-5n));
+    expect(result.start.integral).toBe(-8n);
+    expect(result.start.fractional).toBeCloseTo(0);
+    expect(result.duration).toBeCloseTo(3);
+
+    result = span.fitWithin(t(1n), t(2n));
+    expect(result.start.integral).toBe(1n);
+    expect(result.start.fractional).toBeCloseTo(0);
+    expect(result.duration).toBeCloseTo(1);
+  });
+
+  test('clampDuration', () => {
+    const span = new HPTimeSpan(hptime('5'), 1);
+    const clamped = span.clampDuration(10);
+
+    expect(clamped.start.integral).toBe(5n);
+    expect(clamped.start.fractional).toBeCloseTo(0);
+    expect(clamped.duration).toBeCloseTo(10);
+  });
+
+  test('equality', () => {
+    const span = new HPTimeSpan(hptime('10'), 10);
+    expect(span.equals(span)).toBe(true);
+    expect(span.equals(new HPTimeSpan(hptime('10'), 10.5))).toBe(false);
+    expect(span.equals(new HPTimeSpan(hptime('10.1'), 10))).toBe(false);
+  });
+
+  test('contains', () => {
+    const span = new HPTimeSpan(hptime('10'), 10);
+    expect(span.contains(t(9n))).toBe(false);
+    expect(span.contains(t(10n))).toBe(true);
+    expect(span.contains(t(19n))).toBe(true);
+    expect(span.contains(t(20n))).toBe(false);
+  });
+
+  test('containsSpan', () => {
+    const span = new HPTimeSpan(hptime('10'), 10);
+    expect(span.containsSpan(t(9n), t(15n))).toBe(false);
+    expect(span.containsSpan(t(10n), t(15n))).toBe(true);
+    expect(span.containsSpan(t(15n), t(20n))).toBe(true);
+    expect(span.containsSpan(t(15n), t(21n))).toBe(false);
+    expect(span.containsSpan(t(30n), t(40n))).toBe(false);
+  });
+
+  test('overlapsSpan', () => {
+    const span = new HPTimeSpan(hptime('10'), 10);
+    expect(span.overlaps(t(9n), t(10n))).toBe(false);
+    expect(span.overlaps(t(9n), t(11n))).toBe(true);
+    expect(span.overlaps(t(19n), t(21n))).toBe(true);
+    expect(span.overlaps(t(20n), t(21n))).toBe(false);
+  });
+});
diff --git a/ui/src/base/high_precision_time_unittest.ts b/ui/src/base/high_precision_time_unittest.ts
new file mode 100644
index 0000000..833d473
--- /dev/null
+++ b/ui/src/base/high_precision_time_unittest.ts
@@ -0,0 +1,211 @@
+// 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 {Time, time} from './time';
+import {HighPrecisionTime as HPTime} from './high_precision_time';
+
+const t = Time.fromRaw;
+
+// Quick 'n' dirty function to convert a string to a HPtime
+// Used to make tests more readable
+// E.g. '1.3' -> {base: 1, offset: 0.3}
+// E.g. '-0.3' -> {base: -1, offset: 0.7}
+function mkTime(time: string): HPTime {
+  const array = time.split('.');
+  if (array.length > 2) throw new Error(`Bad time format ${time}`);
+  const [base, fractions] = array;
+  const negative = time.startsWith('-');
+  const numBase = BigInt(base);
+
+  if (fractions) {
+    const numFractions = Number(`0.${fractions}`);
+    if (negative) {
+      return new HPTime(t(numBase - 1n), 1.0 - numFractions);
+    } else {
+      return new HPTime(t(numBase), numFractions);
+    }
+  } else {
+    return new HPTime(t(numBase));
+  }
+}
+
+describe('Time', () => {
+  it('should create a new Time object with the given base and offset', () => {
+    const time = new HPTime(t(136n), 0.3);
+    expect(time.integral).toBe(136n);
+    expect(time.fractional).toBeCloseTo(0.3);
+  });
+
+  it('should normalize when offset is >= 1', () => {
+    let time = new HPTime(t(1n), 2.3);
+    expect(time.integral).toBe(3n);
+    expect(time.fractional).toBeCloseTo(0.3);
+
+    time = new HPTime(t(1n), 1);
+    expect(time.integral).toBe(2n);
+    expect(time.fractional).toBeCloseTo(0);
+  });
+
+  it('should normalize when offset is < 0', () => {
+    const time = new HPTime(t(1n), -0.4);
+    expect(time.integral).toBe(0n);
+    expect(time.fractional).toBeCloseTo(0.6);
+  });
+
+  it('should store timestamps without losing precision', () => {
+    const time = new HPTime(t(1152921504606846976n));
+    expect(time.toTime()).toBe(1152921504606846976n as time);
+  });
+
+  it('should store and manipulate timestamps without losing precision', () => {
+    let time = new HPTime(t(2315700508990407843n));
+    time = time.addTime(2315718101717517451n as time);
+    expect(time.toTime()).toBe(4631418610707925294n);
+  });
+
+  test('add', () => {
+    const result = mkTime('1.3').add(mkTime('3.1'));
+    expect(result.integral).toEqual(4n);
+    expect(result.fractional).toBeCloseTo(0.4);
+  });
+
+  test('addTime', () => {
+    const result = mkTime('200.334').addTime(t(150n));
+    expect(result.integral).toBe(350n);
+    expect(result.fractional).toBeCloseTo(0.334);
+  });
+
+  test('addNumber', () => {
+    const result = mkTime('200.334').addNumber(150.5);
+    expect(result.integral).toBe(350n);
+    expect(result.fractional).toBeCloseTo(0.834);
+  });
+
+  test('sub', () => {
+    const result = mkTime('1.3').sub(mkTime('3.1'));
+    expect(result.integral).toEqual(-2n);
+    expect(result.fractional).toBeCloseTo(0.2);
+  });
+
+  test('addTime', () => {
+    const result = mkTime('200.334').subTime(t(150n));
+    expect(result.integral).toBe(50n);
+    expect(result.fractional).toBeCloseTo(0.334);
+  });
+
+  test('subNumber', () => {
+    const result = mkTime('200.334').subNumber(150.5);
+    expect(result.integral).toBe(49n);
+    expect(result.fractional).toBeCloseTo(0.834);
+  });
+
+  test('gte', () => {
+    expect(mkTime('1.0').gte(t(1n))).toBe(true);
+    expect(mkTime('1.0').gte(t(2n))).toBe(false);
+    expect(mkTime('1.2').gte(t(1n))).toBe(true);
+    expect(mkTime('1.2').gte(t(2n))).toBe(false);
+  });
+
+  test('gt', () => {
+    expect(mkTime('1.0').gt(t(1n))).toBe(false);
+    expect(mkTime('1.0').gt(t(2n))).toBe(false);
+    expect(mkTime('1.2').gt(t(1n))).toBe(true);
+    expect(mkTime('1.2').gt(t(2n))).toBe(false);
+  });
+
+  test('lte', () => {
+    expect(mkTime('1.0').lte(t(0n))).toBe(false);
+    expect(mkTime('1.0').lte(t(1n))).toBe(true);
+    expect(mkTime('1.0').lte(t(2n))).toBe(true);
+    expect(mkTime('1.2').lte(t(1n))).toBe(false);
+    expect(mkTime('1.2').lte(t(2n))).toBe(true);
+  });
+
+  test('lt', () => {
+    expect(mkTime('1.0').lt(t(0n))).toBe(false);
+    expect(mkTime('1.0').lt(t(1n))).toBe(false);
+    expect(mkTime('1.0').lt(t(2n))).toBe(true);
+    expect(mkTime('1.2').lt(t(1n))).toBe(false);
+    expect(mkTime('1.2').lt(t(2n))).toBe(true);
+  });
+
+  test('equals', () => {
+    const time = new HPTime(t(1n), 0.2);
+    expect(time.equals(new HPTime(t(1n), 0.2))).toBeTruthy();
+    expect(time.equals(new HPTime(t(0n), 1.2))).toBeTruthy();
+    expect(time.equals(new HPTime(t(-100n), 101.2))).toBeTruthy();
+    expect(time.equals(new HPTime(t(1n), 0.3))).toBeFalsy();
+    expect(time.equals(new HPTime(t(2n), 0.2))).toBeFalsy();
+  });
+
+  test('containedWithin', () => {
+    expect(mkTime('0.9').containedWithin(t(1n), t(2n))).toBe(false);
+    expect(mkTime('1.0').containedWithin(t(1n), t(2n))).toBe(true);
+    expect(mkTime('1.2').containedWithin(t(1n), t(2n))).toBe(true);
+    expect(mkTime('2.0').containedWithin(t(1n), t(2n))).toBe(false);
+    expect(mkTime('2.1').containedWithin(t(1n), t(2n))).toBe(false);
+  });
+
+  test('clamp', () => {
+    let result = mkTime('1.2').clamp(t(1n), t(2n));
+    expect(result.integral).toBe(1n);
+    expect(result.fractional).toBeCloseTo(0.2);
+
+    result = mkTime('2.2').clamp(t(1n), t(2n));
+    expect(result.integral).toBe(2n);
+    expect(result.fractional).toBeCloseTo(0);
+
+    result = mkTime('0.2').clamp(t(1n), t(2n));
+    expect(result.integral).toBe(1n);
+    expect(result.fractional).toBeCloseTo(0);
+  });
+
+  test('toNumber', () => {
+    expect(new HPTime(t(1n), 0.2).toNumber()).toBeCloseTo(1.2);
+    expect(new HPTime(t(1000000000n), 0.0).toNumber()).toBeCloseTo(1e9);
+  });
+
+  test('toTime', () => {
+    expect(new HPTime(t(1n), 0.2).toTime('round')).toBe(1n);
+    expect(new HPTime(t(1n), 0.5).toTime('round')).toBe(2n);
+    expect(new HPTime(t(1n), 0.2).toTime('floor')).toBe(1n);
+    expect(new HPTime(t(1n), 0.5).toTime('floor')).toBe(1n);
+    expect(new HPTime(t(1n), 0.2).toTime('ceil')).toBe(2n);
+    expect(new HPTime(t(1n), 0.5).toTime('ceil')).toBe(2n);
+  });
+
+  test('toString', () => {
+    expect(mkTime('1.3').toString()).toBe('1.3');
+    expect(mkTime('12983423847.332533').toString()).toBe('12983423847.332533');
+    expect(new HPTime(t(234n)).toString()).toBe('234');
+  });
+
+  test('abs', () => {
+    let result = mkTime('-0.7').abs();
+    expect(result.integral).toEqual(0n);
+    expect(result.fractional).toBeCloseTo(0.7);
+
+    result = mkTime('-1.3').abs();
+    expect(result.integral).toEqual(1n);
+    expect(result.fractional).toBeCloseTo(0.3);
+
+    result = mkTime('-100').abs();
+    expect(result.integral).toEqual(100n);
+    expect(result.fractional).toBeCloseTo(0);
+
+    result = mkTime('34.5345').abs();
+    expect(result.integral).toEqual(34n);
+    expect(result.fractional).toBeCloseTo(0.5345);
+  });
+});
diff --git a/ui/src/base/hotkeys.ts b/ui/src/base/hotkeys.ts
index 2dec16c..17e3a00 100644
--- a/ui/src/base/hotkeys.ts
+++ b/ui/src/base/hotkeys.ts
@@ -46,7 +46,6 @@
 // these keys.
 
 import {elementIsEditable} from './dom_utils';
-import {Optional} from './utils';
 
 type Alphabet =
   | 'A'
@@ -170,7 +169,7 @@
 
 // Deconstruct a hotkey from its string representation into its constituent
 // parts.
-export function parseHotkey(hotkey: Hotkey): Optional<HotkeyParts> {
+export function parseHotkey(hotkey: Hotkey): HotkeyParts | undefined {
   const regex = /^(!?)((?:Mod\+|Shift\+|Alt\+|Ctrl\+)*)(.*)$/;
   const result = hotkey.match(regex);
 
@@ -189,7 +188,7 @@
 export function formatHotkey(
   hotkey: Hotkey,
   spoof?: Platform,
-): Optional<string> {
+): string | undefined {
   const parsed = parseHotkey(hotkey);
   return parsed && formatHotkeyParts(parsed, spoof);
 }
@@ -281,3 +280,26 @@
 export function getPlatform(): Platform {
   return window.navigator.platform.indexOf('Mac') !== -1 ? 'Mac' : 'PC';
 }
+
+// Returns a cross-platform check for whether the event has "Mod" key pressed
+// (e.g. as a part of Mod-Click UX pattern).
+// On Mac, Mod-click is actually Command-click and on PC it's Control-click,
+// so this function handles this for all platforms.
+export function hasModKey(event: {
+  readonly metaKey: boolean;
+  readonly ctrlKey: boolean;
+}): boolean {
+  if (getPlatform() === 'Mac') {
+    return event.metaKey;
+  } else {
+    return event.ctrlKey;
+  }
+}
+
+export function modKey(): {metaKey?: boolean; ctrlKey?: boolean} {
+  if (getPlatform() === 'Mac') {
+    return {metaKey: true};
+  } else {
+    return {ctrlKey: true};
+  }
+}
diff --git a/ui/src/base/http_utils.ts b/ui/src/base/http_utils.ts
index 357fa5d..869dfb6 100644
--- a/ui/src/base/http_utils.ts
+++ b/ui/src/base/http_utils.ts
@@ -32,10 +32,45 @@
   });
 }
 
+export function fetchWithProgress(
+  url: string,
+  onProgress?: (percentage: number) => void,
+): Promise<Blob> {
+  return new Promise((resolve, reject) => {
+    const xhr = new XMLHttpRequest();
+
+    xhr.open('GET', url, /* async= */ true);
+    xhr.responseType = 'blob';
+
+    xhr.onprogress = (event) => {
+      if (event.lengthComputable) {
+        const percentComplete = Math.round((event.loaded / event.total) * 100);
+        onProgress?.(percentComplete);
+      }
+    };
+
+    xhr.onload = () => {
+      if (xhr.status >= 200 && xhr.status < 300) {
+        resolve(xhr.response); // Resolve with the Blob response
+      } else {
+        reject(
+          new Error(`Failed to download: ${xhr.status} ${xhr.statusText}`),
+        );
+      }
+    };
+
+    xhr.onerror = () => {
+      reject(new Error(`Network error in fetchWithProgress(${url})`));
+    };
+
+    xhr.send();
+  });
+}
+
 /**
  * NOTE: this function can only be called from synchronous contexts. It will
  * fail if called in timer handlers or async continuations (e.g. after an await)
- * Use globals.root which caches it on startup.
+ * Use assetSrc(relPath) which caches it on startup.
  * @returns the directory where the app is served from, e.g. 'v46.0-a2082649b'
  */
 export function getServingRoot() {
diff --git a/ui/src/base/logging.ts b/ui/src/base/logging.ts
index bfe7940..6a0dc5c 100644
--- a/ui/src/base/logging.ts
+++ b/ui/src/base/logging.ts
@@ -36,6 +36,11 @@
   return value;
 }
 
+export function assertIsInstance<T>(value: unknown, clazz: Function): T {
+  assertTrue(value instanceof clazz);
+  return value as T;
+}
+
 export function assertTrue(value: boolean, optMsg?: string) {
   if (!value) {
     throw new Error(optMsg ?? 'Failed assertion');
diff --git a/ui/src/base/mithril_utils.ts b/ui/src/base/mithril_utils.ts
index b450f64..9b0615d 100644
--- a/ui/src/base/mithril_utils.ts
+++ b/ui/src/base/mithril_utils.ts
@@ -52,3 +52,36 @@
     );
   },
 };
+
+/**
+ * Utility function to pre-bind some mithril attrs of a component, and leave
+ * the others unbound and passed at run-time.
+ * Example use case: the Page API Passes to the registered page a PageAttrs,
+ * which is {subpage:string}. Imagine you write a MyPage component that takes
+ * some extra input attrs (e.g. the App object) and you want to bind them
+ * onActivate(). The results looks like this:
+ *
+ * interface MyPageAttrs extends PageAttrs { app: App; }
+ *
+ * class MyPage extends m.classComponent<MyPageAttrs> {... view() {...} }
+ *
+ * onActivate(app: App) {
+ *   pages.register(... bindMithrilApps(MyPage, {app: app});
+ * }
+ *
+ * The return value of bindMithrilApps is a mithril component that takes in
+ * input only a {subpage: string} and passes down to MyPage the combination
+ * of pre-bound and runtime attrs, that is {subpage, app}.
+ */
+export function bindMithrilAttrs<BaseAttrs, Attrs>(
+  component: m.ComponentTypes<Attrs>,
+  boundArgs: Omit<Attrs, keyof BaseAttrs>,
+): m.Component<BaseAttrs> {
+  return {
+    view(vnode: m.Vnode<BaseAttrs>) {
+      const attrs = {...vnode.attrs, ...boundArgs} as Attrs;
+      const emptyAttrs: m.CommonAttributes<Attrs, {}> = {}; // Keep tsc happy.
+      return m<Attrs, {}>(component, {...attrs, ...emptyAttrs});
+    },
+  };
+}
diff --git a/ui/src/base/rand.ts b/ui/src/base/rand.ts
new file mode 100644
index 0000000..d6d567a
--- /dev/null
+++ b/ui/src/base/rand.ts
@@ -0,0 +1,34 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Match C++ minstd_rand0 behaviour.
+const MODULUS = 2 ** 31 - 1;
+const MULTIPLIER = 48271;
+const INCREMENT = 1;
+
+// Allow callers to have a private sequence.
+export interface RandState {
+  seed: number;
+}
+
+// If callers don't want to bother maintain their own state, use the global one
+// for the whole app.
+const globalRandState: RandState = {seed: 0};
+
+// Like math.Rand(), but yields a repeateabled sequence (matters for tests).
+export function pseudoRand(state?: RandState): number {
+  state = state ?? globalRandState;
+  state.seed = (MULTIPLIER * state.seed + INCREMENT) % MODULUS;
+  return state.seed / MODULUS;
+}
diff --git a/ui/src/base/semantic_icons.ts b/ui/src/base/semantic_icons.ts
index 96b872a..8025075 100644
--- a/ui/src/base/semantic_icons.ts
+++ b/ui/src/base/semantic_icons.ts
@@ -17,6 +17,7 @@
   static readonly UpdateSelection = 'call_made'; // Could be 'open_in_new'
   static readonly ChangeViewport = 'query_stats'; // Could be 'search'
   static readonly ContextMenu = 'arrow_drop_down'; // Could be 'more_vert'
+  static readonly Menu = 'menu';
   static readonly Copy = 'content_copy';
   static readonly Delete = 'delete';
   static readonly SortedAsc = 'arrow_upward';
diff --git a/ui/src/base/store.ts b/ui/src/base/store.ts
index e08b5db..afdb5a2 100644
--- a/ui/src/base/store.ts
+++ b/ui/src/base/store.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import {produce, Draft} from 'immer';
-
 import {getPath, Path, setPath} from './object_utils';
 
 export type Migrate<T> = (init: unknown) => T;
diff --git a/ui/src/base/store_unittest.ts b/ui/src/base/store_unittest.ts
index 123f876..3bf7d55 100644
--- a/ui/src/base/store_unittest.ts
+++ b/ui/src/base/store_unittest.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import {Draft} from 'immer';
-
 import {createStore} from './store';
 import {exists} from './utils';
 
diff --git a/ui/src/base/string_utils.ts b/ui/src/base/string_utils.ts
index 389551c..df0e3da 100644
--- a/ui/src/base/string_utils.ts
+++ b/ui/src/base/string_utils.ts
@@ -93,6 +93,11 @@
   return `'${str.replaceAll("'", "''")}'`;
 }
 
+// Makes a string safe to be used as a SQL table/view/function name.
+export function sqlNameSafe(str: string): string {
+  return str.replace(/[^a-zA-Z0-9_]+/g, '_');
+}
+
 // Chat apps (including G Chat) sometimes replace ASCII characters with similar
 // looking unicode characters that break code snippets.
 // This function attempts to undo these replacements.
diff --git a/ui/src/base/time.ts b/ui/src/base/time.ts
index 8a3ce62..2837a2a 100644
--- a/ui/src/base/time.ts
+++ b/ui/src/base/time.ts
@@ -28,6 +28,7 @@
 // The conversion factor for converting between different time units.
 const TIME_UNITS_PER_SEC = 1e9;
 const TIME_UNITS_PER_MILLISEC = 1e6;
+const TIME_UNITS_PER_MICROSEC = 1e3;
 
 export class Time {
   // Negative time is never found in a trace - so -1 is commonly used as a flag
@@ -87,6 +88,20 @@
     return Number(t) / TIME_UNITS_PER_MILLISEC;
   }
 
+  // Convert microseconds (number) to a time value.
+  // Note: number -> BigInt conversion is relatively slow.
+  static fromMicros(millis: number): time {
+    return Time.fromRaw(BigInt(Math.floor(millis * TIME_UNITS_PER_MICROSEC)));
+  }
+
+  // Convert time value to microseconds and return as a number (i.e. float).
+  // Warning: This function is lossy, i.e. precision is lost when converting
+  // BigInt -> number.
+  // Note: BigInt -> number conversion is relatively slow.
+  static toMicros(t: time): number {
+    return Number(t) / TIME_UNITS_PER_MICROSEC;
+  }
+
   // Convert a Date object to a time value, given an offset from the unix epoch.
   // Note: number -> BigInt conversion is relatively slow.
   static fromDate(d: Date, offset: duration): time {
@@ -147,11 +162,18 @@
     return Time.fromRaw(BigintMath.quant(a, b));
   }
 
-  // Format time as seconds.
   static formatSeconds(time: time): string {
     return Time.toSeconds(time).toString() + ' s';
   }
 
+  static formatMilliseconds(time: time): string {
+    return Time.toMillis(time).toString() + ' ms';
+  }
+
+  static formatMicroseconds(time: time): string {
+    return Time.toMicros(time).toString() + ' us';
+  }
+
   static toTimecode(time: time): Timecode {
     return new Timecode(time);
   }
@@ -199,6 +221,18 @@
     return Number(d) / TIME_UNITS_PER_SEC;
   }
 
+  // Convert time to seconds as a number.
+  // Use this function with caution. It loses precision and is slow.
+  static toMilliseconds(d: duration) {
+    return Number(d) / TIME_UNITS_PER_MILLISEC;
+  }
+
+  // Convert time to seconds as a number.
+  // Use this function with caution. It loses precision and is slow.
+  static toMicroSeconds(d: duration) {
+    return Number(d) / TIME_UNITS_PER_MICROSEC;
+  }
+
   // Print duration as as human readable string - i.e. to only a handful of
   // significant figues.
   // Use this when readability is more desireable than precision.
@@ -245,6 +279,14 @@
   static formatSeconds(dur: duration): string {
     return Duration.toSeconds(dur).toString() + ' s';
   }
+
+  static formatMilliseconds(dur: duration): string {
+    return Duration.toMilliseconds(dur).toString() + ' s';
+  }
+
+  static formatMicroseconds(dur: duration): string {
+    return Duration.toMicroSeconds(dur).toString() + ' s';
+  }
 }
 
 // This class takes a time and converts it to a set of strings representing a
diff --git a/ui/src/base/time_scale.ts b/ui/src/base/time_scale.ts
new file mode 100644
index 0000000..3b31769
--- /dev/null
+++ b/ui/src/base/time_scale.ts
@@ -0,0 +1,62 @@
+// 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 {duration, time} from './time';
+import {HighPrecisionTime} from './high_precision_time';
+import {HighPrecisionTimeSpan} from './high_precision_time_span';
+import {HorizontalBounds} from './geom';
+
+export class TimeScale {
+  readonly timeSpan: HighPrecisionTimeSpan;
+  readonly pxBounds: HorizontalBounds;
+  private readonly timePerPx: number;
+
+  constructor(timespan: HighPrecisionTimeSpan, pxBounds: HorizontalBounds) {
+    this.pxBounds = pxBounds;
+    this.timeSpan = timespan;
+    const delta = pxBounds.right - pxBounds.left;
+    if (timespan.duration <= 0 || delta <= 0) {
+      this.timePerPx = 1;
+    } else {
+      this.timePerPx = timespan.duration / delta;
+    }
+  }
+
+  timeToPx(ts: time): number {
+    const timeOffset =
+      Number(ts - this.timeSpan.start.integral) -
+      this.timeSpan.start.fractional;
+    return this.pxBounds.left + timeOffset / this.timePerPx;
+  }
+
+  hpTimeToPx(time: HighPrecisionTime): number {
+    const timeOffset = time.sub(this.timeSpan.start).toNumber();
+    return this.pxBounds.left + timeOffset / this.timePerPx;
+  }
+
+  // Convert pixels to a high precision time object, which can be further
+  // converted to other time formats.
+  pxToHpTime(px: number): HighPrecisionTime {
+    const timeOffset = (px - this.pxBounds.left) * this.timePerPx;
+    return this.timeSpan.start.addNumber(timeOffset);
+  }
+
+  durationToPx(dur: duration): number {
+    return Number(dur) / this.timePerPx;
+  }
+
+  pxToDuration(pxDelta: number): number {
+    return pxDelta * this.timePerPx;
+  }
+}
diff --git a/ui/src/base/time_scale_unittest.ts b/ui/src/base/time_scale_unittest.ts
new file mode 100644
index 0000000..f68993e
--- /dev/null
+++ b/ui/src/base/time_scale_unittest.ts
@@ -0,0 +1,75 @@
+// 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 {Time} from './time';
+import {HighPrecisionTime} from './high_precision_time';
+import {HighPrecisionTimeSpan} from './high_precision_time_span';
+import {TimeScale} from './time_scale';
+
+const t = Time.fromRaw;
+
+describe('TimeScale', () => {
+  const ts = new TimeScale(
+    new HighPrecisionTimeSpan(new HighPrecisionTime(t(40n)), 100),
+    {left: 200, right: 1000},
+  );
+
+  it('converts timescales to pixels', () => {
+    expect(ts.timeToPx(Time.fromRaw(40n))).toEqual(200);
+    expect(ts.timeToPx(Time.fromRaw(140n))).toEqual(1000);
+    expect(ts.timeToPx(Time.fromRaw(90n))).toEqual(600);
+
+    expect(ts.timeToPx(Time.fromRaw(240n))).toEqual(1800);
+    expect(ts.timeToPx(Time.fromRaw(-60n))).toEqual(-600);
+  });
+
+  it('converts pixels to HPTime objects', () => {
+    let result = ts.pxToHpTime(200);
+    expect(result.integral).toEqual(40n);
+    expect(result.fractional).toBeCloseTo(0);
+
+    result = ts.pxToHpTime(1000);
+    expect(result.integral).toEqual(140n);
+    expect(result.fractional).toBeCloseTo(0);
+
+    result = ts.pxToHpTime(600);
+    expect(result.integral).toEqual(90n);
+    expect(result.fractional).toBeCloseTo(0);
+
+    result = ts.pxToHpTime(1800);
+    expect(result.integral).toEqual(240n);
+    expect(result.fractional).toBeCloseTo(0);
+
+    result = ts.pxToHpTime(-600);
+    expect(result.integral).toEqual(-60n);
+    expect(result.fractional).toBeCloseTo(0);
+  });
+
+  it('converts durations to pixels', () => {
+    expect(ts.durationToPx(0n)).toEqual(0);
+    expect(ts.durationToPx(1n)).toEqual(8);
+    expect(ts.durationToPx(1000n)).toEqual(8000);
+  });
+
+  it('converts pxDeltaToDurations to HPTime durations', () => {
+    let result = ts.pxToDuration(0);
+    expect(result).toBeCloseTo(0);
+
+    result = ts.pxToDuration(1);
+    expect(result).toBeCloseTo(0.125);
+
+    result = ts.pxToDuration(100);
+    expect(result).toBeCloseTo(12.5);
+  });
+});
diff --git a/ui/src/base/utils.ts b/ui/src/base/utils.ts
index d443051..a82062c 100644
--- a/ui/src/base/utils.ts
+++ b/ui/src/base/utils.ts
@@ -25,10 +25,66 @@
   | {success: true; result: T}
   | {success: false; error: E};
 
-// Generic "optional" type
-export type Optional<T> = T | undefined;
+// Type util to make sure that exactly one of the passed keys is defined.
+// Example usage:
+// type FooOrBar = ExactlyOne<{foo: number; bar: number}>;
+// const x : FooOrBar = {foo: 42};      // OK
+// const x : FooOrBar = {bar: 42};      // OK
+// const x : FooOrBar = {};             // Compiler error
+// const x : FooOrBar = {foo:1, bar:2}; // Compiler error
+export type ExactlyOne<T, K extends keyof T = keyof T> = K extends keyof T
+  ? {[P in K]: T[P]} & {[P in Exclude<keyof T, K>]?: undefined}
+  : never;
 
 // Escape characters that are not allowed inside a css selector
 export function escapeCSSSelector(selector: string): string {
   return selector.replace(/([!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, '\\$1');
 }
+
+// Make field K required in T
+export type RequiredField<T, K extends keyof T> = Omit<T, K> &
+  Required<Pick<T, K>>;
+
+// The lowest common denoninator between Map<> and WeakMap<>.
+// This is just to avoid duplication of the getOrCreate below.
+interface MapLike<K, V> {
+  get(key: K): V | undefined;
+  set(key: K, value: V): this;
+}
+
+export function getOrCreate<K, V>(
+  map: MapLike<K, V>,
+  key: K,
+  factory: () => V,
+): V {
+  let value = map.get(key);
+  if (value !== undefined) return value;
+  value = factory();
+  map.set(key, value);
+  return value;
+}
+
+// Allows to take an existing class instance (`target`) and override some of its
+// methods via `overrides`. We use this for cases where we want to expose a
+// "manager" (e.g. TrackManager, SidebarManager) to the plugins, but we want to
+// override few of its methods (e.g. to inject the pluginId in the args).
+export function createProxy<T extends object>(
+  target: T,
+  overrides: Partial<T>,
+): T {
+  return new Proxy(target, {
+    get: (target: T, prop: string | symbol, receiver) => {
+      // If the property is overriden, use that; otherwise, use target
+      const overrideValue = (overrides as {[key: symbol | string]: {}})[prop];
+      if (overrideValue !== undefined) {
+        return typeof overrideValue === 'function'
+          ? overrideValue.bind(overrides)
+          : overrideValue;
+      }
+      const baseValue = Reflect.get(target, prop, receiver);
+      return typeof baseValue === 'function'
+        ? baseValue.bind(target)
+        : baseValue;
+    },
+  }) as T;
+}
diff --git a/ui/src/base/uuid.ts b/ui/src/base/uuid.ts
index 1c595ba..1ffdd37 100644
--- a/ui/src/base/uuid.ts
+++ b/ui/src/base/uuid.ts
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import {v4} from 'uuid';
+import {sqlNameSafe} from './string_utils';
 
 export const uuidv4 = v4;
 
@@ -23,5 +24,5 @@
  */
 export function uuidv4Sql(uuid?: string): string {
   const str = uuid ?? uuidv4();
-  return str.replace(/[^a-zA-Z0-9_]+/g, '_');
+  return sqlNameSafe(str);
 }
diff --git a/ui/src/bigtrace/index.ts b/ui/src/bigtrace/index.ts
index 739594e..db3e71b 100644
--- a/ui/src/bigtrace/index.ts
+++ b/ui/src/bigtrace/index.ts
@@ -14,9 +14,7 @@
 
 // Keep this import first.
 import '../base/static_initializers';
-
 import m from 'mithril';
-
 import {defer} from '../base/deferred';
 import {reportError, addErrorHandler, ErrorDetails} from '../base/logging';
 import {initLiveReloadIfLocalhost} from '../core/live_reload';
diff --git a/ui/src/chrome_extension/chrome_tracing_controller.ts b/ui/src/chrome_extension/chrome_tracing_controller.ts
index 916fca9..de15873 100644
--- a/ui/src/chrome_extension/chrome_tracing_controller.ts
+++ b/ui/src/chrome_extension/chrome_tracing_controller.ts
@@ -21,13 +21,13 @@
   ConsumerPortResponse,
   GetTraceStatsResponse,
   ReadBuffersResponse,
-} from '../controller/consumer_port_types';
-import {RpcConsumerPort} from '../controller/record_controller_interfaces';
+} from '../plugins/dev.perfetto.RecordTrace/consumer_port_types';
+import {RpcConsumerPort} from '../plugins/dev.perfetto.RecordTrace/record_controller_interfaces';
 import {
   browserSupportsPerfettoConfig,
   extractTraceConfig,
   hasSystemDataSourceConfig,
-} from '../core/trace_config_utils';
+} from '../plugins/dev.perfetto.RecordTrace/trace_config_utils';
 import {ITraceStats, TraceConfig} from '../protos';
 
 import {DevToolsSocket} from './devtools_socket';
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
deleted file mode 100644
index 51e765d..0000000
--- a/ui/src/common/actions.ts
+++ /dev/null
@@ -1,1044 +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 {Draft} from 'immer';
-
-import {SortDirection} from '../base/comparison_utils';
-import {assertExists, assertTrue} from '../base/logging';
-import {duration, time} from '../base/time';
-import {RecordConfig} from '../controller/record_config_types';
-import {randomColor} from '../core/colorizer';
-import {
-  GenericSliceDetailsTabConfig,
-  GenericSliceDetailsTabConfigBase,
-} from '../frontend/generic_slice_details_tab';
-import {
-  Aggregation,
-  AggregationFunction,
-  TableColumn,
-  tableColumnEquals,
-  toggleEnabled,
-} from '../frontend/pivot_table_types';
-
-import {
-  computeIntervals,
-  DropDirection,
-  performReordering,
-} from './dragndrop_logic';
-import {createEmptyState} from './empty_state';
-import {
-  MetatraceTrackId,
-  traceEventBegin,
-  traceEventEnd,
-  TraceEventScope,
-} from './metatracing';
-import {
-  AdbRecordingTarget,
-  EngineMode,
-  LoadedConfig,
-  NewEngineMode,
-  OmniboxMode,
-  OmniboxState,
-  PendingDeeplinkState,
-  PivotTableResult,
-  PrimaryTrackSortKey,
-  ProfileType,
-  RecordingTarget,
-  SCROLLING_TRACK_GROUP,
-  State,
-  Status,
-  ThreadTrackSortKey,
-  TrackSortKey,
-  UtidToTrackSortKey,
-} from './state';
-
-type StateDraft = Draft<State>;
-
-export interface AddTrackArgs {
-  key?: string;
-  uri: string;
-  name: string;
-  trackSortKey: TrackSortKey;
-  trackGroup?: string;
-  closeable?: boolean;
-}
-
-export interface PostedTrace {
-  buffer: ArrayBuffer;
-  title: string;
-  fileName?: string;
-  url?: string;
-  uuid?: string;
-  localOnly?: boolean;
-  keepApiOpen?: boolean;
-
-  // Allows to pass extra arguments to plugins. This can be read by plugins
-  // onTraceLoad() and can be used to trigger plugin-specific-behaviours (e.g.
-  // allow dashboards like APC to pass extra data to materialize onto tracks).
-  // The format is the following:
-  // pluginArgs: {
-  //   'dev.perfetto.PluginFoo': { 'key1': 'value1', 'key2': 1234 }
-  //   'dev.perfetto.PluginBar': { 'key3': '...', 'key4': ... }
-  // }
-  pluginArgs?: {[pluginId: string]: {[key: string]: unknown}};
-}
-
-export interface PostedScrollToRange {
-  timeStart: number;
-  timeEnd: number;
-  viewPercentage?: number;
-}
-
-function clearTraceState(state: StateDraft) {
-  const nextId = state.nextId;
-  const recordConfig = state.recordConfig;
-  const recordingTarget = state.recordingTarget;
-  const fetchChromeCategories = state.fetchChromeCategories;
-  const extensionInstalled = state.extensionInstalled;
-  const availableAdbDevices = state.availableAdbDevices;
-  const chromeCategories = state.chromeCategories;
-  const newEngineMode = state.newEngineMode;
-
-  Object.assign(state, createEmptyState());
-  state.nextId = nextId;
-  state.recordConfig = recordConfig;
-  state.recordingTarget = recordingTarget;
-  state.fetchChromeCategories = fetchChromeCategories;
-  state.extensionInstalled = extensionInstalled;
-  state.availableAdbDevices = availableAdbDevices;
-  state.chromeCategories = chromeCategories;
-  state.newEngineMode = newEngineMode;
-}
-
-function generateNextId(draft: StateDraft): string {
-  const nextId = String(Number(draft.nextId) + 1);
-  draft.nextId = nextId;
-  return nextId;
-}
-
-// A helper to clean the state for a given removeable track.
-// This is not exported as action to make it clear that not all
-// tracks are removeable.
-function removeTrack(state: StateDraft, trackKey: string) {
-  const track = state.tracks[trackKey];
-  if (track === undefined) {
-    return;
-  }
-  delete state.tracks[trackKey];
-
-  const removeTrackId = (arr: string[]) => {
-    const index = arr.indexOf(trackKey);
-    if (index !== -1) arr.splice(index, 1);
-  };
-
-  if (track.trackGroup === SCROLLING_TRACK_GROUP) {
-    removeTrackId(state.scrollingTracks);
-  } else if (track.trackGroup !== undefined) {
-    const trackGroup = state.trackGroups[track.trackGroup];
-    if (trackGroup !== undefined) {
-      removeTrackId(trackGroup.tracks);
-    }
-  }
-  state.pinnedTracks = state.pinnedTracks.filter((key) => key !== trackKey);
-}
-
-let statusTraceEvent: TraceEventScope | undefined;
-
-export const StateActions = {
-  openTraceFromFile(state: StateDraft, args: {file: File}): void {
-    clearTraceState(state);
-    const id = generateNextId(state);
-    state.engine = {
-      id,
-      ready: false,
-      source: {type: 'FILE', file: args.file},
-    };
-  },
-
-  openTraceFromBuffer(state: StateDraft, args: PostedTrace): void {
-    clearTraceState(state);
-    const id = generateNextId(state);
-    state.engine = {
-      id,
-      ready: false,
-      source: {type: 'ARRAY_BUFFER', ...args},
-    };
-  },
-
-  openTraceFromUrl(state: StateDraft, args: {url: string}): void {
-    clearTraceState(state);
-    const id = generateNextId(state);
-    state.engine = {
-      id,
-      ready: false,
-      source: {type: 'URL', url: args.url},
-    };
-  },
-
-  openTraceFromHttpRpc(state: StateDraft, _args: {}): void {
-    clearTraceState(state);
-    const id = generateNextId(state);
-    state.engine = {
-      id,
-      ready: false,
-      source: {type: 'HTTP_RPC'},
-    };
-  },
-
-  setTraceUuid(state: StateDraft, args: {traceUuid: string}) {
-    state.traceUuid = args.traceUuid;
-  },
-
-  addTracks(state: StateDraft, args: {tracks: AddTrackArgs[]}) {
-    args.tracks.forEach((track) => {
-      const trackKey =
-        track.key === undefined ? generateNextId(state) : track.key;
-      const name = track.name;
-      state.tracks[trackKey] = {
-        key: trackKey,
-        name,
-        trackSortKey: track.trackSortKey,
-        trackGroup: track.trackGroup,
-        uri: track.uri,
-        closeable: track.closeable,
-      };
-      if (track.trackGroup === SCROLLING_TRACK_GROUP) {
-        state.scrollingTracks.push(trackKey);
-      } else if (track.trackGroup !== undefined) {
-        const group = state.trackGroups[track.trackGroup];
-        if (group !== undefined) {
-          group.tracks.push(trackKey);
-        }
-      }
-    });
-  },
-
-  // Note: While this action has traditionally been omitted, with more and more
-  // dynamic tracks being added and existing ones being moved to plugins, it
-  // makes sense to have a generic "removeTracks" action which is un-opinionated
-  // about what type of tracks we are removing.
-  // E.g. Once debug tracks have been moved to a plugin, it makes no sense to
-  // keep the "removeDebugTrack()" action, as the core should have no concept of
-  // what debug tracks are.
-  removeTracks(state: StateDraft, args: {trackKeys: string[]}) {
-    for (const trackKey of args.trackKeys) {
-      removeTrack(state, trackKey);
-    }
-  },
-
-  setUtidToTrackSortKey(
-    state: StateDraft,
-    args: {threadOrderingMetadata: UtidToTrackSortKey},
-  ) {
-    state.utidToThreadSortKey = args.threadOrderingMetadata;
-  },
-
-  addTrack(state: StateDraft, args: AddTrackArgs): void {
-    this.addTracks(state, {tracks: [args]});
-  },
-
-  addTrackGroup(
-    state: StateDraft,
-    // Define ID in action so a track group can be referred to without running
-    // the reducer.
-    args: {
-      name: string;
-      key: string;
-      summaryTrackKey?: string;
-      collapsed: boolean;
-      fixedOrdering?: boolean;
-    },
-  ): void {
-    state.trackGroups[args.key] = {
-      name: args.name,
-      key: args.key,
-      collapsed: args.collapsed,
-      tracks: [],
-      summaryTrack: args.summaryTrackKey,
-      fixedOrdering: args.fixedOrdering,
-    };
-  },
-
-  maybeExpandOnlyTrackGroup(state: StateDraft, _: {}): void {
-    const trackGroups = Object.values(state.trackGroups);
-    if (trackGroups.length === 1) {
-      trackGroups[0].collapsed = false;
-    }
-  },
-
-  sortThreadTracks(state: StateDraft, _: {}) {
-    const getFullKey = (a: string) => {
-      const track = state.tracks[a];
-      const threadTrackSortKey = track.trackSortKey as ThreadTrackSortKey;
-      if (threadTrackSortKey.utid === undefined) {
-        const sortKey = track.trackSortKey as PrimaryTrackSortKey;
-        return [sortKey, 0, 0, 0];
-      }
-      const threadSortKey = state.utidToThreadSortKey[threadTrackSortKey.utid];
-      return [
-        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-        threadSortKey
-          ? threadSortKey.sortKey
-          : PrimaryTrackSortKey.ORDINARY_THREAD,
-        threadSortKey && threadSortKey.tid !== undefined
-          ? threadSortKey.tid
-          : Number.MAX_VALUE,
-        /* eslint-enable */
-        threadTrackSortKey.utid,
-        threadTrackSortKey.priority,
-      ];
-    };
-
-    // Use a numeric collator so threads are sorted as T1, T2, ..., T10, T11,
-    // rather than T1, T10, T11, ..., T2, T20, T21 .
-    const coll = new Intl.Collator([], {sensitivity: 'base', numeric: true});
-    for (const group of Object.values(state.trackGroups)) {
-      if (group.fixedOrdering) continue;
-
-      group.tracks.sort((a: string, b: string) => {
-        const aRank = getFullKey(a);
-        const bRank = getFullKey(b);
-        for (let i = 0; i < aRank.length; i++) {
-          if (aRank[i] !== bRank[i]) return aRank[i] - bRank[i];
-        }
-
-        const aName = state.tracks[a].name.toLocaleLowerCase();
-        const bName = state.tracks[b].name.toLocaleLowerCase();
-        return coll.compare(aName, bName);
-      });
-    }
-  },
-
-  updateAggregateSorting(
-    state: StateDraft,
-    args: {id: string; column: string},
-  ) {
-    let prefs = state.aggregatePreferences[args.id];
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    if (!prefs) {
-      prefs = {id: args.id};
-      state.aggregatePreferences[args.id] = prefs;
-    }
-
-    if (!prefs.sorting || prefs.sorting.column !== args.column) {
-      // No sorting set for current column.
-      state.aggregatePreferences[args.id].sorting = {
-        column: args.column,
-        direction: 'DESC',
-      };
-    } else if (prefs.sorting.direction === 'DESC') {
-      // Toggle the direction if the column is currently sorted.
-      state.aggregatePreferences[args.id].sorting = {
-        column: args.column,
-        direction: 'ASC',
-      };
-    } else {
-      // If direction is currently 'ASC' toggle to no sorting.
-      state.aggregatePreferences[args.id].sorting = undefined;
-    }
-  },
-
-  moveTrack(
-    state: StateDraft,
-    args: {srcId: string; op: 'before' | 'after'; dstId: string},
-  ): void {
-    const moveWithinTrackList = (trackList: string[]) => {
-      const newList: string[] = [];
-      for (let i = 0; i < trackList.length; i++) {
-        const curTrackId = trackList[i];
-        if (curTrackId === args.dstId && args.op === 'before') {
-          newList.push(args.srcId);
-        }
-        if (curTrackId !== args.srcId) {
-          newList.push(curTrackId);
-        }
-        if (curTrackId === args.dstId && args.op === 'after') {
-          newList.push(args.srcId);
-        }
-      }
-      trackList.splice(0);
-      newList.forEach((x) => {
-        trackList.push(x);
-      });
-    };
-
-    moveWithinTrackList(state.pinnedTracks);
-    moveWithinTrackList(state.scrollingTracks);
-  },
-
-  toggleTrackPinned(state: StateDraft, args: {trackKey: string}): void {
-    const key = args.trackKey;
-    const isPinned = state.pinnedTracks.includes(key);
-    const trackGroup = assertExists(state.tracks[key]).trackGroup;
-
-    if (isPinned) {
-      state.pinnedTracks.splice(state.pinnedTracks.indexOf(key), 1);
-      if (trackGroup === SCROLLING_TRACK_GROUP) {
-        state.scrollingTracks.unshift(key);
-      }
-    } else {
-      if (trackGroup === SCROLLING_TRACK_GROUP) {
-        state.scrollingTracks.splice(state.scrollingTracks.indexOf(key), 1);
-      }
-      state.pinnedTracks.push(key);
-    }
-  },
-
-  toggleTrackGroupCollapsed(state: StateDraft, args: {groupKey: string}): void {
-    const trackGroup = assertExists(state.trackGroups[args.groupKey]);
-    trackGroup.collapsed = !trackGroup.collapsed;
-  },
-
-  requestTrackReload(state: StateDraft, _: {}) {
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    if (state.lastTrackReloadRequest) {
-      state.lastTrackReloadRequest++;
-    } else {
-      state.lastTrackReloadRequest = 1;
-    }
-  },
-
-  maybeSetPendingDeeplink(state: StateDraft, args: PendingDeeplinkState) {
-    state.pendingDeeplink = args;
-  },
-
-  clearPendingDeeplink(state: StateDraft, _: {}) {
-    state.pendingDeeplink = undefined;
-  },
-
-  // TODO(hjd): engine.ready should be a published thing. If it's part
-  // of the state it interacts badly with permalinks.
-  setEngineReady(
-    state: StateDraft,
-    args: {engineId: string; ready: boolean; mode: EngineMode},
-  ): void {
-    const engine = state.engine;
-    if (engine === undefined || engine.id !== args.engineId) {
-      return;
-    }
-    engine.ready = args.ready;
-    engine.mode = args.mode;
-  },
-
-  setNewEngineMode(state: StateDraft, args: {mode: NewEngineMode}): void {
-    state.newEngineMode = args.mode;
-  },
-
-  // Marks all engines matching the given |mode| as failed.
-  setEngineFailed(
-    state: StateDraft,
-    args: {mode: EngineMode; failure: string},
-  ): void {
-    if (state.engine !== undefined && state.engine.mode === args.mode) {
-      state.engine.failed = args.failure;
-    }
-  },
-
-  updateStatus(state: StateDraft, args: Status): void {
-    if (statusTraceEvent) {
-      traceEventEnd(statusTraceEvent);
-    }
-    statusTraceEvent = traceEventBegin(args.msg, {
-      track: MetatraceTrackId.kOmniboxStatus,
-    });
-    state.status = args;
-  },
-
-  // TODO(hjd): Remove setState - it causes problems due to reuse of ids.
-  setState(state: StateDraft, args: {newState: State}): void {
-    for (const key of Object.keys(state)) {
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      delete (state as any)[key];
-    }
-    for (const key of Object.keys(args.newState)) {
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      (state as any)[key] = (args.newState as any)[key];
-    }
-
-    // If we're loading from a permalink then none of the engines can
-    // possibly be ready:
-    if (state.engine !== undefined) {
-      state.engine.ready = false;
-    }
-  },
-
-  setRecordConfig(
-    state: StateDraft,
-    args: {config: RecordConfig; configType?: LoadedConfig},
-  ): void {
-    state.recordConfig = args.config;
-    state.lastLoadedConfig = args.configType || {type: 'NONE'};
-  },
-
-  selectNote(state: StateDraft, args: {id: string}): void {
-    state.selection = {
-      kind: 'note',
-      id: args.id,
-    };
-  },
-
-  addNote(
-    state: StateDraft,
-    args: {timestamp: time; color: string; id?: string; text?: string},
-  ): void {
-    const {timestamp, color, id = generateNextId(state), text = ''} = args;
-    state.notes[id] = {
-      noteType: 'DEFAULT',
-      id,
-      timestamp,
-      color,
-      text,
-    };
-  },
-
-  addSpanNote(
-    state: StateDraft,
-    args: {start: time; end: time; id?: string; color?: string},
-  ): void {
-    const {
-      id = generateNextId(state),
-      color = randomColor(),
-      end,
-      start,
-    } = args;
-
-    state.notes[id] = {
-      noteType: 'SPAN',
-      start,
-      end,
-      color,
-      id,
-      text: '',
-    };
-  },
-
-  changeNoteColor(
-    state: StateDraft,
-    args: {id: string; newColor: string},
-  ): void {
-    const note = state.notes[args.id];
-    if (note === undefined) return;
-    note.color = args.newColor;
-  },
-
-  changeNoteText(state: StateDraft, args: {id: string; newText: string}): void {
-    const note = state.notes[args.id];
-    if (note === undefined) return;
-    note.text = args.newText;
-  },
-
-  removeNote(state: StateDraft, args: {id: string}): void {
-    delete state.notes[args.id];
-
-    // Clear the selection if this note was selected
-    if (state.selection.kind === 'note' && state.selection.id === args.id) {
-      state.selection = {kind: 'empty'};
-    }
-  },
-
-  selectHeapProfile(
-    state: StateDraft,
-    args: {id: number; upid: number; ts: time; type: ProfileType},
-  ): void {
-    state.selection = {
-      kind: 'legacy',
-      legacySelection: {
-        kind: 'HEAP_PROFILE',
-        id: args.id,
-        upid: args.upid,
-        ts: args.ts,
-        type: args.type,
-      },
-    };
-  },
-
-  selectPerfSamples(
-    state: StateDraft,
-    args: {
-      id: number;
-      utid?: number;
-      upid?: number;
-      leftTs: time;
-      rightTs: time;
-      type: ProfileType;
-    },
-  ): void {
-    state.selection = {
-      kind: 'legacy',
-      legacySelection: {
-        kind: 'PERF_SAMPLES',
-        id: args.id,
-        utid: args.utid,
-        upid: args.upid,
-        leftTs: args.leftTs,
-        rightTs: args.rightTs,
-        type: args.type,
-      },
-    };
-  },
-
-  selectCpuProfileSample(
-    state: StateDraft,
-    args: {id: number; utid: number; ts: time},
-  ): void {
-    state.selection = {
-      kind: 'legacy',
-      legacySelection: {
-        kind: 'CPU_PROFILE_SAMPLE',
-        id: args.id,
-        utid: args.utid,
-        ts: args.ts,
-      },
-    };
-  },
-
-  selectSlice(
-    state: StateDraft,
-    args: {id: number; trackKey: string; table?: string; scroll?: boolean},
-  ): void {
-    state.selection = {
-      kind: 'legacy',
-      legacySelection: {
-        kind: 'SLICE',
-        id: args.id,
-        trackKey: args.trackKey,
-        table: args.table,
-      },
-    };
-    state.pendingScrollId = args.scroll ? args.id : undefined;
-  },
-
-  selectGenericSlice(
-    state: StateDraft,
-    args: {
-      id: number;
-      sqlTableName: string;
-      start: time;
-      duration: duration;
-      trackKey: string;
-      detailsPanelConfig: {
-        kind: string;
-        config: GenericSliceDetailsTabConfigBase;
-      };
-    },
-  ): void {
-    const detailsPanelConfig: GenericSliceDetailsTabConfig = {
-      id: args.id,
-      ...args.detailsPanelConfig.config,
-    };
-
-    state.selection = {
-      kind: 'legacy',
-      legacySelection: {
-        kind: 'GENERIC_SLICE',
-        id: args.id,
-        sqlTableName: args.sqlTableName,
-        start: args.start,
-        duration: args.duration,
-        trackKey: args.trackKey,
-        detailsPanelConfig: {
-          kind: args.detailsPanelConfig.kind,
-          config: detailsPanelConfig,
-        },
-      },
-    };
-  },
-
-  setPendingScrollId(state: StateDraft, args: {pendingScrollId: number}): void {
-    state.pendingScrollId = args.pendingScrollId;
-  },
-
-  clearPendingScrollId(state: StateDraft, _: {}): void {
-    state.pendingScrollId = undefined;
-  },
-
-  selectThreadState(
-    state: StateDraft,
-    args: {id: number; trackKey: string},
-  ): void {
-    state.selection = {
-      kind: 'legacy',
-      legacySelection: {
-        kind: 'THREAD_STATE',
-        id: args.id,
-        trackKey: args.trackKey,
-      },
-    };
-  },
-
-  startRecording(state: StateDraft, _: {}): void {
-    state.recordingInProgress = true;
-    state.lastRecordingError = undefined;
-    state.recordingCancelled = false;
-  },
-
-  stopRecording(state: StateDraft, _: {}): void {
-    state.recordingInProgress = false;
-  },
-
-  cancelRecording(state: StateDraft, _: {}): void {
-    state.recordingInProgress = false;
-    state.recordingCancelled = true;
-  },
-
-  setExtensionAvailable(state: StateDraft, args: {available: boolean}): void {
-    state.extensionInstalled = args.available;
-  },
-
-  setRecordingTarget(state: StateDraft, args: {target: RecordingTarget}): void {
-    state.recordingTarget = args.target;
-  },
-
-  setFetchChromeCategories(state: StateDraft, args: {fetch: boolean}): void {
-    state.fetchChromeCategories = args.fetch;
-  },
-
-  setAvailableAdbDevices(
-    state: StateDraft,
-    args: {devices: AdbRecordingTarget[]},
-  ): void {
-    state.availableAdbDevices = args.devices;
-  },
-
-  setOmnibox(state: StateDraft, args: OmniboxState): void {
-    state.omniboxState = args;
-  },
-
-  setOmniboxMode(state: StateDraft, args: {mode: OmniboxMode}): void {
-    state.omniboxState.mode = args.mode;
-  },
-
-  selectArea(
-    state: StateDraft,
-    args: {start: time; end: time; tracks: string[]},
-  ): void {
-    const {start, end, tracks} = args;
-    assertTrue(start <= end);
-    state.selection = {
-      kind: 'area',
-      start,
-      end,
-      tracks,
-    };
-  },
-
-  toggleTrackSelection(
-    state: StateDraft,
-    args: {key: string; isTrackGroup: boolean},
-  ) {
-    const selection = state.selection;
-    if (selection.kind !== 'area') {
-      return;
-    }
-
-    const index = selection.tracks.indexOf(args.key);
-    if (index > -1) {
-      selection.tracks.splice(index, 1);
-      if (args.isTrackGroup) {
-        // Also remove all child tracks.
-        for (const childTrack of state.trackGroups[args.key].tracks) {
-          const childIndex = selection.tracks.indexOf(childTrack);
-          if (childIndex > -1) {
-            selection.tracks.splice(childIndex, 1);
-          }
-        }
-      }
-    } else {
-      selection.tracks.push(args.key);
-      if (args.isTrackGroup) {
-        // Also add all child tracks.
-        for (const childTrack of state.trackGroups[args.key].tracks) {
-          if (!selection.tracks.includes(childTrack)) {
-            selection.tracks.push(childTrack);
-          }
-        }
-      }
-    }
-    // It's super unexpected that |toggleTrackSelection| does not cause
-    // selection to be updated and this leads to bugs for people who do:
-    // if (oldSelection !== state.selection) etc.
-    // To solve this re-create the selection object here:
-    state.selection = Object.assign({}, state.selection);
-  },
-
-  setChromeCategories(state: StateDraft, args: {categories: string[]}): void {
-    state.chromeCategories = args.categories;
-  },
-
-  setLastRecordingError(state: StateDraft, args: {error?: string}): void {
-    state.lastRecordingError = args.error;
-    state.recordingStatus = undefined;
-  },
-
-  setRecordingStatus(state: StateDraft, args: {status?: string}): void {
-    state.recordingStatus = args.status;
-    state.lastRecordingError = undefined;
-  },
-
-  togglePerfDebug(state: StateDraft, _: {}): void {
-    state.perfDebug = !state.perfDebug;
-  },
-
-  setSidebar(state: StateDraft, args: {visible: boolean}): void {
-    state.sidebarVisible = args.visible;
-  },
-
-  setHoveredUtidAndPid(state: StateDraft, args: {utid: number; pid: number}) {
-    state.hoveredPid = args.pid;
-    state.hoveredUtid = args.utid;
-  },
-
-  setHighlightedSliceId(state: StateDraft, args: {sliceId: number}) {
-    state.highlightedSliceId = args.sliceId;
-  },
-
-  setHighlightedFlowLeftId(state: StateDraft, args: {flowId: number}) {
-    state.focusedFlowIdLeft = args.flowId;
-  },
-
-  setHighlightedFlowRightId(state: StateDraft, args: {flowId: number}) {
-    state.focusedFlowIdRight = args.flowId;
-  },
-
-  setSearchIndex(state: StateDraft, args: {index: number}) {
-    state.searchIndex = args.index;
-  },
-
-  setHoverCursorTimestamp(state: StateDraft, args: {ts: time}) {
-    state.hoverCursorTimestamp = args.ts;
-  },
-
-  setHoveredNoteTimestamp(state: StateDraft, args: {ts: time}) {
-    state.hoveredNoteTimestamp = args.ts;
-  },
-
-  // Add a tab with a given URI to the tab bar and show it.
-  // If the tab is already present in the tab bar, just show it.
-  showTab(state: StateDraft, args: {uri: string}) {
-    // Add tab, unless we're talking about the special current_selection tab
-    if (args.uri !== 'current_selection') {
-      // Add tab to tab list if not already
-      if (!state.tabs.openTabs.some((uri) => uri === args.uri)) {
-        state.tabs.openTabs.push(args.uri);
-      }
-    }
-    state.tabs.currentTab = args.uri;
-  },
-
-  // Hide a tab in the tab bar pick a new tab to show.
-  // Note: Attempting to hide the "current_selection" tab doesn't work. This tab
-  // is special and cannot be removed.
-  hideTab(state: StateDraft, args: {uri: string}) {
-    const tabs = state.tabs;
-    // If the removed tab is the "current" tab, we must find a new tab to focus
-    if (args.uri === tabs.currentTab) {
-      // Remember the index of the current tab
-      const currentTabIdx = tabs.openTabs.findIndex((uri) => uri === args.uri);
-
-      // Remove the tab
-      tabs.openTabs = tabs.openTabs.filter((uri) => uri !== args.uri);
-
-      if (currentTabIdx !== -1) {
-        if (tabs.openTabs.length === 0) {
-          // No more tabs, use current selection
-          tabs.currentTab = 'current_selection';
-        } else if (currentTabIdx < tabs.openTabs.length - 1) {
-          // Pick the tab to the right
-          tabs.currentTab = tabs.openTabs[currentTabIdx];
-        } else {
-          // Pick the last tab
-          const lastTab = tabs.openTabs[tabs.openTabs.length - 1];
-          tabs.currentTab = lastTab;
-        }
-      }
-    } else {
-      // Otherwise just remove the tab
-      tabs.openTabs = tabs.openTabs.filter((uri) => uri !== args.uri);
-    }
-  },
-
-  clearAllPinnedTracks(state: StateDraft, _: {}) {
-    const pinnedTracks = state.pinnedTracks.slice();
-    for (let index = pinnedTracks.length - 1; index >= 0; index--) {
-      const trackKey = pinnedTracks[index];
-      this.toggleTrackPinned(state, {trackKey});
-    }
-  },
-
-  togglePivotTable(
-    state: StateDraft,
-    args: {area?: {start: time; end: time; tracks: string[]}},
-  ) {
-    state.nonSerializableState.pivotTable.selectionArea = args.area;
-    state.nonSerializableState.pivotTable.queryResult = null;
-  },
-
-  setPivotStateQueryResult(
-    state: StateDraft,
-    args: {queryResult: PivotTableResult | null},
-  ) {
-    state.nonSerializableState.pivotTable.queryResult = args.queryResult;
-  },
-
-  setPivotTableConstrainToArea(state: StateDraft, args: {constrain: boolean}) {
-    state.nonSerializableState.pivotTable.constrainToArea = args.constrain;
-  },
-
-  dismissFlamegraphModal(state: StateDraft, _: {}) {
-    state.flamegraphModalDismissed = true;
-  },
-
-  addPivotTableAggregation(
-    state: StateDraft,
-    args: {aggregation: Aggregation; after: number},
-  ) {
-    state.nonSerializableState.pivotTable.selectedAggregations.splice(
-      args.after,
-      0,
-      args.aggregation,
-    );
-  },
-
-  removePivotTableAggregation(state: StateDraft, args: {index: number}) {
-    state.nonSerializableState.pivotTable.selectedAggregations.splice(
-      args.index,
-      1,
-    );
-  },
-
-  setPivotTableQueryRequested(
-    state: StateDraft,
-    args: {queryRequested: boolean},
-  ) {
-    state.nonSerializableState.pivotTable.queryRequested = args.queryRequested;
-  },
-
-  setPivotTablePivotSelected(
-    state: StateDraft,
-    args: {column: TableColumn; selected: boolean},
-  ) {
-    toggleEnabled(
-      tableColumnEquals,
-      state.nonSerializableState.pivotTable.selectedPivots,
-      args.column,
-      args.selected,
-    );
-  },
-
-  setPivotTableAggregationFunction(
-    state: StateDraft,
-    args: {index: number; function: AggregationFunction},
-  ) {
-    state.nonSerializableState.pivotTable.selectedAggregations[
-      args.index
-    ].aggregationFunction = args.function;
-  },
-
-  setPivotTableSortColumn(
-    state: StateDraft,
-    args: {aggregationIndex: number; order: SortDirection},
-  ) {
-    state.nonSerializableState.pivotTable.selectedAggregations =
-      state.nonSerializableState.pivotTable.selectedAggregations.map(
-        (agg, index) => ({
-          column: agg.column,
-          aggregationFunction: agg.aggregationFunction,
-          sortDirection:
-            index === args.aggregationIndex ? args.order : undefined,
-        }),
-      );
-  },
-
-  changePivotTablePivotOrder(
-    state: StateDraft,
-    args: {from: number; to: number; direction: DropDirection},
-  ) {
-    const pivots = state.nonSerializableState.pivotTable.selectedPivots;
-    state.nonSerializableState.pivotTable.selectedPivots = performReordering(
-      computeIntervals(pivots.length, args.from, args.to, args.direction),
-      pivots,
-    );
-  },
-
-  changePivotTableAggregationOrder(
-    state: StateDraft,
-    args: {from: number; to: number; direction: DropDirection},
-  ) {
-    const aggregations =
-      state.nonSerializableState.pivotTable.selectedAggregations;
-    state.nonSerializableState.pivotTable.selectedAggregations =
-      performReordering(
-        computeIntervals(
-          aggregations.length,
-          args.from,
-          args.to,
-          args.direction,
-        ),
-        aggregations,
-      );
-  },
-
-  setTrackFilterTerm(
-    state: StateDraft,
-    args: {filterTerm: string | undefined},
-  ) {
-    state.trackFilterTerm = args.filterTerm;
-  },
-};
-
-// When we are on the frontend side, we don't really want to execute the
-// actions above, we just want to serialize them and marshal their
-// arguments, send them over to the controller side and have them being
-// executed there. The magic below takes care of turning each action into a
-// function that returns the marshaled args.
-
-// A DeferredAction is a bundle of Args and a method name. This is the marshaled
-// version of a StateActions method call.
-export interface DeferredAction<Args = {}> {
-  type: string;
-  args: Args;
-}
-
-// This type magic creates a type function DeferredActions<T> which takes a type
-// T and 'maps' its attributes. For each attribute on T matching the signature:
-// (state: StateDraft, args: Args) => void
-// DeferredActions<T> has an attribute:
-// (args: Args) => DeferredAction<Args>
-type ActionFunction<Args> = (state: StateDraft, args: Args) => void;
-type DeferredActionFunc<T> =
-  T extends ActionFunction<infer Args>
-    ? (args: Args) => DeferredAction<Args>
-    : never;
-type DeferredActions<C> = {
-  [P in keyof C]: DeferredActionFunc<C[P]>;
-};
-
-// Actions is an implementation of DeferredActions<typeof StateActions>.
-// (since StateActions is a variable not a type we have to do
-// 'typeof StateActions' to access the (unnamed) type of StateActions).
-// It's a Proxy such that any attribute access returns a function:
-// (args) => {return {type: ATTRIBUTE_NAME, args};}
-export const Actions =
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  new Proxy<DeferredActions<typeof StateActions>>({} as any, {
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    get(_: any, prop: string, _2: any) {
-      return (args: {}): DeferredAction<{}> => {
-        return {
-          type: prop,
-          args,
-        };
-      };
-    },
-  });
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
deleted file mode 100644
index 9289d09..0000000
--- a/ui/src/common/actions_unittest.ts
+++ /dev/null
@@ -1,459 +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 {produce} from 'immer';
-
-import {assertExists} from '../base/logging';
-import {PrimaryTrackSortKey} from '../public';
-import {PROCESS_SCHEDULING_TRACK_KIND} from '../core_plugins/process_summary/process_scheduling_track';
-
-import {StateActions} from './actions';
-import {createEmptyState} from './empty_state';
-import {
-  InThreadTrackSortKey,
-  SCROLLING_TRACK_GROUP,
-  State,
-  TraceUrlSource,
-  TrackSortKey,
-} from './state';
-import {
-  HEAP_PROFILE_TRACK_KIND,
-  THREAD_SLICE_TRACK_KIND,
-  THREAD_STATE_TRACK_KIND,
-} from '../core/track_kinds';
-
-function fakeTrack(
-  state: State,
-  args: {
-    key: string;
-    uri?: string;
-    trackGroup?: string;
-    trackSortKey?: TrackSortKey;
-    name?: string;
-    tid?: string;
-  },
-): State {
-  return produce(state, (draft) => {
-    StateActions.addTrack(draft, {
-      uri: args.uri ?? 'sometrack',
-      key: args.key,
-      name: args.name ?? 'A track',
-      trackSortKey:
-        args.trackSortKey === undefined
-          ? PrimaryTrackSortKey.ORDINARY_TRACK
-          : args.trackSortKey,
-      trackGroup: args.trackGroup ?? SCROLLING_TRACK_GROUP,
-    });
-  });
-}
-
-function fakeTrackGroup(
-  state: State,
-  args: {key: string; summaryTrackKey: string},
-): State {
-  return produce(state, (draft) => {
-    StateActions.addTrackGroup(draft, {
-      name: 'A group',
-      key: args.key,
-      collapsed: false,
-      summaryTrackKey: args.summaryTrackKey,
-    });
-  });
-}
-
-function pinnedAndScrollingTracks(
-  state: State,
-  keys: string[],
-  pinnedTracks: string[],
-  scrollingTracks: string[],
-): State {
-  for (const key of keys) {
-    state = fakeTrack(state, {key});
-  }
-  state = produce(state, (draft) => {
-    draft.pinnedTracks = pinnedTracks;
-    draft.scrollingTracks = scrollingTracks;
-  });
-  return state;
-}
-
-test('add scrolling tracks', () => {
-  const once = produce(createEmptyState(), (draft) => {
-    StateActions.addTrack(draft, {
-      uri: 'cpu',
-      name: 'Cpu 1',
-      trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-      trackGroup: SCROLLING_TRACK_GROUP,
-    });
-  });
-  const twice = produce(once, (draft) => {
-    StateActions.addTrack(draft, {
-      uri: 'cpu',
-      name: 'Cpu 2',
-      trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-      trackGroup: SCROLLING_TRACK_GROUP,
-    });
-  });
-
-  expect(Object.values(twice.tracks).length).toBe(2);
-  expect(twice.scrollingTracks.length).toBe(2);
-});
-
-test('add track to track group', () => {
-  let state = createEmptyState();
-  state = fakeTrack(state, {key: 's'});
-
-  const afterGroup = produce(state, (draft) => {
-    StateActions.addTrackGroup(draft, {
-      name: 'A track group',
-      key: '123-123-123',
-      summaryTrackKey: 's',
-      collapsed: false,
-    });
-  });
-
-  const afterTrackAdd = produce(afterGroup, (draft) => {
-    StateActions.addTrack(draft, {
-      key: '1',
-      uri: 'slices',
-      name: 'renderer 1',
-      trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-      trackGroup: '123-123-123',
-    });
-  });
-
-  expect(afterTrackAdd.trackGroups['123-123-123'].summaryTrack).toBe('s');
-  expect(afterTrackAdd.trackGroups['123-123-123'].tracks[0]).toBe('1');
-});
-
-test('reorder tracks', () => {
-  const once = produce(createEmptyState(), (draft) => {
-    StateActions.addTrack(draft, {
-      uri: 'cpu',
-      name: 'Cpu 1',
-      trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-    });
-    StateActions.addTrack(draft, {
-      uri: 'cpu',
-      name: 'Cpu 2',
-      trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-    });
-  });
-
-  const firstTrackKey = once.scrollingTracks[0];
-  const secondTrackKey = once.scrollingTracks[1];
-
-  const twice = produce(once, (draft) => {
-    StateActions.moveTrack(draft, {
-      srcId: `${firstTrackKey}`,
-      op: 'after',
-      dstId: `${secondTrackKey}`,
-    });
-  });
-
-  expect(twice.scrollingTracks[0]).toBe(secondTrackKey);
-  expect(twice.scrollingTracks[1]).toBe(firstTrackKey);
-});
-
-test('reorder pinned to scrolling', () => {
-  let state = createEmptyState();
-  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a', 'b'], ['c']);
-
-  const after = produce(state, (draft) => {
-    StateActions.moveTrack(draft, {
-      srcId: 'b',
-      op: 'before',
-      dstId: 'c',
-    });
-  });
-
-  expect(after.pinnedTracks).toEqual(['a']);
-  expect(after.scrollingTracks).toEqual(['b', 'c']);
-});
-
-test('reorder scrolling to pinned', () => {
-  let state = createEmptyState();
-  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a'], ['b', 'c']);
-
-  const after = produce(state, (draft) => {
-    StateActions.moveTrack(draft, {
-      srcId: 'b',
-      op: 'after',
-      dstId: 'a',
-    });
-  });
-
-  expect(after.pinnedTracks).toEqual(['a', 'b']);
-  expect(after.scrollingTracks).toEqual(['c']);
-});
-
-test('reorder clamp bottom', () => {
-  let state = createEmptyState();
-  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a', 'b'], ['c']);
-
-  const after = produce(state, (draft) => {
-    StateActions.moveTrack(draft, {
-      srcId: 'a',
-      op: 'before',
-      dstId: 'a',
-    });
-  });
-  expect(after).toEqual(state);
-});
-
-test('reorder clamp top', () => {
-  let state = createEmptyState();
-  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a'], ['b', 'c']);
-
-  const after = produce(state, (draft) => {
-    StateActions.moveTrack(draft, {
-      srcId: 'c',
-      op: 'after',
-      dstId: 'c',
-    });
-  });
-  expect(after).toEqual(state);
-});
-
-test('pin', () => {
-  let state = createEmptyState();
-  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a'], ['b', 'c']);
-
-  const after = produce(state, (draft) => {
-    StateActions.toggleTrackPinned(draft, {
-      trackKey: 'c',
-    });
-  });
-  expect(after.pinnedTracks).toEqual(['a', 'c']);
-  expect(after.scrollingTracks).toEqual(['b']);
-});
-
-test('unpin', () => {
-  let state = createEmptyState();
-  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a', 'b'], ['c']);
-
-  const after = produce(state, (draft) => {
-    StateActions.toggleTrackPinned(draft, {
-      trackKey: 'a',
-    });
-  });
-  expect(after.pinnedTracks).toEqual(['b']);
-  expect(after.scrollingTracks).toEqual(['a', 'c']);
-});
-
-test('open trace', () => {
-  const state = createEmptyState();
-  const recordConfig = state.recordConfig;
-  const after = produce(state, (draft) => {
-    StateActions.openTraceFromUrl(draft, {
-      url: 'https://example.com/bar',
-    });
-  });
-
-  expect(after.engine).not.toBeUndefined();
-  expect((after.engine!!.source as TraceUrlSource).url).toBe(
-    'https://example.com/bar',
-  );
-  expect(after.recordConfig).toBe(recordConfig);
-});
-
-test('open second trace from file', () => {
-  const once = produce(createEmptyState(), (draft) => {
-    StateActions.openTraceFromUrl(draft, {
-      url: 'https://example.com/bar',
-    });
-  });
-
-  const twice = produce(once, (draft) => {
-    StateActions.addTrack(draft, {
-      uri: 'cpu',
-      name: 'Cpu 1',
-      trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-    });
-  });
-
-  const thrice = produce(twice, (draft) => {
-    StateActions.openTraceFromUrl(draft, {
-      url: 'https://example.com/foo',
-    });
-  });
-
-  expect(thrice.engine).not.toBeUndefined();
-  expect((thrice.engine!!.source as TraceUrlSource).url).toBe(
-    'https://example.com/foo',
-  );
-  expect(thrice.pinnedTracks.length).toBe(0);
-  expect(thrice.scrollingTracks.length).toBe(0);
-});
-
-test('setEngineReady with missing engine is ignored', () => {
-  const state = createEmptyState();
-  produce(state, (draft) => {
-    StateActions.setEngineReady(draft, {
-      engineId: '1',
-      ready: true,
-      mode: 'WASM',
-    });
-  });
-});
-
-test('setEngineReady', () => {
-  const state = createEmptyState();
-  const after = produce(state, (draft) => {
-    StateActions.openTraceFromUrl(draft, {
-      url: 'https://example.com/bar',
-    });
-    const latestEngineId = assertExists(draft.engine).id;
-    StateActions.setEngineReady(draft, {
-      engineId: latestEngineId,
-      ready: true,
-      mode: 'WASM',
-    });
-  });
-  expect(after.engine!!.ready).toBe(true);
-});
-
-test('sortTracksByPriority', () => {
-  let state = createEmptyState();
-  state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'b'});
-  state = fakeTrack(state, {
-    key: 'b',
-    uri: HEAP_PROFILE_TRACK_KIND,
-    trackSortKey: PrimaryTrackSortKey.HEAP_PROFILE_TRACK,
-    trackGroup: 'g',
-  });
-  state = fakeTrack(state, {
-    key: 'a',
-    uri: PROCESS_SCHEDULING_TRACK_KIND,
-    trackSortKey: PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK,
-    trackGroup: 'g',
-  });
-
-  const after = produce(state, (draft) => {
-    StateActions.sortThreadTracks(draft, {});
-  });
-
-  // High Priority tracks should be sorted before Low Priority tracks:
-  // 'b' appears twice because it's the summary track
-  expect(after.trackGroups['g'].tracks).toEqual(['a', 'b']);
-});
-
-test('sortTracksByPriorityAndKindAndName', () => {
-  let state = createEmptyState();
-  state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'b'});
-  state = fakeTrack(state, {
-    key: 'a',
-    uri: PROCESS_SCHEDULING_TRACK_KIND,
-    trackSortKey: PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK,
-    trackGroup: 'g',
-  });
-  state = fakeTrack(state, {
-    key: 'b',
-    uri: THREAD_SLICE_TRACK_KIND,
-    trackGroup: 'g',
-    trackSortKey: PrimaryTrackSortKey.MAIN_THREAD,
-  });
-  state = fakeTrack(state, {
-    key: 'c',
-    uri: THREAD_SLICE_TRACK_KIND,
-    trackGroup: 'g',
-    trackSortKey: PrimaryTrackSortKey.RENDER_THREAD,
-  });
-  state = fakeTrack(state, {
-    key: 'd',
-    uri: THREAD_SLICE_TRACK_KIND,
-    trackGroup: 'g',
-    trackSortKey: PrimaryTrackSortKey.GPU_COMPLETION_THREAD,
-  });
-  state = fakeTrack(state, {
-    key: 'e',
-    uri: HEAP_PROFILE_TRACK_KIND,
-    trackGroup: 'g',
-  });
-  state = fakeTrack(state, {
-    key: 'f',
-    uri: THREAD_SLICE_TRACK_KIND,
-    trackGroup: 'g',
-    name: 'T2',
-  });
-  state = fakeTrack(state, {
-    key: 'g',
-    uri: THREAD_SLICE_TRACK_KIND,
-    trackGroup: 'g',
-    name: 'T10',
-  });
-
-  const after = produce(state, (draft) => {
-    StateActions.sortThreadTracks(draft, {});
-  });
-
-  // The order should be determined by:
-  // 1.High priority
-  // 2.Non ordinary track kinds
-  // 3.Low priority
-  // 4.Collated name string (ie. 'T2' will be before 'T10')
-  expect(after.trackGroups['g'].tracks).toEqual([
-    'a',
-    'b',
-    'c',
-    'd',
-    'e',
-    'f',
-    'g',
-  ]);
-});
-
-test('sortTracksByTidThenName', () => {
-  let state = createEmptyState();
-  state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'a'});
-  state = fakeTrack(state, {
-    key: 'a',
-    uri: THREAD_SLICE_TRACK_KIND,
-    trackSortKey: {
-      utid: 1,
-      priority: InThreadTrackSortKey.ORDINARY,
-    },
-    trackGroup: 'g',
-    name: 'aaa',
-    tid: '1',
-  });
-  state = fakeTrack(state, {
-    key: 'b',
-    uri: THREAD_SLICE_TRACK_KIND,
-    trackSortKey: {
-      utid: 2,
-      priority: InThreadTrackSortKey.ORDINARY,
-    },
-    trackGroup: 'g',
-    name: 'bbb',
-    tid: '2',
-  });
-  state = fakeTrack(state, {
-    key: 'c',
-    uri: THREAD_STATE_TRACK_KIND,
-    trackSortKey: {
-      utid: 1,
-      priority: InThreadTrackSortKey.ORDINARY,
-    },
-    trackGroup: 'g',
-    name: 'ccc',
-    tid: '1',
-  });
-
-  const after = produce(state, (draft) => {
-    StateActions.sortThreadTracks(draft, {});
-  });
-
-  expect(after.trackGroups['g'].tracks).toEqual(['a', 'c', 'b']);
-});
diff --git a/ui/src/common/addEphemeralTab.ts b/ui/src/common/addEphemeralTab.ts
deleted file mode 100644
index d04148c..0000000
--- a/ui/src/common/addEphemeralTab.ts
+++ /dev/null
@@ -1,30 +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 {uuidv4} from '../base/uuid';
-import {BottomTab} from '../frontend/bottom_tab';
-import {globals} from '../frontend/globals';
-import {BottomTabToTabAdapter} from '../public/utils';
-import {Actions} from './actions';
-
-export function addEphemeralTab(tab: BottomTab, uriPrefix: string): void {
-  const uri = `${uriPrefix}#${uuidv4()}`;
-
-  globals.tabManager.registerTab({
-    uri,
-    content: new BottomTabToTabAdapter(tab),
-    isEphemeral: true,
-  });
-
-  globals.dispatch(Actions.showTab({uri}));
-}
diff --git a/ui/src/common/add_ephemeral_tab.ts b/ui/src/common/add_ephemeral_tab.ts
new file mode 100644
index 0000000..9ef6aca
--- /dev/null
+++ b/ui/src/common/add_ephemeral_tab.ts
@@ -0,0 +1,31 @@
+// 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 {uuidv4} from '../base/uuid';
+import {AppImpl} from '../core/app_impl';
+import {Tab} from '../public/tab';
+
+// TODO(primiano): this method should take a Trace parameter (or probably
+// shouldn't exist at all in favour of some helper in the Trace object).
+export function addEphemeralTab(uriPrefix: string, tab: Tab): void {
+  const uri = `${uriPrefix}#${uuidv4()}`;
+
+  const tabManager = AppImpl.instance.trace?.tabs;
+  if (tabManager === undefined) return;
+  tabManager.registerTab({
+    uri,
+    content: tab,
+    isEphemeral: true,
+  });
+  tabManager.showTab(uri);
+}
diff --git a/ui/src/common/aggregation_data.ts b/ui/src/common/aggregation_data.ts
deleted file mode 100644
index 9ff9373..0000000
--- a/ui/src/common/aggregation_data.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright (C) 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
-//
-//      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 Column = (
-  | StringColumn
-  | TimestampColumn
-  | NumberColumn
-  | StateColumn
-) & {title: string; columnId: string};
-
-export interface StringColumn {
-  kind: 'STRING';
-  data: Uint16Array;
-}
-
-export interface TimestampColumn {
-  kind: 'TIMESTAMP_NS';
-  data: Float64Array;
-}
-
-export interface NumberColumn {
-  kind: 'NUMBER';
-  data: Uint16Array;
-}
-
-export interface StateColumn {
-  kind: 'STATE';
-  data: Uint16Array;
-}
-
-type TypedArrayConstructor =
-  | Uint16ArrayConstructor
-  | Float64ArrayConstructor
-  | Uint32ArrayConstructor;
-export interface ColumnDef {
-  title: string;
-  kind: string;
-  sum?: boolean;
-  columnConstructor: TypedArrayConstructor;
-  columnId: string;
-}
-
-export interface AggregateData {
-  tabName: string;
-  columns: Column[];
-  columnSums: string[];
-  // For string interning.
-  strings: string[];
-  // Some aggregations will have extra info to display;
-  extra?: ThreadStateExtra;
-}
-
-export function isEmptyData(data: AggregateData) {
-  return data.columns.length === 0 || data.columns[0].data.length === 0;
-}
-
-export interface ThreadStateExtra {
-  kind: 'THREAD_STATE';
-  states: string[];
-  values: Float64Array;
-  totalMs: number;
-}
diff --git a/ui/src/common/arg_types.ts b/ui/src/common/arg_types.ts
deleted file mode 100644
index 551333b..0000000
--- a/ui/src/common/arg_types.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size 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 ArgValue =
-  | string
-  | {kind: 'SCHED_SLICE'; trackId: string; sliceId: number; rawValue: string};
-export type Args = Map<string, ArgValue>;
diff --git a/ui/src/common/cache_manager.ts b/ui/src/common/cache_manager.ts
deleted file mode 100644
index 4686cd9..0000000
--- a/ui/src/common/cache_manager.ts
+++ /dev/null
@@ -1,197 +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.
-
-/**
- * This file deals with caching traces in the browser's Cache storage. The
- * traces are cached so that the UI can gracefully reload a trace when the tab
- * containing it is discarded by Chrome (e.g. because the tab was not used for
- * a long time) or when the user accidentally hits reload.
- */
-import {TraceArrayBufferSource, TraceSource} from './state';
-
-const TRACE_CACHE_NAME = 'cached_traces';
-const TRACE_CACHE_SIZE = 10;
-
-let LAZY_CACHE: Cache | undefined = undefined;
-
-async function getCache(): Promise<Cache | undefined> {
-  if (self.caches === undefined) {
-    // The browser doesn't support cache storage or the page is opened from
-    // a non-secure origin.
-    return undefined;
-  }
-  if (LAZY_CACHE !== undefined) {
-    return LAZY_CACHE;
-  }
-  LAZY_CACHE = await caches.open(TRACE_CACHE_NAME);
-  return LAZY_CACHE;
-}
-
-async function cacheDelete(key: Request): Promise<boolean> {
-  try {
-    const cache = await getCache();
-    if (cache === undefined) return false; // Cache storage not supported.
-    return await cache.delete(key);
-  } catch (_) {
-    // TODO(288483453): Reinstate:
-    // return ignoreCacheUnactionableErrors(e, false);
-    return false;
-  }
-}
-
-async function cachePut(key: string, value: Response): Promise<void> {
-  try {
-    const cache = await getCache();
-    if (cache === undefined) return; // Cache storage not supported.
-    await cache.put(key, value);
-  } catch (_) {
-    // TODO(288483453): Reinstate:
-    // ignoreCacheUnactionableErrors(e, undefined);
-  }
-}
-
-async function cacheMatch(
-  key: Request | string,
-): Promise<Response | undefined> {
-  try {
-    const cache = await getCache();
-    if (cache === undefined) return undefined; // Cache storage not supported.
-    return await cache.match(key);
-  } catch (_) {
-    // TODO(288483453): Reinstate:
-    // ignoreCacheUnactionableErrors(e, undefined);
-    return undefined;
-  }
-}
-
-async function cacheKeys(): Promise<readonly Request[]> {
-  try {
-    const cache = await getCache();
-    if (cache === undefined) return []; // Cache storage not supported.
-    return await cache.keys();
-  } catch (e) {
-    // TODO(288483453): Reinstate:
-    // return ignoreCacheUnactionableErrors(e, []);
-    return [];
-  }
-}
-
-export async function cacheTrace(
-  traceSource: TraceSource,
-  traceUuid: string,
-): Promise<boolean> {
-  let trace;
-  let title = '';
-  let fileName = '';
-  let url = '';
-  let contentLength = 0;
-  let localOnly = false;
-  switch (traceSource.type) {
-    case 'ARRAY_BUFFER':
-      trace = traceSource.buffer;
-      title = traceSource.title;
-      fileName = traceSource.fileName ?? '';
-      url = traceSource.url ?? '';
-      contentLength = traceSource.buffer.byteLength;
-      localOnly = traceSource.localOnly || false;
-      break;
-    case 'FILE':
-      trace = await traceSource.file.arrayBuffer();
-      title = traceSource.file.name;
-      contentLength = traceSource.file.size;
-      break;
-    default:
-      return false;
-  }
-
-  const headers = new Headers([
-    ['x-trace-title', encodeURI(title)],
-    ['x-trace-url', url],
-    ['x-trace-filename', fileName],
-    ['x-trace-local-only', `${localOnly}`],
-    ['content-type', 'application/octet-stream'],
-    ['content-length', `${contentLength}`],
-    [
-      'expires',
-      // Expires in a week from now (now = upload time)
-      new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 7).toUTCString(),
-    ],
-  ]);
-  await deleteStaleEntries();
-  await cachePut(
-    `/_${TRACE_CACHE_NAME}/${traceUuid}`,
-    new Response(trace, {headers}),
-  );
-  return true;
-}
-
-export async function tryGetTrace(
-  traceUuid: string,
-): Promise<TraceArrayBufferSource | undefined> {
-  await deleteStaleEntries();
-  const response = await cacheMatch(`/_${TRACE_CACHE_NAME}/${traceUuid}`);
-
-  if (!response) return undefined;
-  return {
-    type: 'ARRAY_BUFFER',
-    buffer: await response.arrayBuffer(),
-    title: decodeURI(response.headers.get('x-trace-title') ?? ''),
-    fileName: response.headers.get('x-trace-filename') ?? undefined,
-    url: response.headers.get('x-trace-url') ?? undefined,
-    uuid: traceUuid,
-    localOnly: response.headers.get('x-trace-local-only') === 'true',
-  };
-}
-
-async function deleteStaleEntries() {
-  // Loop through stored traces and invalidate all but the most recent
-  // TRACE_CACHE_SIZE.
-  const keys = await cacheKeys();
-  const storedTraces: Array<{key: Request; date: Date}> = [];
-  const now = new Date();
-  const deletions = [];
-  for (const key of keys) {
-    const existingTrace = await cacheMatch(key);
-    if (existingTrace === undefined) {
-      continue;
-    }
-    const expires = existingTrace.headers.get('expires');
-    if (expires === undefined || expires === null) {
-      // Missing `expires`, so give up and delete which is better than
-      // keeping it around forever.
-      deletions.push(cacheDelete(key));
-      continue;
-    }
-    const expiryDate = new Date(expires);
-    if (expiryDate < now) {
-      deletions.push(cacheDelete(key));
-    } else {
-      storedTraces.push({key, date: expiryDate});
-    }
-  }
-
-  // Sort the traces descending by time, such that most recent ones are placed
-  // at the beginning. Then, take traces from TRACE_CACHE_SIZE onwards and
-  // delete them from cache.
-  const oldTraces = storedTraces
-    .sort((a, b) => b.date.getTime() - a.date.getTime())
-    .slice(TRACE_CACHE_SIZE);
-  for (const oldTrace of oldTraces) {
-    deletions.push(cacheDelete(oldTrace.key));
-  }
-
-  // TODO(hjd): Wrong Promise.all here, should use the one that
-  // ignores failures but need to upgrade TypeScript for that.
-  await Promise.all(deletions);
-}
diff --git a/ui/src/common/cache_utils.ts b/ui/src/common/cache_utils.ts
deleted file mode 100644
index a04eb2a..0000000
--- a/ui/src/common/cache_utils.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {BigintMath} from '../base/bigint_math';
-import {Duration, duration} from '../base/time';
-import {globals} from '../frontend/globals';
-
-// We choose 100000 as the table size to cache as this is roughly the point
-// where SQLite sorts start to become expensive.
-const MIN_TABLE_SIZE_TO_CACHE = 100000;
-
-// Decides, based on the length of the trace and the number of rows
-// provided whether a TrackController subclass should cache its quantized
-// data. Returns the bucket size (in ns) if caching should happen and
-// undefined otherwise.
-export function calcCachedBucketSize(numRows: number): duration | undefined {
-  // Ensure that we're not caching when the table size isn't even that big.
-  if (numRows < MIN_TABLE_SIZE_TO_CACHE) {
-    return undefined;
-  }
-
-  const traceContext = globals.traceContext;
-  const traceDuration = traceContext.end - traceContext.start;
-
-  // For large traces, going through the raw table in the most zoomed-out
-  // states can be very expensive as this can involve going through O(millions
-  // of rows). The cost of this becomes high even for just iteration but is
-  // especially slow as quantization involves a SQLite sort on the quantized
-  // timestamp (for the group by).
-  //
-  // To get around this, we can cache a pre-quantized table which we can then
-  // in zoomed-out situations and fall back to the real table when zoomed in
-  // (which naturally constrains the amount of data by virtue of the window
-  // covering a smaller timespan)
-  //
-  // This method computes that cached table by computing an approximation for
-  // the bucket size we would use when totally zoomed out and then going a few
-  // resolution levels down which ensures that our cached table works for more
-  // than the literally most zoomed out state. Moving down a resolution level
-  // is defined as moving down a power of 2; this matches the logic in
-  // |globals.getCurResolution|.
-  //
-  // TODO(lalitm): in the future, we should consider having a whole set of
-  // quantized tables each of which cover some portion of resolution lvel
-  // range. As each table covers a large number of resolution levels, even 3-4
-  // tables should really cover the all concievable trace sizes. This set
-  // could be computed by looking at the number of events being processed one
-  // level below the cached table and computing another layer of caching if
-  // that count is too high (with respect to MIN_TABLE_SIZE_TO_CACHE).
-
-  // 4k monitors have 3840 horizontal pixels so use that for a worst case
-  // approximation of the window width.
-  const approxWidthPx = 3840n;
-
-  // Compute the outermost bucket size. This acts as a starting point for
-  // computing the cached size.
-  const outermostBucketSize = BigintMath.bitCeil(traceDuration / approxWidthPx);
-  const outermostResolutionLevel = BigintMath.log2(outermostBucketSize);
-
-  // This constant decides how many resolution levels down from our outermost
-  // bucket computation we want to be able to use the cached table.
-  // We've chosen 7 as it empirically seems to be a good fit for trace data.
-  const resolutionLevelsCovered = 7n;
-
-  // If we've got less resolution levels in the trace than the number of
-  // resolution levels we want to go down, bail out because this cached
-  // table is really not going to be used enough.
-  if (outermostResolutionLevel < resolutionLevelsCovered) {
-    return Duration.MAX;
-  }
-
-  // Another way to look at moving down resolution levels is to consider how
-  // many sub-intervals we are splitting the bucket into.
-  const bucketSubIntervals = 1n << resolutionLevelsCovered;
-
-  // Calculate the smallest bucket we want our table to be able to handle by
-  // dividing the outermsot bucket by the number of subintervals we should
-  // divide by.
-  const cachedBucketSize = outermostBucketSize / bucketSubIntervals;
-
-  return cachedBucketSize;
-}
diff --git a/ui/src/common/canvas_utils.ts b/ui/src/common/canvas_utils.ts
deleted file mode 100644
index d08619a..0000000
--- a/ui/src/common/canvas_utils.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {Size, Vector} from '../base/geom';
-import {isString} from '../base/object_utils';
-
-export function drawDoubleHeadedArrow(
-  ctx: CanvasRenderingContext2D,
-  x: number,
-  y: number,
-  length: number,
-  showArrowHeads: boolean,
-  width = 2,
-  color = 'black',
-) {
-  ctx.beginPath();
-  ctx.lineWidth = width;
-  ctx.lineCap = 'round';
-  ctx.strokeStyle = color;
-  ctx.moveTo(x, y);
-  ctx.lineTo(x + length, y);
-  ctx.stroke();
-  ctx.closePath();
-  // Arrowheads on the each end of the line.
-  if (showArrowHeads) {
-    ctx.beginPath();
-    ctx.moveTo(x + length - 8, y - 4);
-    ctx.lineTo(x + length, y);
-    ctx.lineTo(x + length - 8, y + 4);
-    ctx.stroke();
-    ctx.closePath();
-    ctx.beginPath();
-    ctx.moveTo(x + 8, y - 4);
-    ctx.lineTo(x, y);
-    ctx.lineTo(x + 8, y + 4);
-    ctx.stroke();
-    ctx.closePath();
-  }
-}
-
-export function drawIncompleteSlice(
-  ctx: CanvasRenderingContext2D,
-  x: number,
-  y: number,
-  width: number,
-  height: number,
-  showGradient: boolean = true,
-) {
-  if (width <= 0 || height <= 0) {
-    return;
-  }
-  ctx.beginPath();
-  const triangleSize = height / 4;
-  ctx.moveTo(x, y);
-  ctx.lineTo(x + width, y);
-  ctx.lineTo(x + width - 3, y + triangleSize * 0.5);
-  ctx.lineTo(x + width, y + triangleSize);
-  ctx.lineTo(x + width - 3, y + triangleSize * 1.5);
-  ctx.lineTo(x + width, y + 2 * triangleSize);
-  ctx.lineTo(x + width - 3, y + triangleSize * 2.5);
-  ctx.lineTo(x + width, y + 3 * triangleSize);
-  ctx.lineTo(x + width - 3, y + triangleSize * 3.5);
-  ctx.lineTo(x + width, y + 4 * triangleSize);
-  ctx.lineTo(x, y + height);
-
-  const fillStyle = ctx.fillStyle;
-  if (isString(fillStyle)) {
-    if (showGradient) {
-      const gradient = ctx.createLinearGradient(x, y, x + width, y + height);
-      gradient.addColorStop(0.66, fillStyle);
-      gradient.addColorStop(1, '#FFFFFF');
-      ctx.fillStyle = gradient;
-    }
-  } else {
-    throw new Error(
-      `drawIncompleteSlice() expects fillStyle to be a simple color not ${fillStyle}`,
-    );
-  }
-
-  ctx.fill();
-  ctx.fillStyle = fillStyle;
-}
-
-export function drawTrackHoverTooltip(
-  ctx: CanvasRenderingContext2D,
-  pos: Vector,
-  trackSize: Size,
-  text: string,
-  text2?: string,
-) {
-  ctx.font = '10px Roboto Condensed';
-  ctx.textBaseline = 'middle';
-  ctx.textAlign = 'left';
-
-  // TODO(hjd): Avoid measuring text all the time (just use monospace?)
-  const textMetrics = ctx.measureText(text);
-  const text2Metrics = ctx.measureText(text2 ?? '');
-
-  // Padding on each side of the box containing the tooltip:
-  const paddingPx = 4;
-
-  // Figure out the width of the tool tip box:
-  let width = Math.max(textMetrics.width, text2Metrics.width);
-  width += paddingPx * 2;
-
-  // and the height:
-  let height = 0;
-  height += textMetrics.fontBoundingBoxAscent;
-  height += textMetrics.fontBoundingBoxDescent;
-  if (text2 !== undefined) {
-    height += text2Metrics.fontBoundingBoxAscent;
-    height += text2Metrics.fontBoundingBoxDescent;
-  }
-  height += paddingPx * 2;
-
-  let x = pos.x;
-  let y = pos.y;
-
-  // Move box to the top right of the mouse:
-  x += 10;
-  y -= 10;
-
-  // Ensure the box is on screen:
-  const endPx = trackSize.width;
-  if (x + width > endPx) {
-    x -= x + width - endPx;
-  }
-  if (y < 0) {
-    y = 0;
-  }
-  if (y + height > trackSize.height) {
-    y -= y + height - trackSize.height;
-  }
-
-  // Draw everything:
-  ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
-  ctx.fillRect(x, y, width, height);
-
-  ctx.fillStyle = 'hsl(200, 50%, 40%)';
-  ctx.fillText(
-    text,
-    x + paddingPx,
-    y + paddingPx + textMetrics.fontBoundingBoxAscent,
-  );
-  if (text2 !== undefined) {
-    const yOffsetPx =
-      textMetrics.fontBoundingBoxAscent +
-      textMetrics.fontBoundingBoxDescent +
-      text2Metrics.fontBoundingBoxAscent;
-    ctx.fillText(text2, x + paddingPx, y + paddingPx + yOffsetPx);
-  }
-}
-
-export function canvasClip(
-  ctx: CanvasRenderingContext2D,
-  x: number,
-  y: number,
-  w: number,
-  h: number,
-): void {
-  ctx.beginPath();
-  ctx.rect(x, y, w, h);
-  ctx.clip();
-}
diff --git a/ui/src/common/channels.ts b/ui/src/common/channels.ts
deleted file mode 100644
index d071628..0000000
--- a/ui/src/common/channels.ts
+++ /dev/null
@@ -1,49 +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 {raf} from '../core/raf_scheduler';
-
-export const DEFAULT_CHANNEL = 'stable';
-const CHANNEL_KEY = 'perfettoUiChannel';
-
-let currentChannel: string | undefined = undefined;
-let nextChannel: string | undefined = undefined;
-
-// This is the channel the UI is currently running. It doesn't change once the
-// UI has been loaded.
-export function getCurrentChannel(): string {
-  if (currentChannel === undefined) {
-    currentChannel = localStorage.getItem(CHANNEL_KEY) ?? DEFAULT_CHANNEL;
-  }
-  return currentChannel;
-}
-
-// This is the channel that will be applied on reload.
-export function getNextChannel(): string {
-  if (nextChannel !== undefined) {
-    return nextChannel;
-  }
-  return getCurrentChannel();
-}
-
-export function channelChanged(): boolean {
-  return getCurrentChannel() !== getNextChannel();
-}
-
-export function setChannel(channel: string): void {
-  getCurrentChannel(); // Cache the current channel before mangling next one.
-  nextChannel = channel;
-  localStorage.setItem(CHANNEL_KEY, channel);
-  raf.scheduleFullRedraw();
-}
diff --git a/ui/src/common/commands.ts b/ui/src/common/commands.ts
deleted file mode 100644
index 2375559..0000000
--- a/ui/src/common/commands.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {FuzzyFinder, FuzzySegment} from '../base/fuzzy';
-import {Registry} from '../base/registry';
-import {Command} from '../public';
-
-export interface CommandWithMatchInfo extends Command {
-  segments: FuzzySegment[];
-}
-
-export class CommandManager {
-  private readonly registry = new Registry<Command>((cmd) => cmd.id);
-
-  getCommand(commandId: string): Command {
-    return this.registry.get(commandId);
-  }
-
-  get commands(): Command[] {
-    return Array.from(this.registry.values());
-  }
-
-  registerCommand(cmd: Command): Disposable {
-    return this.registry.register(cmd);
-  }
-
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  runCommand(id: string, ...args: any[]): any {
-    const cmd = this.registry.get(id);
-    return cmd.callback(...args);
-  }
-
-  // Returns a list of commands that match the search term, along with a list
-  // of segments which describe which parts of the command name match and
-  // which don't.
-  fuzzyFilterCommands(searchTerm: string): CommandWithMatchInfo[] {
-    const finder = new FuzzyFinder(this.commands, ({name}) => name);
-    return finder.find(searchTerm).map((result) => {
-      return {segments: result.segments, ...result.item};
-    });
-  }
-}
diff --git a/ui/src/common/comparator_builder.ts b/ui/src/common/comparator_builder.ts
deleted file mode 100644
index bcff7bc..0000000
--- a/ui/src/common/comparator_builder.ts
+++ /dev/null
@@ -1,46 +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.
-
-// Simple builder-style class to implement object equality more succinctly.
-export class EqualsBuilder<T> {
-  result = true;
-  first: T;
-  second: T;
-
-  constructor(first: T, second: T) {
-    this.first = first;
-    this.second = second;
-  }
-
-  comparePrimitive(getter: (arg: T) => string | number): EqualsBuilder<T> {
-    if (this.result) {
-      this.result = getter(this.first) === getter(this.second);
-    }
-    return this;
-  }
-
-  compare<S>(
-    comparator: (first: S, second: S) => boolean,
-    getter: (arg: T) => S,
-  ): EqualsBuilder<T> {
-    if (this.result) {
-      this.result = comparator(getter(this.first), getter(this.second));
-    }
-    return this;
-  }
-
-  equals(): boolean {
-    return this.result;
-  }
-}
diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts
deleted file mode 100644
index cc10366..0000000
--- a/ui/src/common/constants.ts
+++ /dev/null
@@ -1,15 +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.
-
-export const TRACE_SUFFIX = '.perfetto-trace';
diff --git a/ui/src/common/conversion_jobs.ts b/ui/src/common/conversion_jobs.ts
deleted file mode 100644
index 6805bc7..0000000
--- a/ui/src/common/conversion_jobs.ts
+++ /dev/null
@@ -1,30 +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.
-
-export enum ConversionJobStatus {
-  InProgress = 'InProgress',
-  NotRunning = 'NotRunning',
-}
-
-export type ConversionJobName =
-  | 'convert_systrace'
-  | 'convert_json'
-  | 'open_in_legacy'
-  | 'convert_pprof'
-  | 'create_permalink';
-
-export interface ConversionJobStatusUpdate {
-  jobName: ConversionJobName;
-  jobStatus: ConversionJobStatus;
-}
diff --git a/ui/src/common/dragndrop_logic.ts b/ui/src/common/dragndrop_logic.ts
deleted file mode 100644
index c61e426..0000000
--- a/ui/src/common/dragndrop_logic.ts
+++ /dev/null
@@ -1,75 +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 {assertTrue} from '../base/logging';
-
-export type DropDirection = 'left' | 'right';
-
-export interface Interval {
-  from: number;
-  to: number;
-}
-
-/*
- * When a drag'n'drop is performed in a linear sequence, the resulting reordered
- * array will consist of several contiguous subarrays of the original glued
- * together.
- *
- * This function implements the computation of these intervals.
- *
- * The drag'n'drop operation performed is as follows: in the sequence with given
- * length, the element with index `dragFrom` is dropped on the `direction` to
- * the element `dragTo`.
- */
-export function computeIntervals(
-  length: number,
-  dragFrom: number,
-  dragTo: number,
-  direction: DropDirection,
-): Interval[] {
-  assertTrue(dragFrom !== dragTo);
-
-  if (dragTo < dragFrom) {
-    const prefixLen = direction == 'left' ? dragTo : dragTo + 1;
-    return [
-      // First goes unchanged prefix.
-      {from: 0, to: prefixLen},
-      // Then goes dragged element.
-      {from: dragFrom, to: dragFrom + 1},
-      // Then goes suffix up to dragged element (which has already been moved).
-      {from: prefixLen, to: dragFrom},
-      // Then the rest of an array.
-      {from: dragFrom + 1, to: length},
-    ];
-  }
-
-  // Other case: dragTo > dragFrom
-  const prefixLen = direction == 'left' ? dragTo : dragTo + 1;
-  return [
-    {from: 0, to: dragFrom},
-    {from: dragFrom + 1, to: prefixLen},
-    {from: dragFrom, to: dragFrom + 1},
-    {from: prefixLen, to: length},
-  ];
-}
-
-export function performReordering<T>(intervals: Interval[], arr: T[]): T[] {
-  const result: T[] = [];
-
-  for (const interval of intervals) {
-    result.push(...arr.slice(interval.from, interval.to));
-  }
-
-  return result;
-}
diff --git a/ui/src/common/dragndrop_logic_unittest.ts b/ui/src/common/dragndrop_logic_unittest.ts
deleted file mode 100644
index 49b67c6..0000000
--- a/ui/src/common/dragndrop_logic_unittest.ts
+++ /dev/null
@@ -1,46 +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 {computeIntervals, performReordering} from './dragndrop_logic';
-
-describe('performReordering', () => {
-  test('has the same elements in the result', () => {
-    const arr = [1, 2, 3, 4, 5, 6];
-    const arrSet = new Set(arr);
-
-    for (let i = 0; i < arr.length; i++) {
-      for (let j = 0; j < arr.length; j++) {
-        if (i === j) {
-          // The function has a precondition that two indices have to be
-          // different.
-          continue;
-        }
-
-        const permutedLeft = performReordering(
-          computeIntervals(arr.length, i, j, 'left'),
-          arr,
-        );
-        expect(new Set(permutedLeft)).toEqual(arrSet);
-        expect(permutedLeft.length).toEqual(arr.length);
-
-        const permutedRight = performReordering(
-          computeIntervals(arr.length, i, j, 'right'),
-          arr,
-        );
-        expect(new Set(permutedRight)).toEqual(arrSet);
-        expect(permutedRight.length).toEqual(arr.length);
-      }
-    }
-  });
-});
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
deleted file mode 100644
index b7016d2..0000000
--- a/ui/src/common/empty_state.ts
+++ /dev/null
@@ -1,148 +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 {Time} from '../base/time';
-import {createEmptyRecordConfig} from '../controller/record_config_types';
-import {featureFlags} from '../core/feature_flags';
-import {Aggregation} from '../frontend/pivot_table_types';
-import {
-  autosaveConfigStore,
-  recordTargetStore,
-} from '../frontend/record_config';
-import {SqlTables} from '../frontend/widgets/sql/table/well_known_sql_tables';
-
-import {NonSerializableState, State, STATE_VERSION} from './state';
-
-const AUTOLOAD_STARTED_CONFIG_FLAG = featureFlags.register({
-  id: 'autoloadStartedConfig',
-  name: 'Auto-load last used recording config',
-  description:
-    'Starting a recording automatically saves its configuration. ' +
-    'This flag controls whether this config is automatically loaded.',
-  defaultValue: true,
-});
-
-export function keyedMap<T>(
-  keyFn: (key: T) => string,
-  ...values: T[]
-): Map<string, T> {
-  const result = new Map<string, T>();
-
-  for (const value of values) {
-    result.set(keyFn(value), value);
-  }
-
-  return result;
-}
-
-export const COUNT_AGGREGATION: Aggregation = {
-  aggregationFunction: 'COUNT',
-  // Exact column is ignored for count aggregation because it does not matter
-  // what to count, use empty strings.
-  column: {kind: 'regular', table: '', column: ''},
-};
-
-export function createEmptyNonSerializableState(): NonSerializableState {
-  return {
-    pivotTable: {
-      queryResult: null,
-      selectedPivots: [
-        {kind: 'regular', table: SqlTables.slice.name, column: 'name'},
-      ],
-      selectedAggregations: [
-        {
-          aggregationFunction: 'SUM',
-          column: {kind: 'regular', table: SqlTables.slice.name, column: 'dur'},
-          sortDirection: 'DESC',
-        },
-        {
-          aggregationFunction: 'SUM',
-          column: {
-            kind: 'regular',
-            table: SqlTables.slice.name,
-            column: 'thread_dur',
-          },
-        },
-        COUNT_AGGREGATION,
-      ],
-      constrainToArea: true,
-      queryRequested: false,
-    },
-  };
-}
-
-export function createEmptyState(): State {
-  return {
-    version: STATE_VERSION,
-    nextId: '-1',
-    newEngineMode: 'USE_HTTP_RPC_IF_AVAILABLE',
-    tracks: {},
-    utidToThreadSortKey: {},
-    aggregatePreferences: {},
-    trackGroups: {},
-    pinnedTracks: [],
-    scrollingTracks: [],
-    queries: {},
-    notes: {},
-
-    recordConfig: AUTOLOAD_STARTED_CONFIG_FLAG.get()
-      ? autosaveConfigStore.get()
-      : createEmptyRecordConfig(),
-    displayConfigAsPbtxt: false,
-    lastLoadedConfig: {type: 'NONE'},
-
-    omniboxState: {
-      omnibox: '',
-      mode: 'SEARCH',
-    },
-
-    status: {msg: '', timestamp: 0},
-    selection: {
-      kind: 'empty',
-    },
-    traceConversionInProgress: false,
-
-    perfDebug: false,
-    sidebarVisible: true,
-    hoveredUtid: -1,
-    hoveredPid: -1,
-    hoverCursorTimestamp: Time.INVALID,
-    hoveredNoteTimestamp: Time.INVALID,
-    highlightedSliceId: -1,
-    focusedFlowIdLeft: -1,
-    focusedFlowIdRight: -1,
-    searchIndex: -1,
-
-    tabs: {
-      currentTab: 'current_selection',
-      openTabs: [],
-    },
-
-    recordingInProgress: false,
-    recordingCancelled: false,
-    extensionInstalled: false,
-    flamegraphModalDismissed: false,
-    recordingTarget: recordTargetStore.getValidTarget(),
-    availableAdbDevices: [],
-
-    fetchChromeCategories: false,
-    chromeCategories: undefined,
-    nonSerializableState: createEmptyNonSerializableState(),
-
-    // Somewhere to store plugins' persistent state.
-    plugins: {},
-
-    trackFilterTerm: undefined,
-  };
-}
diff --git a/ui/src/common/gcs_uploader.ts b/ui/src/common/gcs_uploader.ts
deleted file mode 100644
index d2012ee..0000000
--- a/ui/src/common/gcs_uploader.ts
+++ /dev/null
@@ -1,207 +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.
-
-import {defer} from '../base/deferred';
-import {Time} from '../base/time';
-import {TraceFileStream} from '../core/trace_stream';
-
-export const BUCKET_NAME = 'perfetto-ui-data';
-export const MIME_JSON = 'application/json; charset=utf-8';
-export const MIME_BINARY = 'application/octet-stream';
-
-export interface GcsUploaderArgs {
-  /**
-   * The mime-type to use for the upload. If undefined uses
-   * application/octet-stream.
-   */
-  mimeType?: string;
-
-  /**
-   * The name to use for the uploaded file. By default it uses a hash of
-   * the passed data/blob and uses content-addressing.
-   */
-  fileName?: string;
-
-  /** An optional callback that is invoked upon upload progress (or failure) */
-  onProgress?: (uploader: GcsUploader) => void;
-}
-
-/**
- * A utility class to handle uploads of possibly large files to
- * Google Cloud Storage.
- * It returns immediately if the file exists already
- */
-export class GcsUploader {
-  state: 'UPLOADING' | 'UPLOADED' | 'ERROR' = 'UPLOADING';
-  error = '';
-  totalSize = 0;
-  uploadedSize = 0;
-  uploadedUrl = '';
-  uploadedFileName = '';
-
-  private args: GcsUploaderArgs;
-  private onProgress: (_: GcsUploader) => void;
-  private req: XMLHttpRequest;
-  private donePromise = defer<void>();
-  private startTime = performance.now();
-
-  constructor(data: Blob | ArrayBuffer | string, args: GcsUploaderArgs) {
-    this.args = args;
-    this.onProgress = args.onProgress ?? ((_: GcsUploader) => {});
-    this.req = new XMLHttpRequest();
-    this.start(data);
-  }
-
-  async start(data: Blob | ArrayBuffer | string) {
-    let fname = this.args.fileName;
-    if (fname === undefined) {
-      // If the file name is unspecified, hash the contents.
-      if (data instanceof Blob) {
-        fname = await hashFileStreaming(data);
-      } else {
-        fname = await sha1(data);
-      }
-    }
-    this.uploadedFileName = fname;
-    this.uploadedUrl = `https://storage.googleapis.com/${BUCKET_NAME}/${fname}`;
-
-    // Check if the file has been uploaded already. If so, skip.
-    const res = await fetch(
-      `https://www.googleapis.com/storage/v1/b/${BUCKET_NAME}/o/${fname}`,
-    );
-    if (res.status === 200) {
-      console.log(
-        `Skipping upload of ${this.uploadedUrl} because it exists already`,
-      );
-      this.state = 'UPLOADED';
-      this.donePromise.resolve();
-      return;
-    }
-
-    const reqUrl =
-      'https://www.googleapis.com/upload/storage/v1/b/' +
-      `${BUCKET_NAME}/o?uploadType=media` +
-      `&name=${fname}&predefinedAcl=publicRead`;
-    this.req.onabort = (e: ProgressEvent) => this.onRpcEvent(e);
-    this.req.onerror = (e: ProgressEvent) => this.onRpcEvent(e);
-    this.req.upload.onprogress = (e: ProgressEvent) => this.onRpcEvent(e);
-    this.req.onloadend = (e: ProgressEvent) => this.onRpcEvent(e);
-    this.req.open('POST', reqUrl, /* async= */ true);
-    const mimeType = this.args.mimeType ?? MIME_BINARY;
-    this.req.setRequestHeader('Content-Type', mimeType);
-    this.req.send(data);
-  }
-
-  waitForCompletion(): Promise<void> {
-    return this.donePromise;
-  }
-
-  abort() {
-    if (this.state === 'UPLOADING') {
-      this.req.abort();
-    }
-  }
-
-  getEtaString() {
-    let str = `${Math.ceil((100 * this.uploadedSize) / this.totalSize)}%`;
-    str += ` (${(this.uploadedSize / 1e6).toFixed(2)} MB)`;
-    const elapsed = (performance.now() - this.startTime) / 1000;
-    const rate = this.uploadedSize / elapsed;
-    const etaSecs = Math.round((this.totalSize - this.uploadedSize) / rate);
-    str += ' - ETA: ' + Time.toTimecode(Time.fromSeconds(etaSecs)).dhhmmss;
-    return str;
-  }
-
-  private onRpcEvent(e: ProgressEvent) {
-    let done = false;
-    switch (e.type) {
-      case 'progress':
-        this.uploadedSize = e.loaded;
-        this.totalSize = e.total;
-        break;
-      case 'abort':
-        this.state = 'ERROR';
-        this.error = 'Upload aborted';
-        break;
-      case 'error':
-        this.state = 'ERROR';
-        this.error = `${this.req.status} - ${this.req.statusText}`;
-        break;
-      case 'loadend':
-        done = true;
-        if (this.req.status === 200) {
-          this.state = 'UPLOADED';
-        } else if (this.state === 'UPLOADING') {
-          this.state = 'ERROR';
-          this.error = `${this.req.status} - ${this.req.statusText}`;
-        }
-        break;
-      default:
-        return;
-    }
-    this.onProgress(this);
-    if (done) {
-      this.donePromise.resolve();
-    }
-  }
-}
-
-/**
- * Computes the SHA-1 of a string or ArrayBuffer(View)
- * @param data a string or ArrayBuffer to hash.
- */
-async function sha1(data: string | ArrayBuffer): Promise<string> {
-  let buffer: ArrayBuffer;
-  if (typeof data === 'string') {
-    buffer = new TextEncoder().encode(data);
-  } else {
-    buffer = data;
-  }
-  const digest = await crypto.subtle.digest('SHA-1', buffer);
-  return digestToHex(digest);
-}
-
-/**
- * Converts a hash for the given file in streaming mode, without loading the
- * whole file into memory. The result is "a" SHA-1 but is not the same of
- * `shasum -a 1 file`. The reason for this is that the crypto APIs support
- * only one-shot digest computation and lack the usual update() + digest()
- * chunked API. So we end up computing a SHA-1 of the concatenation of the
- * SHA-1 of each chunk.
- * Speed: ~800 MB/s on a M2 Macbook Air 2023.
- * @param file The file to hash.
- * @returns A hex-encoded string containing the hash of the file.
- */
-async function hashFileStreaming(file: Blob): Promise<string> {
-  const fileStream = new TraceFileStream(file);
-  let chunkDigests = '';
-  for (;;) {
-    const chunk = await fileStream.readChunk();
-    const digest = await crypto.subtle.digest('SHA-1', chunk.data);
-    chunkDigests += digestToHex(digest);
-    if (chunk.eof) break;
-  }
-  return sha1(chunkDigests);
-}
-
-/**
- * Converts the return value of crypto.digest() to a hex string.
- * @param digest an array of bytes containing the digest
- * @returns hex-encoded string of the digest.
- */
-function digestToHex(digest: ArrayBuffer): string {
-  return Array.from(new Uint8Array(digest))
-    .map((x) => x.toString(16).padStart(2, '0'))
-    .join('');
-}
diff --git a/ui/src/common/high_precision_time.ts b/ui/src/common/high_precision_time.ts
deleted file mode 100644
index b65de01..0000000
--- a/ui/src/common/high_precision_time.ts
+++ /dev/null
@@ -1,241 +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 {assertUnreachable} from '../base/logging';
-import {Time, time} from '../base/time';
-
-export type RoundMode = 'round' | 'floor' | 'ceil';
-
-/**
- * Represents a time value in trace processor's time units, which is capable of
- * representing a time with at least 64 bit integer precision and 53 bits of
- * fractional precision.
- *
- * This class is immutable - any methods that modify this time will return a new
- * copy containing instead.
- */
-export class HighPrecisionTime {
-  // This is the high precision time representing 0
-  static readonly ZERO = new HighPrecisionTime(Time.fromRaw(0n));
-
-  // time value == |integral| + |fractional|
-  // |fractional| is kept in the range 0 <= x < 1 to avoid losing precision
-  readonly integral: time;
-  readonly fractional: number;
-
-  /**
-   * Constructs a HighPrecisionTime object.
-   *
-   * @param integral The integer part of the time value.
-   * @param fractional The fractional part of the time value.
-   */
-  constructor(integral: time, fractional: number = 0) {
-    // Normalize |fractional| to the range 0.0 <= x < 1.0
-    const fractionalFloor = Math.floor(fractional);
-    this.integral = (integral + BigInt(fractionalFloor)) as time;
-    this.fractional = fractional - fractionalFloor;
-  }
-
-  /**
-   * Converts to an integer time value.
-   *
-   * @param round How to round ('round', 'floor', or 'ceil').
-   */
-  toTime(round: RoundMode = 'floor'): time {
-    switch (round) {
-      case 'round':
-        return Time.fromRaw(
-          this.integral + BigInt(Math.round(this.fractional)),
-        );
-      case 'floor':
-        return Time.fromRaw(this.integral);
-      case 'ceil':
-        return Time.fromRaw(this.integral + BigInt(Math.ceil(this.fractional)));
-      default:
-        assertUnreachable(round);
-    }
-  }
-
-  /**
-   * Converts to a JavaScript number. Precision loss should be expected when
-   * integral values are large.
-   */
-  toNumber(): number {
-    return Number(this.integral) + this.fractional;
-  }
-
-  /**
-   * Adds another HighPrecisionTime to this one and returns the result.
-   *
-   * @param time A HighPrecisionTime object to add.
-   */
-  add(time: HighPrecisionTime): HighPrecisionTime {
-    return new HighPrecisionTime(
-      Time.add(this.integral, time.integral),
-      this.fractional + time.fractional,
-    );
-  }
-
-  /**
-   * Adds an integer time value to this HighPrecisionTime and returns the result.
-   *
-   * @param t A time value to add.
-   */
-  addTime(t: time): HighPrecisionTime {
-    return new HighPrecisionTime(Time.add(this.integral, t), this.fractional);
-  }
-
-  /**
-   * Adds a floating point time value to this one and returns the result.
-   *
-   * @param n A floating point value to add.
-   */
-  addNumber(n: number): HighPrecisionTime {
-    return new HighPrecisionTime(this.integral, this.fractional + n);
-  }
-
-  /**
-   * Subtracts another HighPrecisionTime from this one and returns the result.
-   *
-   * @param time A HighPrecisionTime object to subtract.
-   */
-  sub(time: HighPrecisionTime): HighPrecisionTime {
-    return new HighPrecisionTime(
-      Time.sub(this.integral, time.integral),
-      this.fractional - time.fractional,
-    );
-  }
-
-  /**
-   * Subtract an integer time value from this HighPrecisionTime and returns the
-   * result.
-   *
-   * @param t A time value to subtract.
-   */
-  subTime(t: time): HighPrecisionTime {
-    return new HighPrecisionTime(Time.sub(this.integral, t), this.fractional);
-  }
-
-  /**
-   * Subtracts a floating point time value from this one and returns the result.
-   *
-   * @param n A floating point value to subtract.
-   */
-  subNumber(n: number): HighPrecisionTime {
-    return new HighPrecisionTime(this.integral, this.fractional - n);
-  }
-
-  /**
-   * Checks if this HighPrecisionTime is approximately equal to another, within
-   * a given epsilon.
-   *
-   * @param other A HighPrecisionTime object to compare.
-   * @param epsilon The tolerance for equality check.
-   */
-  equals(other: HighPrecisionTime, epsilon: number = 1e-6): boolean {
-    return Math.abs(this.sub(other).toNumber()) < epsilon;
-  }
-
-  /**
-   * Checks if this time value is within the range defined by [start, end).
-   *
-   * @param start The start of the time range (inclusive).
-   * @param end The end of the time range (exclusive).
-   */
-  containedWithin(start: time, end: time): boolean {
-    return this.integral >= start && this.integral < end;
-  }
-
-  /**
-   * Checks if this HighPrecisionTime is less than a given time.
-   *
-   * @param t A time value.
-   */
-  lt(t: time): boolean {
-    return this.integral < t;
-  }
-
-  /**
-   * Checks if this HighPrecisionTime is less than or equal to a given time.
-   *
-   * @param t A time value.
-   */
-  lte(t: time): boolean {
-    return (
-      this.integral < t ||
-      (this.integral === t && Math.abs(this.fractional - 0.0) < Number.EPSILON)
-    );
-  }
-
-  /**
-   * Checks if this HighPrecisionTime is greater than a given time.
-   *
-   * @param t A time value.
-   */
-  gt(t: time): boolean {
-    return (
-      this.integral > t ||
-      (this.integral === t && Math.abs(this.fractional - 0.0) > Number.EPSILON)
-    );
-  }
-
-  /**
-   * Checks if this HighPrecisionTime is greater than or equal to a given time.
-   *
-   * @param t A time value.
-   */
-  gte(t: time): boolean {
-    return this.integral >= t;
-  }
-
-  /**
-   * Clamps this HighPrecisionTime to be within the specified range.
-   *
-   * @param lower The lower bound of the range.
-   * @param upper The upper bound of the range.
-   */
-  clamp(lower: time, upper: time): HighPrecisionTime {
-    if (this.integral < lower) {
-      return new HighPrecisionTime(lower);
-    } else if (this.integral >= upper) {
-      return new HighPrecisionTime(upper);
-    } else {
-      return this;
-    }
-  }
-
-  /**
-   * Returns the absolute value of this HighPrecisionTime.
-   */
-  abs(): HighPrecisionTime {
-    if (this.integral >= 0n) {
-      return this;
-    }
-    const newIntegral = Time.fromRaw(-this.integral);
-    const newFractional = -this.fractional;
-    return new HighPrecisionTime(newIntegral, newFractional);
-  }
-
-  /**
-   * Converts this HighPrecisionTime to a string representation.
-   */
-  toString(): string {
-    const fractionalAsString = this.fractional.toString();
-    if (fractionalAsString === '0') {
-      return this.integral.toString();
-    } else {
-      return `${this.integral}${fractionalAsString.substring(1)}`;
-    }
-  }
-}
diff --git a/ui/src/common/high_precision_time_span.ts b/ui/src/common/high_precision_time_span.ts
deleted file mode 100644
index 02dcad1..0000000
--- a/ui/src/common/high_precision_time_span.ts
+++ /dev/null
@@ -1,215 +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 {TimeSpan, time} from '../base/time';
-import {HighPrecisionTime} from './high_precision_time';
-
-/**
- * Represents a time span using a high precision time value to represent the
- * start of the span, and a number to represent the duration of the span.
- */
-export class HighPrecisionTimeSpan {
-  static readonly ZERO = new HighPrecisionTimeSpan(HighPrecisionTime.ZERO, 0);
-
-  readonly start: HighPrecisionTime;
-  readonly duration: number;
-
-  constructor(start: HighPrecisionTime, duration: number) {
-    this.start = start;
-    this.duration = duration;
-  }
-
-  /**
-   * Create a new span from integral start and end points.
-   *
-   * @param start The start of the span.
-   * @param end The end of the span.
-   */
-  static fromTime(start: time, end: time): HighPrecisionTimeSpan {
-    return new HighPrecisionTimeSpan(
-      new HighPrecisionTime(start),
-      Number(end - start),
-    );
-  }
-
-  /**
-   * The center point of the span.
-   */
-  get midpoint(): HighPrecisionTime {
-    return this.start.addNumber(this.duration / 2);
-  }
-
-  /**
-   * The end of the span.
-   */
-  get end(): HighPrecisionTime {
-    return this.start.addNumber(this.duration);
-  }
-
-  /**
-   * Checks if this span exactly equals another.
-   */
-  equals(other: HighPrecisionTimeSpan): boolean {
-    return this.start.equals(other.start) && this.duration === other.duration;
-  }
-
-  /**
-   * Create a new span with the same duration but the start point moved through
-   * time by some amount of time.
-   */
-  translate(time: number): HighPrecisionTimeSpan {
-    return new HighPrecisionTimeSpan(this.start.addNumber(time), this.duration);
-  }
-
-  /**
-   * Create a new span with the the start of the span moved backward and the end
-   * of the span moved forward by a certain amount of time.
-   */
-  pad(time: number): HighPrecisionTimeSpan {
-    return new HighPrecisionTimeSpan(
-      this.start.subNumber(time),
-      this.duration + 2 * time,
-    );
-  }
-
-  /**
-   * Create a new span which is zoomed in or out centered on a specific point.
-   *
-   * @param ratio The scaling ratio, the new duration will be the current
-   * duration * ratio.
-   * @param center The center point as a normalized value between 0 and 1 where
-   * 0 is the start of the time window and 1 is the end.
-   * @param minDur Don't allow the time span to become shorter than this.
-   */
-  scale(ratio: number, center: number, minDur: number): HighPrecisionTimeSpan {
-    const currentDuration = this.duration;
-    const newDuration = Math.max(currentDuration * ratio, minDur);
-    // Delta between new and old duration
-    // +ve if new duration is shorter than old duration
-    const durationDeltaNanos = currentDuration - newDuration;
-    // If offset is 0, don't move the start at all
-    // If offset if 1, move the start by the amount the duration has changed
-    // If new duration is shorter - move start to right
-    // If new duration is longer - move start to left
-    const start = this.start.addNumber(durationDeltaNanos * center);
-    return new HighPrecisionTimeSpan(start, newDuration);
-  }
-
-  /**
-   * Create a new span that represents the intersection of this span with
-   * another.
-   *
-   * If the two spans do not overlap at all, the empty span is returned.
-   *
-   * @param start THe start of the other span.
-   * @param end The end of the other span.
-   */
-  intersect(start: time, end: time): HighPrecisionTimeSpan {
-    if (!this.overlaps(start, end)) {
-      return HighPrecisionTimeSpan.ZERO;
-    }
-    const newStart = this.start.clamp(start, end);
-    const newEnd = this.end.clamp(start, end);
-    const newDuration = newEnd.sub(newStart).toNumber();
-    return new HighPrecisionTimeSpan(newStart, newDuration);
-  }
-
-  /**
-   * Create a new timespan which fits within the specified bounds, preserving
-   * its duration if possible.
-   *
-   * This function moves the timespan forwards or backwards in time while
-   * keeping its duration unchanged, so that it fits entirely within the range
-   * defined by `start` and `end`.
-   *
-   * If the specified bounds are smaller than the current timespan's duration, a
-   * new timespan matching the bounds is returned.
-   *
-   * @param start The start of the bounds within which the timespan should fit.
-   * @param end The end of the bounds within which the timespan should fit.
-   *
-   * @example
-   * // assume `timespan` is defined as: [5, 8)
-   * timespan.fitWithin(10n, 20n); // -> [10, 13)
-   * timespan.fitWithin(-10n, -5n); // -> [-8, -5)
-   * timespan.fitWithin(1n, 2n); // -> [1, 2)
-   */
-  fitWithin(start: time, end: time): HighPrecisionTimeSpan {
-    if (this.duration > Number(end - start)) {
-      // Current span is greater than the limits
-      return HighPrecisionTimeSpan.fromTime(start, end);
-    }
-    if (this.start.integral < start) {
-      // Current span starts before limits
-      return new HighPrecisionTimeSpan(
-        new HighPrecisionTime(start),
-        this.duration,
-      );
-    }
-    if (this.end.gt(end)) {
-      // Current span ends after limits
-      return new HighPrecisionTimeSpan(
-        new HighPrecisionTime(end).subNumber(this.duration),
-        this.duration,
-      );
-    }
-    return this;
-  }
-
-  /**
-   * Clamp duration to some minimum value. The start remains the same, just the
-   * duration is changed.
-   */
-  clampDuration(minDuration: number): HighPrecisionTimeSpan {
-    if (this.duration < minDuration) {
-      return new HighPrecisionTimeSpan(this.start, minDuration);
-    } else {
-      return this;
-    }
-  }
-
-  /**
-   * Checks whether this span completely contains a time instant.
-   */
-  contains(t: time): boolean {
-    return this.start.lte(t) && this.end.gt(t);
-  }
-
-  /**
-   * Checks whether this span entirely contains another span.
-   *
-   * @param start The start of the span to check.
-   * @param end The end of the span to check.
-   */
-  containsSpan(start: time, end: time): boolean {
-    return this.start.lte(start) && this.end.gte(end);
-  }
-
-  /**
-   * Checks if this span overlaps at all with another.
-   *
-   * @param start The start of the span to check.
-   * @param end The end of the span to check.
-   */
-  overlaps(start: time, end: time): boolean {
-    return !(this.start.gte(end) || this.end.lte(start));
-  }
-
-  /**
-   * Get the span of integer intervals values that overlap this span.
-   */
-  toTimeSpan(): TimeSpan {
-    return new TimeSpan(this.start.toTime('floor'), this.end.toTime('ceil'));
-  }
-}
diff --git a/ui/src/common/high_precision_time_span_unittest.ts b/ui/src/common/high_precision_time_span_unittest.ts
deleted file mode 100644
index 08b7b79..0000000
--- a/ui/src/common/high_precision_time_span_unittest.ts
+++ /dev/null
@@ -1,165 +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 {HighPrecisionTime as HPTime} from './high_precision_time';
-import {HighPrecisionTimeSpan as HPTimeSpan} from './high_precision_time_span';
-import {Time} from '../base/time';
-
-const t = Time.fromRaw;
-
-// Quick 'n' dirty function to convert a string to a HPtime
-// Used to make tests more readable
-// E.g. '1.3' -> {base: 1, offset: 0.3}
-// E.g. '-0.3' -> {base: -1, offset: 0.7}
-function hptime(time: string): HPTime {
-  const array = time.split('.');
-  if (array.length > 2) throw new Error(`Bad time format ${time}`);
-  const [base, fractions] = array;
-  const negative = time.startsWith('-');
-  const numBase = BigInt(base);
-
-  if (fractions) {
-    const numFractions = Number(`0.${fractions}`);
-    if (negative) {
-      return new HPTime(t(numBase - 1n), 1.0 - numFractions);
-    } else {
-      return new HPTime(t(numBase), numFractions);
-    }
-  } else {
-    return new HPTime(t(numBase));
-  }
-}
-
-describe('HighPrecisionTimeSpan', () => {
-  it('can be constructed from integer time', () => {
-    const span = HPTimeSpan.fromTime(t(10n), t(20n));
-    expect(span.start.integral).toEqual(10n);
-    expect(span.start.fractional).toBeCloseTo(0);
-    expect(span.duration).toBeCloseTo(10);
-  });
-
-  test('end', () => {
-    const span = HPTimeSpan.fromTime(t(10n), t(20n));
-    expect(span.end.integral).toEqual(20n);
-    expect(span.end.fractional).toBeCloseTo(0);
-  });
-
-  test('midpoint', () => {
-    const span = HPTimeSpan.fromTime(t(10n), t(20n));
-    expect(span.midpoint.integral).toEqual(15n);
-    expect(span.midpoint.fractional).toBeCloseTo(0);
-  });
-
-  test('translate', () => {
-    const span = HPTimeSpan.fromTime(t(10n), t(20n));
-    expect(span.translate(10).start.integral).toEqual(20n);
-    expect(span.translate(10).start.fractional).toEqual(0);
-    expect(span.translate(10).duration).toBeCloseTo(10);
-  });
-
-  test('pad', () => {
-    const span = HPTimeSpan.fromTime(t(10n), t(20n));
-    expect(span.pad(10).start.integral).toEqual(0n);
-    expect(span.pad(10).start.fractional).toEqual(0);
-    expect(span.pad(10).duration).toBeCloseTo(30);
-  });
-
-  test('scale', () => {
-    const span = HPTimeSpan.fromTime(t(10n), t(20n));
-    const zoomed = span.scale(2, 0.5, 0);
-    expect(zoomed.start.integral).toEqual(5n);
-    expect(zoomed.start.fractional).toBeCloseTo(0);
-    expect(zoomed.duration).toBeCloseTo(20);
-  });
-
-  test('intersect', () => {
-    const span = new HPTimeSpan(hptime('5'), 3);
-
-    let result = span.intersect(t(7n), t(10n));
-    expect(result.start.integral).toBe(7n);
-    expect(result.start.fractional).toBeCloseTo(0);
-    expect(result.duration).toBeCloseTo(1);
-
-    result = span.intersect(t(1n), t(6n));
-    expect(result.start.integral).toBe(5n);
-    expect(result.start.fractional).toBeCloseTo(0);
-    expect(result.duration).toBeCloseTo(1);
-
-    // Non overlapping time spans should return 0
-    result = span.intersect(t(100n), t(200n));
-    expect(result.start.integral).toBe(0n);
-    expect(result.start.fractional).toBeCloseTo(0);
-    expect(result.duration).toBeCloseTo(0);
-  });
-
-  test('fitWithin', () => {
-    const span = new HPTimeSpan(hptime('5'), 3);
-
-    let result = span.fitWithin(t(10n), t(20n));
-    expect(result.start.integral).toBe(10n);
-    expect(result.start.fractional).toBeCloseTo(0);
-    expect(result.duration).toBeCloseTo(3);
-
-    result = span.fitWithin(t(-10n), t(-5n));
-    expect(result.start.integral).toBe(-8n);
-    expect(result.start.fractional).toBeCloseTo(0);
-    expect(result.duration).toBeCloseTo(3);
-
-    result = span.fitWithin(t(1n), t(2n));
-    expect(result.start.integral).toBe(1n);
-    expect(result.start.fractional).toBeCloseTo(0);
-    expect(result.duration).toBeCloseTo(1);
-  });
-
-  test('clampDuration', () => {
-    const span = new HPTimeSpan(hptime('5'), 1);
-    const clamped = span.clampDuration(10);
-
-    expect(clamped.start.integral).toBe(5n);
-    expect(clamped.start.fractional).toBeCloseTo(0);
-    expect(clamped.duration).toBeCloseTo(10);
-  });
-
-  test('equality', () => {
-    const span = new HPTimeSpan(hptime('10'), 10);
-    expect(span.equals(span)).toBe(true);
-    expect(span.equals(new HPTimeSpan(hptime('10'), 10.5))).toBe(false);
-    expect(span.equals(new HPTimeSpan(hptime('10.1'), 10))).toBe(false);
-  });
-
-  test('contains', () => {
-    const span = new HPTimeSpan(hptime('10'), 10);
-    expect(span.contains(t(9n))).toBe(false);
-    expect(span.contains(t(10n))).toBe(true);
-    expect(span.contains(t(19n))).toBe(true);
-    expect(span.contains(t(20n))).toBe(false);
-  });
-
-  test('containsSpan', () => {
-    const span = new HPTimeSpan(hptime('10'), 10);
-    expect(span.containsSpan(t(9n), t(15n))).toBe(false);
-    expect(span.containsSpan(t(10n), t(15n))).toBe(true);
-    expect(span.containsSpan(t(15n), t(20n))).toBe(true);
-    expect(span.containsSpan(t(15n), t(21n))).toBe(false);
-    expect(span.containsSpan(t(30n), t(40n))).toBe(false);
-  });
-
-  test('overlapsSpan', () => {
-    const span = new HPTimeSpan(hptime('10'), 10);
-    expect(span.overlaps(t(9n), t(10n))).toBe(false);
-    expect(span.overlaps(t(9n), t(11n))).toBe(true);
-    expect(span.overlaps(t(19n), t(21n))).toBe(true);
-    expect(span.overlaps(t(20n), t(21n))).toBe(false);
-  });
-});
diff --git a/ui/src/common/high_precision_time_unittest.ts b/ui/src/common/high_precision_time_unittest.ts
deleted file mode 100644
index 2e94955..0000000
--- a/ui/src/common/high_precision_time_unittest.ts
+++ /dev/null
@@ -1,212 +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 {Time, time} from '../base/time';
-
-import {HighPrecisionTime as HPTime} from './high_precision_time';
-
-const t = Time.fromRaw;
-
-// Quick 'n' dirty function to convert a string to a HPtime
-// Used to make tests more readable
-// E.g. '1.3' -> {base: 1, offset: 0.3}
-// E.g. '-0.3' -> {base: -1, offset: 0.7}
-function mkTime(time: string): HPTime {
-  const array = time.split('.');
-  if (array.length > 2) throw new Error(`Bad time format ${time}`);
-  const [base, fractions] = array;
-  const negative = time.startsWith('-');
-  const numBase = BigInt(base);
-
-  if (fractions) {
-    const numFractions = Number(`0.${fractions}`);
-    if (negative) {
-      return new HPTime(t(numBase - 1n), 1.0 - numFractions);
-    } else {
-      return new HPTime(t(numBase), numFractions);
-    }
-  } else {
-    return new HPTime(t(numBase));
-  }
-}
-
-describe('Time', () => {
-  it('should create a new Time object with the given base and offset', () => {
-    const time = new HPTime(t(136n), 0.3);
-    expect(time.integral).toBe(136n);
-    expect(time.fractional).toBeCloseTo(0.3);
-  });
-
-  it('should normalize when offset is >= 1', () => {
-    let time = new HPTime(t(1n), 2.3);
-    expect(time.integral).toBe(3n);
-    expect(time.fractional).toBeCloseTo(0.3);
-
-    time = new HPTime(t(1n), 1);
-    expect(time.integral).toBe(2n);
-    expect(time.fractional).toBeCloseTo(0);
-  });
-
-  it('should normalize when offset is < 0', () => {
-    const time = new HPTime(t(1n), -0.4);
-    expect(time.integral).toBe(0n);
-    expect(time.fractional).toBeCloseTo(0.6);
-  });
-
-  it('should store timestamps without losing precision', () => {
-    const time = new HPTime(t(1152921504606846976n));
-    expect(time.toTime()).toBe(1152921504606846976n as time);
-  });
-
-  it('should store and manipulate timestamps without losing precision', () => {
-    let time = new HPTime(t(2315700508990407843n));
-    time = time.addTime(2315718101717517451n as time);
-    expect(time.toTime()).toBe(4631418610707925294n);
-  });
-
-  test('add', () => {
-    const result = mkTime('1.3').add(mkTime('3.1'));
-    expect(result.integral).toEqual(4n);
-    expect(result.fractional).toBeCloseTo(0.4);
-  });
-
-  test('addTime', () => {
-    const result = mkTime('200.334').addTime(t(150n));
-    expect(result.integral).toBe(350n);
-    expect(result.fractional).toBeCloseTo(0.334);
-  });
-
-  test('addNumber', () => {
-    const result = mkTime('200.334').addNumber(150.5);
-    expect(result.integral).toBe(350n);
-    expect(result.fractional).toBeCloseTo(0.834);
-  });
-
-  test('sub', () => {
-    const result = mkTime('1.3').sub(mkTime('3.1'));
-    expect(result.integral).toEqual(-2n);
-    expect(result.fractional).toBeCloseTo(0.2);
-  });
-
-  test('addTime', () => {
-    const result = mkTime('200.334').subTime(t(150n));
-    expect(result.integral).toBe(50n);
-    expect(result.fractional).toBeCloseTo(0.334);
-  });
-
-  test('subNumber', () => {
-    const result = mkTime('200.334').subNumber(150.5);
-    expect(result.integral).toBe(49n);
-    expect(result.fractional).toBeCloseTo(0.834);
-  });
-
-  test('gte', () => {
-    expect(mkTime('1.0').gte(t(1n))).toBe(true);
-    expect(mkTime('1.0').gte(t(2n))).toBe(false);
-    expect(mkTime('1.2').gte(t(1n))).toBe(true);
-    expect(mkTime('1.2').gte(t(2n))).toBe(false);
-  });
-
-  test('gt', () => {
-    expect(mkTime('1.0').gt(t(1n))).toBe(false);
-    expect(mkTime('1.0').gt(t(2n))).toBe(false);
-    expect(mkTime('1.2').gt(t(1n))).toBe(true);
-    expect(mkTime('1.2').gt(t(2n))).toBe(false);
-  });
-
-  test('lte', () => {
-    expect(mkTime('1.0').lte(t(0n))).toBe(false);
-    expect(mkTime('1.0').lte(t(1n))).toBe(true);
-    expect(mkTime('1.0').lte(t(2n))).toBe(true);
-    expect(mkTime('1.2').lte(t(1n))).toBe(false);
-    expect(mkTime('1.2').lte(t(2n))).toBe(true);
-  });
-
-  test('lt', () => {
-    expect(mkTime('1.0').lt(t(0n))).toBe(false);
-    expect(mkTime('1.0').lt(t(1n))).toBe(false);
-    expect(mkTime('1.0').lt(t(2n))).toBe(true);
-    expect(mkTime('1.2').lt(t(1n))).toBe(false);
-    expect(mkTime('1.2').lt(t(2n))).toBe(true);
-  });
-
-  test('equals', () => {
-    const time = new HPTime(t(1n), 0.2);
-    expect(time.equals(new HPTime(t(1n), 0.2))).toBeTruthy();
-    expect(time.equals(new HPTime(t(0n), 1.2))).toBeTruthy();
-    expect(time.equals(new HPTime(t(-100n), 101.2))).toBeTruthy();
-    expect(time.equals(new HPTime(t(1n), 0.3))).toBeFalsy();
-    expect(time.equals(new HPTime(t(2n), 0.2))).toBeFalsy();
-  });
-
-  test('containedWithin', () => {
-    expect(mkTime('0.9').containedWithin(t(1n), t(2n))).toBe(false);
-    expect(mkTime('1.0').containedWithin(t(1n), t(2n))).toBe(true);
-    expect(mkTime('1.2').containedWithin(t(1n), t(2n))).toBe(true);
-    expect(mkTime('2.0').containedWithin(t(1n), t(2n))).toBe(false);
-    expect(mkTime('2.1').containedWithin(t(1n), t(2n))).toBe(false);
-  });
-
-  test('clamp', () => {
-    let result = mkTime('1.2').clamp(t(1n), t(2n));
-    expect(result.integral).toBe(1n);
-    expect(result.fractional).toBeCloseTo(0.2);
-
-    result = mkTime('2.2').clamp(t(1n), t(2n));
-    expect(result.integral).toBe(2n);
-    expect(result.fractional).toBeCloseTo(0);
-
-    result = mkTime('0.2').clamp(t(1n), t(2n));
-    expect(result.integral).toBe(1n);
-    expect(result.fractional).toBeCloseTo(0);
-  });
-
-  test('toNumber', () => {
-    expect(new HPTime(t(1n), 0.2).toNumber()).toBeCloseTo(1.2);
-    expect(new HPTime(t(1000000000n), 0.0).toNumber()).toBeCloseTo(1e9);
-  });
-
-  test('toTime', () => {
-    expect(new HPTime(t(1n), 0.2).toTime('round')).toBe(1n);
-    expect(new HPTime(t(1n), 0.5).toTime('round')).toBe(2n);
-    expect(new HPTime(t(1n), 0.2).toTime('floor')).toBe(1n);
-    expect(new HPTime(t(1n), 0.5).toTime('floor')).toBe(1n);
-    expect(new HPTime(t(1n), 0.2).toTime('ceil')).toBe(2n);
-    expect(new HPTime(t(1n), 0.5).toTime('ceil')).toBe(2n);
-  });
-
-  test('toString', () => {
-    expect(mkTime('1.3').toString()).toBe('1.3');
-    expect(mkTime('12983423847.332533').toString()).toBe('12983423847.332533');
-    expect(new HPTime(t(234n)).toString()).toBe('234');
-  });
-
-  test('abs', () => {
-    let result = mkTime('-0.7').abs();
-    expect(result.integral).toEqual(0n);
-    expect(result.fractional).toBeCloseTo(0.7);
-
-    result = mkTime('-1.3').abs();
-    expect(result.integral).toEqual(1n);
-    expect(result.fractional).toBeCloseTo(0.3);
-
-    result = mkTime('-100').abs();
-    expect(result.integral).toEqual(100n);
-    expect(result.fractional).toBeCloseTo(0);
-
-    result = mkTime('34.5345').abs();
-    expect(result.integral).toEqual(34n);
-    expect(result.fractional).toBeCloseTo(0.5345);
-  });
-});
diff --git a/ui/src/common/legacy_flamegraph_unittest.ts b/ui/src/common/legacy_flamegraph_unittest.ts
deleted file mode 100644
index 222b2e2..0000000
--- a/ui/src/common/legacy_flamegraph_unittest.ts
+++ /dev/null
@@ -1,1072 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {CallsiteInfo, mergeCallsites} from './legacy_flamegraph_util';
-
-test('zeroCallsitesMerged', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 4,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 2,
-      name: 'B4',
-      depth: 1,
-      totalSize: 4,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 5);
-
-  // Small callsites are not next ot each other, nothing should be changed.
-  expect(mergedCallsites).toEqual(callsites);
-});
-
-test('zeroCallsitesMerged2', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 6,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 4,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 2,
-      name: 'B5',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 5);
-
-  // Small callsites are not next ot each other, nothing should be changed.
-  expect(mergedCallsites).toEqual(callsites);
-});
-
-test('twoCallsitesMerged', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 6);
-
-  expect(mergedCallsites).toEqual([
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-  ]);
-});
-
-test('manyCallsitesMerged', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 1,
-      name: 'A5',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 3,
-      name: 'A36',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 4,
-      name: 'A47',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 5,
-      name: 'A58',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const expectedMergedCallsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 3,
-      name: '[merged]',
-      depth: 2,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 4);
-
-  // In this case, callsites A3, A4 and A5 should be merged since they are
-  // smaller then 4 and are on same depth with same parent. Callsites A36, A47
-  // and A58 should also be merged since their parents are merged.
-  expect(mergedCallsites).toEqual(expectedMergedCallsites);
-});
-
-test('manyCallsitesMergedWithoutChildren', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 1,
-      name: 'A5',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 2,
-      name: 'B6',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 4,
-      name: 'A47',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 6,
-      name: 'B68',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const expectedMergedCallsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 2,
-      name: 'B6',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 3,
-      name: 'A47',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 6,
-      name: 'B68',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 4);
-
-  // In this case, callsites A3, A4 and A5 should be merged since they are
-  // smaller then 4 and are on same depth with same parent. Callsite A47
-  // should not be merged with B68 althought they are small because they don't
-  // have sam parent. A47 should now have parent A3 because A4 is merged.
-  expect(mergedCallsites).toEqual(expectedMergedCallsites);
-});
-
-test('smallCallsitesNotNextToEachOtherInArray', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 20,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 1,
-      name: 'A5',
-      depth: 1,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const expectedMergedCallsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 20,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 4,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 4);
-
-  // In this case, callsites A3, A4 and A5 should be merged since they are
-  // smaller then 4 and are on same depth with same parent. Callsite A47
-  // should not be merged with B68 althought they are small because they don't
-  // have sam parent. A47 should now have parent A3 because A4 is merged.
-  expect(mergedCallsites).toEqual(expectedMergedCallsites);
-});
-
-test('smallCallsitesNotMerged', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 1);
-
-  expect(mergedCallsites).toEqual(callsites);
-});
-
-test('mergingRootCallsites', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 20);
-
-  expect(mergedCallsites).toEqual([
-    {
-      id: 1,
-      parentId: -1,
-      name: '[merged]',
-      depth: 0,
-      totalSize: 12,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-  ]);
-});
-
-test('largerFlamegraph', () => {
-  const data: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 60,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 40,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 25,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 15,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 1,
-      name: 'A5',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 1,
-      name: 'A6',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 2,
-      name: 'B7',
-      depth: 1,
-      totalSize: 30,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 2,
-      name: 'B8',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 9,
-      parentId: 3,
-      name: 'A39',
-      depth: 2,
-      totalSize: 20,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 10,
-      parentId: 4,
-      name: 'A410',
-      depth: 2,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 11,
-      parentId: 4,
-      name: 'A411',
-      depth: 2,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 12,
-      parentId: 4,
-      name: 'A412',
-      depth: 2,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 13,
-      parentId: 5,
-      name: 'A513',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 14,
-      parentId: 5,
-      name: 'A514',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 15,
-      parentId: 7,
-      name: 'A715',
-      depth: 2,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 16,
-      parentId: 7,
-      name: 'A716',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 17,
-      parentId: 7,
-      name: 'A717',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 18,
-      parentId: 7,
-      name: 'A718',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 19,
-      parentId: 9,
-      name: 'A919',
-      depth: 3,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 20,
-      parentId: 17,
-      name: 'A1720',
-      depth: 3,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const expectedData: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 60,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 40,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 25,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 35,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 2,
-      name: 'B7',
-      depth: 1,
-      totalSize: 30,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 2,
-      name: 'B8',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 9,
-      parentId: 3,
-      name: 'A39',
-      depth: 2,
-      totalSize: 20,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 10,
-      parentId: 4,
-      name: '[merged]',
-      depth: 2,
-      totalSize: 25,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 15,
-      parentId: 7,
-      name: '[merged]',
-      depth: 2,
-      totalSize: 25,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 19,
-      parentId: 9,
-      name: 'A919',
-      depth: 3,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 20,
-      parentId: 15,
-      name: 'A1720',
-      depth: 3,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  // In this case, on depth 1, callsites A4, A5 and A6 should be merged and
-  // initiate merging of their children A410, A411, A412, A513, A514. On depth2,
-  // callsites A715, A716, A717 and A718 should be merged.
-  const actualData = mergeCallsites(data, 16);
-
-  expect(actualData).toEqual(expectedData);
-});
diff --git a/ui/src/common/legacy_flamegraph_util.ts b/ui/src/common/legacy_flamegraph_util.ts
deleted file mode 100644
index cb540dd..0000000
--- a/ui/src/common/legacy_flamegraph_util.ts
+++ /dev/null
@@ -1,270 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {featureFlags} from '../core/feature_flags';
-import {ProfileType} from './state';
-
-export enum FlamegraphViewingOption {
-  SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY = 'SPACE',
-  ALLOC_SPACE_MEMORY_ALLOCATED_KEY = 'ALLOC_SPACE',
-  OBJECTS_ALLOCATED_NOT_FREED_KEY = 'OBJECTS',
-  OBJECTS_ALLOCATED_KEY = 'ALLOC_OBJECTS',
-  PERF_SAMPLES_KEY = 'PERF_SAMPLES',
-  DOMINATOR_TREE_OBJ_SIZE_KEY = 'DOMINATED_OBJ_SIZE',
-  DOMINATOR_TREE_OBJ_COUNT_KEY = 'DOMINATED_OBJ_COUNT',
-}
-
-interface ViewingOption {
-  option: FlamegraphViewingOption;
-  name: string;
-}
-
-export interface CallsiteInfo {
-  id: number;
-  parentId: number;
-  depth: number;
-  name?: string;
-  totalSize: number;
-  selfSize: number;
-  mapping: string;
-  merged: boolean;
-  highlighted: boolean;
-  location?: string;
-}
-
-export const SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG = featureFlags.register({
-  id: 'showHeapGraphDominatorTree',
-  name: 'Show heap graph dominator tree',
-  description: 'Show dominated size and objects tabs in Java heap graph view.',
-  defaultValue: true,
-});
-
-export function viewingOptions(profileType: ProfileType): Array<ViewingOption> {
-  switch (profileType) {
-    case ProfileType.PERF_SAMPLE:
-      return [
-        {
-          option: FlamegraphViewingOption.PERF_SAMPLES_KEY,
-          name: 'Samples',
-        },
-      ];
-    case ProfileType.JAVA_HEAP_GRAPH:
-      return [
-        {
-          option: FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
-          name: 'Size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY,
-          name: 'Objects',
-        },
-      ].concat(
-        SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG.get()
-          ? [
-              {
-                option: FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY,
-                name: 'Dominated size',
-              },
-              {
-                option: FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY,
-                name: 'Dominated objects',
-              },
-            ]
-          : [],
-      );
-    case ProfileType.HEAP_PROFILE:
-      return [
-        {
-          option: FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
-          name: 'Unreleased size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY,
-          name: 'Unreleased count',
-        },
-        {
-          option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-          name: 'Total size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY,
-          name: 'Total count',
-        },
-      ];
-    case ProfileType.NATIVE_HEAP_PROFILE:
-      return [
-        {
-          option: FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
-          name: 'Unreleased malloc size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY,
-          name: 'Unreleased malloc count',
-        },
-        {
-          option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-          name: 'Total malloc size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY,
-          name: 'Total malloc count',
-        },
-      ];
-    case ProfileType.JAVA_HEAP_SAMPLES:
-      return [
-        {
-          option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-          name: 'Total allocation size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY,
-          name: 'Total allocation count',
-        },
-      ];
-    case ProfileType.MIXED_HEAP_PROFILE:
-      return [
-        {
-          option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-          name: 'Total allocation size (malloc + java)',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY,
-          name: 'Total allocation count (malloc + java)',
-        },
-      ];
-    default:
-      const exhaustiveCheck: never = profileType;
-      throw new Error(`Unhandled case: ${exhaustiveCheck}`);
-  }
-}
-
-export function defaultViewingOption(
-  profileType: ProfileType,
-): FlamegraphViewingOption {
-  return viewingOptions(profileType)[0].option;
-}
-
-export function expandCallsites(
-  data: ReadonlyArray<CallsiteInfo>,
-  clickedCallsiteIndex: number,
-): ReadonlyArray<CallsiteInfo> {
-  if (clickedCallsiteIndex === -1) return data;
-  const expandedCallsites: CallsiteInfo[] = [];
-  if (clickedCallsiteIndex >= data.length || clickedCallsiteIndex < -1) {
-    return expandedCallsites;
-  }
-  const clickedCallsite = data[clickedCallsiteIndex];
-  expandedCallsites.unshift(clickedCallsite);
-  // Adding parents
-  let parentId = clickedCallsite.parentId;
-  while (parentId > -1) {
-    expandedCallsites.unshift(data[parentId]);
-    parentId = data[parentId].parentId;
-  }
-  // Adding children
-  const parents: number[] = [];
-  parents.push(clickedCallsiteIndex);
-  for (let i = clickedCallsiteIndex + 1; i < data.length; i++) {
-    const element = data[i];
-    if (parents.includes(element.parentId)) {
-      expandedCallsites.push(element);
-      parents.push(element.id);
-    }
-  }
-  return expandedCallsites;
-}
-
-// Merge callsites that have approximately width less than
-// MIN_PIXEL_DISPLAYED. All small callsites in the same depth and with same
-// parent will be merged to one with total size of all merged callsites.
-export function mergeCallsites(
-  data: ReadonlyArray<CallsiteInfo>,
-  minSizeDisplayed: number,
-) {
-  const mergedData: CallsiteInfo[] = [];
-  const mergedCallsites: Map<number, number> = new Map();
-  for (let i = 0; i < data.length; i++) {
-    // When a small callsite is found, it will be merged with other small
-    // callsites of the same depth. So if the current callsite has already been
-    // merged we can skip it.
-    if (mergedCallsites.has(data[i].id)) {
-      continue;
-    }
-    const copiedCallsite = copyCallsite(data[i]);
-    copiedCallsite.parentId = getCallsitesParentHash(
-      copiedCallsite,
-      mergedCallsites,
-    );
-
-    let mergedAny = false;
-    // If current callsite is small, find other small callsites with same depth
-    // and parent and merge them into the current one, marking them as merged.
-    if (copiedCallsite.totalSize <= minSizeDisplayed && i + 1 < data.length) {
-      let j = i + 1;
-      let nextCallsite = data[j];
-      while (j < data.length && copiedCallsite.depth === nextCallsite.depth) {
-        if (
-          copiedCallsite.parentId ===
-            getCallsitesParentHash(nextCallsite, mergedCallsites) &&
-          nextCallsite.totalSize <= minSizeDisplayed
-        ) {
-          copiedCallsite.totalSize += nextCallsite.totalSize;
-          mergedCallsites.set(nextCallsite.id, copiedCallsite.id);
-          mergedAny = true;
-        }
-        j++;
-        nextCallsite = data[j];
-      }
-      if (mergedAny) {
-        copiedCallsite.name = '[merged]';
-        copiedCallsite.merged = true;
-      }
-    }
-    mergedData.push(copiedCallsite);
-  }
-  return mergedData;
-}
-
-function copyCallsite(callsite: CallsiteInfo): CallsiteInfo {
-  return {
-    id: callsite.id,
-    parentId: callsite.parentId,
-    depth: callsite.depth,
-    name: callsite.name,
-    totalSize: callsite.totalSize,
-    mapping: callsite.mapping,
-    selfSize: callsite.selfSize,
-    merged: callsite.merged,
-    highlighted: callsite.highlighted,
-    location: callsite.location,
-  };
-}
-
-function getCallsitesParentHash(
-  callsite: CallsiteInfo,
-  map: Map<number, number>,
-): number {
-  return map.has(callsite.parentId)
-    ? +map.get(callsite.parentId)!
-    : callsite.parentId;
-}
-export function findRootSize(data: ReadonlyArray<CallsiteInfo>) {
-  let totalSize = 0;
-  let i = 0;
-  while (i < data.length && data[i].depth === 0) {
-    totalSize += data[i].totalSize;
-    i++;
-  }
-  return totalSize;
-}
diff --git a/ui/src/common/metatracing.ts b/ui/src/common/metatracing.ts
deleted file mode 100644
index d68db64..0000000
--- a/ui/src/common/metatracing.ts
+++ /dev/null
@@ -1,221 +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 {featureFlags} from '../core/feature_flags';
-import {MetatraceCategories, PerfettoMetatrace} from '../protos';
-import protobuf from 'protobufjs/minimal';
-
-const METATRACING_BUFFER_SIZE = 100000;
-
-export enum MetatraceTrackId {
-  // 1 is reserved for the Trace Processor track.
-  // Events emitted by the JS main thread.
-  kMainThread = 2,
-  // Async track for the status (e.g. "loading tracks") shown to the user
-  // in the omnibox.
-  kOmniboxStatus = 3,
-}
-
-const AOMT_FLAG = featureFlags.register({
-  id: 'alwaysOnMetatracing',
-  name: 'Enable always-on-metatracing',
-  description: 'Enables trace events in the UI and trace processor',
-  defaultValue: false,
-});
-
-const AOMT_DETAILED_FLAG = featureFlags.register({
-  id: 'alwaysOnMetatracing_detailed',
-  name: 'Detailed always-on-metatracing',
-  description: 'Enables recording additional events for trace event',
-  defaultValue: false,
-});
-
-function getInitialCategories(): MetatraceCategories | undefined {
-  if (!AOMT_FLAG.get()) return undefined;
-  if (AOMT_DETAILED_FLAG.get()) return MetatraceCategories.ALL;
-  return MetatraceCategories.QUERY_TIMELINE | MetatraceCategories.API_TIMELINE;
-}
-
-let enabledCategories: MetatraceCategories | undefined = getInitialCategories();
-
-export function enableMetatracing(categories?: MetatraceCategories) {
-  enabledCategories =
-    categories === undefined || categories === MetatraceCategories.NONE
-      ? MetatraceCategories.ALL
-      : categories;
-}
-
-export function disableMetatracingAndGetTrace(): Uint8Array {
-  enabledCategories = undefined;
-  return readMetatrace();
-}
-
-export function isMetatracingEnabled(): boolean {
-  return enabledCategories !== undefined;
-}
-
-export function getEnabledMetatracingCategories():
-  | MetatraceCategories
-  | undefined {
-  return enabledCategories;
-}
-
-interface TraceEvent {
-  eventName: string;
-  startNs: number;
-  durNs: number;
-  track: MetatraceTrackId;
-  args?: {[key: string]: string};
-}
-
-const traceEvents: TraceEvent[] = [];
-
-function readMetatrace(): Uint8Array {
-  const eventToPacket = (e: TraceEvent): Uint8Array => {
-    const metatraceEvent = PerfettoMetatrace.create({
-      eventName: e.eventName,
-      threadId: e.track,
-      eventDurationNs: e.durNs,
-    });
-    for (const [key, value] of Object.entries(e.args ?? {})) {
-      metatraceEvent.args.push(
-        PerfettoMetatrace.Arg.create({
-          key,
-          value,
-        }),
-      );
-    }
-    const PROTO_VARINT_TYPE = 0;
-    const PROTO_LEN_DELIMITED_WIRE_TYPE = 2;
-    const TRACE_PACKET_PROTO_TAG = (1 << 3) | PROTO_LEN_DELIMITED_WIRE_TYPE;
-    const TRACE_PACKET_TIMESTAMP_TAG = (8 << 3) | PROTO_VARINT_TYPE;
-    const TRACE_PACKET_CLOCK_ID_TAG = (58 << 3) | PROTO_VARINT_TYPE;
-    const TRACE_PACKET_METATRACE_TAG =
-      (49 << 3) | PROTO_LEN_DELIMITED_WIRE_TYPE;
-
-    const wri = protobuf.Writer.create();
-    wri.uint32(TRACE_PACKET_PROTO_TAG);
-    wri.fork(); // Start of Trace Packet.
-    wri.uint32(TRACE_PACKET_TIMESTAMP_TAG).int64(e.startNs);
-    wri.uint32(TRACE_PACKET_CLOCK_ID_TAG).int32(1);
-    wri
-      .uint32(TRACE_PACKET_METATRACE_TAG)
-      .bytes(PerfettoMetatrace.encode(metatraceEvent).finish());
-    wri.ldelim();
-    return wri.finish();
-  };
-  const packets: Uint8Array[] = [];
-  for (const event of traceEvents) {
-    packets.push(eventToPacket(event));
-  }
-  const totalLength = packets.reduce((acc, arr) => acc + arr.length, 0);
-  const trace = new Uint8Array(totalLength);
-  let offset = 0;
-  for (const packet of packets) {
-    trace.set(packet, offset);
-    offset += packet.length;
-  }
-  return trace;
-}
-
-interface TraceEventParams {
-  track?: MetatraceTrackId;
-  args?: {[key: string]: string};
-}
-
-export type TraceEventScope = {
-  startNs: number;
-  eventName: string;
-  params?: TraceEventParams;
-};
-
-const correctedTimeOrigin = new Date().getTime() - performance.now();
-
-function msToNs(ms: number) {
-  return Math.round(ms * 1e6);
-}
-
-function now(): number {
-  return msToNs(correctedTimeOrigin + performance.now());
-}
-
-export function traceEvent<T>(
-  name: string,
-  event: () => T,
-  params?: TraceEventParams,
-): T {
-  const scope = traceEventBegin(name, params);
-  try {
-    const result = event();
-    return result;
-  } finally {
-    traceEventEnd(scope);
-  }
-}
-
-export function traceEventBegin(
-  eventName: string,
-  params?: TraceEventParams,
-): TraceEventScope {
-  return {
-    eventName,
-    startNs: now(),
-    params: params,
-  };
-}
-
-export function traceEventEnd(traceEvent: TraceEventScope) {
-  if (!isMetatracingEnabled()) return;
-
-  traceEvents.push({
-    eventName: traceEvent.eventName,
-    startNs: traceEvent.startNs,
-    durNs: now() - traceEvent.startNs,
-    track: traceEvent.params?.track ?? MetatraceTrackId.kMainThread,
-    args: traceEvent.params?.args,
-  });
-  while (traceEvents.length > METATRACING_BUFFER_SIZE) {
-    traceEvents.shift();
-  }
-}
-
-// Flatten arbitrary values so they can be used as args in traceEvent() et al.
-export function flattenArgs(
-  input: unknown,
-  parentKey = '',
-): {[key: string]: string} {
-  if (typeof input !== 'object' || input === null) {
-    return {[parentKey]: String(input)};
-  }
-
-  if (Array.isArray(input)) {
-    const result: Record<string, string> = {};
-
-    (input as Array<unknown>).forEach((item, index) => {
-      const arrayKey = `${parentKey}[${index}]`;
-      Object.assign(result, flattenArgs(item, arrayKey));
-    });
-
-    return result;
-  }
-
-  const result: Record<string, string> = {};
-
-  Object.entries(input as Record<string, unknown>).forEach(([key, value]) => {
-    const newKey = parentKey ? `${parentKey}.${key}` : key;
-    Object.assign(result, flattenArgs(value, newKey));
-  });
-
-  return result;
-}
diff --git a/ui/src/common/metric_data.ts b/ui/src/common/metric_data.ts
deleted file mode 100644
index a9db6c8..0000000
--- a/ui/src/common/metric_data.ts
+++ /dev/null
@@ -1,20 +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.
-
-export interface MetricResult {
-  name: string;
-  // Either result or error should be set.
-  resultString?: string;
-  error?: string;
-}
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
deleted file mode 100644
index c825b90..0000000
--- a/ui/src/common/plugins.ts
+++ /dev/null
@@ -1,621 +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 {v4 as uuidv4} from 'uuid';
-
-import {Registry} from '../base/registry';
-import {TimeSpan, time} from '../base/time';
-import {globals} from '../frontend/globals';
-import {
-  Command,
-  LegacyDetailsPanel,
-  MetricVisualisation,
-  Migrate,
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-  PrimaryTrackSortKey,
-  Store,
-  TabDescriptor,
-  TrackDescriptor,
-  TrackPredicate,
-  GroupPredicate,
-  TrackRef,
-  SidebarMenuItem,
-} from '../public';
-import {EngineBase, Engine} from '../trace_processor/engine';
-import {Actions} from './actions';
-import {SCROLLING_TRACK_GROUP} from './state';
-import {addQueryResultsTab} from '../frontend/query_result_tab';
-import {Flag, featureFlags} from '../core/feature_flags';
-import {assertExists} from '../base/logging';
-import {raf} from '../core/raf_scheduler';
-import {defaultPlugins} from '../core/default_plugins';
-import {PromptOption} from '../frontend/omnibox_manager';
-import {horizontalScrollToTs} from '../frontend/scroll_helper';
-import {DisposableStack} from '../base/disposable_stack';
-import {TraceContext} from '../frontend/trace_context';
-
-// Every plugin gets its own PluginContext. This is how we keep track
-// what each plugin is doing and how we can blame issues on particular
-// plugins.
-// The PluginContext exists for the whole duration a plugin is active.
-export class PluginContextImpl implements PluginContext, Disposable {
-  private trash = new DisposableStack();
-  private alive = true;
-
-  registerCommand(cmd: Command): void {
-    // Silently ignore if context is dead.
-    if (!this.alive) return;
-
-    const disposable = globals.commandManager.registerCommand(cmd);
-    this.trash.use(disposable);
-  }
-
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  runCommand(id: string, ...args: any[]): any {
-    return globals.commandManager.runCommand(id, ...args);
-  }
-
-  constructor(readonly pluginId: string) {}
-
-  [Symbol.dispose]() {
-    this.trash.dispose();
-    this.alive = false;
-  }
-
-  addSidebarMenuItem(menuItem: SidebarMenuItem): void {
-    this.trash.use(globals.sidebarMenuItems.register(menuItem));
-  }
-}
-
-// This PluginContextTrace implementation provides the plugin access to trace
-// related resources, such as the engine and the store.
-// The PluginContextTrace exists for the whole duration a plugin is active AND a
-// trace is loaded.
-class PluginContextTraceImpl implements PluginContextTrace, Disposable {
-  private trash = new DisposableStack();
-  private alive = true;
-  readonly engine: Engine;
-
-  constructor(
-    private ctx: PluginContext,
-    engine: EngineBase,
-  ) {
-    const engineProxy = engine.getProxy(ctx.pluginId);
-    this.trash.use(engineProxy);
-    this.engine = engineProxy;
-  }
-
-  registerCommand(cmd: Command): void {
-    // Silently ignore if context is dead.
-    if (!this.alive) return;
-
-    const dispose = globals.commandManager.registerCommand(cmd);
-    this.trash.use(dispose);
-  }
-
-  addSidebarMenuItem(menuItem: SidebarMenuItem): void {
-    // Silently ignore if context is dead.
-    if (!this.alive) return;
-
-    this.trash.use(globals.sidebarMenuItems.register(menuItem));
-  }
-
-  registerTrack(trackDesc: TrackDescriptor): void {
-    // Silently ignore if context is dead.
-    if (!this.alive) return;
-
-    const dispose = globals.trackManager.registerTrack({
-      ...trackDesc,
-      pluginId: this.pluginId,
-    });
-    this.trash.use(dispose);
-  }
-
-  addDefaultTrack(track: TrackRef): void {
-    // Silently ignore if context is dead.
-    if (!this.alive) return;
-
-    const dispose = globals.trackManager.addPotentialTrack(track);
-    this.trash.use(dispose);
-  }
-
-  registerStaticTrack(track: TrackDescriptor & TrackRef): void {
-    this.registerTrack(track);
-    this.addDefaultTrack(track);
-  }
-
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  runCommand(id: string, ...args: any[]): any {
-    return this.ctx.runCommand(id, ...args);
-  }
-
-  registerTab(desc: TabDescriptor): void {
-    if (!this.alive) return;
-
-    const unregister = globals.tabManager.registerTab(desc);
-    this.trash.use(unregister);
-  }
-
-  addDefaultTab(uri: string): void {
-    const remove = globals.tabManager.addDefaultTab(uri);
-    this.trash.use(remove);
-  }
-
-  registerDetailsPanel(detailsPanel: LegacyDetailsPanel): void {
-    if (!this.alive) return;
-
-    const tabMan = globals.tabManager;
-    const unregister = tabMan.registerLegacyDetailsPanel(detailsPanel);
-    this.trash.use(unregister);
-  }
-
-  readonly tabs = {
-    openQuery: (query: string, title: string) => {
-      addQueryResultsTab({query, title});
-    },
-
-    showTab(uri: string): void {
-      globals.dispatch(Actions.showTab({uri}));
-    },
-
-    hideTab(uri: string): void {
-      globals.dispatch(Actions.hideTab({uri}));
-    },
-  };
-
-  get pluginId(): string {
-    return this.ctx.pluginId;
-  }
-
-  readonly timeline = {
-    // Add a new track to the timeline, returning its key.
-    addTrack(uri: string, displayName: string): string {
-      const trackKey = uuidv4();
-      globals.dispatch(
-        Actions.addTrack({
-          key: trackKey,
-          uri,
-          name: displayName,
-          trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-          trackGroup: SCROLLING_TRACK_GROUP,
-        }),
-      );
-      return trackKey;
-    },
-
-    removeTrack(key: string): void {
-      globals.dispatch(Actions.removeTracks({trackKeys: [key]}));
-    },
-
-    pinTrack(key: string) {
-      if (!isPinned(key)) {
-        globals.dispatch(Actions.toggleTrackPinned({trackKey: key}));
-      }
-    },
-
-    unpinTrack(key: string) {
-      if (isPinned(key)) {
-        globals.dispatch(Actions.toggleTrackPinned({trackKey: key}));
-      }
-    },
-
-    pinTracksByPredicate(predicate: TrackPredicate) {
-      const tracks = Object.values(globals.state.tracks);
-      for (const track of tracks) {
-        const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackDesc && predicate(trackDesc) && !isPinned(track.key)) {
-          globals.dispatch(
-            Actions.toggleTrackPinned({
-              trackKey: track.key,
-            }),
-          );
-        }
-      }
-    },
-
-    unpinTracksByPredicate(predicate: TrackPredicate) {
-      const tracks = Object.values(globals.state.tracks);
-      for (const track of tracks) {
-        const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackDesc && predicate(trackDesc) && isPinned(track.key)) {
-          globals.dispatch(
-            Actions.toggleTrackPinned({
-              trackKey: track.key,
-            }),
-          );
-        }
-      }
-    },
-
-    removeTracksByPredicate(predicate: TrackPredicate) {
-      const trackKeysToRemove = Object.values(globals.state.tracks)
-        .filter((track) => {
-          const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
-          return trackDesc && predicate(trackDesc);
-        })
-        .map((trackState) => trackState.key);
-
-      globals.dispatch(Actions.removeTracks({trackKeys: trackKeysToRemove}));
-    },
-
-    expandGroupsByPredicate(predicate: GroupPredicate) {
-      const groups = globals.state.trackGroups;
-      const groupsToExpand = Object.values(groups)
-        .filter((group) => group.collapsed)
-        .filter((group) => {
-          const ref = {
-            displayName: group.name,
-            collapsed: group.collapsed,
-          };
-          return predicate(ref);
-        })
-        .map((group) => group.key);
-
-      for (const groupKey of groupsToExpand) {
-        globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
-      }
-    },
-
-    collapseGroupsByPredicate(predicate: GroupPredicate) {
-      const groups = globals.state.trackGroups;
-      const groupsToCollapse = Object.values(groups)
-        .filter((group) => !group.collapsed)
-        .filter((group) => {
-          const ref = {
-            displayName: group.name,
-            collapsed: group.collapsed,
-          };
-          return predicate(ref);
-        })
-        .map((group) => group.key);
-
-      for (const groupKey of groupsToCollapse) {
-        globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
-      }
-    },
-
-    get tracks(): TrackRef[] {
-      const tracks = Object.values(globals.state.tracks);
-      const pinnedTracks = globals.state.pinnedTracks;
-      const groups = globals.state.trackGroups;
-      return tracks.map((trackState) => {
-        const group = trackState.trackGroup
-          ? groups[trackState.trackGroup]
-          : undefined;
-        return {
-          title: trackState.name,
-          uri: trackState.uri,
-          key: trackState.key,
-          groupName: group?.name,
-          isPinned: pinnedTracks.includes(trackState.key),
-        };
-      });
-    },
-
-    panToTimestamp(ts: time): void {
-      horizontalScrollToTs(ts);
-    },
-
-    setViewportTime(start: time, end: time): void {
-      globals.timeline.updateVisibleTime(new TimeSpan(start, end));
-    },
-
-    get viewport(): TimeSpan {
-      return globals.timeline.visibleWindow.toTimeSpan();
-    },
-  };
-
-  [Symbol.dispose]() {
-    this.trash.dispose();
-    this.alive = false;
-  }
-
-  mountStore<T>(migrate: Migrate<T>): Store<T> {
-    return globals.store.createSubStore(['plugins', this.pluginId], migrate);
-  }
-
-  get trace(): TraceContext {
-    return globals.traceContext;
-  }
-
-  get openerPluginArgs(): {[key: string]: unknown} | undefined {
-    if (globals.state.engine?.source.type !== 'ARRAY_BUFFER') {
-      return undefined;
-    }
-    const pluginArgs = globals.state.engine?.source.pluginArgs;
-    return (pluginArgs ?? {})[this.pluginId];
-  }
-
-  async prompt(
-    text: string,
-    options?: PromptOption[] | undefined,
-  ): Promise<string> {
-    return globals.omnibox.prompt(text, options);
-  }
-}
-
-function isPinned(trackId: string): boolean {
-  return globals.state.pinnedTracks.includes(trackId);
-}
-
-// 'Static' registry of all known plugins.
-export class PluginRegistry extends Registry<PluginDescriptor> {
-  constructor() {
-    super((info) => info.pluginId);
-  }
-}
-
-export interface PluginDetails {
-  plugin: Plugin;
-  context: PluginContext & Disposable;
-  traceContext?: PluginContextTraceImpl;
-  previousOnTraceLoadTimeMillis?: number;
-}
-
-function makePlugin(info: PluginDescriptor): Plugin {
-  const {plugin} = info;
-
-  // Class refs are functions, concrete plugins are not
-  if (typeof plugin === 'function') {
-    const PluginClass = plugin;
-    return new PluginClass();
-  } else {
-    return plugin;
-  }
-}
-
-export class PluginManager {
-  private registry: PluginRegistry;
-  private _plugins: Map<string, PluginDetails>;
-  private engine?: EngineBase;
-  private flags = new Map<string, Flag>();
-
-  constructor(registry: PluginRegistry) {
-    this.registry = registry;
-    this._plugins = new Map();
-  }
-
-  get plugins(): Map<string, PluginDetails> {
-    return this._plugins;
-  }
-
-  // Must only be called once on startup
-  async initialize(): Promise<void> {
-    // Shuffle the order of plugins to weed out any implicit inter-plugin
-    // dependencies.
-    const pluginsShuffled = Array.from(pluginRegistry.values())
-      .map(({pluginId}) => ({pluginId, sort: Math.random()}))
-      .sort((a, b) => a.sort - b.sort);
-
-    for (const {pluginId} of pluginsShuffled) {
-      const flagId = `plugin_${pluginId}`;
-      const name = `Plugin: ${pluginId}`;
-      const flag = featureFlags.register({
-        id: flagId,
-        name,
-        description: `Overrides '${pluginId}' plugin.`,
-        defaultValue: defaultPlugins.includes(pluginId),
-      });
-      this.flags.set(pluginId, flag);
-      if (flag.get()) {
-        await this.activatePlugin(pluginId);
-      }
-    }
-  }
-
-  /**
-   * Enable plugin flag - i.e. configure a plugin to start on boot.
-   * @param id The ID of the plugin.
-   * @param now Optional: If true, also activate the plugin now.
-   */
-  async enablePlugin(id: string, now?: boolean): Promise<void> {
-    const flag = this.flags.get(id);
-    if (flag) {
-      flag.set(true);
-    }
-    now && (await this.activatePlugin(id));
-  }
-
-  /**
-   * Disable plugin flag - i.e. configure a plugin not to start on boot.
-   * @param id The ID of the plugin.
-   * @param now Optional: If true, also deactivate the plugin now.
-   */
-  async disablePlugin(id: string, now?: boolean): Promise<void> {
-    const flag = this.flags.get(id);
-    if (flag) {
-      flag.set(false);
-    }
-    now && (await this.deactivatePlugin(id));
-  }
-
-  /**
-   * Start a plugin just for this session. This setting is not persisted.
-   * @param id The ID of the plugin to start.
-   */
-  async activatePlugin(id: string): Promise<void> {
-    if (this.isActive(id)) {
-      return;
-    }
-
-    const pluginInfo = this.registry.get(id);
-    const plugin = makePlugin(pluginInfo);
-
-    const context = new PluginContextImpl(id);
-
-    plugin.onActivate?.(context);
-
-    const pluginDetails: PluginDetails = {
-      plugin,
-      context,
-    };
-
-    // If a trace is already loaded when plugin is activated, make sure to
-    // call onTraceLoad().
-    if (this.engine) {
-      await doPluginTraceLoad(pluginDetails, this.engine);
-      await doPluginTraceReady(pluginDetails);
-    }
-
-    this._plugins.set(id, pluginDetails);
-
-    raf.scheduleFullRedraw();
-  }
-
-  /**
-   * Stop a plugin just for this session. This setting is not persisted.
-   * @param id The ID of the plugin to stop.
-   */
-  async deactivatePlugin(id: string): Promise<void> {
-    const pluginDetails = this.getPluginContext(id);
-    if (pluginDetails === undefined) {
-      return;
-    }
-    const {context, plugin} = pluginDetails;
-
-    await doPluginTraceUnload(pluginDetails);
-
-    plugin.onDeactivate && plugin.onDeactivate(context);
-    context[Symbol.dispose]();
-
-    this._plugins.delete(id);
-
-    raf.scheduleFullRedraw();
-  }
-
-  /**
-   * Restore all plugins enable/disabled flags to their default values.
-   * @param now Optional: Also activates/deactivates plugins to match flag
-   * settings.
-   */
-  async restoreDefaults(now?: boolean): Promise<void> {
-    for (const plugin of pluginRegistry.values()) {
-      const pluginId = plugin.pluginId;
-      const flag = assertExists(this.flags.get(pluginId));
-      flag.reset();
-      if (now) {
-        if (flag.get()) {
-          await this.activatePlugin(plugin.pluginId);
-        } else {
-          await this.deactivatePlugin(plugin.pluginId);
-        }
-      }
-    }
-  }
-
-  isActive(pluginId: string): boolean {
-    return this.getPluginContext(pluginId) !== undefined;
-  }
-
-  isEnabled(pluginId: string): boolean {
-    return Boolean(this.flags.get(pluginId)?.get());
-  }
-
-  getPluginContext(pluginId: string): PluginDetails | undefined {
-    return this._plugins.get(pluginId);
-  }
-
-  async onTraceLoad(
-    engine: EngineBase,
-    beforeEach?: (id: string) => void,
-  ): Promise<void> {
-    this.engine = engine;
-
-    // Shuffle the order of plugins to weed out any implicit inter-plugin
-    // dependencies.
-    const pluginsShuffled = Array.from(this._plugins.entries())
-      .map(([id, plugin]) => ({id, plugin, sort: Math.random()}))
-      .sort((a, b) => a.sort - b.sort);
-
-    // Awaiting all plugins in parallel will skew timing data as later plugins
-    // will spend most of their time waiting for earlier plugins to load.
-    // Running in parallel will have very little performance benefit assuming
-    // most plugins use the same engine, which can only process one query at a
-    // time.
-    for (const {id, plugin} of pluginsShuffled) {
-      beforeEach?.(id);
-      await doPluginTraceLoad(plugin, engine);
-    }
-  }
-
-  async onTraceReady(): Promise<void> {
-    const pluginsShuffled = Array.from(this._plugins.values())
-      .map((plugin) => ({plugin, sort: Math.random()}))
-      .sort((a, b) => a.sort - b.sort);
-
-    for (const {plugin} of pluginsShuffled) {
-      await doPluginTraceReady(plugin);
-    }
-  }
-
-  onTraceClose() {
-    for (const pluginDetails of this._plugins.values()) {
-      doPluginTraceUnload(pluginDetails);
-    }
-    this.engine = undefined;
-  }
-
-  metricVisualisations(): MetricVisualisation[] {
-    return Array.from(this._plugins.values()).flatMap((ctx) => {
-      const tracePlugin = ctx.plugin;
-      if (tracePlugin.metricVisualisations) {
-        return tracePlugin.metricVisualisations(ctx.context);
-      } else {
-        return [];
-      }
-    });
-  }
-}
-
-async function doPluginTraceReady(pluginDetails: PluginDetails): Promise<void> {
-  const {plugin, traceContext} = pluginDetails;
-  await Promise.resolve(plugin.onTraceReady?.(assertExists(traceContext)));
-  raf.scheduleFullRedraw();
-}
-
-async function doPluginTraceLoad(
-  pluginDetails: PluginDetails,
-  engine: EngineBase,
-): Promise<void> {
-  const {plugin, context} = pluginDetails;
-
-  const traceCtx = new PluginContextTraceImpl(context, engine);
-  pluginDetails.traceContext = traceCtx;
-
-  const startTime = performance.now();
-  await Promise.resolve(plugin.onTraceLoad?.(traceCtx));
-  const loadTime = performance.now() - startTime;
-  pluginDetails.previousOnTraceLoadTimeMillis = loadTime;
-
-  raf.scheduleFullRedraw();
-}
-
-async function doPluginTraceUnload(
-  pluginDetails: PluginDetails,
-): Promise<void> {
-  const {traceContext, plugin} = pluginDetails;
-
-  if (traceContext) {
-    plugin.onTraceUnload && (await plugin.onTraceUnload(traceContext));
-    traceContext[Symbol.dispose]();
-    pluginDetails.traceContext = undefined;
-  }
-}
-
-// TODO(hjd): Sort out the story for global singletons like these:
-export const pluginRegistry = new PluginRegistry();
-export const pluginManager = new PluginManager(pluginRegistry);
diff --git a/ui/src/common/plugins_unittest.ts b/ui/src/common/plugins_unittest.ts
deleted file mode 100644
index 7efccda..0000000
--- a/ui/src/common/plugins_unittest.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {globals} from '../frontend/globals';
-import {Plugin} from '../public';
-import {EngineBase} from '../trace_processor/engine';
-
-import {createEmptyState} from './empty_state';
-import {PluginManager, PluginRegistry} from './plugins';
-
-class FakeEngine extends EngineBase {
-  id: string = 'TestEngine';
-
-  rpcSendRequestBytes(_data: Uint8Array) {}
-}
-
-function makeMockPlugin(): Plugin {
-  return {
-    onActivate: jest.fn(),
-    onDeactivate: jest.fn(),
-    onTraceLoad: jest.fn(),
-    onTraceUnload: jest.fn(),
-  };
-}
-
-const engine = new FakeEngine();
-globals.initStore(createEmptyState());
-
-let mockPlugin: Plugin;
-let manager: PluginManager;
-
-describe('PluginManger', () => {
-  beforeEach(() => {
-    mockPlugin = makeMockPlugin();
-    const registry = new PluginRegistry();
-    registry.register({
-      pluginId: 'foo',
-      plugin: mockPlugin,
-    });
-    manager = new PluginManager(registry);
-  });
-
-  it('can activate plugin', async () => {
-    await manager.activatePlugin('foo');
-
-    expect(manager.isActive('foo')).toBe(true);
-    expect(mockPlugin.onActivate).toHaveBeenCalledTimes(1);
-  });
-
-  it('can deactivate plugin', async () => {
-    await manager.activatePlugin('foo');
-    await manager.deactivatePlugin('foo');
-
-    expect(manager.isActive('foo')).toBe(false);
-    expect(mockPlugin.onDeactivate).toHaveBeenCalledTimes(1);
-  });
-
-  it('invokes onTraceLoad when trace is loaded', async () => {
-    await manager.activatePlugin('foo');
-    await manager.onTraceLoad(engine);
-
-    expect(mockPlugin.onTraceLoad).toHaveBeenCalledTimes(1);
-  });
-
-  it('invokes onTraceLoad when plugin activated while trace loaded', async () => {
-    await manager.onTraceLoad(engine);
-    await manager.activatePlugin('foo');
-
-    expect(mockPlugin.onTraceLoad).toHaveBeenCalledTimes(1);
-  });
-
-  it('invokes onTraceUnload when plugin deactivated while trace loaded', async () => {
-    await manager.activatePlugin('foo');
-    await manager.onTraceLoad(engine);
-    await manager.deactivatePlugin('foo');
-
-    expect(mockPlugin.onTraceUnload).toHaveBeenCalledTimes(1);
-  });
-});
diff --git a/ui/src/common/queries.ts b/ui/src/common/queries.ts
deleted file mode 100644
index 227b9cd..0000000
--- a/ui/src/common/queries.ts
+++ /dev/null
@@ -1,98 +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 {Engine} from '../trace_processor/engine';
-import {Row} from '../trace_processor/query_result';
-
-const MAX_DISPLAY_ROWS = 10000;
-
-export interface QueryResponse {
-  query: string;
-  error?: string;
-  totalRowCount: number;
-  durationMs: number;
-  columns: string[];
-  rows: Row[];
-  statementCount: number;
-  statementWithOutputCount: number;
-  lastStatementSql: string;
-}
-
-export interface QueryRunParams {
-  // If true, replaces nulls with "NULL" string. Default is true.
-  convertNullsToString?: boolean;
-}
-
-export async function runQuery(
-  sqlQuery: string,
-  engine: Engine,
-  params?: QueryRunParams,
-): Promise<QueryResponse> {
-  const startMs = performance.now();
-
-  // TODO(primiano): once the controller thread is gone we should pass down
-  // the result objects directly to the frontend, iterate over the result
-  // and deal with pagination there. For now we keep the old behavior and
-  // truncate to 10k rows.
-
-  const maybeResult = await engine.tryQuery(sqlQuery);
-
-  if (maybeResult.success) {
-    const queryRes = maybeResult.result;
-    const convertNullsToString = params?.convertNullsToString ?? true;
-
-    const durationMs = performance.now() - startMs;
-    const rows: Row[] = [];
-    const columns = queryRes.columns();
-    let numRows = 0;
-    for (const iter = queryRes.iter({}); iter.valid(); iter.next()) {
-      const row: Row = {};
-      for (const colName of columns) {
-        const value = iter.get(colName);
-        row[colName] = value === null && convertNullsToString ? 'NULL' : value;
-      }
-      rows.push(row);
-      if (++numRows >= MAX_DISPLAY_ROWS) break;
-    }
-
-    const result: QueryResponse = {
-      query: sqlQuery,
-      durationMs,
-      error: queryRes.error(),
-      totalRowCount: queryRes.numRows(),
-      columns,
-      rows,
-      statementCount: queryRes.statementCount(),
-      statementWithOutputCount: queryRes.statementWithOutputCount(),
-      lastStatementSql: queryRes.lastStatementSql(),
-    };
-    return result;
-  } else {
-    // In the case of a query error we don't want the exception to bubble up
-    // as a crash. The |queryRes| object will be populated anyways.
-    // queryRes.error() is used to tell if the query errored or not. If it
-    // errored, the frontend will show a graceful message instead.
-    return {
-      query: sqlQuery,
-      durationMs: performance.now() - startMs,
-      error: maybeResult.error.message,
-      totalRowCount: 0,
-      columns: [],
-      rows: [],
-      statementCount: 0,
-      statementWithOutputCount: 0,
-      lastStatementSql: '',
-    };
-  }
-}
diff --git a/ui/src/common/recordingV2/adb_connection_impl.ts b/ui/src/common/recordingV2/adb_connection_impl.ts
deleted file mode 100644
index 70209fb..0000000
--- a/ui/src/common/recordingV2/adb_connection_impl.ts
+++ /dev/null
@@ -1,84 +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/common/recordingV2/adb_connection_over_websocket.ts b/ui/src/common/recordingV2/adb_connection_over_websocket.ts
deleted file mode 100644
index 160b257..0000000
--- a/ui/src/common/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/common/recordingV2/adb_connection_over_webusb.ts b/ui/src/common/recordingV2/adb_connection_over_webusb.ts
deleted file mode 100644
index 713d8b3..0000000
--- a/ui/src/common/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 '../../controller/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/common/recordingV2/adb_file_handler.ts b/ui/src/common/recordingV2/adb_file_handler.ts
deleted file mode 100644
index 1016fe7..0000000
--- a/ui/src/common/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/common/recordingV2/auth/adb_auth.ts b/ui/src/common/recordingV2/auth/adb_auth.ts
deleted file mode 100644
index 02dbcec..0000000
--- a/ui/src/common/recordingV2/auth/adb_auth.ts
+++ /dev/null
@@ -1,200 +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 {BigInteger, RSAKey} from 'jsbn-rsa';
-
-import {assertExists, assertTrue} from '../../../base/logging';
-import {
-  base64Decode,
-  base64Encode,
-  hexEncode,
-} from '../../../base/string_utils';
-import {RecordingError} from '../recording_error_handling';
-
-const WORD_SIZE = 4;
-const MODULUS_SIZE_BITS = 2048;
-const MODULUS_SIZE = MODULUS_SIZE_BITS / 8;
-const MODULUS_SIZE_WORDS = MODULUS_SIZE / WORD_SIZE;
-const PUBKEY_ENCODED_SIZE = 3 * WORD_SIZE + 2 * MODULUS_SIZE;
-const ADB_WEB_CRYPTO_ALGORITHM = {
-  name: 'RSASSA-PKCS1-v1_5',
-  hash: {name: 'SHA-1'},
-  publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537
-  modulusLength: MODULUS_SIZE_BITS,
-};
-
-const ADB_WEB_CRYPTO_EXPORTABLE = true;
-const ADB_WEB_CRYPTO_OPERATIONS: KeyUsage[] = ['sign'];
-
-const SIGNING_ASN1_PREFIX = [
-  0x00, 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05,
-  0x00, 0x04, 0x14,
-];
-
-const R32 = BigInteger.ONE.shiftLeft(32); // 1 << 32
-
-interface ValidJsonWebKey {
-  n: string;
-  e: string;
-  d: string;
-  p: string;
-  q: string;
-  dp: string;
-  dq: string;
-  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
-  // - create a public key and send it to the device
-  // - serialize the JsonWebKey and send it to the device (or retrieve it
-  // from the device and deserialize)
-  jwkPrivate: ValidJsonWebKey;
-
-  private constructor(jwkPrivate: ValidJsonWebKey) {
-    this.jwkPrivate = jwkPrivate;
-  }
-
-  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,
-      ADB_WEB_CRYPTO_EXPORTABLE,
-      ADB_WEB_CRYPTO_OPERATIONS,
-    );
-    const jwkPrivate = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
-    if (!isValidJsonWebKey(jwkPrivate)) {
-      throw new RecordingError('Could not generate a valid 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
-  // had the SHA-1 message digest applied.
-  //
-  // However, the adb token we receive from the device is made up of 20 randomly
-  // generated bytes that are treated like a SHA-1. Therefore, we need to update
-  // the message format.
-  sign(token: Uint8Array): Uint8Array {
-    const rsaKey = new RSAKey();
-    rsaKey.setPrivateEx(
-      hexEncode(base64Decode(this.jwkPrivate.n)),
-      hexEncode(base64Decode(this.jwkPrivate.e)),
-      hexEncode(base64Decode(this.jwkPrivate.d)),
-      hexEncode(base64Decode(this.jwkPrivate.p)),
-      hexEncode(base64Decode(this.jwkPrivate.q)),
-      hexEncode(base64Decode(this.jwkPrivate.dp)),
-      hexEncode(base64Decode(this.jwkPrivate.dq)),
-      hexEncode(base64Decode(this.jwkPrivate.qi)),
-    );
-    assertTrue(rsaKey.n.bitLength() === MODULUS_SIZE_BITS);
-
-    // Message Layout (size equals that of the key modulus):
-    // 00 01 FF FF FF FF ... FF [ASN.1 PREFIX] [TOKEN]
-    const message = new Uint8Array(MODULUS_SIZE);
-
-    // Initially fill the buffer with the padding
-    message.fill(0xff);
-
-    // add prefix
-    message[0] = 0x00;
-    message[1] = 0x01;
-
-    // add the ASN.1 prefix
-    message.set(
-      SIGNING_ASN1_PREFIX,
-      message.length - SIGNING_ASN1_PREFIX.length - token.length,
-    );
-
-    // then the actual token at the end
-    message.set(token, message.length - token.length);
-
-    const messageInteger = new BigInteger(Array.from(message));
-    const signature = rsaKey.doPrivate(messageInteger);
-    return new Uint8Array(bigIntToFixedByteArray(signature, MODULUS_SIZE));
-  }
-
-  // Construct public key to match the adb format:
-  // go/codesearch/rvc-arc/system/core/libcrypto_utils/android_pubkey.c;l=38-53
-  getPublicKey(): string {
-    const rsaKey = new RSAKey();
-    rsaKey.setPublic(
-      hexEncode(base64Decode(assertExists(this.jwkPrivate.n))),
-      hexEncode(base64Decode(assertExists(this.jwkPrivate.e))),
-    );
-
-    const n0inv = R32.subtract(rsaKey.n.modInverse(R32)).intValue();
-    const r = BigInteger.ONE.shiftLeft(1).pow(MODULUS_SIZE_BITS);
-    const rr = r.multiply(r).mod(rsaKey.n);
-
-    const buffer = new ArrayBuffer(PUBKEY_ENCODED_SIZE);
-    const dv = new DataView(buffer);
-    dv.setUint32(0, MODULUS_SIZE_WORDS, true);
-    dv.setUint32(WORD_SIZE, n0inv, true);
-
-    const dvU8 = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
-    dvU8.set(
-      bigIntToFixedByteArray(rsaKey.n, MODULUS_SIZE).reverse(),
-      2 * WORD_SIZE,
-    );
-    dvU8.set(
-      bigIntToFixedByteArray(rr, MODULUS_SIZE).reverse(),
-      2 * WORD_SIZE + MODULUS_SIZE,
-    );
-
-    dv.setUint32(2 * WORD_SIZE + 2 * MODULUS_SIZE, rsaKey.e, true);
-    return base64Encode(dvU8) + ' ui.perfetto.dev';
-  }
-
-  serializeKey(): string {
-    return JSON.stringify(this.jwkPrivate);
-  }
-}
diff --git a/ui/src/common/recordingV2/auth/adb_key_manager.ts b/ui/src/common/recordingV2/auth/adb_key_manager.ts
deleted file mode 100644
index 9f6e4e7..0000000
--- a/ui/src/common/recordingV2/auth/adb_key_manager.ts
+++ /dev/null
@@ -1,102 +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 {globals} from '../../../frontend/globals';
-
-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: `${globals.root}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/common/recordingV2/chrome_traced_tracing_session.ts b/ui/src/common/recordingV2/chrome_traced_tracing_session.ts
deleted file mode 100644
index 11db36b..0000000
--- a/ui/src/common/recordingV2/chrome_traced_tracing_session.ts
+++ /dev/null
@@ -1,237 +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 '../../controller/chrome_proxy_record_controller';
-import {
-  isDisableTracingResponse,
-  isEnableTracingResponse,
-  isFreeBuffersResponse,
-  isGetTraceStatsResponse,
-  isReadBuffersResponse,
-} from '../../controller/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/common/recordingV2/host_os_byte_stream.ts b/ui/src/common/recordingV2/host_os_byte_stream.ts
deleted file mode 100644
index 336ffcb..0000000
--- a/ui/src/common/recordingV2/host_os_byte_stream.ts
+++ /dev/null
@@ -1,84 +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/common/recordingV2/recording_config_utils.ts b/ui/src/common/recordingV2/recording_config_utils.ts
deleted file mode 100644
index 6f42f01..0000000
--- a/ui/src/common/recordingV2/recording_config_utils.ts
+++ /dev/null
@@ -1,864 +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 {isString} from '../../base/object_utils';
-import {base64Encode} from '../../base/string_utils';
-import {exists} from '../../base/utils';
-import {RecordConfig} from '../../controller/record_config_types';
-import {
-  AndroidLogConfig,
-  AndroidLogId,
-  AndroidPowerConfig,
-  BufferConfig,
-  ChromeConfig,
-  DataSourceConfig,
-  EtwConfig,
-  FtraceConfig,
-  HeapprofdConfig,
-  JavaContinuousDumpConfig,
-  JavaHprofConfig,
-  MeminfoCounters,
-  NativeContinuousDumpConfig,
-  NetworkPacketTraceConfig,
-  PerfEventConfig,
-  PerfEvents,
-  ProcessStatsConfig,
-  SysStatsConfig,
-  TraceConfig,
-  TrackEventConfig,
-  VmstatCounters,
-} from '../../protos';
-
-import {TargetInfo} from './recording_interfaces_v2';
-
-import PerfClock = PerfEvents.PerfClock;
-import Timebase = PerfEvents.Timebase;
-import CallstackSampling = PerfEventConfig.CallstackSampling;
-import Scope = PerfEventConfig.Scope;
-
-export interface ConfigProtoEncoded {
-  configProtoText?: string;
-  configProtoBase64?: string;
-  hasDataSources: boolean;
-}
-
-export class RecordingConfigUtils {
-  private lastConfig?: RecordConfig;
-  private lastTargetInfo?: TargetInfo;
-  private configProtoText?: string;
-  private configProtoBase64?: string;
-  private hasDataSources: boolean = false;
-
-  fetchLatestRecordCommand(
-    recordConfig: RecordConfig,
-    targetInfo: TargetInfo,
-  ): ConfigProtoEncoded {
-    if (
-      recordConfig === this.lastConfig &&
-      targetInfo === this.lastTargetInfo
-    ) {
-      return {
-        configProtoText: this.configProtoText,
-        configProtoBase64: this.configProtoBase64,
-        hasDataSources: this.hasDataSources,
-      };
-    }
-    this.lastConfig = recordConfig;
-    this.lastTargetInfo = targetInfo;
-
-    const traceConfig = genTraceConfig(recordConfig, targetInfo);
-    const configProto = TraceConfig.encode(traceConfig).finish();
-    this.configProtoText = toPbtxt(configProto);
-    this.configProtoBase64 = base64Encode(configProto);
-    this.hasDataSources = traceConfig.dataSources.length > 0;
-    return {
-      configProtoText: this.configProtoText,
-      configProtoBase64: this.configProtoBase64,
-      hasDataSources: this.hasDataSources,
-    };
-  }
-}
-
-function enableSchedBlockedReason(androidApiLevel?: number): boolean {
-  return androidApiLevel !== undefined && androidApiLevel >= 31;
-}
-
-function enableCompactSched(androidApiLevel?: number): boolean {
-  return androidApiLevel !== undefined && androidApiLevel >= 31;
-}
-
-export function genTraceConfig(
-  uiCfg: RecordConfig,
-  targetInfo: TargetInfo,
-): TraceConfig {
-  const isAndroid = targetInfo.targetType === 'ANDROID';
-  const isLinux = targetInfo.targetType === 'LINUX';
-  const androidApiLevel = isAndroid ? targetInfo.androidApiLevel : undefined;
-  const protoCfg = new TraceConfig();
-  protoCfg.durationMs = uiCfg.durationMs;
-
-  // Auxiliary buffer for slow-rate events.
-  // Set to 1/8th of the main buffer size, with reasonable limits.
-  let slowBufSizeKb = uiCfg.bufferSizeMb * (1024 / 8);
-  slowBufSizeKb = Math.min(slowBufSizeKb, 2 * 1024);
-  slowBufSizeKb = Math.max(slowBufSizeKb, 256);
-
-  // Main buffer for ftrace and other high-freq events.
-  const fastBufSizeKb = uiCfg.bufferSizeMb * 1024 - slowBufSizeKb;
-
-  protoCfg.buffers.push(new BufferConfig());
-  protoCfg.buffers.push(new BufferConfig());
-  protoCfg.buffers[0].sizeKb = fastBufSizeKb;
-  protoCfg.buffers[1].sizeKb = slowBufSizeKb;
-
-  if (uiCfg.mode === 'STOP_WHEN_FULL') {
-    protoCfg.buffers[0].fillPolicy = BufferConfig.FillPolicy.DISCARD;
-    protoCfg.buffers[1].fillPolicy = BufferConfig.FillPolicy.DISCARD;
-  } else {
-    protoCfg.buffers[0].fillPolicy = BufferConfig.FillPolicy.RING_BUFFER;
-    protoCfg.buffers[1].fillPolicy = BufferConfig.FillPolicy.RING_BUFFER;
-    protoCfg.flushPeriodMs = 30000;
-    if (uiCfg.mode === 'LONG_TRACE') {
-      protoCfg.writeIntoFile = true;
-      protoCfg.fileWritePeriodMs = uiCfg.fileWritePeriodMs;
-      protoCfg.maxFileSizeBytes = uiCfg.maxFileSizeMb * 1e6;
-    }
-
-    // Clear incremental state every 5 seconds when tracing into a ring
-    // buffer.
-    const incStateConfig = new TraceConfig.IncrementalStateConfig();
-    incStateConfig.clearPeriodMs = 5000;
-    protoCfg.incrementalStateConfig = incStateConfig;
-  }
-
-  const ftraceEvents = new Set<string>(uiCfg.ftrace ? uiCfg.ftraceEvents : []);
-  const atraceCats = new Set<string>(uiCfg.atrace ? uiCfg.atraceCats : []);
-  const atraceApps = new Set<string>();
-  const chromeCategories = new Set<string>();
-  uiCfg.chromeCategoriesSelected.forEach((it) => chromeCategories.add(it));
-  uiCfg.chromeHighOverheadCategoriesSelected.forEach((it) =>
-    chromeCategories.add(it),
-  );
-
-  let procThreadAssociationPolling = false;
-  let procThreadAssociationFtrace = false;
-  let trackInitialOomScore = false;
-
-  if (isAndroid) {
-    const ds = new TraceConfig.DataSource();
-    ds.config = new DataSourceConfig();
-    ds.config.targetBuffer = 1;
-    ds.config.name = 'android.packages_list';
-    protoCfg.dataSources.push(ds);
-  }
-
-  if (isAndroid || isLinux) {
-    const ds = new TraceConfig.DataSource();
-    ds.config = new DataSourceConfig();
-    ds.config.targetBuffer = 1;
-    ds.config.name = 'linux.system_info';
-    protoCfg.dataSources.push(ds);
-  }
-
-  let ftrace = false;
-  let symbolizeKsyms = false;
-  if (uiCfg.cpuSched) {
-    procThreadAssociationPolling = true;
-    procThreadAssociationFtrace = true;
-    ftrace = true;
-    if (enableSchedBlockedReason(androidApiLevel)) {
-      symbolizeKsyms = true;
-    }
-    ftraceEvents.add('sched/sched_switch');
-    ftraceEvents.add('power/suspend_resume');
-    ftraceEvents.add('sched/sched_wakeup');
-    ftraceEvents.add('sched/sched_wakeup_new');
-    ftraceEvents.add('sched/sched_waking');
-    ftraceEvents.add('power/suspend_resume');
-  }
-
-  let sysStatsCfg: SysStatsConfig | undefined = undefined;
-
-  if (uiCfg.cpuFreq) {
-    ftraceEvents.add('power/cpu_frequency');
-    ftraceEvents.add('power/cpu_idle');
-    ftraceEvents.add('power/suspend_resume');
-
-    sysStatsCfg = new SysStatsConfig();
-    sysStatsCfg.cpufreqPeriodMs = uiCfg.cpuFreqPollMs;
-  }
-
-  if (uiCfg.gpuFreq) {
-    ftraceEvents.add('power/gpu_frequency');
-  }
-
-  if (uiCfg.gpuMemTotal) {
-    ftraceEvents.add('gpu_mem/gpu_mem_total');
-
-    if (targetInfo.targetType !== 'CHROME') {
-      const ds = new TraceConfig.DataSource();
-      ds.config = new DataSourceConfig();
-      ds.config.name = 'android.gpu.memory';
-      protoCfg.dataSources.push(ds);
-    }
-  }
-
-  if (uiCfg.gpuWorkPeriod) {
-    ftraceEvents.add('power/gpu_work_period');
-  }
-
-  if (uiCfg.cpuSyscall) {
-    ftraceEvents.add('raw_syscalls/sys_enter');
-    ftraceEvents.add('raw_syscalls/sys_exit');
-  }
-
-  if (uiCfg.batteryDrain) {
-    const ds = new TraceConfig.DataSource();
-    ds.config = new DataSourceConfig();
-    if (
-      targetInfo.targetType === 'CHROME_OS' ||
-      targetInfo.targetType === 'LINUX'
-    ) {
-      ds.config.name = 'linux.sysfs_power';
-    } else {
-      ds.config.name = 'android.power';
-      ds.config.androidPowerConfig = new AndroidPowerConfig();
-      ds.config.androidPowerConfig.batteryPollMs = uiCfg.batteryDrainPollMs;
-      ds.config.androidPowerConfig.batteryCounters = [
-        AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CAPACITY_PERCENT,
-        AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CHARGE,
-        AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CURRENT,
-      ];
-      ds.config.androidPowerConfig.collectPowerRails = true;
-    }
-    if (targetInfo.targetType !== 'CHROME') {
-      protoCfg.dataSources.push(ds);
-    }
-  }
-
-  if (uiCfg.boardSensors) {
-    ftraceEvents.add('regulator/regulator_set_voltage');
-    ftraceEvents.add('regulator/regulator_set_voltage_complete');
-    ftraceEvents.add('power/clock_enable');
-    ftraceEvents.add('power/clock_disable');
-    ftraceEvents.add('power/clock_set_rate');
-    ftraceEvents.add('power/suspend_resume');
-  }
-
-  if (uiCfg.cpuCoarse) {
-    if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
-    sysStatsCfg.statPeriodMs = uiCfg.cpuCoarsePollMs;
-    sysStatsCfg.statCounters = [
-      SysStatsConfig.StatCounters.STAT_CPU_TIMES,
-      SysStatsConfig.StatCounters.STAT_FORK_COUNT,
-    ];
-  }
-
-  if (uiCfg.memHiFreq) {
-    procThreadAssociationPolling = true;
-    procThreadAssociationFtrace = true;
-    ftraceEvents.add('mm_event/mm_event_record');
-    ftraceEvents.add('kmem/rss_stat');
-    ftraceEvents.add('ion/ion_stat');
-    ftraceEvents.add('dmabuf_heap/dma_heap_stat');
-    ftraceEvents.add('kmem/ion_heap_grow');
-    ftraceEvents.add('kmem/ion_heap_shrink');
-  }
-
-  if (procThreadAssociationFtrace) {
-    ftraceEvents.add('sched/sched_process_exit');
-    ftraceEvents.add('sched/sched_process_free');
-    ftraceEvents.add('task/task_newtask');
-    ftraceEvents.add('task/task_rename');
-  }
-
-  if (uiCfg.linuxDeviceRpm) {
-    ftraceEvents.add('rpm/rpm_status');
-  }
-
-  if (uiCfg.meminfo) {
-    if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
-    sysStatsCfg.meminfoPeriodMs = uiCfg.meminfoPeriodMs;
-    sysStatsCfg.meminfoCounters = uiCfg.meminfoCounters.map((name) => {
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      return MeminfoCounters[name as any as number] as any as number;
-    });
-  }
-
-  if (uiCfg.vmstat) {
-    if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
-    sysStatsCfg.vmstatPeriodMs = uiCfg.vmstatPeriodMs;
-    sysStatsCfg.vmstatCounters = uiCfg.vmstatCounters.map((name) => {
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      return VmstatCounters[name as any as number] as any as number;
-    });
-  }
-
-  if (uiCfg.memLmk) {
-    // For in-kernel LMK (roughly older devices until Go and Pixel 3).
-    ftraceEvents.add('lowmemorykiller/lowmemory_kill');
-
-    // 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.
-    atraceApps.add('lmkd');
-
-    ftraceEvents.add('oom/oom_score_adj_update');
-    procThreadAssociationPolling = true;
-    trackInitialOomScore = true;
-  }
-
-  let heapprofd: HeapprofdConfig | undefined = undefined;
-  if (uiCfg.heapProfiling) {
-    // TODO(hjd): Check or inform user if buffer size are too small.
-    const cfg = new HeapprofdConfig();
-    cfg.samplingIntervalBytes = uiCfg.hpSamplingIntervalBytes;
-    if (
-      uiCfg.hpSharedMemoryBuffer >= 8192 &&
-      uiCfg.hpSharedMemoryBuffer % 4096 === 0
-    ) {
-      cfg.shmemSizeBytes = uiCfg.hpSharedMemoryBuffer;
-    }
-    for (const value of uiCfg.hpProcesses.split('\n')) {
-      if (value === '') {
-        // Ignore empty lines
-      } else if (isNaN(+value)) {
-        cfg.processCmdline.push(value);
-      } else {
-        cfg.pid.push(+value);
-      }
-    }
-    if (uiCfg.hpContinuousDumpsInterval > 0) {
-      const cdc = (cfg.continuousDumpConfig = new NativeContinuousDumpConfig());
-      cdc.dumpIntervalMs = uiCfg.hpContinuousDumpsInterval;
-      if (uiCfg.hpContinuousDumpsPhase > 0) {
-        cdc.dumpPhaseMs = uiCfg.hpContinuousDumpsPhase;
-      }
-    }
-    cfg.blockClient = uiCfg.hpBlockClient;
-    if (uiCfg.hpAllHeaps) {
-      cfg.allHeaps = true;
-    }
-    heapprofd = cfg;
-  }
-
-  let javaHprof: JavaHprofConfig | undefined = undefined;
-  if (uiCfg.javaHeapDump) {
-    const cfg = new JavaHprofConfig();
-    for (const value of uiCfg.jpProcesses.split('\n')) {
-      if (value === '') {
-        // Ignore empty lines
-      } else if (isNaN(+value)) {
-        cfg.processCmdline.push(value);
-      } else {
-        cfg.pid.push(+value);
-      }
-    }
-    if (uiCfg.jpContinuousDumpsInterval > 0) {
-      const cdc = (cfg.continuousDumpConfig = new JavaContinuousDumpConfig());
-      cdc.dumpIntervalMs = uiCfg.jpContinuousDumpsInterval;
-      if (uiCfg.hpContinuousDumpsPhase > 0) {
-        cdc.dumpPhaseMs = uiCfg.jpContinuousDumpsPhase;
-      }
-    }
-    javaHprof = cfg;
-  }
-
-  if (uiCfg.procStats || procThreadAssociationPolling || trackInitialOomScore) {
-    const ds = new TraceConfig.DataSource();
-    ds.config = new DataSourceConfig();
-    ds.config.targetBuffer = 1; // Aux
-    ds.config.name = 'linux.process_stats';
-    ds.config.processStatsConfig = new ProcessStatsConfig();
-    if (uiCfg.procStats) {
-      ds.config.processStatsConfig.procStatsPollMs = uiCfg.procStatsPeriodMs;
-    }
-    if (procThreadAssociationPolling || trackInitialOomScore) {
-      ds.config.processStatsConfig.scanAllProcessesOnStart = true;
-    }
-    if (targetInfo.targetType !== 'CHROME') {
-      protoCfg.dataSources.push(ds);
-    }
-  }
-
-  if (uiCfg.androidLogs) {
-    const ds = new TraceConfig.DataSource();
-    ds.config = new DataSourceConfig();
-    ds.config.name = 'android.log';
-    ds.config.androidLogConfig = new AndroidLogConfig();
-    ds.config.androidLogConfig.logIds = uiCfg.androidLogBuffers.map((name) => {
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      return AndroidLogId[name as any as number] as any as number;
-    });
-
-    if (targetInfo.targetType !== 'CHROME') {
-      protoCfg.dataSources.push(ds);
-    }
-  }
-
-  if (uiCfg.androidFrameTimeline) {
-    const ds = new TraceConfig.DataSource();
-    ds.config = new DataSourceConfig();
-    ds.config.name = 'android.surfaceflinger.frametimeline';
-    if (targetInfo.targetType !== 'CHROME') {
-      protoCfg.dataSources.push(ds);
-    }
-  }
-
-  if (uiCfg.androidGameInterventionList) {
-    const ds = new TraceConfig.DataSource();
-    ds.config = new DataSourceConfig();
-    ds.config.name = 'android.game_interventions';
-    if (targetInfo.targetType !== 'CHROME') {
-      protoCfg.dataSources.push(ds);
-    }
-  }
-
-  if (uiCfg.androidNetworkTracing) {
-    if (targetInfo.targetType !== 'CHROME') {
-      const net = new TraceConfig.DataSource();
-      net.config = new DataSourceConfig();
-      net.config.name = 'android.network_packets';
-      net.config.networkPacketTraceConfig = new NetworkPacketTraceConfig();
-      net.config.networkPacketTraceConfig.pollMs =
-        uiCfg.androidNetworkTracingPollMs;
-      protoCfg.dataSources.push(net);
-
-      // Record package info so that Perfetto can display the package name for
-      // network packet events based on the event uid.
-      const pkg = new TraceConfig.DataSource();
-      pkg.config = new DataSourceConfig();
-      pkg.config.name = 'android.packages_list';
-      protoCfg.dataSources.push(pkg);
-    }
-  }
-
-  if (uiCfg.chromeLogs) {
-    chromeCategories.add('log');
-  }
-
-  if (uiCfg.taskScheduling) {
-    chromeCategories.add('toplevel');
-    chromeCategories.add('toplevel.flow');
-    chromeCategories.add('scheduler');
-    chromeCategories.add('sequence_manager');
-    chromeCategories.add('disabled-by-default-toplevel.flow');
-  }
-
-  if (uiCfg.ipcFlows) {
-    chromeCategories.add('toplevel');
-    chromeCategories.add('toplevel.flow');
-    chromeCategories.add('disabled-by-default-ipc.flow');
-    chromeCategories.add('mojom');
-  }
-
-  if (uiCfg.jsExecution) {
-    chromeCategories.add('toplevel');
-    chromeCategories.add('v8');
-  }
-
-  if (uiCfg.webContentRendering) {
-    chromeCategories.add('toplevel');
-    chromeCategories.add('blink');
-    chromeCategories.add('cc');
-    chromeCategories.add('gpu');
-  }
-
-  if (uiCfg.uiRendering) {
-    chromeCategories.add('toplevel');
-    chromeCategories.add('cc');
-    chromeCategories.add('gpu');
-    chromeCategories.add('viz');
-    chromeCategories.add('ui');
-    chromeCategories.add('views');
-  }
-
-  if (uiCfg.inputEvents) {
-    chromeCategories.add('toplevel');
-    chromeCategories.add('benchmark');
-    chromeCategories.add('evdev');
-    chromeCategories.add('input');
-    chromeCategories.add('disabled-by-default-toplevel.flow');
-  }
-
-  if (uiCfg.navigationAndLoading) {
-    chromeCategories.add('loading');
-    chromeCategories.add('net');
-    chromeCategories.add('netlog');
-    chromeCategories.add('navigation');
-    chromeCategories.add('browser');
-  }
-
-  if (uiCfg.audio) {
-    function addCategoryAndDisabledByDefault(category: string) {
-      chromeCategories.add(category);
-      chromeCategories.add('disabled-by-default-' + category);
-    }
-
-    addCategoryAndDisabledByDefault('audio');
-    addCategoryAndDisabledByDefault('webaudio');
-    addCategoryAndDisabledByDefault('webaudio.audionode');
-    addCategoryAndDisabledByDefault('webrtc');
-    addCategoryAndDisabledByDefault('audio-worklet');
-    addCategoryAndDisabledByDefault('mediastream');
-    addCategoryAndDisabledByDefault('v8.gc');
-    addCategoryAndDisabledByDefault('toplevel');
-    addCategoryAndDisabledByDefault('toplevel.flow');
-    addCategoryAndDisabledByDefault('wakeup.flow');
-    addCategoryAndDisabledByDefault('cpu_profiler');
-    addCategoryAndDisabledByDefault('scheduler');
-    addCategoryAndDisabledByDefault('p2p');
-    addCategoryAndDisabledByDefault('net');
-    chromeCategories.add('base');
-  }
-
-  if (uiCfg.video) {
-    chromeCategories.add('base');
-    chromeCategories.add('gpu');
-    chromeCategories.add('gpu.capture');
-    chromeCategories.add('media');
-    chromeCategories.add('toplevel');
-    chromeCategories.add('toplevel.flow');
-    chromeCategories.add('scheduler');
-    chromeCategories.add('wakeup.flow');
-    chromeCategories.add('webrtc');
-    chromeCategories.add('disabled-by-default-video_and_image_capture');
-    chromeCategories.add('disabled-by-default-webrtc');
-  }
-
-  // linux.perf stack sampling
-  if (uiCfg.tracePerf) {
-    const ds = new TraceConfig.DataSource();
-    ds.config = new DataSourceConfig();
-    ds.config.name = 'linux.perf';
-
-    const perfEventConfig = new PerfEventConfig();
-    perfEventConfig.timebase = new Timebase();
-    perfEventConfig.timebase.frequency = uiCfg.timebaseFrequency;
-    // TODO: The timestampClock needs to be changed to MONOTONIC once we start
-    // offering a choice of counter to record on through the recording UI, as
-    // not all clocks are compatible with hardware counters).
-    perfEventConfig.timebase.timestampClock = PerfClock.PERF_CLOCK_BOOTTIME;
-
-    const callstackSampling = new CallstackSampling();
-    if (uiCfg.targetCmdLine.length > 0) {
-      const scope = new Scope();
-      for (const cmdLine of uiCfg.targetCmdLine) {
-        if (cmdLine == '') {
-          continue;
-        }
-        scope.targetCmdline?.push(cmdLine.trim());
-      }
-      callstackSampling.scope = scope;
-    }
-
-    perfEventConfig.callstackSampling = callstackSampling;
-
-    ds.config.perfEventConfig = perfEventConfig;
-    protoCfg.dataSources.push(ds);
-  }
-
-  if (chromeCategories.size !== 0) {
-    let chromeRecordMode;
-    if (uiCfg.mode === 'STOP_WHEN_FULL') {
-      chromeRecordMode = 'record-until-full';
-    } else {
-      chromeRecordMode = 'record-continuously';
-    }
-    const configStruct = {
-      record_mode: chromeRecordMode,
-      included_categories: [...chromeCategories.values()],
-      // Only include explicitly selected categories
-      excluded_categories: ['*'],
-      memory_dump_config: {},
-    };
-    if (chromeCategories.has('disabled-by-default-memory-infra')) {
-      configStruct.memory_dump_config = {
-        allowed_dump_modes: ['background', 'light', 'detailed'],
-        triggers: [
-          {
-            min_time_between_dumps_ms: 10000,
-            mode: 'detailed',
-            type: 'periodic_interval',
-          },
-        ],
-      };
-    }
-    const chromeConfig = new ChromeConfig();
-    chromeConfig.clientPriority = ChromeConfig.ClientPriority.USER_INITIATED;
-    chromeConfig.privacyFilteringEnabled = uiCfg.chromePrivacyFiltering;
-    chromeConfig.traceConfig = JSON.stringify(configStruct);
-
-    const traceDs = new TraceConfig.DataSource();
-    traceDs.config = new DataSourceConfig();
-    traceDs.config.name = 'org.chromium.trace_event';
-    traceDs.config.chromeConfig = chromeConfig;
-    protoCfg.dataSources.push(traceDs);
-
-    // Configure "track_event" datasource for the Chrome SDK build.
-    const trackEventDs = new TraceConfig.DataSource();
-    trackEventDs.config = new DataSourceConfig();
-    trackEventDs.config.name = 'track_event';
-    trackEventDs.config.chromeConfig = chromeConfig;
-    trackEventDs.config.trackEventConfig = new TrackEventConfig();
-    trackEventDs.config.trackEventConfig.disabledCategories = ['*'];
-    trackEventDs.config.trackEventConfig.enabledCategories = [
-      ...chromeCategories.values(),
-      '__metadata',
-    ];
-    trackEventDs.config.trackEventConfig.enableThreadTimeSampling = true;
-    trackEventDs.config.trackEventConfig.timestampUnitMultiplier = 1000;
-    trackEventDs.config.trackEventConfig.filterDynamicEventNames =
-      uiCfg.chromePrivacyFiltering;
-    trackEventDs.config.trackEventConfig.filterDebugAnnotations =
-      uiCfg.chromePrivacyFiltering;
-    protoCfg.dataSources.push(trackEventDs);
-
-    const metadataDs = new TraceConfig.DataSource();
-    metadataDs.config = new DataSourceConfig();
-    metadataDs.config.name = 'org.chromium.trace_metadata';
-    metadataDs.config.chromeConfig = chromeConfig;
-    protoCfg.dataSources.push(metadataDs);
-
-    if (chromeCategories.has('disabled-by-default-memory-infra')) {
-      const memoryDs = new TraceConfig.DataSource();
-      memoryDs.config = new DataSourceConfig();
-      memoryDs.config.name = 'org.chromium.memory_instrumentation';
-      memoryDs.config.chromeConfig = chromeConfig;
-      protoCfg.dataSources.push(memoryDs);
-
-      const HeapProfDs = new TraceConfig.DataSource();
-      HeapProfDs.config = new DataSourceConfig();
-      HeapProfDs.config.name = 'org.chromium.native_heap_profiler';
-      HeapProfDs.config.chromeConfig = chromeConfig;
-      protoCfg.dataSources.push(HeapProfDs);
-    }
-
-    if (
-      chromeCategories.has('disabled-by-default-cpu_profiler') ||
-      chromeCategories.has('disabled-by-default-cpu_profiler.debug')
-    ) {
-      const dataSource = new TraceConfig.DataSource();
-      dataSource.config = new DataSourceConfig();
-      dataSource.config.name = 'org.chromium.sampler_profiler';
-      dataSource.config.chromeConfig = chromeConfig;
-      protoCfg.dataSources.push(dataSource);
-    }
-  }
-
-  // Keep these last. The stages above can enrich them.
-  if (
-    targetInfo.targetType !== 'WINDOWS' &&
-    targetInfo.targetType !== 'CHROME'
-  ) {
-    if (sysStatsCfg !== undefined) {
-      const ds = new TraceConfig.DataSource();
-      ds.config = new DataSourceConfig();
-      ds.config.name = 'linux.sys_stats';
-      ds.config.sysStatsConfig = sysStatsCfg;
-      protoCfg.dataSources.push(ds);
-    }
-
-    if (heapprofd !== undefined) {
-      const ds = new TraceConfig.DataSource();
-      ds.config = new DataSourceConfig();
-      ds.config.targetBuffer = 0;
-      ds.config.name = 'android.heapprofd';
-      ds.config.heapprofdConfig = heapprofd;
-      protoCfg.dataSources.push(ds);
-    }
-
-    if (javaHprof !== undefined) {
-      const ds = new TraceConfig.DataSource();
-      ds.config = new DataSourceConfig();
-      ds.config.targetBuffer = 0;
-      ds.config.name = 'android.java_hprof';
-      ds.config.javaHprofConfig = javaHprof;
-      protoCfg.dataSources.push(ds);
-    }
-  }
-
-  if (
-    uiCfg.ftrace ||
-    ftrace ||
-    uiCfg.atrace ||
-    ftraceEvents.size > 0 ||
-    atraceCats.size > 0 ||
-    atraceApps.size > 0
-  ) {
-    const ds = new TraceConfig.DataSource();
-    ds.config = new DataSourceConfig();
-    ds.config.name = 'linux.ftrace';
-    ds.config.ftraceConfig = new FtraceConfig();
-    // Override the advanced ftrace parameters only if the user has ticked the
-    // "Advanced ftrace config" tab.
-    if (uiCfg.ftrace || ftrace) {
-      if (uiCfg.ftraceBufferSizeKb) {
-        ds.config.ftraceConfig.bufferSizeKb = uiCfg.ftraceBufferSizeKb;
-      }
-      if (uiCfg.ftraceDrainPeriodMs) {
-        ds.config.ftraceConfig.drainPeriodMs = uiCfg.ftraceDrainPeriodMs;
-      }
-      if (uiCfg.symbolizeKsyms || symbolizeKsyms) {
-        ds.config.ftraceConfig.symbolizeKsyms = true;
-        ftraceEvents.add('sched/sched_blocked_reason');
-      }
-      for (const line of uiCfg.ftraceExtraEvents.split('\n')) {
-        if (line.trim().length > 0) ftraceEvents.add(line.trim());
-      }
-    }
-
-    if (uiCfg.atrace) {
-      if (uiCfg.allAtraceApps) {
-        atraceApps.clear();
-        atraceApps.add('*');
-      } else {
-        for (const line of uiCfg.atraceApps.split('\n')) {
-          if (line.trim().length > 0) atraceApps.add(line.trim());
-        }
-      }
-    }
-
-    if (atraceCats.size > 0 || atraceApps.size > 0) {
-      ftraceEvents.add('ftrace/print');
-    }
-
-    let ftraceEventsArray: string[] = [];
-    if (exists(androidApiLevel) && androidApiLevel === 28) {
-      for (const ftraceEvent of ftraceEvents) {
-        // On P, we don't support groups so strip all group names from ftrace
-        // events.
-        const groupAndName = ftraceEvent.split('/');
-        if (groupAndName.length !== 2) {
-          ftraceEventsArray.push(ftraceEvent);
-          continue;
-        }
-        // Filter out any wildcard event groups which was not supported
-        // before Q.
-        if (groupAndName[1] === '*') {
-          continue;
-        }
-        ftraceEventsArray.push(groupAndName[1]);
-      }
-    } else {
-      ftraceEventsArray = Array.from(ftraceEvents);
-    }
-
-    ds.config.ftraceConfig.ftraceEvents = ftraceEventsArray;
-    ds.config.ftraceConfig.atraceCategories = Array.from(atraceCats);
-    ds.config.ftraceConfig.atraceApps = Array.from(atraceApps);
-
-    if (enableCompactSched(androidApiLevel)) {
-      const compact = new FtraceConfig.CompactSchedConfig();
-      compact.enabled = true;
-      ds.config.ftraceConfig.compactSched = compact;
-    }
-
-    if (targetInfo.targetType !== 'CHROME') {
-      protoCfg.dataSources.push(ds);
-    }
-  }
-
-  if (
-    targetInfo.targetType === 'WINDOWS' ||
-    uiCfg.etwCSwitch ||
-    uiCfg.etwThreadState
-  ) {
-    const ds = new TraceConfig.DataSource();
-    ds.config = new DataSourceConfig();
-    ds.config.name = 'org.chromium.etw_system';
-    ds.config.etwConfig = new EtwConfig();
-
-    const kernelFlags: EtwConfig.KernelFlag[] = [];
-
-    if (uiCfg.etwCSwitch) {
-      kernelFlags.push(EtwConfig.KernelFlag.CSWITCH);
-    }
-    if (uiCfg.etwThreadState) {
-      kernelFlags.push(EtwConfig.KernelFlag.DISPATCHER);
-    }
-    ds.config.etwConfig.kernelFlags = kernelFlags;
-    protoCfg.dataSources.push(ds);
-  }
-
-  return protoCfg;
-}
-
-function toPbtxt(configBuffer: Uint8Array): string {
-  const msg = TraceConfig.decode(configBuffer);
-  const json = msg.toJSON();
-  function snakeCase(s: string): string {
-    return s.replace(/[A-Z]/g, (c) => '_' + c.toLowerCase());
-  }
-  // With the ahead of time compiled protos we can't seem to tell which
-  // fields are enums.
-  function isEnum(value: string): boolean {
-    return (
-      value.startsWith('MEMINFO_') ||
-      value.startsWith('VMSTAT_') ||
-      value.startsWith('STAT_') ||
-      value.startsWith('LID_') ||
-      value.startsWith('BATTERY_COUNTER_') ||
-      value === 'DISCARD' ||
-      value === 'RING_BUFFER' ||
-      value.startsWith('PERF_CLOCK_')
-    );
-  }
-  // Since javascript doesn't have 64 bit numbers when converting protos to
-  // json the proto library encodes them as strings. This is lossy since
-  // we can't tell which strings that look like numbers are actually strings
-  // and which are actually numbers. Ideally we would reflect on the proto
-  // definition somehow but for now we just hard code keys which have this
-  // problem in the config.
-  function is64BitNumber(key: string): boolean {
-    return [
-      'maxFileSizeBytes',
-      'samplingIntervalBytes',
-      'shmemSizeBytes',
-      'pid',
-      'frequency',
-    ].includes(key);
-  }
-  function* message(msg: {}, indent: number): IterableIterator<string> {
-    for (const [key, value] of Object.entries(msg)) {
-      const isRepeated = Array.isArray(value);
-      const isNested = typeof value === 'object' && !isRepeated;
-      for (const entry of isRepeated ? (value as Array<{}>) : [value]) {
-        yield ' '.repeat(indent) + `${snakeCase(key)}${isNested ? '' : ':'} `;
-        if (isString(entry)) {
-          if (isEnum(entry) || is64BitNumber(key)) {
-            yield entry;
-          } else {
-            yield `"${entry.replace(new RegExp('"', 'g'), '\\"')}"`;
-          }
-        } else if (typeof entry === 'number') {
-          yield entry.toString();
-        } else if (typeof entry === 'boolean') {
-          yield entry.toString();
-        } else if (typeof entry === 'object' && entry !== null) {
-          yield '{\n';
-          yield* message(entry, indent + 4);
-          yield ' '.repeat(indent) + '}';
-        } else {
-          throw new Error(
-            `Record proto entry "${entry}" with unexpected type ${typeof entry}`,
-          );
-        }
-        yield '\n';
-      }
-    }
-  }
-  return [...message(json, 0)].join('');
-}
diff --git a/ui/src/common/recordingV2/recording_config_utils_unittest.ts b/ui/src/common/recordingV2/recording_config_utils_unittest.ts
deleted file mode 100644
index fed45d1..0000000
--- a/ui/src/common/recordingV2/recording_config_utils_unittest.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {createEmptyRecordConfig} from '../../controller/record_config_types';
-
-import {genTraceConfig} from './recording_config_utils';
-import {AndroidTargetInfo} from './recording_interfaces_v2';
-
-test('genTraceConfig() can run without manipulating the input config', () => {
-  const config = createEmptyRecordConfig();
-  config.cpuSched = true; // Exercise ftrace
-
-  const targetInfo: AndroidTargetInfo = {
-    name: 'test',
-    targetType: 'ANDROID',
-    androidApiLevel: 31, // >= 32 to exercise symbolizeKsyms
-    dataSources: [],
-  };
-
-  Object.freeze(config);
-  const actual = genTraceConfig(config, targetInfo);
-
-  const expected = {
-    buffers: [
-      {
-        sizeKb: 63488,
-        fillPolicy: 'DISCARD',
-      },
-      {
-        sizeKb: 2048,
-        fillPolicy: 'DISCARD',
-      },
-    ],
-    dataSources: [
-      {
-        config: {
-          name: 'android.packages_list',
-          targetBuffer: 1,
-        },
-      },
-      {
-        config: {
-          name: 'linux.system_info',
-          targetBuffer: 1,
-        },
-      },
-      {
-        config: {
-          name: 'linux.process_stats',
-          targetBuffer: 1,
-          processStatsConfig: {
-            scanAllProcessesOnStart: true,
-          },
-        },
-      },
-      {
-        config: {
-          name: 'linux.ftrace',
-          ftraceConfig: {
-            ftraceEvents: [
-              'sched/sched_switch',
-              'power/suspend_resume',
-              'sched/sched_wakeup',
-              'sched/sched_wakeup_new',
-              'sched/sched_waking',
-              'sched/sched_process_exit',
-              'sched/sched_process_free',
-              'task/task_newtask',
-              'task/task_rename',
-              'sched/sched_blocked_reason',
-            ],
-            compactSched: {
-              enabled: true,
-            },
-            symbolizeKsyms: true,
-          },
-        },
-      },
-    ],
-    durationMs: 10000,
-  };
-
-  // Compare stringified versions to void issues with JS objects.
-  expect(JSON.stringify(actual)).toEqual(JSON.stringify(expected));
-});
diff --git a/ui/src/common/recordingV2/recording_error_handling.ts b/ui/src/common/recordingV2/recording_error_handling.ts
deleted file mode 100644
index 3fda093..0000000
--- a/ui/src/common/recordingV2/recording_error_handling.ts
+++ /dev/null
@@ -1,142 +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 {
-  showAllowUSBDebugging,
-  showConnectionLostError,
-  showExtensionNotInstalled,
-  showFailedToPushBinary,
-  showIssueParsingTheTracedResponse,
-  showNoDeviceSelected,
-  showWebsocketConnectionIssue,
-  showWebUSBErrorV2,
-} from '../../frontend/error_dialog';
-
-import {OnMessageCallback} from './recording_interfaces_v2';
-import {
-  ALLOW_USB_DEBUGGING,
-  BINARY_PUSH_FAILURE,
-  BINARY_PUSH_UNKNOWN_RESPONSE,
-  EXTENSION_NOT_INSTALLED,
-  NO_DEVICE_SELECTED,
-  PARSING_UNABLE_TO_DECODE_METHOD,
-  PARSING_UNKNWON_REQUEST_ID,
-  PARSING_UNRECOGNIZED_MESSAGE,
-  PARSING_UNRECOGNIZED_PORT,
-  WEBSOCKET_UNABLE_TO_CONNECT,
-} from './recording_utils';
-
-// The pattern for handling recording error can have the following nesting in
-// case of errors:
-// A. wrapRecordingError -> wraps a promise
-// B. onFailure -> has user defined logic and calls showRecordingModal
-// C. showRecordingModal -> shows UX for a given error; this is not called
-//    directly by wrapRecordingError, because we want the caller (such as the
-//    UI) to dictate the UX
-
-// This method takes a promise and a callback to be execute in case the promise
-// fails. It then awaits the promise and executes the callback in case of
-// failure. In the recording code it is used to wrap:
-// 1. Acessing the WebUSB API.
-// 2. Methods returning promises which can be rejected. For instance:
-// a) When the user clicks 'Add a new device' but then doesn't select a valid
-//    device.
-// b) When the user starts a tracing session, but cancels it before they
-//    authorize the session on the device.
-export async function wrapRecordingError<T>(
-  promise: Promise<T>,
-  onFailure: OnMessageCallback,
-): Promise<T | undefined> {
-  try {
-    return await promise;
-  } catch (e) {
-    // Sometimes the message is wrapped in an Error object, sometimes not, so
-    // we make sure we transform it into a string.
-    const errorMessage = getErrorMessage(e);
-    onFailure(errorMessage);
-    return undefined;
-  }
-}
-
-// Shows a modal for every known type of error which can arise during recording.
-// In this way, errors occuring at different levels of the recording process
-// can be handled in a central location.
-export function showRecordingModal(message: string): void {
-  if (
-    [
-      'Unable to claim interface.',
-      'The specified endpoint is not part of a claimed and selected ' +
-        'alternate interface.',
-      // thrown when calling the 'reset' method on a WebUSB device.
-      'Unable to reset the device.',
-    ].some((partOfMessage) => message.includes(partOfMessage))
-  ) {
-    showWebUSBErrorV2();
-  } else if (
-    [
-      'A transfer error has occurred.',
-      'The device was disconnected.',
-      'The transfer was cancelled.',
-    ].some((partOfMessage) => message.includes(partOfMessage)) ||
-    isDeviceDisconnectedError(message)
-  ) {
-    showConnectionLostError();
-  } else if (message === ALLOW_USB_DEBUGGING) {
-    showAllowUSBDebugging();
-  } else if (
-    isMessageComposedOf(message, [
-      BINARY_PUSH_FAILURE,
-      BINARY_PUSH_UNKNOWN_RESPONSE,
-    ])
-  ) {
-    showFailedToPushBinary(message.substring(message.indexOf(':') + 1));
-  } else if (message === NO_DEVICE_SELECTED) {
-    showNoDeviceSelected();
-  } else if (WEBSOCKET_UNABLE_TO_CONNECT === message) {
-    showWebsocketConnectionIssue(message);
-  } else if (message === EXTENSION_NOT_INSTALLED) {
-    showExtensionNotInstalled();
-  } else if (
-    isMessageComposedOf(message, [
-      PARSING_UNKNWON_REQUEST_ID,
-      PARSING_UNABLE_TO_DECODE_METHOD,
-      PARSING_UNRECOGNIZED_PORT,
-      PARSING_UNRECOGNIZED_MESSAGE,
-    ])
-  ) {
-    showIssueParsingTheTracedResponse(message);
-  } else {
-    throw new Error(`${message}`);
-  }
-}
-
-function isDeviceDisconnectedError(message: string) {
-  return (
-    message.includes('Device with serial') &&
-    message.includes('was disconnected.')
-  );
-}
-
-function isMessageComposedOf(message: string, issues: string[]) {
-  for (const issue of issues) {
-    if (message.includes(issue)) {
-      return true;
-    }
-  }
-  return false;
-}
-
-// Exception thrown by the Recording logic.
-export class RecordingError extends Error {}
diff --git a/ui/src/common/recordingV2/recording_interfaces_v2.ts b/ui/src/common/recordingV2/recording_interfaces_v2.ts
deleted file mode 100644
index 954a145..0000000
--- a/ui/src/common/recordingV2/recording_interfaces_v2.ts
+++ /dev/null
@@ -1,228 +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 {TraceConfig} from '../../protos';
-
-// TargetFactory connects, disconnects and keeps track of targets.
-// There is one factory for AndroidWebusb, AndroidWebsocket, Chrome etc.
-// For instance, the AndroidWebusb factory returns a RecordingTargetV2 for each
-// device.
-export interface TargetFactory {
-  // Store the kind explicitly as a string as opposed to using class.kind in
-  // case we ever minify our code.
-  readonly kind: string;
-
-  // Setter for OnTargetChange, which is executed when a target is
-  // added/removed or when its information is updated.
-  setOnTargetChange(onTargetChange: OnTargetChangeCallback): void;
-
-  getName(): string;
-
-  listTargets(): RecordingTargetV2[];
-  // Returns recording problems that we encounter when not directly using the
-  // target. For instance we connect webusb devices when Perfetto is loaded. If
-  // there is an issue with connecting a webusb device, we do not want to crash
-  // all of Perfetto, as the user may not want to use the recording
-  // functionality at all.
-  listRecordingProblems(): string[];
-
-  connectNewTarget(): Promise<RecordingTargetV2>;
-}
-
-export interface DataSource {
-  name: string;
-
-  // Contains information that is opaque to the recording code. The caller can
-  // use the DataSource name to type cast the DataSource descriptor.
-  // For targets calling QueryServiceState, 'descriptor' will hold the
-  // datasource descriptor:
-  // https://source.corp.google.com/android/external/perfetto/protos/perfetto/
-  // common/data_source_descriptor.proto;l=28-60
-  // For Chrome, 'descriptor' will contain the answer received from
-  // 'GetCategories':
-  // https://source.corp.google.com/android/external/perfetto/ui/src/
-  // chrome_extension/chrome_tracing_controller.ts;l=220
-  descriptor: unknown;
-}
-
-// Common fields for all types of targetInfo: Chrome, Android, Linux etc.
-interface TargetInfoBase {
-  name: string;
-
-  // The dataSources exposed by a target. They are fetched from the target
-  // (ex: using QSS for Android or GetCategories for Chrome).
-  dataSources: DataSource[];
-}
-
-export interface AndroidTargetInfo extends TargetInfoBase {
-  targetType: 'ANDROID';
-
-  // This is the Android API level. For instance, it can be 32, 31, 30 etc.
-  // It is the "API level" column here:
-  // https://source.android.com/setup/start/build-numbers
-  androidApiLevel?: number;
-}
-
-export interface ChromeTargetInfo extends TargetInfoBase {
-  targetType: 'CHROME' | 'CHROME_OS' | 'WINDOWS';
-}
-
-export interface HostOsTargetInfo extends TargetInfoBase {
-  targetType: 'LINUX' | 'MACOS';
-}
-
-// Holds information about a target. It's used by the UI and the logic which
-// generates a config.
-export type TargetInfo =
-  | AndroidTargetInfo
-  | ChromeTargetInfo
-  | HostOsTargetInfo;
-
-// RecordingTargetV2 is subclassed by Android devices and the Chrome browser/OS.
-// It creates tracing sessions which are used by the UI. For Android, it manages
-// the connection with the device.
-export interface RecordingTargetV2 {
-  // Allows targets to surface target specific information such as
-  // well known key/value pairs: OS, targetType('ANDROID', 'CHROME', etc.)
-  getInfo(): TargetInfo;
-
-  // Disconnects the target.
-  disconnect(disconnectMessage?: string): Promise<void>;
-
-  // Returns true if we are able to connect to the target without interfering
-  // with other processes. For example, for adb devices connected over WebUSB,
-  // this will be false when we can not claim the interface (Which most likely
-  // means that 'adb server' is running locally.). After querrying this method,
-  // the caller can decide if they want to connect to the target and as a side
-  // effect take the connection away from other processes.
-  canConnectWithoutContention(): Promise<boolean>;
-
-  // Whether the recording target can be used in a tracing session. For example,
-  // virtual targets do not support a tracing session.
-  canCreateTracingSession(recordingMode?: string): boolean;
-
-  // Some target information can only be obtained after connecting to the
-  // target. This will establish a connection and retrieve data such as
-  // dataSources and apiLevel for Android.
-  fetchTargetInfo(
-    tracingSessionListener: TracingSessionListener,
-  ): Promise<void>;
-
-  createTracingSession(
-    tracingSessionListener: TracingSessionListener,
-  ): Promise<TracingSession>;
-}
-
-// TracingSession is used by the UI to record a trace. Depending on user
-// actions, the UI can start/stop/cancel a session. During the recording, it
-// provides updates about buffer usage. It is subclassed by
-// TracedTracingSession, which manages the communication with traced and has
-// logic for encoding/decoding Perfetto client requests/replies.
-export interface TracingSession {
-  // Starts the tracing session.
-  start(config: TraceConfig): void;
-
-  // Will stop the tracing session and NOT return any trace.
-  cancel(): void;
-
-  // Will stop the tracing session. The implementing class may also return
-  // the trace using a callback.
-  stop(): void;
-
-  // Returns the percentage of the trace buffer that is currently being
-  // occupied.
-  getTraceBufferUsage(): Promise<number>;
-}
-
-// Connection with an Adb device. Implementations will have logic specific to
-// the connection protocol used(Ex: WebSocket, WebUsb).
-export interface AdbConnection {
-  // Will push a binary to a given path.
-  push(binary: ArrayBuffer, path: string): Promise<void>;
-
-  // Will issue a shell command to the device.
-  shell(cmd: string): Promise<ByteStream>;
-
-  // Will establish a connection(a ByteStream) with the device.
-  connectSocket(path: string): Promise<ByteStream>;
-
-  // Returns true if we are able to connect without interfering
-  // with other processes. For example, for adb devices connected over WebUSB,
-  // this will be false when we can not claim the interface (Which most likely
-  // means that 'adb server' is running locally.).
-  canConnectWithoutContention(): Promise<boolean>;
-
-  // Ends the connection.
-  disconnect(disconnectMessage?: string): Promise<void>;
-}
-
-// A stream for a connection between a target and a tracing session.
-export interface ByteStream {
-  // The caller can add callbacks, to be executed when the stream receives new
-  // data or when it finished closing itself.
-  addOnStreamDataCallback(onStreamData: OnStreamDataCallback): void;
-  addOnStreamCloseCallback(onStreamClose: OnStreamCloseCallback): void;
-
-  isConnected(): boolean;
-  write(data: string | Uint8Array): void;
-
-  close(): void;
-  closeAndWaitForTeardown(): Promise<void>;
-}
-
-// Handles binary messages received over the ByteStream.
-export interface OnStreamDataCallback {
-  (data: Uint8Array): void;
-}
-
-// Called when the ByteStream is closed.
-export interface OnStreamCloseCallback {
-  (): void;
-}
-
-// OnTraceDataCallback will return the entire trace when it has been fully
-// assembled. This will be changed in the following CL aosp/2057640.
-export interface OnTraceDataCallback {
-  (trace: Uint8Array): void;
-}
-
-// Handles messages that are useful in the UI and that occur at any layer of the
-// recording (trace, connection). The messages includes both status messages and
-// error messages.
-export interface OnMessageCallback {
-  (message: string): void;
-}
-
-// Handles the loss of the connection at the connection layer (used by the
-// AdbConnection).
-export interface OnDisconnectCallback {
-  (errorMessage?: string): void;
-}
-
-// Called when there is a change of targets or within a target.
-// For instance, it's used when an Adb device becomes connected/disconnected.
-// It's also executed by a target when the information it stores gets updated.
-export interface OnTargetChangeCallback {
-  (): void;
-}
-
-// A collection of callbacks that is passed to RecordingTargetV2 and
-// subsequently to TracingSession. The callbacks are decided by the UI, so the
-// recording code is not coupled with the rendering logic.
-export interface TracingSessionListener {
-  onTraceData: OnTraceDataCallback;
-  onStatus: OnMessageCallback;
-  onDisconnect: OnDisconnectCallback;
-  onError: OnMessageCallback;
-}
diff --git a/ui/src/common/recordingV2/recording_page_controller.ts b/ui/src/common/recordingV2/recording_page_controller.ts
deleted file mode 100644
index 675977d..0000000
--- a/ui/src/common/recordingV2/recording_page_controller.ts
+++ /dev/null
@@ -1,569 +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, assertTrue} from '../../base/logging';
-import {currentDateHourAndMinute} from '../../base/time';
-import {raf} from '../../core/raf_scheduler';
-import {globals} from '../../frontend/globals';
-import {autosaveConfigStore} from '../../frontend/record_config';
-import {
-  DEFAULT_ADB_WEBSOCKET_URL,
-  DEFAULT_TRACED_WEBSOCKET_URL,
-} from '../../frontend/recording/recording_ui_utils';
-import {couldNotClaimInterface} from '../../frontend/recording/reset_interface_modal';
-import {TraceConfig} from '../../protos';
-import {Actions} from '../actions';
-import {TRACE_SUFFIX} from '../constants';
-
-import {genTraceConfig} from './recording_config_utils';
-import {RecordingError, showRecordingModal} from './recording_error_handling';
-import {
-  RecordingTargetV2,
-  TargetInfo,
-  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 {targetFactoryRegistry} from './target_factory_registry';
-
-// The recording page can be in any of these states. It can transition between
-// states:
-// a) because of a user actions - pressing a UI button ('Start', 'Stop',
-//    'Cancel', 'Force reset' of the target), selecting a different target in
-//    the UI, authorizing authentication on an Android device,
-//    pulling the cable which connects an Android device.
-// b) automatically - if there is no need to reset the device or if the user
-//    has previously authorised the device to be debugged via USB.
-//
-// Recording state machine: https://screenshot.googleplex.com/BaX5EGqQMajgV7G
-export enum RecordingState {
-  NO_TARGET = 0,
-  TARGET_SELECTED = 1,
-  // P1 stands for 'Part 1', where we first connect to the device in order to
-  // obtain target information.
-  ASK_TO_FORCE_P1 = 2,
-  AUTH_P1 = 3,
-  TARGET_INFO_DISPLAYED = 4,
-  // P2 stands for 'Part 2', where we connect to device for the 2nd+ times, to
-  // record a tracing session.
-  ASK_TO_FORCE_P2 = 5,
-  AUTH_P2 = 6,
-  RECORDING = 7,
-  WAITING_FOR_TRACE_DISPLAY = 8,
-}
-
-// Wraps a tracing session promise while the promise is being resolved (e.g.
-// while we are awaiting for ADB auth).
-class TracingSessionWrapper {
-  private tracingSession?: TracingSession = undefined;
-  private isCancelled = false;
-  // We only execute the logic in the callbacks if this TracingSessionWrapper
-  // is the one referenced by the controller. Otherwise this can hold a
-  // tracing session which the user has already cancelled, so it shouldn't
-  // influence the UI.
-  private tracingSessionListener: TracingSessionListener = {
-    onTraceData: (trace: Uint8Array) =>
-      this.controller.maybeOnTraceData(this, trace),
-    onStatus: (message) => this.controller.maybeOnStatus(this, message),
-    onDisconnect: (errorMessage?: string) =>
-      this.controller.maybeOnDisconnect(this, errorMessage),
-    onError: (errorMessage: string) =>
-      this.controller.maybeOnError(this, errorMessage),
-  };
-
-  private target: RecordingTargetV2;
-  private controller: RecordingPageController;
-
-  constructor(target: RecordingTargetV2, controller: RecordingPageController) {
-    this.target = target;
-    this.controller = controller;
-  }
-
-  async start(traceConfig: TraceConfig) {
-    let stateGeneratioNr = this.controller.getStateGeneration();
-    const createSession = async () => {
-      try {
-        this.controller.maybeSetState(
-          this,
-          RecordingState.AUTH_P2,
-          stateGeneratioNr,
-        );
-        stateGeneratioNr += 1;
-
-        const session = await this.target.createTracingSession(
-          this.tracingSessionListener,
-        );
-
-        // We check the `isCancelled` to see if the user has cancelled the
-        // tracing session before it becomes available in TracingSessionWrapper.
-        if (this.isCancelled) {
-          session.cancel();
-          return;
-        }
-
-        this.tracingSession = session;
-        this.controller.maybeSetState(
-          this,
-          RecordingState.RECORDING,
-          stateGeneratioNr,
-        );
-        // When the session is resolved, the traceConfig has been instantiated.
-        this.tracingSession.start(assertExists(traceConfig));
-      } catch (e) {
-        this.tracingSessionListener.onError(e.message);
-      }
-    };
-
-    if (await this.target.canConnectWithoutContention()) {
-      await createSession();
-    } else {
-      // If we need to reset the connection to be able to connect, we ask
-      // the user if they want to reset the connection.
-      this.controller.maybeSetState(
-        this,
-        RecordingState.ASK_TO_FORCE_P2,
-        stateGeneratioNr,
-      );
-      stateGeneratioNr += 1;
-      couldNotClaimInterface(createSession, () =>
-        this.controller.maybeClearRecordingState(this),
-      );
-    }
-  }
-
-  async fetchTargetInfo() {
-    let stateGeneratioNr = this.controller.getStateGeneration();
-    const createSession = async () => {
-      try {
-        this.controller.maybeSetState(
-          this,
-          RecordingState.AUTH_P1,
-          stateGeneratioNr,
-        );
-        stateGeneratioNr += 1;
-        await this.target.fetchTargetInfo(this.tracingSessionListener);
-        this.controller.maybeSetState(
-          this,
-          RecordingState.TARGET_INFO_DISPLAYED,
-          stateGeneratioNr,
-        );
-      } catch (e) {
-        this.tracingSessionListener.onError(e.message);
-      }
-    };
-
-    if (await this.target.canConnectWithoutContention()) {
-      await createSession();
-    } else {
-      // If we need to reset the connection to be able to connect, we ask
-      // the user if they want to reset the connection.
-      this.controller.maybeSetState(
-        this,
-        RecordingState.ASK_TO_FORCE_P1,
-        stateGeneratioNr,
-      );
-      stateGeneratioNr += 1;
-      couldNotClaimInterface(createSession, () =>
-        this.controller.maybeSetState(
-          this,
-          RecordingState.TARGET_SELECTED,
-          stateGeneratioNr,
-        ),
-      );
-    }
-  }
-
-  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
-// user actions such as starting/stopping/cancelling a tracing session.
-export class RecordingPageController {
-  // State of the recording page. This is set by user actions and/or automatic
-  // transitions. This is queried by the UI in order to
-  private state: RecordingState = RecordingState.NO_TARGET;
-  // Currently selected target.
-  private target?: RecordingTargetV2 = undefined;
-  // We wrap the tracing session in an object, because for some targets
-  // (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;
-
-  getBufferUsagePercentage(): number {
-    return this.bufferUsagePercentage;
-  }
-
-  getState(): RecordingState {
-    return this.state;
-  }
-
-  getStateGeneration(): number {
-    return this.stateGeneration;
-  }
-
-  maybeSetState(
-    tracingSessionWrapper: TracingSessionWrapper,
-    state: RecordingState,
-    stateGeneration: number,
-  ): void {
-    if (this.tracingSessionWrapper !== tracingSessionWrapper) {
-      return;
-    }
-    if (stateGeneration !== this.stateGeneration) {
-      throw new RecordingError('Recording page state transition out of order.');
-    }
-    this.setState(state);
-    globals.dispatch(Actions.setRecordingStatus({status: undefined}));
-    raf.scheduleFullRedraw();
-  }
-
-  maybeClearRecordingState(tracingSessionWrapper: TracingSessionWrapper): void {
-    if (this.tracingSessionWrapper === tracingSessionWrapper) {
-      this.clearRecordingState();
-    }
-  }
-
-  maybeOnTraceData(
-    tracingSessionWrapper: TracingSessionWrapper,
-    trace: Uint8Array,
-  ) {
-    if (this.tracingSessionWrapper !== tracingSessionWrapper) {
-      return;
-    }
-    globals.dispatch(
-      Actions.openTraceFromBuffer({
-        title: 'Recorded trace',
-        buffer: trace.buffer,
-        fileName: `trace_${currentDateHourAndMinute()}${TRACE_SUFFIX}`,
-      }),
-    );
-    this.clearRecordingState();
-  }
-
-  maybeOnStatus(tracingSessionWrapper: TracingSessionWrapper, message: string) {
-    if (this.tracingSessionWrapper !== tracingSessionWrapper) {
-      return;
-    }
-    // For the 'Recording in progress for 7000ms we don't show a
-    // modal.'
-    if (message.startsWith(RECORDING_IN_PROGRESS)) {
-      globals.dispatch(Actions.setRecordingStatus({status: message}));
-    } else {
-      // For messages such as 'Please allow USB debugging on your
-      // device, which require a user action, we show a modal.
-      showRecordingModal(message);
-    }
-  }
-
-  maybeOnDisconnect(
-    tracingSessionWrapper: TracingSessionWrapper,
-    errorMessage?: string,
-  ) {
-    if (this.tracingSessionWrapper !== tracingSessionWrapper) {
-      return;
-    }
-    if (errorMessage) {
-      showRecordingModal(errorMessage);
-    }
-    this.clearRecordingState();
-    this.onTargetChange();
-  }
-
-  maybeOnError(
-    tracingSessionWrapper: TracingSessionWrapper,
-    errorMessage: string,
-  ) {
-    if (this.tracingSessionWrapper !== tracingSessionWrapper) {
-      return;
-    }
-    showRecordingModal(errorMessage);
-    this.clearRecordingState();
-  }
-
-  getTargetInfo(): TargetInfo | undefined {
-    if (!this.target) {
-      return undefined;
-    }
-    return this.target.getInfo();
-  }
-
-  canCreateTracingSession() {
-    if (!this.target) {
-      return false;
-    }
-    return this.target.canCreateTracingSession();
-  }
-
-  selectTarget(selectedTarget?: RecordingTargetV2) {
-    assertTrue(
-      RecordingState.NO_TARGET <= this.state &&
-        this.state < RecordingState.RECORDING,
-    );
-    // If the selected target exists and is the same as the previous one, we
-    // don't need to do anything.
-    if (selectedTarget && selectedTarget === this.target) {
-      return;
-    }
-
-    // We assign the new target and redraw the page.
-    this.target = selectedTarget;
-
-    if (!this.target) {
-      this.setState(RecordingState.NO_TARGET);
-      raf.scheduleFullRedraw();
-      return;
-    }
-    this.setState(RecordingState.TARGET_SELECTED);
-    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(globals.state.recordConfig);
-
-    const target = this.getTarget();
-    const targetInfo = target.getInfo();
-    globals.logging.logEvent(
-      'Record Trace',
-      `Record trace (${targetInfo.targetType})`,
-    );
-    const traceConfig = genTraceConfig(globals.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.
-    raf.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)) {
-      raf.scheduleFullRedraw();
-      return;
-    }
-    // If the change happens to a new target or the controller does not have a
-    // defined target, the selection process again is run again.
-    this.selectTarget();
-  }
-
-  private createTracingSessionWrapper(
-    target: RecordingTargetV2,
-  ): TracingSessionWrapper {
-    return new TracingSessionWrapper(target, this);
-  }
-
-  private clearRecordingState(): void {
-    this.bufferUsagePercentage = 0;
-    this.tracingSessionWrapper = undefined;
-    this.setState(RecordingState.TARGET_INFO_DISPLAYED);
-    globals.dispatch(Actions.setRecordingStatus({status: undefined}));
-    // Redrawing because this method has changed the RecordingState, which will
-    // affect the display of the record_page.
-    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/common/recordingV2/target_factories/android_websocket_target_factory.ts b/ui/src/common/recordingV2/target_factories/android_websocket_target_factory.ts
deleted file mode 100644
index 21097eb..0000000
--- a/ui/src/common/recordingV2/target_factories/android_websocket_target_factory.ts
+++ /dev/null
@@ -1,275 +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 {RECORDING_V2_FLAG} from '../../../core/feature_flags';
-import {
-  OnTargetChangeCallback,
-  RecordingTargetV2,
-  TargetFactory,
-} from '../recording_interfaces_v2';
-import {
-  buildAbdWebsocketCommand,
-  WEBSOCKET_CLOSED_ABNORMALLY_CODE,
-} from '../recording_utils';
-import {targetFactoryRegistry} from '../target_factory_registry';
-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;
-  }
-}
-
-// We only want to instantiate this class if Recording V2 is enabled.
-if (RECORDING_V2_FLAG.get()) {
-  targetFactoryRegistry.register(new AndroidWebsocketTargetFactory());
-}
diff --git a/ui/src/common/recordingV2/target_factories/android_webusb_target_factory.ts b/ui/src/common/recordingV2/target_factories/android_webusb_target_factory.ts
deleted file mode 100644
index d27ab07..0000000
--- a/ui/src/common/recordingV2/target_factories/android_webusb_target_factory.ts
+++ /dev/null
@@ -1,165 +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 {RECORDING_V2_FLAG} from '../../../core/feature_flags';
-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 {targetFactoryRegistry} from '../target_factory_registry';
-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;
-  }
-}
-
-// We only want to instantiate this class if:
-// 1. The browser implements the USB functionality.
-// 2. Recording V2 is enabled.
-// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-if (navigator.usb && RECORDING_V2_FLAG.get()) {
-  targetFactoryRegistry.register(new AndroidWebusbTargetFactory(navigator.usb));
-}
diff --git a/ui/src/common/recordingV2/target_factory_registry.ts b/ui/src/common/recordingV2/target_factory_registry.ts
deleted file mode 100644
index 54cec4a..0000000
--- a/ui/src/common/recordingV2/target_factory_registry.ts
+++ /dev/null
@@ -1,47 +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 {Registry} from '../../base/registry';
-
-import {RecordingTargetV2, TargetFactory} from './recording_interfaces_v2';
-
-export class TargetFactoryRegistry extends Registry<TargetFactory> {
-  listTargets(): RecordingTargetV2[] {
-    const targets: RecordingTargetV2[] = [];
-    for (const factory of this.registry.values()) {
-      for (const target of factory.listTargets()) {
-        targets.push(target);
-      }
-    }
-    return targets;
-  }
-
-  listTargetFactories(): TargetFactory[] {
-    return Array.from(this.registry.values());
-  }
-
-  listRecordingProblems(): string[] {
-    const recordingProblems: string[] = [];
-    for (const factory of this.registry.values()) {
-      for (const recordingProblem of factory.listRecordingProblems()) {
-        recordingProblems.push(recordingProblem);
-      }
-    }
-    return recordingProblems;
-  }
-}
-
-export const targetFactoryRegistry = new TargetFactoryRegistry((f) => {
-  return f.kind;
-});
diff --git a/ui/src/common/recordingV2/targets/android_target.ts b/ui/src/common/recordingV2/targets/android_target.ts
deleted file mode 100644
index 926846d..0000000
--- a/ui/src/common/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/common/recordingV2/targets/android_webusb_target.ts b/ui/src/common/recordingV2/targets/android_webusb_target.ts
deleted file mode 100644
index e70a19a..0000000
--- a/ui/src/common/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/common/recordingV2/traced_tracing_session.ts b/ui/src/common/recordingV2/traced_tracing_session.ts
deleted file mode 100644
index 635581a..0000000
--- a/ui/src/common/recordingV2/traced_tracing_session.ts
+++ /dev/null
@@ -1,441 +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/common/recordingV2/websocket_menu_controller.ts b/ui/src/common/recordingV2/websocket_menu_controller.ts
deleted file mode 100644
index f371672..0000000
--- a/ui/src/common/recordingV2/websocket_menu_controller.ts
+++ /dev/null
@@ -1,75 +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 '../../frontend/recording/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/common/resolution.ts b/ui/src/common/resolution.ts
index bef056f..43a9fe9 100644
--- a/ui/src/common/resolution.ts
+++ b/ui/src/common/resolution.ts
@@ -14,7 +14,7 @@
 
 import {BigintMath} from '../base/bigint_math';
 import {duration} from '../base/time';
-import {HighPrecisionTimeSpan} from './high_precision_time_span';
+import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
 
 /**
  * Work out an appropriate "resolution" for a given time span stretched over a
diff --git a/ui/src/common/schema.ts b/ui/src/common/schema.ts
deleted file mode 100644
index c3f33e4..0000000
--- a/ui/src/common/schema.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {Engine} from '../trace_processor/engine';
-import {STR} from '../trace_processor/query_result';
-
-const CACHED_SCHEMAS = new WeakMap<Engine, DatabaseSchema>();
-
-export class SchemaError extends Error {
-  constructor(message: string) {
-    super(message);
-  }
-}
-
-// POJO representing the table structure of trace_processor.
-// Exposed for testing.
-export interface DatabaseInfo {
-  tables: TableInfo[];
-}
-
-interface TableInfo {
-  name: string;
-  parent?: TableInfo;
-  columns: ColumnInfo[];
-}
-
-interface ColumnInfo {
-  name: string;
-}
-
-async function getColumns(
-  engine: Engine,
-  table: string,
-): Promise<ColumnInfo[]> {
-  const result = await engine.query(`PRAGMA table_info(${table});`);
-  const it = result.iter({
-    name: STR,
-  });
-  const columns = [];
-  for (; it.valid(); it.next()) {
-    columns.push({name: it['name']});
-  }
-  return columns;
-}
-
-// Opinionated view on the schema of the given trace_processor instance
-// suitable for EventSets to use for query generation.
-export class DatabaseSchema {
-  private tableToKeys: Map<string, Set<string>>;
-
-  constructor(info: DatabaseInfo) {
-    this.tableToKeys = new Map();
-    for (const tableInfo of info.tables) {
-      const columns = new Set(tableInfo.columns.map((c) => c.name));
-      this.tableToKeys.set(tableInfo.name, columns);
-    }
-  }
-
-  // Return all the EventSet keys available for a given table. This
-  // includes the direct columns on the table (and all parent tables)
-  // as well as all direct and indirect joinable tables where the join
-  // is N:1 or 1:1. e.g. for the table thread_slice we also include
-  // the columns from thread, process, thread_track etc.
-  getKeys(tableName: string): Set<string> {
-    const columns = this.tableToKeys.get(tableName);
-    if (columns === undefined) {
-      throw new SchemaError(`Unknown table '${tableName}'`);
-    }
-    return columns;
-  }
-}
-
-// Deliberately not exported. Users should call getSchema below and
-// participate in cacheing.
-async function createSchema(engine: Engine): Promise<DatabaseSchema> {
-  const tables: TableInfo[] = [];
-  const result = await engine.query(`SELECT name from perfetto_tables;`);
-  const it = result.iter({
-    name: STR,
-  });
-  for (; it.valid(); it.next()) {
-    const name = it['name'];
-    tables.push({
-      name,
-      columns: await getColumns(engine, name),
-    });
-  }
-
-  const database: DatabaseInfo = {
-    tables,
-  };
-
-  return new DatabaseSchema(database);
-}
-
-// Get the schema for the given engine (from the cache if possible).
-// The schemas are per-engine (i.e. they can't be statically determined
-// at build time) since we might be in httpd mode and not-running
-// against the version of trace_processor we build with.
-export async function getSchema(engine: Engine): Promise<DatabaseSchema> {
-  const schema = CACHED_SCHEMAS.get(engine);
-  if (schema === undefined) {
-    const newSchema = await createSchema(engine);
-    CACHED_SCHEMAS.set(engine, newSchema);
-    return newSchema;
-  }
-  return schema;
-}
diff --git a/ui/src/common/schema_unittest.ts b/ui/src/common/schema_unittest.ts
deleted file mode 100644
index f0a6a58..0000000
--- a/ui/src/common/schema_unittest.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {DatabaseInfo, DatabaseSchema, SchemaError} from './schema';
-
-test('DatabaseSchema > getKeys', () => {
-  const info: DatabaseInfo = {
-    tables: [
-      {
-        name: 'slice',
-        columns: [{name: 'id'}, {name: 'ts'}, {name: 'dur'}],
-      },
-    ],
-  };
-  const schema = new DatabaseSchema(info);
-  expect(schema.getKeys('slice')).toEqual(new Set(['id', 'ts', 'dur']));
-});
-
-test('DatabaseSchema > getKeys > Sad path', () => {
-  const info: DatabaseInfo = {
-    tables: [],
-  };
-  const schema = new DatabaseSchema(info);
-  expect(() => schema.getKeys('foo')).toThrow(SchemaError);
-});
diff --git a/ui/src/common/search_data.ts b/ui/src/common/search_data.ts
deleted file mode 100644
index 7209c04..0000000
--- a/ui/src/common/search_data.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 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
-//
-//      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 SearchSource = 'cpu' | 'log' | 'slice' | 'track';
-
-export interface SearchSummary {
-  tsStarts: BigInt64Array;
-  tsEnds: BigInt64Array;
-  count: Uint8Array;
-}
-
-export interface CurrentSearchResults {
-  eventIds: Float64Array;
-  tses: BigInt64Array;
-  utids: Float64Array;
-  trackKeys: string[];
-  sources: SearchSource[];
-  totalResults: number;
-}
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
deleted file mode 100644
index 3ad7c5d..0000000
--- a/ui/src/common/state.ts
+++ /dev/null
@@ -1,883 +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 {time} from '../base/time';
-import {RecordConfig} from '../controller/record_config_types';
-import {
-  Aggregation,
-  PivotTree,
-  TableColumn,
-} from '../frontend/pivot_table_types';
-
-import {
-  selectionToLegacySelection,
-  Selection,
-  LegacySelection,
-} from '../core/selection_manager';
-
-export {
-  Selection,
-  SelectionKind,
-  NoteSelection,
-  SliceSelection,
-  HeapProfileSelection,
-  PerfSamplesSelection,
-  LegacySelection,
-  AreaSelection,
-  ProfileType,
-  ThreadSliceSelection,
-  CpuProfileSampleSelection,
-} from '../core/selection_manager';
-
-// Tracks within track groups (usually corresponding to processes) are sorted.
-// As we want to group all tracks related to a given thread together, we use
-// two keys:
-// - Primary key corresponds to a priority of a track block (all tracks related
-//   to a given thread or a single track if it's not thread-associated).
-// - Secondary key corresponds to a priority of a given thread-associated track
-//   within its thread track block.
-// Each track will have a sort key, which either a primary sort key
-// (for non-thread tracks) or a tid and secondary sort key (mapping of tid to
-// primary sort key is done independently).
-export enum PrimaryTrackSortKey {
-  DEBUG_TRACK,
-  NULL_TRACK,
-  PROCESS_SCHEDULING_TRACK,
-  PROCESS_SUMMARY_TRACK,
-  EXPECTED_FRAMES_SLICE_TRACK,
-  ACTUAL_FRAMES_SLICE_TRACK,
-  PERF_SAMPLES_PROFILE_TRACK,
-  HEAP_PROFILE_TRACK,
-  MAIN_THREAD,
-  RENDER_THREAD,
-  GPU_COMPLETION_THREAD,
-  CHROME_IO_THREAD,
-  CHROME_COMPOSITOR_THREAD,
-  ORDINARY_THREAD,
-  COUNTER_TRACK,
-  ASYNC_SLICE_TRACK,
-  ORDINARY_TRACK,
-}
-
-/**
- * A plain js object, holding objects of type |Class| keyed by string id.
- * We use this instead of using |Map| object since it is simpler and faster to
- * serialize for use in postMessage.
- */
-export interface ObjectById<Class extends {id: string}> {
-  [id: string]: Class;
-}
-
-// Same as ObjectById but the key parameter is called `key` rather than `id`.
-export interface ObjectByKey<Class extends {key: string}> {
-  [key: string]: Class;
-}
-
-export type OmniboxMode = 'SEARCH' | 'COMMAND';
-
-export interface OmniboxState {
-  omnibox: string;
-  mode: OmniboxMode;
-  force?: boolean;
-}
-
-export interface Area {
-  start: time;
-  end: time;
-  tracks: string[];
-}
-
-export const MAX_TIME = 180;
-
-// 3: TrackKindPriority and related sorting changes.
-// 5: Move a large number of items off frontendLocalState and onto state.
-// 6: Common PivotTableConfig and pivot table specific PivotTableState.
-// 7: Split Chrome categories in two and add 'symbolize ksyms' flag.
-// 8: Rename several variables
-// "[...]HeapProfileFlamegraph[...]" -> "[...]Flamegraph[...]".
-// 9: Add a field to track last loaded recording profile name
-// 10: Change last loaded profile tracking type to accommodate auto-save.
-// 11: Rename updateChromeCategories to fetchChromeCategories.
-// 12: Add a field to cache mapping from UI track ID to trace track ID in order
-//     to speed up flow arrows rendering.
-// 13: FlamegraphState changed to support area selection.
-// 14: Changed the type of uiTrackIdByTraceTrackId from `Map` to an object with
-// typed key/value because a `Map` does not preserve type during
-// serialisation+deserialisation.
-// 15: Added state for Pivot Table V2
-// 16: Added boolean tracking if the flamegraph modal was dismissed
-// 17:
-// - add currentEngineId to track the id of the current engine
-// - remove nextNoteId, nextAreaId and use nextId as a unique counter for all
-//   indexing except the indexing of the engines
-// 18: areaSelection change see b/235869542
-// 19: Added visualisedArgs state.
-// 20: Refactored thread sorting order.
-// 21: Updated perf sample selection to include a ts range instead of single ts
-// 22: Add log selection kind.
-// 23: Add log filtering criteria for Android log entries.
-// 24: Store only a single Engine.
-// 25: Move omnibox state off VisibleState.
-// 26: Add tags for filtering Android log entries.
-// 27. Add a text entry for filtering Android log entries.
-// 28. Add a boolean indicating if non matching log entries are hidden.
-// 29. Add ftrace state. <-- Borked, state contains a non-serializable object.
-// 30. Convert ftraceFilter.excludedNames from Set<string> to string[].
-// 31. Convert all timestamps to bigints.
-// 32. Add pendingDeeplink.
-// 33. Add plugins state.
-// 34. Add additional pendingDeeplink fields (query, pid).
-// 35. Add force to OmniboxState
-// 36. Remove metrics
-// 37. Add additional pendingDeeplink fields (visStart, visEnd).
-// 38. Add track tags.
-// 39. Ported cpu_slice, ftrace, and android_log tracks to plugin tracks. Track
-//     state entries now require a URI and old track implementations are no
-//     longer registered.
-// 40. Ported counter, process summary/sched, & cpu_freq to plugin tracks.
-// 41. Ported all remaining tracks.
-// 42. Rename trackId -> trackKey.
-// 43. Remove visibleTracks.
-// 44. Add TabsV2 state.
-// 45. Remove v1 tracks.
-// 46. Remove trackKeyByTrackId.
-// 47. Selection V2
-// 48. Rename legacySelection -> selection and introduce new Selection type.
-// 49. Remove currentTab, which is only relevant to TabsV1.
-// 50. Remove ftrace filter state.
-// 51. Changed structure of FlamegraphState.expandedCallsiteByViewingOption.
-// 52. Update track group state - don't make the summary track the first track.
-// 53. Remove android log state.
-// 54. Remove traceTime.
-// 55. Rename TrackGroupState.id -> TrackGroupState.key.
-// 56. Renamed chrome slice to thread slice everywhere.
-// 57. Remove flamegraph related code from state.
-// 58. Remove area map.
-// 59. Deprecate old area selection type.
-// 60. Deprecate old note selection type.
-// 61. Remove params/state from TrackState.
-export const STATE_VERSION = 61;
-
-export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
-
-export type EngineMode = 'WASM' | 'HTTP_RPC';
-
-export type NewEngineMode = 'USE_HTTP_RPC_IF_AVAILABLE' | 'FORCE_BUILTIN_WASM';
-
-// Key that is used to sort tracks within a block of tracks associated with a
-// given thread.
-export enum InThreadTrackSortKey {
-  THREAD_COUNTER_TRACK,
-  THREAD_SCHEDULING_STATE_TRACK,
-  CPU_STACK_SAMPLES_TRACK,
-  VISUALISED_ARGS_TRACK,
-  ORDINARY,
-  DEFAULT_TRACK,
-}
-
-// Sort key used for sorting tracks associated with a thread.
-export type ThreadTrackSortKey = {
-  utid: number;
-  priority: InThreadTrackSortKey;
-};
-
-// Sort key for all tracks: both thread-associated and non-thread associated.
-export type TrackSortKey = PrimaryTrackSortKey | ThreadTrackSortKey;
-
-// Mapping which defines order for threads within a given process.
-export type UtidToTrackSortKey = {
-  [utid: number]: {
-    tid?: number;
-    sortKey: PrimaryTrackSortKey;
-  };
-};
-
-export interface TraceFileSource {
-  type: 'FILE';
-  file: File;
-}
-
-export interface TraceArrayBufferSource {
-  type: 'ARRAY_BUFFER';
-  buffer: ArrayBuffer;
-  title: string;
-  url?: string;
-  fileName?: string;
-
-  // |uuid| is set only when loading via ?local_cache_key=1234. When set,
-  // this matches global.state.traceUuid, with the exception of the following
-  // time window: When a trace T1 is loaded and the user loads another trace T2,
-  // this |uuid| will be == T2, but the globals.state.traceUuid will be
-  // temporarily == T1 until T2 has been loaded (consistently to what happens
-  // with all other state fields).
-  uuid?: string;
-  // if |localOnly| is true then the trace should not be shared or downloaded.
-  localOnly?: boolean;
-
-  // The set of extra args, keyed by plugin, that can be passed when opening the
-  // trace via postMessge deep-linking. See post_message_handler.ts for details.
-  pluginArgs?: {[pluginId: string]: {[key: string]: unknown}};
-}
-
-export interface TraceUrlSource {
-  type: 'URL';
-  url: string;
-}
-
-export interface TraceHttpRpcSource {
-  type: 'HTTP_RPC';
-}
-
-export type TraceSource =
-  | TraceFileSource
-  | TraceArrayBufferSource
-  | TraceUrlSource
-  | TraceHttpRpcSource;
-
-export interface TrackState {
-  uri: string;
-  key: string;
-  name: string;
-  trackSortKey: TrackSortKey;
-  trackGroup?: string;
-  closeable?: boolean;
-}
-
-export interface TrackGroupState {
-  key: string;
-  name: string;
-  collapsed: boolean;
-  tracks: string[]; // Child track ids.
-  fixedOrdering?: boolean; // Render tracks without sorting.
-  summaryTrack: string | undefined;
-}
-
-export interface EngineConfig {
-  id: string;
-  mode?: EngineMode; // Is undefined until |ready| is true.
-  ready: boolean;
-  failed?: string; // If defined the engine has crashed with the given message.
-  source: TraceSource;
-}
-
-export interface QueryConfig {
-  id: string;
-  engineId?: string;
-  query: string;
-}
-
-export interface Status {
-  msg: string;
-  timestamp: number; // Epoch in seconds (Date.now() / 1000).
-}
-
-export interface Note {
-  noteType: 'DEFAULT';
-  id: string;
-  timestamp: time;
-  color: string;
-  text: string;
-}
-
-export interface SpanNote {
-  noteType: 'SPAN';
-  id: string;
-  start: time;
-  end: time;
-  color: string;
-  text: string;
-}
-
-export interface Pagination {
-  offset: number;
-  count: number;
-}
-
-export interface RecordingTarget {
-  name: string;
-  os: TargetOs;
-}
-
-export interface AdbRecordingTarget extends RecordingTarget {
-  serial: string;
-}
-
-export interface Sorting {
-  column: string;
-  direction: 'DESC' | 'ASC';
-}
-
-export interface AggregationState {
-  id: string;
-  sorting?: Sorting;
-}
-
-// Auxiliary metadata needed to parse the query result, as well as to render it
-// correctly. Generated together with the text of query and passed without the
-// change to the query response.
-export interface PivotTableQueryMetadata {
-  pivotColumns: TableColumn[];
-  aggregationColumns: Aggregation[];
-  countIndex: number;
-}
-
-// Everything that's necessary to run the query for pivot table
-export interface PivotTableQuery {
-  text: string;
-  metadata: PivotTableQueryMetadata;
-}
-
-// Pivot table query result
-export interface PivotTableResult {
-  // Hierarchical pivot structure on top of rows
-  tree: PivotTree;
-  // Copy of the query metadata from the request, bundled up with the query
-  // result to ensure the correct rendering.
-  metadata: PivotTableQueryMetadata;
-}
-
-// Input parameters to check whether the pivot table needs to be re-queried.
-export interface PivotTableAreaState {
-  start: time;
-  end: time;
-  tracks: string[];
-}
-
-export interface PivotTableState {
-  // Currently selected area, if null, pivot table is not going to be visible.
-  selectionArea?: PivotTableAreaState;
-
-  // Query response
-  queryResult: PivotTableResult | null;
-
-  // Selected pivots for tables other than slice.
-  // Because of the query generation, pivoting happens first on non-slice
-  // pivots; therefore, those can't be put after slice pivots. In order to
-  // maintain the separation more clearly, slice and non-slice pivots are
-  // located in separate arrays.
-  selectedPivots: TableColumn[];
-
-  // Selected aggregation columns. Stored same way as pivots.
-  selectedAggregations: Aggregation[];
-
-  // Whether the pivot table results should be constrained to the selected area.
-  constrainToArea: boolean;
-
-  // Set to true by frontend to request controller to perform the query to
-  // acquire the necessary data from the engine.
-  queryRequested: boolean;
-}
-
-export interface LoadedConfigNone {
-  type: 'NONE';
-}
-
-export interface LoadedConfigAutomatic {
-  type: 'AUTOMATIC';
-}
-
-export interface LoadedConfigNamed {
-  type: 'NAMED';
-  name: string;
-}
-
-export type LoadedConfig =
-  | LoadedConfigNone
-  | LoadedConfigAutomatic
-  | LoadedConfigNamed;
-
-export interface NonSerializableState {
-  pivotTable: PivotTableState;
-}
-
-export interface PendingDeeplinkState {
-  ts?: string;
-  dur?: string;
-  tid?: string;
-  pid?: string;
-  query?: string;
-  visStart?: string;
-  visEnd?: string;
-}
-
-export interface TabsV2State {
-  openTabs: string[];
-  currentTab: string;
-}
-
-export interface State {
-  version: number;
-  nextId: string;
-
-  /**
-   * State of the ConfigEditor.
-   */
-  recordConfig: RecordConfig;
-  displayConfigAsPbtxt: boolean;
-  lastLoadedConfig: LoadedConfig;
-
-  /**
-   * Open traces.
-   */
-  newEngineMode: NewEngineMode;
-  engine?: EngineConfig;
-  traceUuid?: string;
-  trackGroups: ObjectByKey<TrackGroupState>;
-  tracks: ObjectByKey<TrackState>;
-  utidToThreadSortKey: UtidToTrackSortKey;
-  aggregatePreferences: ObjectById<AggregationState>;
-  scrollingTracks: string[];
-  pinnedTracks: string[];
-  debugTrackId?: string;
-  lastTrackReloadRequest?: number;
-  queries: ObjectById<QueryConfig>;
-  notes: ObjectById<Note | SpanNote>;
-  status: Status;
-  selection: Selection;
-  traceConversionInProgress: boolean;
-  flamegraphModalDismissed: boolean;
-
-  // Show track perf debugging overlay
-  perfDebug: boolean;
-
-  // Show the sidebar extended
-  sidebarVisible: boolean;
-
-  // Hovered and focused events
-  hoveredUtid: number;
-  hoveredPid: number;
-  hoverCursorTimestamp: time;
-  hoveredNoteTimestamp: time;
-  highlightedSliceId: number;
-  focusedFlowIdLeft: number;
-  focusedFlowIdRight: number;
-  pendingScrollId?: number;
-
-  searchIndex: number;
-
-  tabs: TabsV2State;
-
-  /**
-   * Trace recording
-   */
-  recordingInProgress: boolean;
-  recordingCancelled: boolean;
-  extensionInstalled: boolean;
-  recordingTarget: RecordingTarget;
-  availableAdbDevices: AdbRecordingTarget[];
-  lastRecordingError?: string;
-  recordingStatus?: string;
-
-  fetchChromeCategories: boolean;
-  chromeCategories: string[] | undefined;
-
-  // Special key: this part of the state is not going to be serialized when
-  // using permalink. Can be used to store those parts of the state that can't
-  // be serialized at the moment, such as ES6 Set and Map.
-  nonSerializableState: NonSerializableState;
-
-  // Omnibox info.
-  omniboxState: OmniboxState;
-
-  // Pending deeplink which will happen when we first finish opening a
-  // trace.
-  pendingDeeplink?: PendingDeeplinkState;
-
-  // Individual plugin states
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  plugins: {[key: string]: any};
-
-  trackFilterTerm: string | undefined;
-}
-
-export declare type RecordMode =
-  | 'STOP_WHEN_FULL'
-  | 'RING_BUFFER'
-  | 'LONG_TRACE';
-
-// 'Q','P','O' for Android, 'L' for Linux, 'C' for Chrome.
-export declare type TargetOs =
-  | 'S'
-  | 'R'
-  | 'Q'
-  | 'P'
-  | 'O'
-  | 'C'
-  | 'L'
-  | 'CrOS'
-  | 'Win';
-
-export function isAndroidP(target: RecordingTarget) {
-  return target.os === 'P';
-}
-
-export function isAndroidTarget(target: RecordingTarget) {
-  return ['Q', 'P', 'O'].includes(target.os);
-}
-
-export function isChromeTarget(target: RecordingTarget) {
-  return ['C', 'CrOS'].includes(target.os);
-}
-
-export function isCrOSTarget(target: RecordingTarget) {
-  return target.os === 'CrOS';
-}
-
-export function isLinuxTarget(target: RecordingTarget) {
-  return target.os === 'L';
-}
-
-export function isWindowsTarget(target: RecordingTarget) {
-  return target.os === 'Win';
-}
-
-export function isAdbTarget(
-  target: RecordingTarget,
-): target is AdbRecordingTarget {
-  return !!(target as AdbRecordingTarget).serial;
-}
-
-export function hasActiveProbes(config: RecordConfig) {
-  const fieldsWithEmptyResult = new Set<string>([
-    'hpBlockClient',
-    'allAtraceApps',
-    'chromePrivacyFiltering',
-  ]);
-  let key: keyof RecordConfig;
-  for (key in config) {
-    if (
-      typeof config[key] === 'boolean' &&
-      config[key] === true &&
-      !fieldsWithEmptyResult.has(key)
-    ) {
-      return true;
-    }
-  }
-  if (config.chromeCategoriesSelected.length > 0) {
-    return true;
-  }
-  return config.chromeHighOverheadCategoriesSelected.length > 0;
-}
-
-export function getDefaultRecordingTargets(): RecordingTarget[] {
-  return [
-    {os: 'Q', name: 'Android Q+ / 10+'},
-    {os: 'P', name: 'Android P / 9'},
-    {os: 'O', name: 'Android O- / 8-'},
-    {os: 'C', name: 'Chrome'},
-    {os: 'CrOS', name: 'Chrome OS (system trace)'},
-    {os: 'L', name: 'Linux desktop'},
-    {os: 'Win', name: 'Windows desktop'},
-  ];
-}
-
-export function getBuiltinChromeCategoryList(): string[] {
-  // List of static Chrome categories, last updated at 2024-05-15 from HEAD of
-  // Chromium's //base/trace_event/builtin_categories.h.
-  return [
-    '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',
-  ];
-}
-
-export function getContainingGroupKey(
-  state: State,
-  trackKey: string,
-): null | string {
-  const track = state.tracks[trackKey];
-  if (track === undefined) {
-    return null;
-  }
-  const parentGroupKey = track.trackGroup;
-  if (!parentGroupKey) {
-    return null;
-  }
-  return parentGroupKey;
-}
-
-export function getLegacySelection(state: State): LegacySelection | null {
-  return selectionToLegacySelection(state.selection);
-}
diff --git a/ui/src/common/state_serialization.ts b/ui/src/common/state_serialization.ts
deleted file mode 100644
index b72551c..0000000
--- a/ui/src/common/state_serialization.ts
+++ /dev/null
@@ -1,281 +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 {globals} from '../frontend/globals';
-import {
-  SERIALIZED_STATE_VERSION,
-  APP_STATE_SCHEMA,
-  SerializedNote,
-  SerializedPluginState,
-  SerializedSelection,
-  SerializedAppState,
-} from './state_serialization_schema';
-import {ProfileType} from './state';
-import {TimeSpan} from '../base/time';
-
-// When it comes to serialization & permalinks there are two different use cases
-// 1. Uploading the current trace in a Cloud Storage (GCS) file AND serializing
-//    the app state into a different GCS JSON file. This is what happens when
-//    clicking on "share trace" on a local file manually opened.
-// 2. [future use case] Uploading the current state in a GCS JSON file, but
-//    letting the trace file come from a deep-link via postMessage().
-//    This is the case when traces are opened via Dashboards (e.g. APC) and we
-//    want to persist only the state itself, not the trace file.
-//
-// In order to do so, we have two layers of serialization
-// 1. Serialization of the app state (This file):
-//    This is a JSON object that represents the visual app state (pinned tracks,
-//    visible viewport bounds, etc) BUT not the trace source.
-// 2. An outer layer that contains the app state AND a link to the trace file.
-//    (permalink.ts)
-//
-// In a nutshell:
-//   AppState:  {viewport: {...}, pinnedTracks: {...}, notes: {...}}
-//   Permalink: {appState: {see above}, traceUrl: 'https://gcs/trace/file'}
-//
-// This file deals with the app state. permalink.ts deals with the outer layer.
-
-/**
- * Serializes the current app state into a JSON-friendly POJO that can be stored
- * in a permalink (@see permalink.ts).
- * @returns A @type {SerializedAppState} object, @see state_serialization_schema.ts
- */
-export function serializeAppState(): SerializedAppState {
-  const vizWindow = globals.timeline.visibleWindow.toTimeSpan();
-
-  const notes = new Array<SerializedNote>();
-  for (const [id, note] of Object.entries(globals.state.notes)) {
-    if (note.noteType === 'DEFAULT') {
-      notes.push({
-        noteType: 'DEFAULT',
-        id,
-        start: note.timestamp,
-        color: note.color,
-        text: note.text,
-      });
-    } else if (note.noteType === 'SPAN') {
-      notes.push({
-        noteType: 'SPAN',
-        id,
-        start: note.start,
-        end: note.end,
-        color: note.color,
-        text: note.text,
-      });
-    }
-  }
-
-  const selection = new Array<SerializedSelection>();
-  const stateSel = globals.state.selection;
-  if (stateSel.kind === 'single') {
-    selection.push({
-      kind: 'TRACK_EVENT',
-      trackKey: stateSel.trackKey,
-      eventId: stateSel.eventId.toString(),
-    });
-  } else if (stateSel.kind === 'legacy') {
-    // TODO(primiano): get rid of these once we unify selection.
-    switch (stateSel.legacySelection.kind) {
-      case 'SLICE':
-        selection.push({
-          kind: 'LEGACY_SLICE',
-          id: stateSel.legacySelection.id,
-        });
-        break;
-      case 'SCHED_SLICE':
-        selection.push({
-          kind: 'LEGACY_SCHED_SLICE',
-          id: stateSel.legacySelection.id,
-        });
-        break;
-      case 'THREAD_STATE':
-        selection.push({
-          kind: 'LEGACY_THREAD_STATE',
-          id: stateSel.legacySelection.id,
-        });
-        break;
-      case 'HEAP_PROFILE':
-        selection.push({
-          kind: 'LEGACY_HEAP_PROFILE',
-          id: stateSel.legacySelection.id,
-          upid: stateSel.legacySelection.upid,
-          ts: stateSel.legacySelection.ts,
-          type: stateSel.legacySelection.type,
-        });
-    }
-  }
-
-  const plugins = new Array<SerializedPluginState>();
-  for (const [id, pluginState] of Object.entries(globals.state.plugins)) {
-    plugins.push({id, state: pluginState});
-  }
-
-  return {
-    version: SERIALIZED_STATE_VERSION,
-    pinnedTracks: globals.state.pinnedTracks,
-    viewport: {
-      start: vizWindow.start,
-      end: vizWindow.end,
-    },
-    notes,
-    selection,
-    plugins,
-  };
-}
-
-export type ParseStateResult =
-  | {success: true; data: SerializedAppState}
-  | {success: false; error: string};
-
-/**
- * Parses the app state from a JSON blob.
- * @param jsonDecodedObj the output of JSON.parse() that needs validation
- * @returns Either a @type {SerializedAppState} object or an error.
- */
-export function parseAppState(jsonDecodedObj: unknown): ParseStateResult {
-  const parseRes = APP_STATE_SCHEMA.safeParse(jsonDecodedObj);
-  if (parseRes.success) {
-    if (parseRes.data.version == SERIALIZED_STATE_VERSION) {
-      return {success: true, data: parseRes.data};
-    } else {
-      return {
-        success: false,
-        error:
-          `SERIALIZED_STATE_VERSION mismatch ` +
-          `(actual: ${parseRes.data.version}, ` +
-          `expected: ${SERIALIZED_STATE_VERSION})`,
-      };
-    }
-  }
-  return {success: false, error: parseRes.error.toString()};
-}
-
-/**
- * This function gets invoked after the trace is loaded, but before plugins,
- * track decider and initial selections are run.
- * @param appState the .data object returned by parseAppState() when successful.
- */
-export function deserializeAppStatePhase1(appState: SerializedAppState): void {
-  // Restore the plugin state.
-  globals.store.edit((draft) => {
-    for (const p of appState.plugins ?? []) {
-      draft.plugins[p.id] = p.state ?? {};
-    }
-  });
-}
-
-/**
- * This function gets invoked after the trace controller has run and all plugins
- * have executed.
- * @param appState the .data object returned by parseAppState() when successful.
- */
-export function deserializeAppStatePhase2(appState: SerializedAppState): void {
-  if (appState.viewport !== undefined) {
-    globals.timeline.updateVisibleTime(
-      new TimeSpan(appState.viewport.start, appState.viewport.end),
-    );
-  }
-  globals.store.edit((draft) => {
-    // Restore the pinned tracks, if they exist.
-    const tracksToPin: string[] = [];
-    for (const trackKey of appState.pinnedTracks) {
-      if (trackKey in globals.state.tracks) {
-        tracksToPin.push(trackKey);
-      }
-    }
-    draft.pinnedTracks = tracksToPin;
-
-    // Restore notes.
-    for (const note of appState.notes) {
-      const commonArgs = {
-        id: note.id,
-        timestamp: note.start,
-        color: note.color,
-        text: note.text,
-      };
-      if (note.noteType === 'DEFAULT') {
-        draft.notes[note.id] = {
-          noteType: 'DEFAULT',
-          ...commonArgs,
-        };
-      } else if (note.noteType === 'SPAN') {
-        draft.notes[note.id] = {
-          noteType: 'SPAN',
-          start: commonArgs.timestamp,
-          end: note.end,
-          ...commonArgs,
-        };
-      }
-    }
-
-    // Restore the selection
-    const sel = appState.selection[0];
-    if (sel !== undefined) {
-      switch (sel.kind) {
-        case 'TRACK_EVENT':
-          draft.selection = {
-            kind: 'single',
-            trackKey: sel.trackKey,
-            eventId: parseInt(sel.eventId),
-          };
-          break;
-        case 'LEGACY_SCHED_SLICE':
-          draft.selection = {
-            kind: 'legacy',
-            legacySelection: {kind: 'SCHED_SLICE', id: sel.id},
-          };
-          break;
-        case 'LEGACY_SLICE':
-          draft.selection = {
-            kind: 'legacy',
-            legacySelection: {kind: 'SLICE', id: sel.id},
-          };
-          break;
-        case 'LEGACY_THREAD_STATE':
-          draft.selection = {
-            kind: 'legacy',
-            legacySelection: {kind: 'THREAD_STATE', id: sel.id},
-          };
-          break;
-        case 'LEGACY_HEAP_PROFILE':
-          draft.selection = {
-            kind: 'legacy',
-            legacySelection: {
-              kind: 'HEAP_PROFILE',
-              id: sel.id,
-              upid: sel.upid,
-              ts: sel.ts,
-              type: sel.type as ProfileType,
-            },
-          };
-          break;
-      }
-    }
-  });
-}
-
-/**
- * Performs JSON serialization, taking care of also serializing BigInt->string.
- * For the matching deserializer see zType in state_serialization_schema.ts.
- * @param obj A POJO, typically a SerializedAppState or PermalinkState.
- * @returns JSON-encoded string.
- */
-export function JsonSerialize(obj: Object): string {
-  return JSON.stringify(obj, (_key, value) => {
-    if (typeof value === 'bigint') {
-      return value.toString();
-    }
-    return value;
-  });
-}
diff --git a/ui/src/common/state_serialization_schema.ts b/ui/src/common/state_serialization_schema.ts
deleted file mode 100644
index 7adbecb..0000000
--- a/ui/src/common/state_serialization_schema.ts
+++ /dev/null
@@ -1,88 +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 {z} from 'zod';
-import {Time} from '../base/time';
-
-// This should be bumped only in case of breaking changes that cannot be
-// addressed using zod's z.optional(), z.default() or z.coerce.xxx().
-// Ideally these cases should be extremely rare.
-export const SERIALIZED_STATE_VERSION = 1;
-
-// At deserialization time this takes a string as input and converts it into a
-// BigInt. The serialization side of this is handled by JsonSerialize(), which
-// converts BigInt into strings when invoking JSON.stringify.
-const zTime = z
-  .string()
-  .regex(/[-]?\d+/)
-  .transform((s) => Time.fromRaw(BigInt(s)));
-
-const SELECTION_SCHEMA = z.discriminatedUnion('kind', [
-  z.object({
-    kind: z.literal('TRACK_EVENT'),
-    trackKey: z.string(),
-    eventId: z.string(),
-  }),
-  z.object({kind: z.literal('LEGACY_SLICE'), id: z.number()}),
-  z.object({kind: z.literal('LEGACY_SCHED_SLICE'), id: z.number()}),
-  z.object({kind: z.literal('LEGACY_THREAD_STATE'), id: z.number()}),
-  z.object({
-    kind: z.literal('LEGACY_HEAP_PROFILE'),
-    id: z.number(),
-    upid: z.number(),
-    ts: zTime,
-    type: z.string(),
-  }),
-]);
-
-export type SerializedSelection = z.infer<typeof SELECTION_SCHEMA>;
-
-const NOTE_SCHEMA = z
-  .object({
-    id: z.string(),
-    start: zTime,
-    color: z.string(),
-    text: z.string(),
-  })
-  .and(
-    z.discriminatedUnion('noteType', [
-      z.object({noteType: z.literal('DEFAULT')}),
-      z.object({noteType: z.literal('SPAN'), end: zTime}),
-    ]),
-  );
-
-export type SerializedNote = z.infer<typeof NOTE_SCHEMA>;
-
-const PLUGIN_SCHEMA = z.object({
-  id: z.string(),
-  state: z.any(),
-});
-
-export type SerializedPluginState = z.infer<typeof PLUGIN_SCHEMA>;
-
-export const APP_STATE_SCHEMA = z.object({
-  version: z.number(),
-  pinnedTracks: z.array(z.string()).default([]),
-  viewport: z
-    .object({
-      start: zTime,
-      end: zTime,
-    })
-    .optional(),
-  selection: z.array(SELECTION_SCHEMA).default([]),
-  notes: z.array(NOTE_SCHEMA).default([]),
-  plugins: z.array(PLUGIN_SCHEMA).default([]),
-});
-
-export type SerializedAppState = z.infer<typeof APP_STATE_SCHEMA>;
diff --git a/ui/src/common/state_unittest.ts b/ui/src/common/state_unittest.ts
deleted file mode 100644
index 3dc65e2..0000000
--- a/ui/src/common/state_unittest.ts
+++ /dev/null
@@ -1,45 +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 {PrimaryTrackSortKey} from '../public';
-
-import {createEmptyState} from './empty_state';
-import {getContainingGroupKey, State} from './state';
-
-test('createEmptyState', () => {
-  const state: State = createEmptyState();
-  expect(state.engine).toEqual(undefined);
-});
-
-test('getContainingTrackId', () => {
-  const state: State = createEmptyState();
-  state.tracks['a'] = {
-    key: 'a',
-    uri: 'Foo',
-    name: 'a track',
-    trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-  };
-
-  state.tracks['b'] = {
-    key: 'b',
-    uri: 'Foo',
-    name: 'b track',
-    trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-    trackGroup: 'containsB',
-  };
-
-  expect(getContainingGroupKey(state, 'z')).toEqual(null);
-  expect(getContainingGroupKey(state, 'a')).toEqual(null);
-  expect(getContainingGroupKey(state, 'b')).toEqual('containsB');
-});
diff --git a/ui/src/common/tab_registry.ts b/ui/src/common/tab_registry.ts
deleted file mode 100644
index db4ce2f..0000000
--- a/ui/src/common/tab_registry.ts
+++ /dev/null
@@ -1,150 +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 {DetailsPanel, LegacyDetailsPanel, TabDescriptor} from '../public';
-
-export interface ResolvedTab {
-  uri: string;
-  tab?: TabDescriptor;
-}
-
-/**
- * Stores tab & current selection section registries.
- * Keeps track of tab lifecycles.
- */
-export class TabManager implements Disposable {
-  private _registry = new Map<string, TabDescriptor>();
-  private _defaultTabs = new Set<string>();
-  private _legacyDetailsPanelRegistry = new Set<LegacyDetailsPanel>();
-  private _detailsPanelRegistry = new Set<DetailsPanel>();
-  private _currentTabs = new Map<string, TabDescriptor>();
-
-  [Symbol.dispose]() {
-    // Dispose of all tabs that are currently alive
-    for (const tab of this._currentTabs.values()) {
-      this.disposeTab(tab);
-    }
-    this._currentTabs.clear();
-  }
-
-  registerTab(desc: TabDescriptor): Disposable {
-    this._registry.set(desc.uri, desc);
-    return {
-      [Symbol.dispose]: () => this._registry.delete(desc.uri),
-    };
-  }
-
-  addDefaultTab(uri: string): Disposable {
-    this._defaultTabs.add(uri);
-    return {
-      [Symbol.dispose]: () => this._defaultTabs.delete(uri),
-    };
-  }
-
-  registerLegacyDetailsPanel(section: LegacyDetailsPanel): Disposable {
-    this._legacyDetailsPanelRegistry.add(section);
-    return {
-      [Symbol.dispose]: () => this._legacyDetailsPanelRegistry.delete(section),
-    };
-  }
-
-  registerDetailsPanel(section: DetailsPanel): Disposable {
-    this._detailsPanelRegistry.add(section);
-    return {
-      [Symbol.dispose]: () => this._detailsPanelRegistry.delete(section),
-    };
-  }
-
-  resolveTab(uri: string): TabDescriptor | undefined {
-    return this._registry.get(uri);
-  }
-
-  get tabs(): TabDescriptor[] {
-    return Array.from(this._registry.values());
-  }
-
-  get defaultTabs(): string[] {
-    return Array.from(this._defaultTabs);
-  }
-
-  get legacyDetailsPanels(): LegacyDetailsPanel[] {
-    return Array.from(this._legacyDetailsPanelRegistry);
-  }
-
-  get detailsPanels(): DetailsPanel[] {
-    return Array.from(this._detailsPanelRegistry);
-  }
-
-  /**
-   * Resolves a list of URIs to tabs and manages tab lifecycles.
-   * @param tabUris List of tabs.
-   * @returns List of resolved tabs.
-   */
-  resolveTabs(tabUris: string[]): ResolvedTab[] {
-    // Refresh the list of old tabs
-    const newTabs = new Map<string, TabDescriptor>();
-    const tabs: ResolvedTab[] = [];
-
-    tabUris.forEach((uri) => {
-      const newTab = this._registry.get(uri);
-      tabs.push({uri, tab: newTab});
-
-      if (newTab) {
-        newTabs.set(uri, newTab);
-      }
-    });
-
-    // Call onShow() on any new tabs.
-    for (const [uri, tab] of newTabs) {
-      const oldTab = this._currentTabs.get(uri);
-      if (!oldTab) {
-        this.initTab(tab);
-      }
-    }
-
-    // Call onHide() on any tabs that have been removed.
-    for (const [uri, tab] of this._currentTabs) {
-      const newTab = newTabs.get(uri);
-      if (!newTab) {
-        this.disposeTab(tab);
-      }
-    }
-
-    this._currentTabs = newTabs;
-
-    return tabs;
-  }
-
-  /**
-   * Call onShow() on this tab.
-   * @param tab The tab to initialize.
-   */
-  private initTab(tab: TabDescriptor): void {
-    tab.onShow?.();
-  }
-
-  /**
-   * Call onHide() and maybe remove from registry if tab is ephemeral.
-   * @param tab The tab to dispose.
-   */
-  private disposeTab(tab: TabDescriptor): void {
-    // Attempt to call onHide
-    tab.onHide?.();
-
-    // If ephemeral, also unregister the tab
-    if (tab.isEphemeral) {
-      this._registry.delete(tab.uri);
-    }
-  }
-}
diff --git a/ui/src/common/thread_state.ts b/ui/src/common/thread_state.ts
deleted file mode 100644
index d94625c..0000000
--- a/ui/src/common/thread_state.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright (C) 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
-//
-//      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.
-
-const states: {[key: string]: string} = {
-  'R': 'Runnable',
-  'S': 'Sleeping',
-  'D': 'Uninterruptible Sleep',
-  'T': 'Stopped',
-  't': 'Traced',
-  'X': 'Exit (Dead)',
-  'Z': 'Exit (Zombie)',
-  'x': 'Task Dead',
-  'I': 'Idle',
-  'K': 'Wake Kill',
-  'W': 'Waking',
-  'P': 'Parked',
-  'N': 'No Load',
-  '+': '(Preempted)',
-};
-
-export function translateState(
-  state: string | undefined | null,
-  ioWait: boolean | undefined = undefined,
-) {
-  if (state === undefined) return '';
-
-  // Self describing states
-  switch (state) {
-    case 'Running':
-    case 'Initialized':
-    case 'Deferred Ready':
-    case 'Transition':
-    case 'Stand By':
-    case 'Waiting':
-      return state;
-  }
-
-  if (state === null) {
-    return 'Unknown';
-  }
-  let result = states[state[0]];
-  if (ioWait === true) {
-    result += ' (IO)';
-  } else if (ioWait === false) {
-    result += ' (non-IO)';
-  }
-  for (let i = 1; i < state.length; i++) {
-    result += state[i] === '+' ? ' ' : ' + ';
-    result += states[state[i]];
-  }
-  // state is some string we don't know how to translate.
-  if (result === undefined) return state;
-
-  return result;
-}
diff --git a/ui/src/common/track_cache.ts b/ui/src/common/track_cache.ts
deleted file mode 100644
index 5234240..0000000
--- a/ui/src/common/track_cache.ts
+++ /dev/null
@@ -1,270 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {Optional, exists} from '../base/utils';
-import {Registry} from '../base/registry';
-import {Store} from '../base/store';
-import {Track, TrackContext, TrackDescriptor, TrackRef} from '../public';
-
-import {ObjectByKey, State, TrackState} from './state';
-import {AsyncLimiter} from '../base/async_limiter';
-import {assertFalse} from '../base/logging';
-import {TrackRenderContext} from '../public/tracks';
-
-export interface TrackCacheEntry extends Disposable {
-  readonly trackKey: string;
-  readonly track: Track;
-  desc: TrackDescriptor;
-  render(ctx: TrackRenderContext): void;
-  getError(): Optional<Error>;
-}
-
-// This class is responsible for managing the lifecycle of tracks over render
-// cycles.
-
-// Example usage:
-// function render() {
-//   const trackCache = new TrackCache();
-//   const foo = trackCache.resolveTrack('foo', 'exampleURI', {});
-//   const bar = trackCache.resolveTrack('bar', 'exampleURI', {});
-//   trackCache.flushOldTracks(); // <-- Destroys any unused cached tracks
-// }
-
-// Example of how flushing works:
-// First cycle
-//   resolveTrack('foo', ...) <-- new track 'foo' created
-//   resolveTrack('bar', ...) <-- new track 'bar' created
-//   flushTracks()
-// Second cycle
-//   resolveTrack('foo', ...) <-- returns cached 'foo' track
-//   flushTracks() <-- 'bar' is destroyed, as it was not resolved this cycle
-// Third cycle
-//   flushTracks() <-- 'foo' is destroyed.
-export class TrackManager {
-  private _trackKeyByTrackId = new Map<number, string>();
-  private newTracks = new Map<string, TrackCacheEntry>();
-  private currentTracks = new Map<string, TrackCacheEntry>();
-  private trackRegistry = new Registry<TrackDescriptor>(({uri}) => uri);
-  private defaultTracks = new Set<TrackRef>();
-
-  private store: Store<State>;
-  private trackState?: ObjectByKey<TrackState>;
-
-  constructor(store: Store<State>) {
-    this.store = store;
-  }
-
-  get trackKeyByTrackId() {
-    this.updateTrackKeyByTrackIdMap();
-    return this._trackKeyByTrackId;
-  }
-
-  registerTrack(trackDesc: TrackDescriptor): Disposable {
-    return this.trackRegistry.register(trackDesc);
-  }
-
-  addPotentialTrack(track: TrackRef): Disposable {
-    this.defaultTracks.add(track);
-    return {
-      [Symbol.dispose]: () => this.defaultTracks.delete(track),
-    };
-  }
-
-  findPotentialTracks(): TrackRef[] {
-    return Array.from(this.defaultTracks);
-  }
-
-  getAllTracks(): TrackDescriptor[] {
-    return Array.from(this.trackRegistry.values());
-  }
-
-  // Look up track into for a given track's URI.
-  // Returns |undefined| if no track can be found.
-  resolveTrackInfo(uri: string): TrackDescriptor | undefined {
-    return this.trackRegistry.tryGet(uri);
-  }
-
-  // Creates a new track using |uri| and |params| or retrieves a cached track if
-  // |key| exists in the cache.
-  resolveTrack(key: string, trackDesc: TrackDescriptor): TrackCacheEntry {
-    // Search for a cached version of this track,
-    const cached = this.currentTracks.get(key);
-
-    // Ensure the cached track has the same factory type as the resolved track.
-    // If this has changed, the track should be re-created.
-    if (cached && trackDesc.trackFactory === cached.desc.trackFactory) {
-      // Keep our cached track descriptor up to date, if anything's changed.
-      cached.desc = trackDesc;
-
-      // Move this track from the recycle bin to the safe cache, which means
-      // it's safe from disposal for this cycle.
-      this.newTracks.set(key, cached);
-
-      return cached;
-    } else {
-      // Cached track doesn't exist or is out of date, create a new one.
-      const trackContext: TrackContext = {
-        trackKey: key,
-      };
-      const track = trackDesc.trackFactory(trackContext);
-      const entry = new TrackFSM(key, track, trackDesc, trackContext);
-
-      // Push track into the safe cache.
-      this.newTracks.set(key, entry);
-      return entry;
-    }
-  }
-
-  // Destroys all current tracks not present in the new cache.
-  flushOldTracks() {
-    for (const [key, entry] of this.currentTracks.entries()) {
-      if (!this.newTracks.has(key)) {
-        entry[Symbol.dispose]();
-      }
-    }
-
-    this.currentTracks = this.newTracks;
-    this.newTracks = new Map<string, TrackCacheEntry>();
-  }
-
-  private updateTrackKeyByTrackIdMap() {
-    if (this.trackState === this.store.state.tracks) {
-      return;
-    }
-
-    const trackKeyByTrackId = new Map<number, string>();
-
-    const trackList = Object.entries(this.store.state.tracks);
-    trackList.forEach(([key, {uri}]) => {
-      const desc = this.trackRegistry.get(uri);
-      for (const trackId of desc?.tags?.trackIds ?? []) {
-        const existingKey = trackKeyByTrackId.get(trackId);
-        if (exists(existingKey)) {
-          throw new Error(
-            `Trying to map track id ${trackId} to UI track ${key}, already mapped to ${existingKey}`,
-          );
-        }
-        trackKeyByTrackId.set(trackId, key);
-      }
-    });
-
-    this._trackKeyByTrackId = trackKeyByTrackId;
-    this.trackState = this.store.state.tracks;
-  }
-}
-
-/**
- * This function describes the asynchronous lifecycle of a track using an async
- * generator. This saves us having to build out the state machine explicitly,
- * using conventional serial programming techniques to describe the lifecycle
- * instead, which is more natural and easier to understand.
- *
- * We expect the params to onUpdate to be passed into the generator via the
- * yield function.
- *
- * @param track The track to run the lifecycle for.
- * @param ctx The trace context, passed to various lifecycle methods.
- */
-async function* trackLifecycle(
-  track: Track,
-  ctx: TrackContext,
-): AsyncGenerator<void, void, TrackRenderContext> {
-  try {
-    // Wait for parameters to be passed in before initializing the track
-    const trackRenderCtx = yield;
-    await Promise.resolve(track.onCreate?.(ctx));
-    await Promise.resolve(track.onUpdate?.(trackRenderCtx));
-
-    // Wait for parameters to be passed in before subsequent calls to onUpdate()
-    while (true) {
-      await Promise.resolve(track.onUpdate?.(yield));
-    }
-  } finally {
-    // Ensure we always clean up, even on throw or early return
-    await Promise.resolve(track.onDestroy?.());
-  }
-}
-
-/**
- * Wrapper that manages lifecycle hooks on behalf of a track, ensuring lifecycle
- * hooks are called synchronously and in the correct order.
- */
-class TrackFSM implements TrackCacheEntry {
-  public readonly trackKey: string;
-  public readonly track: Track;
-  public readonly desc: TrackDescriptor;
-
-  private readonly limiter = new AsyncLimiter();
-  private readonly ctx: TrackContext;
-  private readonly generator: ReturnType<typeof trackLifecycle>;
-
-  private error?: Error;
-  private isDisposed = false;
-
-  constructor(
-    trackKey: string,
-    track: Track,
-    desc: TrackDescriptor,
-    ctx: TrackContext,
-  ) {
-    this.trackKey = trackKey;
-    this.track = track;
-    this.desc = desc;
-    this.ctx = ctx;
-
-    this.generator = trackLifecycle(this.track, this.ctx);
-
-    // This just starts the generator, which will pause at the first yield
-    // without doing anything - note that the parameter to the first next() call
-    // is ignored in generators
-    this.generator.next();
-  }
-
-  render(ctx: TrackRenderContext): void {
-    assertFalse(this.isDisposed);
-
-    // The generator will ensure that track lifecycle calls don't overlap, but
-    // it'll also enqueue every single call to next() which can create a large
-    // backlog of updates assuming render is called faster than updates can
-    // complete (this is usually the case), so we use an AsyncLimiter here to
-    // avoid enqueueing more than one next().
-    this.limiter
-      .schedule(async () => {
-        // Pass in the parameters to onUpdate() here (i.e. the track size)
-        await this.generator.next(ctx);
-      })
-      .catch((e) => {
-        // Errors thrown inside lifecycle hooks will bubble up through the
-        // generator and AsyncLimiter to here, where we can swallow and capture
-        // the error
-        this.error = e;
-      });
-
-    // Always call render synchronously
-    this.track.render(ctx);
-  }
-
-  [Symbol.dispose](): void {
-    assertFalse(this.isDisposed);
-    this.isDisposed = true;
-
-    // Ask the generator to stop, it'll handle any cleanup and return at the
-    // next yield
-    this.generator.return();
-  }
-
-  getError(): Optional<Error> {
-    return this.error;
-  }
-}
diff --git a/ui/src/common/track_cache_unittest.ts b/ui/src/common/track_cache_unittest.ts
deleted file mode 100644
index b1b6e85..0000000
--- a/ui/src/common/track_cache_unittest.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {Duration} from '../base/time';
-import {PxSpan, TimeScale} from '../frontend/time_scale';
-import {createStore, TrackDescriptor} from '../public';
-import {TrackRenderContext} from '../public/tracks';
-
-import {createEmptyState} from './empty_state';
-import {HighPrecisionTime} from './high_precision_time';
-import {HighPrecisionTimeSpan} from './high_precision_time_span';
-import {TrackManager} from './track_cache';
-
-function makeMockTrack() {
-  return {
-    onCreate: jest.fn(),
-    onUpdate: jest.fn(),
-    onDestroy: jest.fn(),
-
-    render: jest.fn(),
-    onFullRedraw: jest.fn(),
-    getSliceRect: jest.fn(),
-    getHeight: jest.fn(),
-    getTrackShellButtons: jest.fn(),
-    onMouseMove: jest.fn(),
-    onMouseClick: jest.fn(),
-    onMouseOut: jest.fn(),
-  };
-}
-
-async function settle() {
-  await new Promise((r) => setTimeout(r, 0));
-}
-
-let mockTrack: ReturnType<typeof makeMockTrack>;
-let td: TrackDescriptor;
-let trackManager: TrackManager;
-const visibleWindow = new HighPrecisionTimeSpan(HighPrecisionTime.ZERO, 0);
-const dummyCtx: TrackRenderContext = {
-  trackKey: 'foo',
-  ctx: new CanvasRenderingContext2D(),
-  size: {width: 123, height: 123},
-  visibleWindow,
-  resolution: Duration.ZERO,
-  timescale: new TimeScale(visibleWindow, new PxSpan(0, 0)),
-};
-
-beforeEach(() => {
-  mockTrack = makeMockTrack();
-  td = {
-    uri: 'test',
-    title: 'foo',
-    trackFactory: () => mockTrack,
-  };
-  const store = createStore(createEmptyState());
-  trackManager = new TrackManager(store);
-});
-
-describe('TrackManager', () => {
-  it('calls track lifecycle hooks', async () => {
-    const entry = trackManager.resolveTrack('foo', td);
-
-    entry.render(dummyCtx);
-    await settle();
-    expect(mockTrack.onCreate).toHaveBeenCalledTimes(1);
-    expect(mockTrack.onUpdate).toHaveBeenCalledTimes(1);
-
-    entry[Symbol.dispose]();
-    await settle();
-    expect(mockTrack.onDestroy).toHaveBeenCalledTimes(1);
-  });
-
-  it('calls onCrate lazily', async () => {
-    // Check we wait until the first call to render before calling onCreate
-    const entry = trackManager.resolveTrack('foo', td);
-    await settle();
-    expect(mockTrack.onCreate).not.toHaveBeenCalled();
-
-    entry.render(dummyCtx);
-    await settle();
-    expect(mockTrack.onCreate).toHaveBeenCalledTimes(1);
-  });
-
-  it('reuses tracks', async () => {
-    const first = trackManager.resolveTrack('foo', td);
-    trackManager.flushOldTracks();
-    first.render(dummyCtx);
-    await settle();
-
-    const second = trackManager.resolveTrack('foo', td);
-    trackManager.flushOldTracks();
-    second.render(dummyCtx);
-    await settle();
-
-    expect(first).toBe(second);
-    // Ensure onCreate called only once
-    expect(mockTrack.onCreate).toHaveBeenCalledTimes(1);
-  });
-
-  it('destroys tracks when they are not resolved for one cycle', async () => {
-    const entry = trackManager.resolveTrack('foo', td);
-    entry.render(dummyCtx);
-
-    // Double flush should destroy all tracks
-    trackManager.flushOldTracks();
-    trackManager.flushOldTracks();
-
-    await settle();
-
-    expect(mockTrack.onDestroy).toHaveBeenCalledTimes(1);
-  });
-
-  it('throws on render after destroy', async () => {
-    const entry = trackManager.resolveTrack('foo', td);
-
-    // Double flush should destroy all tracks
-    trackManager.flushOldTracks();
-    trackManager.flushOldTracks();
-
-    await settle();
-
-    expect(() => entry.render(dummyCtx)).toThrow();
-  });
-
-  it('contains crash inside onCreate()', async () => {
-    const entry = trackManager.resolveTrack('foo', td);
-    const e = new Error();
-
-    // Mock crash inside onCreate
-    mockTrack.onCreate.mockImplementationOnce(() => {
-      throw e;
-    });
-
-    entry.render(dummyCtx);
-    await settle();
-
-    expect(mockTrack.onCreate).toHaveBeenCalledTimes(1);
-    expect(mockTrack.onUpdate).not.toHaveBeenCalled();
-    expect(mockTrack.onDestroy).toHaveBeenCalledTimes(1);
-    expect(entry.getError()).toBe(e);
-  });
-
-  it('contains crash inside onUpdate()', async () => {
-    const entry = trackManager.resolveTrack('foo', td);
-    const e = new Error();
-
-    // Mock crash inside onUpdate
-    mockTrack.onUpdate.mockImplementationOnce(() => {
-      throw e;
-    });
-
-    entry.render(dummyCtx);
-    await settle();
-
-    expect(mockTrack.onCreate).toHaveBeenCalledTimes(1);
-    expect(mockTrack.onDestroy).toHaveBeenCalledTimes(1);
-    expect(entry.getError()).toBe(e);
-  });
-
-  it('handles dispose after crash', async () => {
-    const entry = trackManager.resolveTrack('foo', td);
-    const e = new Error();
-
-    // Mock crash inside onUpdate
-    mockTrack.onUpdate.mockImplementationOnce(() => {
-      throw e;
-    });
-
-    entry.render(dummyCtx);
-    await settle();
-
-    // Ensure we don't crash while disposing
-    entry[Symbol.dispose]();
-    await settle();
-  });
-});
diff --git a/ui/src/common/track_helper.ts b/ui/src/common/track_helper.ts
index 2406587..3087228 100644
--- a/ui/src/common/track_helper.ts
+++ b/ui/src/common/track_helper.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import {BigintMath} from '../base/bigint_math';
-
 import {duration, Time, time, TimeSpan} from '../base/time';
 export {Store} from '../base/store';
 import {raf} from '../core/raf_scheduler';
diff --git a/ui/src/controller/adb.ts b/ui/src/controller/adb.ts
deleted file mode 100644
index e188ea7..0000000
--- a/ui/src/controller/adb.ts
+++ /dev/null
@@ -1,701 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {isString} from '../base/object_utils';
-import {utf8Decode, utf8Encode} from '../base/string_utils';
-import {Adb, AdbMsg, AdbStream, CmdType} from './adb_interfaces';
-
-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 AdbOverWebUsb's handleAuthentication().
-  AUTH_STEP1 = 1,
-  AUTH_STEP2 = 2,
-  AUTH_STEP3 = 3,
-
-  CONNECTED = 2,
-}
-
-enum AuthCmd {
-  TOKEN = 1,
-  SIGNATURE = 2,
-  RSAPUBLICKEY = 3,
-}
-
-const DEVICE_NOT_SET_ERROR = 'Device not set.';
-
-// This class is a basic TypeScript implementation of adb that only supports
-// shell commands. It is used to send the start tracing command to the connected
-// android device, and to automatically pull the trace after the end of the
-// recording. It works through the webUSB API. A brief description of how it
-// works is the following:
-// - The connection with the device is initiated by findAndConnect, which shows
-//   a dialog with a list of connected devices. Once one is selected the
-//   authentication begins. The authentication has to pass different steps, as
-//   described in the "handeAuthentication" method.
-// - AdbOverWebUsb tracks the state of the authentication via a state machine
-//   (see AdbState).
-// - A Message handler loop is executed to keep receiving the messages.
-// - All the messages received from the device are passed to "onMessage" that is
-//   implemented as a state machine.
-// - When a new shell is established, it becomes an AdbStream, and is kept in
-//   the "streams" map. Each time a message from the device is for a specific
-//   previously opened stream, the "onMessage" function will forward it to the
-//   stream (identified by a number).
-export class AdbOverWebUsb implements Adb {
-  state: AdbState = AdbState.DISCONNECTED;
-  streams = new Map<number, AdbStream>();
-  devProps = '';
-  maxPayload = DEFAULT_MAX_PAYLOAD_BYTES;
-  key?: CryptoKeyPair;
-  onConnected = () => {};
-
-  // Devices after Dec 2017 don't use checksum. This will be auto-detected
-  // during the connection.
-  useChecksum = true;
-
-  private lastStreamId = 0;
-  private dev?: USBDevice;
-  private usbInterfaceNumber?: number;
-  private usbReadEndpoint = -1;
-  private usbWriteEpEndpoint = -1;
-  private filter = {
-    classCode: 255, // USB vendor specific code
-    subclassCode: 66, // Android vendor specific subclass
-    protocolCode: 1, // Adb protocol
-  };
-
-  async findDevice() {
-    if (!('usb' in navigator)) {
-      throw new Error('WebUSB not supported by the browser (requires HTTPS)');
-    }
-    return navigator.usb.requestDevice({filters: [this.filter]});
-  }
-
-  async getPairedDevices() {
-    try {
-      return await navigator.usb.getDevices();
-    } catch (e) {
-      // WebUSB not available.
-      return Promise.resolve([]);
-    }
-  }
-
-  async connect(device: USBDevice): Promise<void> {
-    // If we are already connected, we are also already authenticated, so we can
-    // skip doing the authentication again.
-    if (this.state === AdbState.CONNECTED) {
-      if (this.dev === device && device.opened) {
-        this.onConnected();
-        this.onConnected = () => {};
-        return;
-      }
-      // Another device was connected.
-      await this.disconnect();
-    }
-
-    this.dev = device;
-    this.useChecksum = true;
-    this.key = await AdbOverWebUsb.initKey();
-
-    await this.dev.open();
-
-    const {configValue, usbInterfaceNumber, endpoints} =
-      this.findInterfaceAndEndpoint();
-    this.usbInterfaceNumber = usbInterfaceNumber;
-
-    this.usbReadEndpoint = this.findEndpointNumber(endpoints, 'in');
-    this.usbWriteEpEndpoint = this.findEndpointNumber(endpoints, 'out');
-
-    console.assert(this.usbReadEndpoint >= 0 && this.usbWriteEpEndpoint >= 0);
-
-    await this.dev.selectConfiguration(configValue);
-    await this.dev.claimInterface(usbInterfaceNumber);
-
-    await this.startAuthentication();
-
-    // This will start a message handler loop.
-    this.receiveDeviceMessages();
-    // The promise will be resolved after the handshake.
-    return new Promise<void>((resolve, _) => (this.onConnected = resolve));
-  }
-
-  async disconnect(): Promise<void> {
-    if (this.state === AdbState.DISCONNECTED) {
-      return;
-    }
-    this.state = AdbState.DISCONNECTED;
-
-    if (!this.dev) return;
-
-    new Map(this.streams).forEach((stream, _id) => stream.setClosed());
-    console.assert(this.streams.size === 0);
-
-    await this.dev.releaseInterface(assertExists(this.usbInterfaceNumber));
-    this.dev = undefined;
-    this.usbInterfaceNumber = undefined;
-  }
-
-  async startAuthentication() {
-    // USB connected, now let's authenticate.
-    const VERSION = this.useChecksum
-      ? VERSION_WITH_CHECKSUM
-      : VERSION_NO_CHECKSUM;
-    this.state = AdbState.AUTH_STEP1;
-    await this.send('CNXN', VERSION, this.maxPayload, 'host:1:UsbADB');
-  }
-
-  findInterfaceAndEndpoint() {
-    if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
-    for (const config of this.dev.configurations) {
-      for (const interface_ of config.interfaces) {
-        for (const alt of interface_.alternates) {
-          if (
-            alt.interfaceClass === this.filter.classCode &&
-            alt.interfaceSubclass === this.filter.subclassCode &&
-            alt.interfaceProtocol === this.filter.protocolCode
-          ) {
-            return {
-              configValue: config.configurationValue,
-              usbInterfaceNumber: interface_.interfaceNumber,
-              endpoints: alt.endpoints,
-            };
-          } // if (alternate)
-        } // for (interface.alternates)
-      } // for (configuration.interfaces)
-    } // for (configurations)
-
-    throw Error('Cannot find interfaces and endpoints');
-  }
-
-  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 Error(`Cannot find ${direction} endpoint`);
-  }
-
-  receiveDeviceMessages() {
-    this.recv()
-      .then((msg) => {
-        this.onMessage(msg);
-        this.receiveDeviceMessages();
-      })
-      .catch((e) => {
-        // Ignore error with "DEVICE_NOT_SET_ERROR" message since it is always
-        // thrown after the device disconnects.
-        if (e.message !== DEVICE_NOT_SET_ERROR) {
-          console.error(`Exception in recv: ${e.name}. error: ${e.message}`);
-        }
-        this.disconnect();
-      });
-  }
-
-  async onMessage(msg: AdbMsg) {
-    if (!this.key) throw Error('ADB key not initialized');
-
-    if (msg.cmd === 'AUTH' && msg.arg0 === AuthCmd.TOKEN) {
-      this.handleAuthentication(msg);
-    } else if (msg.cmd === 'CNXN') {
-      console.assert(
-        [AdbState.AUTH_STEP2, AdbState.AUTH_STEP3].includes(this.state),
-      );
-      this.state = AdbState.CONNECTED;
-      this.handleConnectedMessage(msg);
-    } else if (
-      this.state === AdbState.CONNECTED &&
-      ['OKAY', 'WRTE', 'CLSE'].indexOf(msg.cmd) >= 0
-    ) {
-      const stream = this.streams.get(msg.arg1);
-      if (!stream) {
-        console.warn(`Received message ${msg} for unknown stream ${msg.arg1}`);
-        return;
-      }
-      stream.onMessage(msg);
-    } else {
-      console.error(`Unexpected message `, msg, ` in state ${this.state}`);
-    }
-  }
-
-  async handleAuthentication(msg: AdbMsg) {
-    if (!this.key) throw Error('ADB key not initialized');
-
-    console.assert(msg.cmd === 'AUTH' && msg.arg0 === AuthCmd.TOKEN);
-    const token = msg.data;
-
-    if (this.state === AdbState.AUTH_STEP1) {
-      // 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 will not be displayed. Otherwise we will receive another message
-      // ending up in AUTH_STEP3.
-      this.state = AdbState.AUTH_STEP2;
-
-      const signedToken = await signAdbTokenWithPrivateKey(
-        this.key.privateKey,
-        token,
-      );
-      this.send('AUTH', AuthCmd.SIGNATURE, 0, new Uint8Array(signedToken));
-      return;
-    }
-
-    console.assert(this.state === AdbState.AUTH_STEP2);
-
-    // During this step, we send our public key. The dialog will appear, and
-    // if the user chooses to remember our public key, it will be
-    // saved, so that the next time we will only pass through AUTH_STEP1.
-    this.state = AdbState.AUTH_STEP3;
-    const encodedPubKey = await encodePubKey(this.key.publicKey);
-    this.send('AUTH', AuthCmd.RSAPUBLICKEY, 0, encodedPubKey);
-  }
-
-  private handleConnectedMessage(msg: AdbMsg) {
-    console.assert(msg.cmd === 'CNXN');
-
-    this.maxPayload = msg.arg1;
-    this.devProps = utf8Decode(msg.data);
-
-    const deviceVersion = msg.arg0;
-
-    if (![VERSION_WITH_CHECKSUM, VERSION_NO_CHECKSUM].includes(deviceVersion)) {
-      console.error('Version ', msg.arg0, ' not really supported!');
-    }
-    this.useChecksum = deviceVersion === VERSION_WITH_CHECKSUM;
-    this.state = AdbState.CONNECTED;
-
-    // This will resolve the promise returned by "onConnect"
-    this.onConnected();
-    this.onConnected = () => {};
-  }
-
-  shell(cmd: string): Promise<AdbStream> {
-    return this.openStream('shell:' + cmd);
-  }
-
-  socket(path: string): Promise<AdbStream> {
-    return this.openStream('localfilesystem:' + path);
-  }
-
-  openStream(svc: string): Promise<AdbStream> {
-    const stream = new AdbStreamImpl(this, ++this.lastStreamId);
-    this.streams.set(stream.localStreamId, stream);
-    this.send('OPEN', stream.localStreamId, 0, svc);
-
-    //  The stream will resolve this promise once it receives the
-    //  acknowledgement message from the device.
-    return new Promise<AdbStream>((resolve, reject) => {
-      stream.onConnect = () => {
-        stream.onClose = () => {};
-        resolve(stream);
-      };
-      stream.onClose = () =>
-        reject(new Error(`Failed to openStream svc=${svc}`));
-    });
-  }
-
-  async shellOutputAsString(cmd: string): Promise<string> {
-    const shell = await this.shell(cmd);
-
-    return new Promise<string>((resolve, _) => {
-      const output: string[] = [];
-      shell.onData = (raw) => output.push(utf8Decode(raw));
-      shell.onClose = () => resolve(output.join());
-    });
-  }
-
-  async send(
-    cmd: CmdType,
-    arg0: number,
-    arg1: number,
-    data?: Uint8Array | string,
-  ) {
-    await this.sendMsg(
-      AdbMsgImpl.create({cmd, arg0, arg1, data, useChecksum: this.useChecksum}),
-    );
-  }
-
-  //  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.
-  async sendMsg(msg: AdbMsgImpl) {
-    const sendPromises = [this.sendRaw(msg.encodeHeader())];
-    if (msg.data.length > 0) sendPromises.push(this.sendRaw(msg.data));
-    await Promise.all(sendPromises);
-  }
-
-  async recv(): Promise<AdbMsg> {
-    const res = await this.recvRaw(ADB_MSG_SIZE);
-    console.assert(res.status === 'ok');
-    const msg = AdbMsgImpl.decodeHeader(res.data!);
-
-    if (msg.dataLen > 0) {
-      const resp = await this.recvRaw(msg.dataLen);
-      msg.data = new Uint8Array(
-        resp.data!.buffer,
-        resp.data!.byteOffset,
-        resp.data!.byteLength,
-      );
-    }
-    if (this.useChecksum) {
-      console.assert(AdbOverWebUsb.checksum(msg.data) === msg.dataChecksum);
-    }
-    return msg;
-  }
-
-  static async initKey(): Promise<CryptoKeyPair> {
-    const KEY_SIZE = 2048;
-
-    const keySpec = {
-      name: 'RSASSA-PKCS1-v1_5',
-      modulusLength: KEY_SIZE,
-      publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
-      hash: {name: 'SHA-1'},
-    };
-
-    const key = await crypto.subtle.generateKey(
-      keySpec,
-      /* extractable=*/ true,
-      ['sign', 'verify'],
-    );
-    return key;
-  }
-
-  static checksum(data: Uint8Array): number {
-    let res = 0;
-    for (let i = 0; i < data.byteLength; i++) res += data[i];
-    return res & 0xffffffff;
-  }
-
-  sendRaw(buf: Uint8Array): Promise<USBOutTransferResult> {
-    console.assert(buf.length <= this.maxPayload);
-    if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
-    return this.dev.transferOut(this.usbWriteEpEndpoint, buf.buffer);
-  }
-
-  recvRaw(dataLen: number): Promise<USBInTransferResult> {
-    if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
-    return this.dev.transferIn(this.usbReadEndpoint, dataLen);
-  }
-}
-
-enum AdbStreamState {
-  WAITING_INITIAL_OKAY = 0,
-  CONNECTED = 1,
-  CLOSED = 2,
-}
-
-// An AdbStream is instantiated after the creation of a shell 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. Also streams have an initialization flow:
-//   1. WAITING_INITIAL_OKAY: waiting for first "OKAY" message. Once received,
-//      the next state will be "CONNECTED".
-//   2. CONNECTED: ready to receive or send messages.
-//   3. WRITING: this is needed because we must receive an ack after sending
-//      each message (so, before sending the next one). For this reason, many
-//      subsequent "write" calls will result in different messages in the
-//      writeQueue. After each new acknowledgement ('OKAY') a new one will be
-//      sent. When the queue is empty, the state will return to CONNECTED.
-//   4. CLOSED: entered when the device closes the stream or close() is called.
-//      For shell commands, the stream is closed after the command completed.
-export class AdbStreamImpl implements AdbStream {
-  private adb: AdbOverWebUsb;
-  localStreamId: number;
-  private remoteStreamId = -1;
-  private state: AdbStreamState = AdbStreamState.WAITING_INITIAL_OKAY;
-  private writeQueue: Uint8Array[] = [];
-  private sendInProgress = false;
-
-  onData: AdbStreamReadCallback = (_) => {};
-  onConnect = () => {};
-  onClose = () => {};
-
-  constructor(adb: AdbOverWebUsb, localStreamId: number) {
-    this.adb = adb;
-    this.localStreamId = localStreamId;
-  }
-
-  close() {
-    console.assert(this.state === AdbStreamState.CONNECTED);
-
-    if (this.writeQueue.length > 0) {
-      console.error(
-        `Dropping ${this.writeQueue.length} queued messages due to stream closing.`,
-      );
-      this.writeQueue = [];
-    }
-
-    this.adb.send('CLSE', this.localStreamId, this.remoteStreamId);
-  }
-
-  async write(msg: string | Uint8Array) {
-    const raw = isString(msg) ? utf8Encode(msg) : msg;
-    if (
-      this.sendInProgress ||
-      this.state === AdbStreamState.WAITING_INITIAL_OKAY
-    ) {
-      this.writeQueue.push(raw);
-      return;
-    }
-    console.assert(this.state === AdbStreamState.CONNECTED);
-    this.sendInProgress = true;
-    await this.adb.send('WRTE', this.localStreamId, this.remoteStreamId, raw);
-  }
-
-  setClosed() {
-    this.state = AdbStreamState.CLOSED;
-    this.adb.streams.delete(this.localStreamId);
-    this.onClose();
-  }
-
-  onMessage(msg: AdbMsgImpl) {
-    console.assert(msg.arg1 === this.localStreamId);
-
-    if (
-      this.state === AdbStreamState.WAITING_INITIAL_OKAY &&
-      msg.cmd === 'OKAY'
-    ) {
-      this.remoteStreamId = msg.arg0;
-      this.state = AdbStreamState.CONNECTED;
-      this.onConnect();
-      return;
-    }
-
-    if (msg.cmd === 'WRTE') {
-      this.adb.send('OKAY', this.localStreamId, this.remoteStreamId);
-      this.onData(msg.data);
-      return;
-    }
-
-    if (msg.cmd === 'OKAY') {
-      console.assert(this.sendInProgress);
-      this.sendInProgress = false;
-      const queuedMsg = this.writeQueue.shift();
-      if (queuedMsg !== undefined) this.write(queuedMsg);
-      return;
-    }
-
-    if (msg.cmd === 'CLSE') {
-      this.setClosed();
-      return;
-    }
-    console.error(
-      `Unexpected stream msg ${msg.toString()} in state ${this.state}`,
-    );
-  }
-}
-
-interface AdbStreamReadCallback {
-  (raw: Uint8Array): void;
-}
-
-const ADB_MSG_SIZE = 6 * 4; // 6 * int32.
-
-export class AdbMsgImpl implements AdbMsg {
-  cmd: CmdType;
-  arg0: number;
-  arg1: number;
-  data: Uint8Array;
-  dataLen: number;
-  dataChecksum: number;
-
-  useChecksum: boolean;
-
-  constructor(
-    cmd: CmdType,
-    arg0: number,
-    arg1: number,
-    dataLen: number,
-    dataChecksum: number,
-    useChecksum = false,
-  ) {
-    console.assert(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;
-  }): AdbMsgImpl {
-    const encodedData = this.encodeData(data);
-    const msg = new AdbMsgImpl(
-      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): AdbMsgImpl {
-    console.assert(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);
-    console.assert(cmdNum === (cmdChecksum ^ 0xffffffff));
-    return new AdbMsgImpl(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 = AdbMsgImpl.encodeData(this.data);
-    const checksum = this.useChecksum ? AdbOverWebUsb.checksum(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;
-  }
-}
-
-function base64StringToArray(s: string) {
-  const decoded = atob(s.replaceAll('-', '+').replaceAll('_', '/'));
-  return [...decoded].map((char) => char.charCodeAt(0));
-}
-
-const ANDROID_PUBKEY_MODULUS_SIZE = 2048;
-const MODULUS_SIZE_BYTES = ANDROID_PUBKEY_MODULUS_SIZE / 8;
-
-// RSA Public keys are encoded in a rather unique way. It's a base64 encoded
-// struct of 524 bytes in total as follows (see
-// libcrypto_utils/android_pubkey.c):
-//
-// typedef struct RSAPublicKey {
-//   // Modulus length. This must be ANDROID_PUBKEY_MODULUS_SIZE.
-//   uint32_t modulus_size_words;
-//
-//   // Precomputed montgomery parameter: -1 / n[0] mod 2^32
-//   uint32_t n0inv;
-//
-//   // RSA modulus as a little-endian array.
-//   uint8_t modulus[ANDROID_PUBKEY_MODULUS_SIZE];
-//
-//   // Montgomery parameter R^2 as a little-endian array of little-endian
-//   words. uint8_t rr[ANDROID_PUBKEY_MODULUS_SIZE];
-//
-//   // RSA modulus: 3 or 65537
-//   uint32_t exponent;
-// } RSAPublicKey;
-//
-// However, the Montgomery params (n0inv and rr) are not really used, see
-// comment in android_pubkey_decode() ("Note that we don't extract the
-// montgomery parameters...")
-async function encodePubKey(key: CryptoKey) {
-  const expPubKey = await crypto.subtle.exportKey('jwk', key);
-  const nArr = base64StringToArray(expPubKey.n as string).reverse();
-  const eArr = base64StringToArray(expPubKey.e as string).reverse();
-
-  const arr = new Uint8Array(3 * 4 + 2 * MODULUS_SIZE_BYTES);
-  const dv = new DataView(arr.buffer);
-  dv.setUint32(0, MODULUS_SIZE_BYTES / 4, true);
-
-  // The Mongomery params (n0inv and rr) are not computed.
-  dv.setUint32(4, 0 /* n0inv*/, true);
-  // Modulus
-  for (let i = 0; i < MODULUS_SIZE_BYTES; i++) dv.setUint8(8 + i, nArr[i]);
-
-  // rr:
-  for (let i = 0; i < MODULUS_SIZE_BYTES; i++) {
-    dv.setUint8(8 + MODULUS_SIZE_BYTES + i, 0 /* rr*/);
-  }
-  // Exponent
-  for (let i = 0; i < 4; i++) {
-    dv.setUint8(8 + 2 * MODULUS_SIZE_BYTES + i, eArr[i]);
-  }
-  return (
-    btoa(String.fromCharCode(...new Uint8Array(dv.buffer))) + ' perfetto@webusb'
-  );
-}
-
-// TODO(nicomazz): This token signature will be useful only when we save the
-// generated keys. So far, we are not doing so. As a consequence, a dialog is
-// displayed every time a tracing session is started.
-// The reason why it has not already been implemented is that the standard
-// crypto.subtle.sign function assumes that the input needs hashing, which is
-// not the case for ADB, where the 20 bytes token is already hashed.
-// A solution to this is implementing a custom private key signature with a js
-// implementation of big integers. Maybe, wrapping the key like in the following
-// CL can work:
-// https://android-review.googlesource.com/c/platform/external/perfetto/+/1105354/18
-async function signAdbTokenWithPrivateKey(
-  _privateKey: CryptoKey,
-  token: Uint8Array,
-): Promise<ArrayBuffer> {
-  // This function is not implemented.
-  return token.buffer;
-}
diff --git a/ui/src/controller/adb_base_controller.ts b/ui/src/controller/adb_base_controller.ts
deleted file mode 100644
index 84d1a20..0000000
--- a/ui/src/controller/adb_base_controller.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {isAdbTarget} from '../common/state';
-import {
-  extractDurationFromTraceConfig,
-  extractTraceConfig,
-} from '../core/trace_config_utils';
-import {globals} from '../frontend/globals';
-
-import {Adb} from './adb_interfaces';
-import {ReadBuffersResponse} from './consumer_port_types';
-import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
-
-export enum AdbConnectionState {
-  READY_TO_CONNECT,
-  AUTH_IN_PROGRESS,
-  CONNECTED,
-  CLOSED,
-}
-
-interface Command {
-  method: string;
-  params: Uint8Array;
-}
-
-export abstract class AdbBaseConsumerPort extends RpcConsumerPort {
-  // Contains the commands sent while the authentication is in progress. They
-  // will all be executed afterwards. If the device disconnects, they are
-  // removed.
-  private commandQueue: Command[] = [];
-
-  protected adb: Adb;
-  protected state = AdbConnectionState.READY_TO_CONNECT;
-  protected device?: USBDevice;
-
-  protected constructor(adb: Adb, consumer: Consumer) {
-    super(consumer);
-    this.adb = adb;
-  }
-
-  async handleCommand(method: string, params: Uint8Array) {
-    try {
-      if (method === 'FreeBuffers') {
-        // When we finish tracing, we disconnect the adb device interface.
-        // Otherwise, we will keep holding the device interface and won't allow
-        // adb to access it. https://wicg.github.io/webusb/#abusing-a-device
-        // "Lastly, since USB devices are unable to distinguish requests from
-        // multiple sources, operating systems only allow a USB interface to
-        // have a single owning user-space or kernel-space driver."
-        this.state = AdbConnectionState.CLOSED;
-        await this.adb.disconnect();
-      } else if (method === 'EnableTracing') {
-        this.state = AdbConnectionState.READY_TO_CONNECT;
-      }
-
-      if (this.state === AdbConnectionState.CLOSED) return;
-
-      this.commandQueue.push({method, params});
-
-      if (
-        this.state === AdbConnectionState.READY_TO_CONNECT ||
-        this.deviceDisconnected()
-      ) {
-        this.state = AdbConnectionState.AUTH_IN_PROGRESS;
-        this.device = await this.findDevice();
-        if (!this.device) {
-          this.state = AdbConnectionState.READY_TO_CONNECT;
-          const target = globals.state.recordingTarget;
-          throw Error(
-            `Device with serial ${
-              isAdbTarget(target) ? target.serial : 'n/a'
-            } not found.`,
-          );
-        }
-
-        this.sendStatus(`Please allow USB debugging on device.
-          If you press cancel, reload the page.`);
-
-        await this.adb.connect(this.device);
-
-        // During the authentication the device may have been disconnected.
-        if (!globals.state.recordingInProgress || this.deviceDisconnected()) {
-          throw Error('Recording not in progress after adb authorization.');
-        }
-
-        this.state = AdbConnectionState.CONNECTED;
-        this.sendStatus('Device connected.');
-      }
-
-      if (this.state === AdbConnectionState.AUTH_IN_PROGRESS) return;
-
-      console.assert(this.state === AdbConnectionState.CONNECTED);
-
-      for (const cmd of this.commandQueue) this.invoke(cmd.method, cmd.params);
-
-      this.commandQueue = [];
-    } catch (e) {
-      this.commandQueue = [];
-      this.state = AdbConnectionState.READY_TO_CONNECT;
-      this.sendErrorMessage(e.message);
-    }
-  }
-
-  private deviceDisconnected() {
-    return !this.device || !this.device.opened;
-  }
-
-  setDurationStatus(enableTracingProto: Uint8Array) {
-    const traceConfigProto = extractTraceConfig(enableTracingProto);
-    if (!traceConfigProto) return;
-    const duration = extractDurationFromTraceConfig(traceConfigProto);
-    this.sendStatus(
-      `Recording in progress${
-        exists(duration) ? ' for ' + duration.toString() + ' ms' : ''
-      }...`,
-    );
-  }
-
-  abstract invoke(method: string, argsProto: Uint8Array): void;
-
-  generateChunkReadResponse(
-    data: Uint8Array,
-    last = false,
-  ): ReadBuffersResponse {
-    return {
-      type: 'ReadBuffersResponse',
-      slices: [{data, lastSliceForPacket: last}],
-    };
-  }
-
-  async findDevice(): Promise<USBDevice | undefined> {
-    if (!('usb' in navigator)) return undefined;
-    const connectedDevice = globals.state.recordingTarget;
-    if (!isAdbTarget(connectedDevice)) return undefined;
-    const devices = await navigator.usb.getDevices();
-    return devices.find((d) => d.serialNumber === connectedDevice.serial);
-  }
-}
diff --git a/ui/src/controller/adb_jsdomtest.ts b/ui/src/controller/adb_jsdomtest.ts
deleted file mode 100644
index 9f51a97..0000000
--- a/ui/src/controller/adb_jsdomtest.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {
-  AdbMsgImpl,
-  AdbOverWebUsb,
-  AdbState,
-  DEFAULT_MAX_PAYLOAD_BYTES,
-  VERSION_WITH_CHECKSUM,
-} from './adb';
-import {utf8Encode} from '../base/string_utils';
-
-test('startAuthentication', async () => {
-  const adb = new AdbOverWebUsb();
-
-  const sendRaw = jest.fn();
-  adb.sendRaw = sendRaw;
-  const recvRaw = jest.fn();
-  adb.recvRaw = recvRaw;
-
-  const expectedAuthMessage = AdbMsgImpl.create({
-    cmd: 'CNXN',
-    arg0: VERSION_WITH_CHECKSUM,
-    arg1: DEFAULT_MAX_PAYLOAD_BYTES,
-    data: 'host:1:UsbADB',
-    useChecksum: true,
-  });
-  await adb.startAuthentication();
-
-  expect(sendRaw).toHaveBeenCalledTimes(2);
-  expect(sendRaw).toBeCalledWith(expectedAuthMessage.encodeHeader());
-  expect(sendRaw).toBeCalledWith(expectedAuthMessage.data);
-});
-
-test('connectedMessage', async () => {
-  const adb = new AdbOverWebUsb();
-  adb.key = {} as unknown as CryptoKeyPair;
-  adb.state = AdbState.AUTH_STEP2;
-
-  const onConnected = jest.fn();
-  adb.onConnected = onConnected;
-
-  const expectedMaxPayload = 42;
-  const connectedMsg = AdbMsgImpl.create({
-    cmd: 'CNXN',
-    arg0: VERSION_WITH_CHECKSUM,
-    arg1: expectedMaxPayload,
-    data: utf8Encode('device'),
-    useChecksum: true,
-  });
-  await adb.onMessage(connectedMsg);
-
-  expect(adb.state).toBe(AdbState.CONNECTED);
-  expect(adb.maxPayload).toBe(expectedMaxPayload);
-  expect(adb.devProps).toBe('device');
-  expect(adb.useChecksum).toBe(true);
-  expect(onConnected).toHaveBeenCalledTimes(1);
-});
-
-test('shellOpening', () => {
-  const adb = new AdbOverWebUsb();
-  const openStream = jest.fn();
-  adb.openStream = openStream;
-
-  adb.shell('test');
-  expect(openStream).toBeCalledWith('shell:test');
-});
diff --git a/ui/src/controller/adb_record_controller_jsdomtest.ts b/ui/src/controller/adb_record_controller_jsdomtest.ts
deleted file mode 100644
index 9107269..0000000
--- a/ui/src/controller/adb_record_controller_jsdomtest.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {dingus} from 'dingusjs';
-
-import {utf8Encode} from '../base/string_utils';
-import {EnableTracingRequest, TraceConfig} from '../protos';
-
-import {AdbStream, MockAdb, MockAdbStream} from './adb_interfaces';
-import {AdbConsumerPort} from './adb_shell_controller';
-import {Consumer} from './record_controller_interfaces';
-
-function generateMockConsumer(): Consumer {
-  return {
-    onConsumerPortResponse: jest.fn(),
-    onError: jest.fn(),
-    onStatus: jest.fn(),
-  };
-}
-const mainCallback = generateMockConsumer();
-const adbMock = new MockAdb();
-const adbController = new AdbConsumerPort(adbMock, mainCallback);
-const mockIntArray = new Uint8Array();
-
-const enableTracingRequest = new EnableTracingRequest();
-enableTracingRequest.traceConfig = new TraceConfig();
-const enableTracingRequestProto =
-  EnableTracingRequest.encode(enableTracingRequest).finish();
-
-test('handleCommand', async () => {
-  adbController.findDevice = () => {
-    return Promise.resolve(dingus<USBDevice>());
-  };
-
-  const enableTracing = jest.fn();
-  adbController.enableTracing = enableTracing;
-  await adbController.invoke('EnableTracing', mockIntArray);
-  expect(enableTracing).toHaveBeenCalledTimes(1);
-
-  const readBuffers = jest.fn();
-  adbController.readBuffers = readBuffers;
-  adbController.invoke('ReadBuffers', mockIntArray);
-  expect(readBuffers).toHaveBeenCalledTimes(1);
-
-  const sendErrorMessage = jest.fn();
-  adbController.sendErrorMessage = sendErrorMessage;
-  adbController.invoke('unknown', mockIntArray);
-  expect(sendErrorMessage).toBeCalledWith('Method not recognized: unknown');
-});
-
-test('enableTracing', async () => {
-  const mainCallback = generateMockConsumer();
-  const adbMock = new MockAdb();
-  const adbController = new AdbConsumerPort(adbMock, mainCallback);
-
-  adbController.sendErrorMessage = jest
-    .fn()
-    .mockImplementation((s) => console.error(s));
-
-  const findDevice = jest.fn().mockImplementation(() => {
-    return Promise.resolve({} as unknown as USBDevice);
-  });
-  adbController.findDevice = findDevice;
-
-  const connectToDevice = jest
-    .fn()
-    .mockImplementation((_: USBDevice) => Promise.resolve());
-  adbMock.connect = connectToDevice;
-
-  const stream: AdbStream = new MockAdbStream();
-  const adbShell = jest
-    .fn()
-    .mockImplementation((_: string) => Promise.resolve(stream));
-  adbMock.shell = adbShell;
-
-  const sendMessage = jest.fn();
-  adbController.sendMessage = sendMessage;
-
-  adbController.generateStartTracingCommand = (_) => 'CMD';
-
-  await adbController.enableTracing(enableTracingRequestProto);
-  expect(adbShell).toBeCalledWith('CMD');
-  expect(sendMessage).toHaveBeenCalledTimes(0);
-
-  stream.onData(utf8Encode('starting tracing Wrote 123 bytes'));
-  stream.onClose();
-
-  expect(adbController.sendErrorMessage).toHaveBeenCalledTimes(0);
-  expect(sendMessage).toBeCalledWith({type: 'EnableTracingResponse'});
-});
-
-test('generateStartTracing', () => {
-  adbController.traceDestFile = 'DEST';
-  const testArray = new Uint8Array(1);
-  testArray[0] = 65;
-  const generatedCmd = adbController.generateStartTracingCommand(testArray);
-  expect(generatedCmd).toBe(
-    `echo '${btoa('A')}' | base64 -d | perfetto -c - -o DEST`,
-  );
-});
-
-test('tracingEndedSuccessfully', () => {
-  expect(
-    adbController.tracingEndedSuccessfully(
-      'Connected to the Perfetto traced service, starting tracing for 10000 ms\nWrote 564 bytes into /data/misc/perfetto-traces/trace',
-    ),
-  ).toBe(true);
-  expect(
-    adbController.tracingEndedSuccessfully(
-      'Connected to the Perfetto traced service, starting tracing for 10000 ms',
-    ),
-  ).toBe(false);
-  expect(
-    adbController.tracingEndedSuccessfully(
-      'Connected to the Perfetto traced service, starting tracing for 0 ms',
-    ),
-  ).toBe(false);
-});
diff --git a/ui/src/controller/adb_shell_controller.ts b/ui/src/controller/adb_shell_controller.ts
deleted file mode 100644
index 7f7f359..0000000
--- a/ui/src/controller/adb_shell_controller.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {base64Encode, utf8Decode} from '../base/string_utils';
-import {extractTraceConfig} from '../core/trace_config_utils';
-import {AdbBaseConsumerPort, AdbConnectionState} from './adb_base_controller';
-import {Adb, AdbStream} from './adb_interfaces';
-import {ReadBuffersResponse} from './consumer_port_types';
-import {Consumer} from './record_controller_interfaces';
-
-enum AdbShellState {
-  READY,
-  RECORDING,
-  FETCHING,
-}
-const DEFAULT_DESTINATION_FILE = '/data/misc/perfetto-traces/trace-by-ui';
-
-export class AdbConsumerPort extends AdbBaseConsumerPort {
-  traceDestFile = DEFAULT_DESTINATION_FILE;
-  shellState: AdbShellState = AdbShellState.READY;
-  private recordShell?: AdbStream;
-
-  constructor(adb: Adb, consumer: Consumer) {
-    super(adb, consumer);
-    this.adb = adb;
-  }
-
-  async invoke(method: string, params: Uint8Array) {
-    // ADB connection & authentication is handled by the superclass.
-    console.assert(this.state === AdbConnectionState.CONNECTED);
-
-    switch (method) {
-      case 'EnableTracing':
-        this.enableTracing(params);
-        break;
-      case 'ReadBuffers':
-        this.readBuffers();
-        break;
-      case 'DisableTracing':
-        this.disableTracing();
-        break;
-      case 'FreeBuffers':
-        this.freeBuffers();
-        break;
-      case 'GetTraceStats':
-        break;
-      default:
-        this.sendErrorMessage(`Method not recognized: ${method}`);
-        break;
-    }
-  }
-
-  async enableTracing(enableTracingProto: Uint8Array) {
-    try {
-      const traceConfigProto = extractTraceConfig(enableTracingProto);
-      if (!traceConfigProto) {
-        this.sendErrorMessage('Invalid config.');
-        return;
-      }
-
-      await this.startRecording(traceConfigProto);
-      this.setDurationStatus(enableTracingProto);
-    } catch (e) {
-      this.sendErrorMessage(e.message);
-    }
-  }
-
-  async startRecording(configProto: Uint8Array) {
-    this.shellState = AdbShellState.RECORDING;
-    const recordCommand = this.generateStartTracingCommand(configProto);
-    this.recordShell = await this.adb.shell(recordCommand);
-    const output: string[] = [];
-    this.recordShell.onData = (raw) => output.push(utf8Decode(raw));
-    this.recordShell.onClose = () => {
-      const response = output.join();
-      if (!this.tracingEndedSuccessfully(response)) {
-        this.sendErrorMessage(response);
-        this.shellState = AdbShellState.READY;
-        return;
-      }
-      this.sendStatus('Recording ended successfully. Fetching the trace..');
-      this.sendMessage({type: 'EnableTracingResponse'});
-      this.recordShell = undefined;
-    };
-  }
-
-  tracingEndedSuccessfully(response: string): boolean {
-    return !response.includes(' 0 ms') && response.includes('Wrote ');
-  }
-
-  async readBuffers() {
-    console.assert(this.shellState === AdbShellState.RECORDING);
-    this.shellState = AdbShellState.FETCHING;
-
-    const readTraceShell = await this.adb.shell(
-      this.generateReadTraceCommand(),
-    );
-    readTraceShell.onData = (raw) =>
-      this.sendMessage(this.generateChunkReadResponse(raw));
-
-    readTraceShell.onClose = () => {
-      this.sendMessage(
-        this.generateChunkReadResponse(new Uint8Array(), /* last */ true),
-      );
-    };
-  }
-
-  async getPidFromShellAsString() {
-    const pidStr = await this.adb.shellOutputAsString(
-      `ps -u shell | grep perfetto`,
-    );
-    // We used to use awk '{print $2}' but older phones/Go phones don't have
-    // awk installed. Instead we implement similar functionality here.
-    const awk = pidStr.split(' ').filter((str) => str !== '');
-    if (awk.length < 1) {
-      throw Error(`Unabled to find perfetto pid in string "${pidStr}"`);
-    }
-    return awk[1];
-  }
-
-  async disableTracing() {
-    if (!this.recordShell) return;
-    try {
-      // We are not using 'pidof perfetto' so that we can use more filters. 'ps
-      // -u shell' is meant to catch processes started from shell, so if there
-      // are other ongoing tracing sessions started by others, we are not
-      // killing them.
-      const pid = await this.getPidFromShellAsString();
-
-      if (pid.length === 0 || isNaN(Number(pid))) {
-        throw Error(`Perfetto pid not found. Impossible to stop/cancel the
-     recording. Command output: ${pid}`);
-      }
-      // Perfetto stops and finalizes the tracing session on SIGINT.
-      const killOutput = await this.adb.shellOutputAsString(
-        `kill -SIGINT ${pid}`,
-      );
-
-      if (killOutput.length !== 0) {
-        throw Error(`Unable to kill perfetto: ${killOutput}`);
-      }
-    } catch (e) {
-      this.sendErrorMessage(e.message);
-    }
-  }
-
-  freeBuffers() {
-    this.shellState = AdbShellState.READY;
-    if (this.recordShell) {
-      this.recordShell.close();
-      this.recordShell = undefined;
-    }
-  }
-
-  generateChunkReadResponse(
-    data: Uint8Array,
-    last = false,
-  ): ReadBuffersResponse {
-    return {
-      type: 'ReadBuffersResponse',
-      slices: [{data, lastSliceForPacket: last}],
-    };
-  }
-
-  generateReadTraceCommand(): string {
-    // We attempt to delete the trace file after tracing. On a non-root shell,
-    // this will fail (due to selinux denial), but perfetto cmd will be able to
-    // override the file later. However, on a root shell, we need to clean up
-    // the file since perfetto cmd might otherwise fail to override it in a
-    // future session.
-    return `gzip -c ${this.traceDestFile} && rm -f ${this.traceDestFile}`;
-  }
-
-  generateStartTracingCommand(tracingConfig: Uint8Array) {
-    const configBase64 = base64Encode(tracingConfig);
-    const perfettoCmd = `perfetto -c - -o ${this.traceDestFile}`;
-    return `echo '${configBase64}' | base64 -d | ${perfettoCmd}`;
-  }
-}
diff --git a/ui/src/controller/adb_socket_controller.ts b/ui/src/controller/adb_socket_controller.ts
deleted file mode 100644
index 9eecec5..0000000
--- a/ui/src/controller/adb_socket_controller.ts
+++ /dev/null
@@ -1,395 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {
-  DisableTracingResponse,
-  EnableTracingResponse,
-  FreeBuffersResponse,
-  GetTraceStatsResponse,
-  IPCFrame,
-  ReadBuffersResponse,
-} from '../protos';
-
-import {AdbBaseConsumerPort, AdbConnectionState} from './adb_base_controller';
-import {Adb, AdbStream} from './adb_interfaces';
-import {isReadBuffersResponse} from './consumer_port_types';
-import {Consumer} from './record_controller_interfaces';
-import {exists} from '../base/utils';
-import {assertTrue} from '../base/logging';
-
-enum SocketState {
-  DISCONNECTED,
-  BINDING_IN_PROGRESS,
-  BOUND,
-}
-
-// See wire_protocol.proto for more details.
-const WIRE_PROTOCOL_HEADER_SIZE = 4;
-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;
-
-declare type Frame = IPCFrame;
-declare type IMethodInfo = IPCFrame.BindServiceReply.IMethodInfo;
-declare type ISlice = ReadBuffersResponse.ISlice;
-
-interface Command {
-  method: string;
-  params: Uint8Array;
-}
-
-const TRACED_SOCKET = '/dev/socket/traced_consumer';
-
-export class AdbSocketConsumerPort extends AdbBaseConsumerPort {
-  private socketState = SocketState.DISCONNECTED;
-
-  private socket?: AdbStream;
-  // 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;
-
-  // Buffers received wire protocol data.
-  private incomingBuffer = new Uint8Array(MAX_IPC_BUFFER_SIZE);
-  private incomingBufferLen = 0;
-  private frameToParseLen = 0;
-
-  private availableMethods: IMethodInfo[] = [];
-  private serviceId = -1;
-
-  private resolveBindingPromise!: VoidFunction;
-  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();
-
-  private socketCommandQueue: Command[] = [];
-
-  constructor(adb: Adb, consumer: Consumer) {
-    super(adb, consumer);
-  }
-
-  async invoke(method: string, params: Uint8Array) {
-    // ADB connection & authentication is handled by the superclass.
-    console.assert(this.state === AdbConnectionState.CONNECTED);
-    this.socketCommandQueue.push({method, params});
-
-    if (this.socketState === SocketState.BINDING_IN_PROGRESS) return;
-    if (this.socketState === SocketState.DISCONNECTED) {
-      this.socketState = SocketState.BINDING_IN_PROGRESS;
-      await this.listenForMessages();
-      await this.bind();
-      this.traceProtoWriter = protobuf.Writer.create();
-      this.socketState = SocketState.BOUND;
-    }
-
-    console.assert(this.socketState === SocketState.BOUND);
-
-    for (const cmd of this.socketCommandQueue) {
-      this.invokeInternal(cmd.method, cmd.params);
-    }
-    this.socketCommandQueue = [];
-  }
-
-  private invokeInternal(method: string, argsProto: Uint8Array) {
-    // Socket is bound in invoke().
-    console.assert(this.socketState === SocketState.BOUND);
-    const requestId = this.requestId++;
-    const methodId = this.findMethodId(method);
-    if (methodId === undefined) {
-      // This can happen with 'GetTraceStats': it seems that not all the Android
-      // <= 9 devices support it.
-      console.error(`Method ${method} not supported by the target`);
-      return;
-    }
-    const frame = new IPCFrame({
-      requestId,
-      msgInvokeMethod: new IPCFrame.InvokeMethod({
-        serviceId: this.serviceId,
-        methodId,
-        argsProto,
-      }),
-    });
-    this.requestMethods.set(requestId, method);
-    this.sendFrame(frame);
-
-    if (method === 'EnableTracing') this.setDurationStatus(argsProto);
-  }
-
-  static generateFrameBufferToSend(frame: Frame): Uint8Array {
-    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]);
-    }
-    return buf;
-  }
-
-  async sendFrame(frame: Frame) {
-    console.assert(this.socket !== undefined);
-    if (!this.socket) return;
-    const buf = AdbSocketConsumerPort.generateFrameBufferToSend(frame);
-    await this.socket.write(buf);
-  }
-
-  async listenForMessages() {
-    this.socket = await this.adb.socket(TRACED_SOCKET);
-    this.socket.onData = (raw) => this.handleReceivedData(raw);
-    this.socket.onClose = () => {
-      this.socketState = SocketState.DISCONNECTED;
-      this.socketCommandQueue = [];
-    };
-  }
-
-  private parseMessageSize(buffer: Uint8Array) {
-    const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.length);
-    return dv.getUint32(0, true);
-  }
-
-  private parseMessage(frameBuffer: Uint8Array) {
-    // Copy message to new array:
-    const buf = new ArrayBuffer(frameBuffer.byteLength);
-    const arr = new Uint8Array(buf);
-    arr.set(frameBuffer);
-    const frame = IPCFrame.decode(arr);
-    this.handleIncomingFrame(frame);
-  }
-
-  private incompleteSizeHeader() {
-    if (!this.frameToParseLen) {
-      console.assert(this.incomingBufferLen < WIRE_PROTOCOL_HEADER_SIZE);
-      return true;
-    }
-    return false;
-  }
-
-  private canCompleteSizeHeader(newData: Uint8Array) {
-    return newData.length + this.incomingBufferLen > WIRE_PROTOCOL_HEADER_SIZE;
-  }
-
-  private canParseFullMessage(newData: Uint8Array) {
-    return (
-      this.frameToParseLen &&
-      this.incomingBufferLen + newData.length >= this.frameToParseLen
-    );
-  }
-
-  private appendToIncomingBuffer(array: Uint8Array) {
-    this.incomingBuffer.set(array, this.incomingBufferLen);
-    this.incomingBufferLen += array.length;
-  }
-
-  handleReceivedData(newData: Uint8Array) {
-    if (this.incompleteSizeHeader() && this.canCompleteSizeHeader(newData)) {
-      const newDataBytesToRead =
-        WIRE_PROTOCOL_HEADER_SIZE - this.incomingBufferLen;
-      // Add to the incoming buffer the remaining bytes to arrive at
-      // WIRE_PROTOCOL_HEADER_SIZE
-      this.appendToIncomingBuffer(newData.subarray(0, newDataBytesToRead));
-      newData = newData.subarray(newDataBytesToRead);
-
-      this.frameToParseLen = this.parseMessageSize(this.incomingBuffer);
-      this.incomingBufferLen = 0;
-    }
-
-    // Parse all complete messages in incomingBuffer and newData.
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    while (this.canParseFullMessage(newData)) {
-      // All the message is in the newData buffer.
-      if (this.incomingBufferLen === 0) {
-        this.parseMessage(newData.subarray(0, this.frameToParseLen));
-        newData = newData.subarray(this.frameToParseLen);
-      } else {
-        // We need to complete the local buffer.
-        // Read the remaining part of this message.
-        const bytesToCompleteMessage =
-          this.frameToParseLen - this.incomingBufferLen;
-        this.appendToIncomingBuffer(
-          newData.subarray(0, bytesToCompleteMessage),
-        );
-        this.parseMessage(
-          this.incomingBuffer.subarray(0, this.frameToParseLen),
-        );
-        this.incomingBufferLen = 0;
-        // Remove the data just parsed.
-        newData = newData.subarray(bytesToCompleteMessage);
-      }
-      this.frameToParseLen = 0;
-      if (!this.canCompleteSizeHeader(newData)) break;
-
-      this.frameToParseLen = this.parseMessageSize(
-        newData.subarray(0, WIRE_PROTOCOL_HEADER_SIZE),
-      );
-      newData = newData.subarray(WIRE_PROTOCOL_HEADER_SIZE);
-    }
-    // Buffer the remaining data (part of the next header + message).
-    this.appendToIncomingBuffer(newData);
-  }
-
-  decodeResponse(
-    requestId: number,
-    responseProto: Uint8Array,
-    hasMore = false,
-  ) {
-    const method = this.requestMethods.get(requestId);
-    if (!method) {
-      console.error(`Unknown request id: ${requestId}`);
-      this.sendErrorMessage(`Wire protocol error.`);
-      return;
-    }
-    const decoder = decoders.get(method);
-    if (decoder === undefined) {
-      console.error(`Unable to decode method: ${method}`);
-      return;
-    }
-    const decodedResponse = decoder(responseProto);
-    const response = {type: `${method}Response`, ...decodedResponse};
-
-    // TODO(nicomazz): Fix this.
-    // We assemble all the trace and then send it back to the main controller.
-    // This is a temporary solution, that will be changed in a following CL,
-    // because now both the chrome consumer port and the other adb consumer port
-    // send back the entire trace, while the correct behavior should be to send
-    // back the slices, that are assembled by the main record controller.
-    if (isReadBuffersResponse(response)) {
-      if (response.slices) this.handleSlices(response.slices);
-      if (!hasMore) this.sendReadBufferResponse();
-      return;
-    }
-    this.sendMessage(response);
-  }
-
-  handleSlices(slices: ISlice[]) {
-    for (const slice of slices) {
-      this.partialPacket.push(slice);
-      if (slice.lastSliceForPacket) {
-        const tracePacket = this.generateTracePacket(this.partialPacket);
-        this.traceProtoWriter.uint32(TRACE_PACKET_PROTO_TAG);
-        this.traceProtoWriter.bytes(tracePacket);
-        this.partialPacket = [];
-      }
-    }
-  }
-
-  generateTracePacket(slices: ISlice[]): Uint8Array {
-    let bufferSize = 0;
-    for (const slice of slices) bufferSize += slice.data!.length;
-    const fullBuffer = new Uint8Array(bufferSize);
-    let written = 0;
-    for (const slice of slices) {
-      const data = slice.data!;
-      fullBuffer.set(data, written);
-      written += data.length;
-    }
-    return fullBuffer;
-  }
-
-  sendReadBufferResponse() {
-    this.sendMessage(
-      this.generateChunkReadResponse(
-        this.traceProtoWriter.finish(),
-        /* last */ true,
-      ),
-    );
-    this.traceProtoWriter = protobuf.Writer.create();
-  }
-
-  bind() {
-    console.assert(this.socket !== undefined);
-    const requestId = this.requestId++;
-    const frame = new IPCFrame({
-      requestId,
-      msgBindService: new IPCFrame.BindService({serviceName: 'ConsumerPort'}),
-    });
-    return new Promise<void>((resolve, _) => {
-      this.resolveBindingPromise = resolve;
-      this.sendFrame(frame);
-    });
-  }
-
-  findMethodId(method: string): number | undefined {
-    const methodObject = this.availableMethods.find((m) => m.name === method);
-    return methodObject?.id ?? undefined;
-  }
-
-  static async hasSocketAccess(device: USBDevice, adb: Adb): Promise<boolean> {
-    await adb.connect(device);
-    try {
-      const socket = await adb.socket(TRACED_SOCKET);
-      socket.close();
-      return true;
-    } catch (e) {
-      return false;
-    }
-  }
-
-  handleIncomingFrame(frame: IPCFrame) {
-    const requestId = frame.requestId;
-    switch (frame.msg) {
-      case '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();
-          this.resolveBindingPromise = () => {};
-        }
-        return;
-      }
-      case 'msgInvokeMethodReply': {
-        const msgInvokeMethodReply = frame.msgInvokeMethodReply;
-        if (msgInvokeMethodReply && msgInvokeMethodReply.replyProto) {
-          if (!msgInvokeMethodReply.success) {
-            console.error(
-              'Unsuccessful method invocation: ',
-              msgInvokeMethodReply,
-            );
-            return;
-          }
-          this.decodeResponse(
-            requestId,
-            msgInvokeMethodReply.replyProto,
-            msgInvokeMethodReply.hasMore === true,
-          );
-        }
-        return;
-      }
-      default:
-        console.error(`not recognized frame message: ${frame.msg}`);
-    }
-  }
-}
-
-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);
diff --git a/ui/src/controller/aggregation/aggregation_controller.ts b/ui/src/controller/aggregation/aggregation_controller.ts
deleted file mode 100644
index e8f54a0..0000000
--- a/ui/src/controller/aggregation/aggregation_controller.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-// Copyright (C) 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
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {AsyncLimiter} from '../../base/async_limiter';
-import {Monitor} from '../../base/monitor';
-import {isString} from '../../base/object_utils';
-import {
-  AggregateData,
-  Column,
-  ColumnDef,
-  ThreadStateExtra,
-} from '../../common/aggregation_data';
-import {Area, Sorting} from '../../common/state';
-import {globals} from '../../frontend/globals';
-import {publishAggregateData} from '../../frontend/publish';
-import {Engine} from '../../trace_processor/engine';
-import {NUM} from '../../trace_processor/query_result';
-import {Controller} from '../controller';
-
-export interface AggregationControllerArgs {
-  engine: Engine;
-  kind: string;
-}
-
-function isStringColumn(column: Column): boolean {
-  return column.kind === 'STRING' || column.kind === 'STATE';
-}
-
-export abstract class AggregationController extends Controller<'main'> {
-  readonly kind: string;
-  private readonly monitor: Monitor;
-  private readonly limiter = new AsyncLimiter();
-
-  abstract createAggregateView(engine: Engine, area: Area): Promise<boolean>;
-
-  abstract getExtra(
-    engine: Engine,
-    area: Area,
-  ): Promise<ThreadStateExtra | void>;
-
-  abstract getTabName(): string;
-  abstract getDefaultSorting(): Sorting;
-  abstract getColumnDefinitions(): ColumnDef[];
-
-  constructor(private args: AggregationControllerArgs) {
-    super('main');
-    this.kind = this.args.kind;
-    this.monitor = new Monitor([
-      () => globals.state.selection,
-      () => globals.state.aggregatePreferences[this.args.kind],
-    ]);
-  }
-
-  run() {
-    if (this.monitor.ifStateChanged()) {
-      const selection = globals.state.selection;
-      if (selection.kind !== 'area') {
-        publishAggregateData({
-          data: {
-            tabName: this.getTabName(),
-            columns: [],
-            strings: [],
-            columnSums: [],
-          },
-          kind: this.args.kind,
-        });
-        return;
-      } else {
-        this.limiter.schedule(async () => {
-          const data = await this.getAggregateData(selection, true);
-          publishAggregateData({data, kind: this.args.kind});
-        });
-      }
-    }
-  }
-
-  async getAggregateData(
-    area: Area,
-    areaChanged: boolean,
-  ): Promise<AggregateData> {
-    if (areaChanged) {
-      const viewExists = await this.createAggregateView(this.args.engine, area);
-      if (!viewExists) {
-        return {
-          tabName: this.getTabName(),
-          columns: [],
-          strings: [],
-          columnSums: [],
-        };
-      }
-    }
-
-    const defs = this.getColumnDefinitions();
-    const colIds = defs.map((col) => col.columnId);
-    const pref = globals.state.aggregatePreferences[this.kind];
-    let sorting = `${this.getDefaultSorting().column} ${
-      this.getDefaultSorting().direction
-    }`;
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    if (pref && pref.sorting) {
-      sorting = `${pref.sorting.column} ${pref.sorting.direction}`;
-    }
-    const query = `select ${colIds} from ${this.kind} order by ${sorting}`;
-    const result = await this.args.engine.query(query);
-
-    const numRows = result.numRows();
-    const columns = defs.map((def) => this.columnFromColumnDef(def, numRows));
-    const columnSums = await Promise.all(defs.map((def) => this.getSum(def)));
-    const extraData = await this.getExtra(this.args.engine, area);
-    const extra = extraData ? extraData : undefined;
-    const data: AggregateData = {
-      tabName: this.getTabName(),
-      columns,
-      columnSums,
-      strings: [],
-      extra,
-    };
-
-    const stringIndexes = new Map<string, number>();
-    function internString(str: string) {
-      let idx = stringIndexes.get(str);
-      if (idx !== undefined) return idx;
-      idx = data.strings.length;
-      data.strings.push(str);
-      stringIndexes.set(str, idx);
-      return idx;
-    }
-
-    const it = result.iter({});
-    for (let i = 0; it.valid(); it.next(), ++i) {
-      for (const column of data.columns) {
-        const item = it.get(column.columnId);
-        if (item === null) {
-          column.data[i] = isStringColumn(column) ? internString('NULL') : 0;
-        } else if (isString(item)) {
-          column.data[i] = internString(item);
-        } else if (item instanceof Uint8Array) {
-          column.data[i] = internString('<Binary blob>');
-        } else if (typeof item === 'bigint') {
-          // TODO(stevegolton) It would be nice to keep bigints as bigints for
-          // the purposes of aggregation, however the aggregation infrastructure
-          // is likely to be significantly reworked when we introduce EventSet,
-          // and the complexity of supporting bigints throughout the aggregation
-          // panels in its current form is not worth it. Thus, we simply
-          // convert bigints to numbers.
-          column.data[i] = Number(item);
-        } else {
-          column.data[i] = item;
-        }
-      }
-    }
-
-    return data;
-  }
-
-  async getSum(def: ColumnDef): Promise<string> {
-    if (!def.sum) return '';
-    const result = await this.args.engine.query(
-      `select ifnull(sum(${def.columnId}), 0) as s from ${this.kind}`,
-    );
-    let sum = result.firstRow({s: NUM}).s;
-    if (def.kind === 'TIMESTAMP_NS') {
-      sum = sum / 1e6;
-    }
-    return `${sum}`;
-  }
-
-  columnFromColumnDef(def: ColumnDef, numRows: number): Column {
-    // TODO(hjd): The Column type should be based on the
-    // ColumnDef type or vice versa to avoid this cast.
-    return {
-      title: def.title,
-      kind: def.kind,
-      data: new def.columnConstructor(numRows),
-      columnId: def.columnId,
-    } as Column;
-  }
-}
diff --git a/ui/src/controller/aggregation/counter_aggregation_controller.ts b/ui/src/controller/aggregation/counter_aggregation_controller.ts
deleted file mode 100644
index f8f25c4..0000000
--- a/ui/src/controller/aggregation/counter_aggregation_controller.ts
+++ /dev/null
@@ -1,179 +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.
-
-import {Duration} from '../../base/time';
-import {ColumnDef} from '../../common/aggregation_data';
-import {Area, Sorting} from '../../common/state';
-import {globals} from '../../frontend/globals';
-import {COUNTER_TRACK_KIND} from '../../public';
-import {Engine} from '../../trace_processor/engine';
-
-import {AggregationController} from './aggregation_controller';
-
-export class CounterAggregationController extends AggregationController {
-  async createAggregateView(engine: Engine, area: Area) {
-    const trackIds: (string | number)[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === COUNTER_TRACK_KIND) {
-          trackInfo.tags?.trackIds && trackIds.push(...trackInfo.tags.trackIds);
-        }
-      }
-    }
-    if (trackIds.length === 0) return false;
-    const duration = area.end - area.start;
-    const durationSec = Duration.toSeconds(duration);
-
-    // 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.kind} AS
-      WITH aggregated AS (
-        SELECT
-          COUNT(1) AS count,
-          ROUND(SUM(
-            (MIN(ts + dur, ${area.end}) - MAX(ts,${area.start}))*value)/${duration},
-            2
-          ) AS avg_value,
-          (SELECT value FROM experimental_counter_dur WHERE track_id = ${trackIds[0]}
-            AND ts + dur >= ${area.start}
-            AND ts <= ${area.end} ORDER BY ts DESC LIMIT 1)
-            AS last_value,
-          (SELECT value FROM experimental_counter_dur WHERE track_id = ${trackIds[0]}
-            AND ts + dur >= ${area.start}
-            AND ts <= ${area.end} ORDER BY ts ASC LIMIT 1)
-            AS first_value,
-          MIN(value) AS min_value,
-          MAX(value) AS max_value
-        FROM experimental_counter_dur
-          WHERE track_id = ${trackIds[0]}
-          AND ts + dur >= ${area.start}
-          AND ts <= ${area.end})
-      SELECT
-        (SELECT name FROM counter_track WHERE id = ${trackIds[0]}) AS name,
-        *,
-        MAX(last_value) - MIN(first_value) AS delta_value,
-        ROUND((MAX(last_value) - MIN(first_value))/${durationSec}, 2) AS rate
-      FROM aggregated`;
-    } else {
-      // Slower, but general purspose query that can aggregate multiple tracks
-      query = `CREATE OR REPLACE PERFETTO TABLE ${this.kind} AS
-      WITH aggregated AS (
-        SELECT track_id,
-          COUNT(1) AS count,
-          ROUND(SUM(
-            (MIN(ts + dur, ${area.end}) - MAX(ts,${area.start}))*value)/${duration},
-            2
-          ) AS avg_value,
-          value_at_max_ts(-ts, value) AS first,
-          value_at_max_ts(ts, value) AS last,
-          MIN(value) AS min_value,
-          MAX(value) AS max_value
-        FROM experimental_counter_dur
-          WHERE track_id IN (${trackIds})
-          AND ts + dur >= ${area.start} AND
-          ts <= ${area.end}
-        GROUP BY track_id
-      )
-      SELECT
-        name,
-        count,
-        avg_value,
-        last AS last_value,
-        first AS first_value,
-        last - first AS delta_value,
-        ROUND((last - first)/${durationSec}, 2) AS rate,
-        min_value,
-        max_value
-      FROM aggregated JOIN counter_track ON
-        track_id = counter_track.id
-      GROUP BY track_id`;
-    }
-    await engine.query(query);
-    return true;
-  }
-
-  getColumnDefinitions(): ColumnDef[] {
-    return [
-      {
-        title: 'Name',
-        kind: 'STRING',
-        columnConstructor: Uint16Array,
-        columnId: 'name',
-      },
-      {
-        title: 'Delta value',
-        kind: 'NUMBER',
-        columnConstructor: Float64Array,
-        columnId: 'delta_value',
-      },
-      {
-        title: 'Rate /s',
-        kind: 'Number',
-        columnConstructor: Float64Array,
-        columnId: 'rate',
-      },
-      {
-        title: 'Weighted avg value',
-        kind: 'Number',
-        columnConstructor: Float64Array,
-        columnId: 'avg_value',
-      },
-      {
-        title: 'Count',
-        kind: 'Number',
-        columnConstructor: Float64Array,
-        columnId: 'count',
-        sum: true,
-      },
-      {
-        title: 'First value',
-        kind: 'NUMBER',
-        columnConstructor: Float64Array,
-        columnId: 'first_value',
-      },
-      {
-        title: 'Last value',
-        kind: 'NUMBER',
-        columnConstructor: Float64Array,
-        columnId: 'last_value',
-      },
-      {
-        title: 'Min value',
-        kind: 'NUMBER',
-        columnConstructor: Float64Array,
-        columnId: 'min_value',
-      },
-      {
-        title: 'Max value',
-        kind: 'NUMBER',
-        columnConstructor: Float64Array,
-        columnId: 'max_value',
-      },
-    ];
-  }
-
-  async getExtra() {}
-
-  getTabName() {
-    return 'Counters';
-  }
-
-  getDefaultSorting(): Sorting {
-    return {column: 'name', direction: 'DESC'};
-  }
-}
diff --git a/ui/src/controller/aggregation/cpu_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
deleted file mode 100644
index 6ea3f67..0000000
--- a/ui/src/controller/aggregation/cpu_aggregation_controller.ts
+++ /dev/null
@@ -1,118 +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.
-
-import {exists} from '../../base/utils';
-import {ColumnDef} from '../../common/aggregation_data';
-import {Area, Sorting} from '../../common/state';
-import {CPU_SLICE_TRACK_KIND} from '../../core/track_kinds';
-import {globals} from '../../frontend/globals';
-import {Engine} from '../../trace_processor/engine';
-
-import {AggregationController} from './aggregation_controller';
-
-export class CpuAggregationController extends AggregationController {
-  async createAggregateView(engine: Engine, area: Area) {
-    const selectedCpus: number[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
-        }
-      }
-    }
-    if (selectedCpus.length === 0) return false;
-
-    await engine.query(`
-      create or replace perfetto table ${this.kind} as
-      select
-        process.name as process_name,
-        pid,
-        thread.name as thread_name,
-        tid,
-        sum(dur) AS total_dur,
-        sum(dur) / count() as avg_dur,
-        count() as occurrences
-      from process
-      join thread using (upid)
-      join sched using (utid)
-      where cpu in (${selectedCpus})
-        and sched.ts + sched.dur > ${area.start}
-        and sched.ts < ${area.end}
-        and utid != 0
-      group by utid
-    `);
-    return true;
-  }
-
-  getTabName() {
-    return 'CPU by thread';
-  }
-
-  async getExtra() {}
-
-  getDefaultSorting(): Sorting {
-    return {column: 'total_dur', direction: 'DESC'};
-  }
-
-  getColumnDefinitions(): ColumnDef[] {
-    return [
-      {
-        title: 'Process',
-        kind: 'STRING',
-        columnConstructor: Uint16Array,
-        columnId: 'process_name',
-      },
-      {
-        title: 'PID',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'pid',
-      },
-      {
-        title: 'Thread',
-        kind: 'STRING',
-        columnConstructor: Uint16Array,
-        columnId: 'thread_name',
-      },
-      {
-        title: 'TID',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'tid',
-      },
-      {
-        title: 'Wall duration (ms)',
-        kind: 'TIMESTAMP_NS',
-        columnConstructor: Float64Array,
-        columnId: 'total_dur',
-        sum: true,
-      },
-      {
-        title: 'Avg Wall duration (ms)',
-        kind: 'TIMESTAMP_NS',
-        columnConstructor: Float64Array,
-        columnId: 'avg_dur',
-      },
-      {
-        title: 'Occurrences',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'occurrences',
-        sum: true,
-      },
-    ];
-  }
-}
diff --git a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
deleted file mode 100644
index eeb0942..0000000
--- a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
+++ /dev/null
@@ -1,105 +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 {exists} from '../../base/utils';
-import {ColumnDef} from '../../common/aggregation_data';
-import {Area, Sorting} from '../../common/state';
-import {globals} from '../../frontend/globals';
-import {Engine} from '../../trace_processor/engine';
-import {CPU_SLICE_TRACK_KIND} from '../../core/track_kinds';
-
-import {AggregationController} from './aggregation_controller';
-
-export class CpuByProcessAggregationController extends AggregationController {
-  async createAggregateView(engine: Engine, area: Area) {
-    const selectedCpus: number[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
-        }
-      }
-    }
-    if (selectedCpus.length === 0) return false;
-
-    await engine.query(`
-      create or replace perfetto table ${this.kind} as
-      select
-        process.name as process_name,
-        process.pid,
-        sum(dur) AS total_dur,
-        sum(dur) / count() as avg_dur,
-        count() as occurrences
-      from sched
-      join thread USING (utid)
-      join process USING (upid)
-      where
-        cpu in (${selectedCpus})
-        and ts + dur > ${area.start}
-        and ts < ${area.end}
-        and utid != 0
-      group by upid
-    `);
-    return true;
-  }
-
-  getTabName() {
-    return 'CPU by process';
-  }
-
-  async getExtra() {}
-
-  getDefaultSorting(): Sorting {
-    return {column: 'total_dur', direction: 'DESC'};
-  }
-
-  getColumnDefinitions(): ColumnDef[] {
-    return [
-      {
-        title: 'Process',
-        kind: 'STRING',
-        columnConstructor: Uint16Array,
-        columnId: 'process_name',
-      },
-      {
-        title: 'PID',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'pid',
-      },
-      {
-        title: 'Wall duration (ms)',
-        kind: 'TIMESTAMP_NS',
-        columnConstructor: Float64Array,
-        columnId: 'total_dur',
-        sum: true,
-      },
-      {
-        title: 'Avg Wall duration (ms)',
-        kind: 'TIMESTAMP_NS',
-        columnConstructor: Float64Array,
-        columnId: 'avg_dur',
-      },
-      {
-        title: 'Occurrences',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'occurrences',
-        sum: true,
-      },
-    ];
-  }
-}
diff --git a/ui/src/controller/aggregation/frame_aggregation_controller.ts b/ui/src/controller/aggregation/frame_aggregation_controller.ts
deleted file mode 100644
index 7c91b4b..0000000
--- a/ui/src/controller/aggregation/frame_aggregation_controller.ts
+++ /dev/null
@@ -1,101 +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 {ColumnDef} from '../../common/aggregation_data';
-import {Area, Sorting} from '../../common/state';
-import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../../core/track_kinds';
-import {globals} from '../../frontend/globals';
-import {Engine} from '../../trace_processor/engine';
-
-import {AggregationController} from './aggregation_controller';
-
-export class FrameAggregationController extends AggregationController {
-  async createAggregateView(engine: Engine, area: Area) {
-    const selectedSqlTrackIds: number[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      // Track will be undefined for track groups.
-      if (track?.uri !== undefined) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === ACTUAL_FRAMES_SLICE_TRACK_KIND) {
-          trackInfo.tags.trackIds &&
-            selectedSqlTrackIds.push(...trackInfo.tags.trackIds);
-        }
-      }
-    }
-    if (selectedSqlTrackIds.length === 0) return false;
-
-    await engine.query(`
-      create or replace perfetto table ${this.kind} as
-      select
-        jank_type,
-        count(1) as occurrences,
-        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}
-        AND ts < ${area.end}
-      group by jank_type
-    `);
-    return true;
-  }
-
-  getTabName() {
-    return 'Frames';
-  }
-
-  async getExtra() {}
-
-  getDefaultSorting(): Sorting {
-    return {column: 'occurrences', direction: 'DESC'};
-  }
-
-  getColumnDefinitions(): ColumnDef[] {
-    return [
-      {
-        title: 'Jank Type',
-        kind: 'STRING',
-        columnConstructor: Uint16Array,
-        columnId: 'jank_type',
-      },
-      {
-        title: 'Min duration',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'minDur',
-      },
-      {
-        title: 'Max duration',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'maxDur',
-      },
-      {
-        title: 'Mean duration',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'meanDur',
-      },
-      {
-        title: 'Occurrences',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'occurrences',
-        sum: true,
-      },
-    ];
-  }
-}
diff --git a/ui/src/controller/aggregation/slice_aggregation_controller.ts b/ui/src/controller/aggregation/slice_aggregation_controller.ts
deleted file mode 100644
index 96cb0ea..0000000
--- a/ui/src/controller/aggregation/slice_aggregation_controller.ts
+++ /dev/null
@@ -1,108 +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.
-
-import {ColumnDef} from '../../common/aggregation_data';
-import {Area, Sorting} from '../../common/state';
-import {globals} from '../../frontend/globals';
-import {Engine} from '../../trace_processor/engine';
-
-import {AggregationController} from './aggregation_controller';
-import {
-  ASYNC_SLICE_TRACK_KIND,
-  THREAD_SLICE_TRACK_KIND,
-} from '../../core/track_kinds';
-
-export function getSelectedTrackKeys(area: Area): number[] {
-  const selectedTrackKeys: number[] = [];
-  for (const trackKey of area.tracks) {
-    const track = globals.state.tracks[trackKey];
-    // Track will be undefined for track groups.
-    if (track?.uri !== undefined) {
-      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-      if (trackInfo?.tags?.kind === THREAD_SLICE_TRACK_KIND) {
-        trackInfo.tags.trackIds &&
-          selectedTrackKeys.push(...trackInfo.tags.trackIds);
-      }
-      if (trackInfo?.tags?.kind === ASYNC_SLICE_TRACK_KIND) {
-        trackInfo.tags.trackIds &&
-          selectedTrackKeys.push(...trackInfo.tags.trackIds);
-      }
-    }
-  }
-  return selectedTrackKeys;
-}
-
-export class SliceAggregationController extends AggregationController {
-  async createAggregateView(engine: Engine, area: Area) {
-    const selectedTrackKeys = getSelectedTrackKeys(area);
-
-    if (selectedTrackKeys.length === 0) return false;
-
-    await engine.query(`
-      create or replace perfetto table ${this.kind} as
-      select
-        name,
-        sum(dur) AS total_dur,
-        sum(dur)/count() as avg_dur,
-        count() as occurrences
-        from slices
-      where track_id in (${selectedTrackKeys})
-        and ts + dur > ${area.start}
-        and ts < ${area.end}
-      group by name
-    `);
-    return true;
-  }
-
-  getTabName() {
-    return 'Slices';
-  }
-
-  async getExtra() {}
-
-  getDefaultSorting(): Sorting {
-    return {column: 'total_dur', direction: 'DESC'};
-  }
-
-  getColumnDefinitions(): ColumnDef[] {
-    return [
-      {
-        title: 'Name',
-        kind: 'STRING',
-        columnConstructor: Uint32Array,
-        columnId: 'name',
-      },
-      {
-        title: 'Wall duration (ms)',
-        kind: 'TIMESTAMP_NS',
-        columnConstructor: Float64Array,
-        columnId: 'total_dur',
-        sum: true,
-      },
-      {
-        title: 'Avg Wall duration (ms)',
-        kind: 'TIMESTAMP_NS',
-        columnConstructor: Float64Array,
-        columnId: 'avg_dur',
-      },
-      {
-        title: 'Occurrences',
-        kind: 'NUMBER',
-        columnConstructor: Uint32Array,
-        columnId: 'occurrences',
-        sum: true,
-      },
-    ];
-  }
-}
diff --git a/ui/src/controller/aggregation/thread_aggregation_controller.ts b/ui/src/controller/aggregation/thread_aggregation_controller.ts
deleted file mode 100644
index 4f502e2..0000000
--- a/ui/src/controller/aggregation/thread_aggregation_controller.ts
+++ /dev/null
@@ -1,174 +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.
-
-import {exists} from '../../base/utils';
-import {ColumnDef, ThreadStateExtra} from '../../common/aggregation_data';
-import {Area, Sorting} from '../../common/state';
-import {translateState} from '../../common/thread_state';
-import {THREAD_STATE_TRACK_KIND} from '../../core/track_kinds';
-import {globals} from '../../frontend/globals';
-import {Engine} from '../../trace_processor/engine';
-import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
-
-import {AggregationController} from './aggregation_controller';
-
-export class ThreadAggregationController extends AggregationController {
-  private utids?: number[];
-
-  setThreadStateUtids(tracks: string[]) {
-    this.utids = [];
-    for (const trackId of tracks) {
-      const track = globals.state.tracks[trackId];
-      // Track will be undefined for track groups.
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === THREAD_STATE_TRACK_KIND) {
-          exists(trackInfo.tags.utid) && this.utids.push(trackInfo.tags.utid);
-        }
-      }
-    }
-  }
-
-  async createAggregateView(engine: Engine, area: Area) {
-    this.setThreadStateUtids(area.tracks);
-    if (this.utids === undefined || this.utids.length === 0) return false;
-
-    await engine.query(`
-      create or replace perfetto table ${this.kind} as
-      select
-        process.name as process_name,
-        process.pid,
-        thread.name as thread_name,
-        thread.tid,
-        tstate.state || ',' || ifnull(tstate.io_wait, 'NULL') as concat_state,
-        sum(tstate.dur) AS total_dur,
-        sum(tstate.dur) / count() as avg_dur,
-        count() as occurrences
-      from thread_state tstate
-      join thread using (utid)
-      left join process using (upid)
-      where
-        utid in (${this.utids})
-        and ts + dur > ${area.start}
-        and ts < ${area.end}
-      group by utid, concat_state
-    `);
-    return true;
-  }
-
-  async getExtra(engine: Engine, area: Area): Promise<ThreadStateExtra | void> {
-    this.setThreadStateUtids(area.tracks);
-    if (this.utids === undefined || this.utids.length === 0) return;
-
-    const query = `
-      select
-        state,
-        io_wait as ioWait,
-        sum(dur) as totalDur
-      from thread
-      join thread_state using (utid)
-      where utid in (${this.utids})
-        and thread_state.ts + thread_state.dur > ${area.start}
-        and thread_state.ts < ${area.end}
-      group by state, io_wait
-    `;
-    const result = await engine.query(query);
-
-    const it = result.iter({
-      state: STR_NULL,
-      ioWait: NUM_NULL,
-      totalDur: NUM,
-    });
-
-    const summary: ThreadStateExtra = {
-      kind: 'THREAD_STATE',
-      states: [],
-      values: new Float64Array(result.numRows()),
-      totalMs: 0,
-    };
-    summary.totalMs = 0;
-    for (let i = 0; it.valid(); ++i, it.next()) {
-      const state = it.state == null ? undefined : it.state;
-      const ioWait = it.ioWait === null ? undefined : it.ioWait > 0;
-      summary.states.push(translateState(state, ioWait));
-      const ms = it.totalDur / 1000000;
-      summary.values[i] = ms;
-      summary.totalMs += ms;
-    }
-    return summary;
-  }
-
-  getColumnDefinitions(): ColumnDef[] {
-    return [
-      {
-        title: 'Process',
-        kind: 'STRING',
-        columnConstructor: Uint16Array,
-        columnId: 'process_name',
-      },
-      {
-        title: 'PID',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'pid',
-      },
-      {
-        title: 'Thread',
-        kind: 'STRING',
-        columnConstructor: Uint16Array,
-        columnId: 'thread_name',
-      },
-      {
-        title: 'TID',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'tid',
-      },
-      {
-        title: 'State',
-        kind: 'STATE',
-        columnConstructor: Uint16Array,
-        columnId: 'concat_state',
-      },
-      {
-        title: 'Wall duration (ms)',
-        kind: 'TIMESTAMP_NS',
-        columnConstructor: Float64Array,
-        columnId: 'total_dur',
-        sum: true,
-      },
-      {
-        title: 'Avg Wall duration (ms)',
-        kind: 'TIMESTAMP_NS',
-        columnConstructor: Float64Array,
-        columnId: 'avg_dur',
-      },
-      {
-        title: 'Occurrences',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'occurrences',
-        sum: true,
-      },
-    ];
-  }
-
-  getTabName() {
-    return 'Thread States';
-  }
-
-  getDefaultSorting(): Sorting {
-    return {column: 'total_dur', direction: 'DESC'};
-  }
-}
diff --git a/ui/src/controller/aggregation/wattson/estimate_aggregation_controller.ts b/ui/src/controller/aggregation/wattson/estimate_aggregation_controller.ts
deleted file mode 100644
index 896b136..0000000
--- a/ui/src/controller/aggregation/wattson/estimate_aggregation_controller.ts
+++ /dev/null
@@ -1,126 +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 {ColumnDef} from '../../../common/aggregation_data';
-import {Area, Sorting} from '../../../common/state';
-import {globals} from '../../../frontend/globals';
-import {Engine} from '../../../trace_processor/engine';
-import {CPUSS_ESTIMATE_TRACK_KIND} from '../../../core/track_kinds';
-import {AggregationController} from '../aggregation_controller';
-import {hasWattsonSupport} from '../../../core/trace_config_utils';
-import {exists} from '../../../base/utils';
-
-export class WattsonEstimateAggregationController extends AggregationController {
-  async createAggregateView(engine: Engine, area: Area) {
-    await engine.query(`drop view if exists ${this.kind};`);
-
-    // Short circuit if Wattson is not supported for this Perfetto trace
-    if (!(await hasWattsonSupport(engine))) return false;
-
-    const estimateTracks: string[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (
-          trackInfo?.tags?.kind === CPUSS_ESTIMATE_TRACK_KIND &&
-          exists(trackInfo.tags?.wattson)
-        ) {
-          estimateTracks.push(`${trackInfo.tags.wattson}`);
-        }
-      }
-    }
-    if (estimateTracks.length === 0) return false;
-
-    const query = this.getEstimateTracksQuery(area, estimateTracks);
-    engine.query(query);
-
-    return true;
-  }
-
-  getEstimateTracksQuery(
-    area: Area,
-    estimateTracks: ReadonlyArray<string>,
-  ): string {
-    const duration = area.end - area.start;
-    let query = `
-      INCLUDE PERFETTO MODULE wattson.curves.ungrouped;
-
-      CREATE OR REPLACE PERFETTO TABLE _ui_selection_window AS
-      SELECT
-        ${area.start} as ts,
-        ${duration} as dur;
-
-      DROP TABLE IF EXISTS _windowed_cpuss_estimate;
-      CREATE VIRTUAL TABLE _windowed_cpuss_estimate
-      USING
-        SPAN_JOIN(_ui_selection_window, _system_state_mw);
-
-      CREATE VIEW ${this.kind} AS
-    `;
-
-    // Convert average power track to total energy in UI window, then divide by
-    // duration of window to get average estimated power of the window
-    estimateTracks.forEach((estimateTrack, i) => {
-      if (i != 0) {
-        query += `UNION ALL `;
-      }
-      query += `
-        SELECT
-        '${estimateTrack}' as name,
-        ROUND(SUM(${estimateTrack}_mw * dur) / ${duration}, 2) as power,
-        ROUND(SUM(${estimateTrack}_mw * dur) / 1000000000, 2) as energy
-        FROM _windowed_cpuss_estimate
-      `;
-    });
-    query += `;`;
-
-    return query;
-  }
-
-  getColumnDefinitions(): ColumnDef[] {
-    return [
-      {
-        title: 'Name',
-        kind: 'STRING',
-        columnConstructor: Uint16Array,
-        columnId: 'name',
-      },
-      {
-        title: 'Average estimated power (mW)',
-        kind: 'NUMBER',
-        columnConstructor: Float64Array,
-        columnId: 'power',
-        sum: true,
-      },
-      {
-        title: 'Total estimated energy (mWs)',
-        kind: 'NUMBER',
-        columnConstructor: Float64Array,
-        columnId: 'energy',
-        sum: true,
-      },
-    ];
-  }
-
-  async getExtra() {}
-
-  getTabName() {
-    return 'Wattson estimates';
-  }
-
-  getDefaultSorting(): Sorting {
-    return {column: 'name', direction: 'ASC'};
-  }
-}
diff --git a/ui/src/controller/aggregation/wattson/package_aggregation_controller.ts b/ui/src/controller/aggregation/wattson/package_aggregation_controller.ts
deleted file mode 100644
index d43efdd..0000000
--- a/ui/src/controller/aggregation/wattson/package_aggregation_controller.ts
+++ /dev/null
@@ -1,116 +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 {exists} from '../../../base/utils';
-import {ColumnDef} from '../../../common/aggregation_data';
-import {Area, Sorting} from '../../../common/state';
-import {globals} from '../../../frontend/globals';
-import {Engine} from '../../../trace_processor/engine';
-import {NUM} from '../../../trace_processor/query_result';
-import {CPU_SLICE_TRACK_KIND} from '../../../core/track_kinds';
-import {AggregationController} from '../aggregation_controller';
-import {hasWattsonSupport} from '../../../core/trace_config_utils';
-
-export class WattsonPackageAggregationController extends AggregationController {
-  async createAggregateView(engine: Engine, area: Area) {
-    await engine.query(`drop view if exists ${this.kind};`);
-
-    // Short circuit if Wattson is not supported for this Perfetto trace
-    if (!(await hasWattsonSupport(engine))) return false;
-    const packageInfo = await engine.query(`
-      INCLUDE PERFETTO MODULE android.process_metadata;
-      SELECT COUNT(*) as isValid FROM android_process_metadata
-      WHERE package_name IS NOT NULL
-    `);
-    if (packageInfo.firstRow({isValid: NUM}).isValid === 0) return false;
-
-    const selectedCpus: number[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
-        }
-      }
-    }
-    if (selectedCpus.length === 0) return false;
-
-    const duration = area.end - area.start;
-
-    // Prerequisite tables are already generated by Wattson thread aggregation,
-    // which is run prior to execution of this module
-    engine.query(`
-      -- Grouped by UID and made CPU agnostic
-      CREATE VIEW ${this.kind} AS
-      SELECT
-        ROUND(SUM(total_pws) / ${duration}, 2) as avg_mw,
-        ROUND(SUM(total_pws) / 1000000000, 2) as total_mws,
-        ROUND(SUM(dur) / 1000000.0, 2) as dur_ms,
-        uid,
-        package_name
-      FROM _unioned_per_cpu_total
-      GROUP BY uid;
-    `);
-
-    return true;
-  }
-
-  getColumnDefinitions(): ColumnDef[] {
-    return [
-      {
-        title: 'Package Name',
-        kind: 'STRING',
-        columnConstructor: Uint16Array,
-        columnId: 'package_name',
-      },
-      {
-        title: 'Android app UID',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'uid',
-      },
-      {
-        title: 'Total Duration (ms)',
-        kind: 'NUMBER',
-        columnConstructor: Float64Array,
-        columnId: 'dur_ms',
-      },
-      {
-        title: 'Average estimated power (mW)',
-        kind: 'NUMBER',
-        columnConstructor: Float64Array,
-        columnId: 'avg_mw',
-        sum: true,
-      },
-      {
-        title: 'Total estimated energy (mWs)',
-        kind: 'NUMBER',
-        columnConstructor: Float64Array,
-        columnId: 'total_mws',
-        sum: true,
-      },
-    ];
-  }
-
-  async getExtra() {}
-
-  getTabName() {
-    return 'Wattson by package';
-  }
-
-  getDefaultSorting(): Sorting {
-    return {column: 'total_mws', direction: 'DESC'};
-  }
-}
diff --git a/ui/src/controller/aggregation/wattson/process_aggregation_controller.ts b/ui/src/controller/aggregation/wattson/process_aggregation_controller.ts
deleted file mode 100644
index 6c94d40..0000000
--- a/ui/src/controller/aggregation/wattson/process_aggregation_controller.ts
+++ /dev/null
@@ -1,102 +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 {exists} from '../../../base/utils';
-import {ColumnDef} from '../../../common/aggregation_data';
-import {Area, Sorting} from '../../../common/state';
-import {globals} from '../../../frontend/globals';
-import {Engine} from '../../../trace_processor/engine';
-import {CPU_SLICE_TRACK_KIND} from '../../../core/track_kinds';
-import {AggregationController} from '../aggregation_controller';
-import {hasWattsonSupport} from '../../../core/trace_config_utils';
-
-export class WattsonProcessAggregationController extends AggregationController {
-  async createAggregateView(engine: Engine, area: Area) {
-    await engine.query(`drop view if exists ${this.kind};`);
-
-    // Short circuit if Wattson is not supported for this Perfetto trace
-    if (!(await hasWattsonSupport(engine))) return false;
-
-    const selectedCpus: number[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
-        }
-      }
-    }
-    if (selectedCpus.length === 0) return false;
-
-    const duration = area.end - area.start;
-
-    // Prerequisite tables are already generated by Wattson thread aggregation,
-    // which is run prior to execution of this module
-    engine.query(`
-      -- Grouped by UPID and made CPU agnostic
-      CREATE VIEW ${this.kind} AS
-      SELECT
-        ROUND(SUM(total_pws) / ${duration}, 2) as avg_mw,
-        ROUND(SUM(total_pws) / 1000000000, 2) as total_mws,
-        pid,
-        process_name
-      FROM _unioned_per_cpu_total
-      GROUP BY upid;
-    `);
-
-    return true;
-  }
-
-  getColumnDefinitions(): ColumnDef[] {
-    return [
-      {
-        title: 'Process Name',
-        kind: 'STRING',
-        columnConstructor: Uint16Array,
-        columnId: 'process_name',
-      },
-      {
-        title: 'PID',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'pid',
-      },
-      {
-        title: 'Average estimated power (mW)',
-        kind: 'NUMBER',
-        columnConstructor: Float64Array,
-        columnId: 'avg_mw',
-        sum: true,
-      },
-      {
-        title: 'Total estimated energy (mWs)',
-        kind: 'NUMBER',
-        columnConstructor: Float64Array,
-        columnId: 'total_mws',
-        sum: true,
-      },
-    ];
-  }
-
-  async getExtra() {}
-
-  getTabName() {
-    return 'Wattson by process';
-  }
-
-  getDefaultSorting(): Sorting {
-    return {column: 'total_mws', direction: 'DESC'};
-  }
-}
diff --git a/ui/src/controller/aggregation/wattson/thread_aggregation_controller.ts b/ui/src/controller/aggregation/wattson/thread_aggregation_controller.ts
deleted file mode 100644
index ae92e55..0000000
--- a/ui/src/controller/aggregation/wattson/thread_aggregation_controller.ts
+++ /dev/null
@@ -1,185 +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 {exists} from '../../../base/utils';
-import {ColumnDef} from '../../../common/aggregation_data';
-import {Area, Sorting} from '../../../common/state';
-import {globals} from '../../../frontend/globals';
-import {Engine} from '../../../trace_processor/engine';
-import {CPU_SLICE_TRACK_KIND} from '../../../core/track_kinds';
-import {AggregationController} from '../aggregation_controller';
-import {hasWattsonSupport} from '../../../core/trace_config_utils';
-
-export class WattsonThreadAggregationController extends AggregationController {
-  async createAggregateView(engine: Engine, area: Area) {
-    await engine.query(`drop view if exists ${this.kind};`);
-
-    // Short circuit if Wattson is not supported for this Perfetto trace
-    if (!(await hasWattsonSupport(engine))) return false;
-
-    const selectedCpus: number[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
-        }
-      }
-    }
-    if (selectedCpus.length === 0) return false;
-
-    const duration = area.end - area.start;
-    engine.query(`
-      INCLUDE PERFETTO MODULE viz.summary.threads_w_processes;
-      INCLUDE PERFETTO MODULE wattson.curves.ungrouped;
-
-      CREATE OR REPLACE PERFETTO TABLE _ui_selection_window AS
-      SELECT
-        ${area.start} as ts,
-        ${duration} as dur;
-
-      -- Processes filtered by CPU within the UI defined time window
-      DROP TABLE IF EXISTS _windowed_summary;
-      CREATE VIRTUAL TABLE _windowed_summary
-      USING
-        SPAN_JOIN(_ui_selection_window, _sched_w_thread_process_package_summary);
-    `);
-    this.runEstimateThreadsQuery(engine, selectedCpus, duration);
-
-    return true;
-  }
-
-  // This function returns a query that gets the average and estimate from
-  // Wattson for the selection in the UI window based on thread. The grouping by
-  // thread needs to 'remove' 2 dimensions; the threads need to be grouped over
-  // time and the threads need to be grouped over CPUs.
-  // 1. Window and associate thread with proper Wattson estimate slice
-  // 2. Group all threads over time on a per CPU basis
-  // 3. Group all threads over all CPUs
-  runEstimateThreadsQuery(
-    engine: Engine,
-    selectedCpu: number[],
-    duration: bigint,
-  ) {
-    // Estimate and total per UTID per CPU
-    selectedCpu.forEach((cpu) => {
-      engine.query(`
-        -- Packages filtered by CPU
-        CREATE OR REPLACE PERFETTO VIEW _windowed_summary_per_cpu${cpu} AS
-        SELECT *
-        FROM _windowed_summary WHERE cpu = ${cpu};
-
-        -- CPU specific track with slices for curves
-        CREATE OR REPLACE PERFETTO VIEW _per_cpu${cpu}_curve AS
-        SELECT ts, dur, cpu${cpu}_curve
-        FROM _system_state_curves;
-
-        -- Filter out track when threads are available
-        DROP TABLE IF EXISTS _windowed_thread_curve${cpu};
-        CREATE VIRTUAL TABLE _windowed_thread_curve${cpu}
-        USING
-          SPAN_JOIN(_per_cpu${cpu}_curve, _windowed_summary_per_cpu${cpu});
-
-        -- Total estimate per UTID per CPU
-        CREATE OR REPLACE PERFETTO VIEW _total_per_cpu${cpu} AS
-        SELECT
-          SUM(cpu${cpu}_curve * dur) as total_pws,
-          SUM(dur) as dur,
-          tid,
-          pid,
-          uid,
-          utid,
-          upid,
-          thread_name,
-          process_name,
-          package_name
-        FROM _windowed_thread_curve${cpu}
-        GROUP BY utid;
-      `);
-    });
-
-    // Estimate and total per UTID, removing CPU dimension
-    let query = `CREATE OR REPLACE PERFETTO TABLE _unioned_per_cpu_total AS `;
-    selectedCpu.forEach((cpu, i) => {
-      query += i != 0 ? `UNION ALL\n` : ``;
-      query += `SELECT * from _total_per_cpu${cpu}\n`;
-    });
-    query += `
-      ;
-
-      -- Grouped again by UTID, but this time to make it CPU agnostic
-      CREATE VIEW ${this.kind} AS
-      SELECT
-        ROUND(SUM(total_pws) / ${duration}, 2) as avg_mw,
-        ROUND(SUM(total_pws) / 1000000000, 2) as total_mws,
-        thread_name,
-        tid,
-        pid
-      FROM _unioned_per_cpu_total
-      GROUP BY utid;
-    `;
-
-    engine.query(query);
-
-    return;
-  }
-
-  getColumnDefinitions(): ColumnDef[] {
-    return [
-      {
-        title: 'Thread Name',
-        kind: 'STRING',
-        columnConstructor: Uint16Array,
-        columnId: 'thread_name',
-      },
-      {
-        title: 'TID',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'tid',
-      },
-      {
-        title: 'PID',
-        kind: 'NUMBER',
-        columnConstructor: Uint16Array,
-        columnId: 'pid',
-      },
-      {
-        title: 'Average estimated power (mW)',
-        kind: 'NUMBER',
-        columnConstructor: Float64Array,
-        columnId: 'avg_mw',
-        sum: true,
-      },
-      {
-        title: 'Total estimated energy (mWs)',
-        kind: 'NUMBER',
-        columnConstructor: Float64Array,
-        columnId: 'total_mws',
-        sum: true,
-      },
-    ];
-  }
-
-  async getExtra() {}
-
-  getTabName() {
-    return 'Wattson by thread';
-  }
-
-  getDefaultSorting(): Sorting {
-    return {column: 'total_mws', direction: 'DESC'};
-  }
-}
diff --git a/ui/src/controller/app_controller.ts b/ui/src/controller/app_controller.ts
deleted file mode 100644
index 7d4d7cd..0000000
--- a/ui/src/controller/app_controller.ts
+++ /dev/null
@@ -1,54 +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 {RECORDING_V2_FLAG} from '../core/feature_flags';
-import {globals} from '../frontend/globals';
-
-import {Child, Controller, ControllerInitializerAny} from './controller';
-import {RecordController} from './record_controller';
-import {TraceController} from './trace_controller';
-
-// The root controller for the entire app. It handles the lifetime of all
-// the other controllers (e.g., track and query controllers) according to the
-// global state.
-export class AppController extends Controller<'main'> {
-  // extensionPort is needed for the RecordController to communicate with the
-  // extension through the frontend. This is because the controller is running
-  // on a worker, and isn't able to directly send messages to the extension.
-  private extensionPort: MessagePort;
-
-  constructor(extensionPort: MessagePort) {
-    super('main');
-    this.extensionPort = extensionPort;
-  }
-
-  // This is the root method that is called every time the controller tree is
-  // re-triggered. This can happen due to:
-  // - An action received from the frontend.
-  // - An internal promise of a nested controller being resolved and manually
-  //   re-triggering the controllers.
-  run() {
-    const childControllers: ControllerInitializerAny[] = [];
-    if (!RECORDING_V2_FLAG.get()) {
-      childControllers.push(
-        Child('record', RecordController, {extensionPort: this.extensionPort}),
-      );
-    }
-    if (globals.state.engine !== undefined) {
-      const engineCfg = globals.state.engine;
-      childControllers.push(Child(engineCfg.id, TraceController, engineCfg.id));
-    }
-    return childControllers;
-  }
-}
diff --git a/ui/src/controller/args_parser_unittest.ts b/ui/src/controller/args_parser_unittest.ts
deleted file mode 100644
index ca0cb31..0000000
--- a/ui/src/controller/args_parser_unittest.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {convertArgsToObject, convertArgsToTree} from './args_parser';
-
-const args = [
-  {key: 'simple_key', value: 'simple_value'},
-  {key: 'thing.key', value: 'value'},
-  {key: 'thing.point[0].x', value: 10},
-  {key: 'thing.point[0].y', value: 20},
-  {key: 'thing.point[1].x', value: 0},
-  {key: 'thing.point[1].y', value: -10},
-  {key: 'foo.bar.foo.bar', value: 'baz'},
-];
-
-describe('convertArgsToTree', () => {
-  test('converts example arg set', () => {
-    expect(convertArgsToTree(args)).toEqual([
-      {
-        key: 'simple_key',
-        value: {key: 'simple_key', value: 'simple_value'},
-      },
-      {
-        key: 'thing',
-        children: [
-          {key: 'key', value: {key: 'thing.key', value: 'value'}},
-          {
-            key: 'point',
-            children: [
-              {
-                key: 0,
-                children: [
-                  {
-                    key: 'x',
-                    value: {key: 'thing.point[0].x', value: 10},
-                  },
-                  {
-                    key: 'y',
-                    value: {key: 'thing.point[0].y', value: 20},
-                  },
-                ],
-              },
-              {
-                key: 1,
-                children: [
-                  {
-                    key: 'x',
-                    value: {key: 'thing.point[1].x', value: 0},
-                  },
-                  {
-                    key: 'y',
-                    value: {key: 'thing.point[1].y', value: -10},
-                  },
-                ],
-              },
-            ],
-          },
-        ],
-      },
-      {
-        key: 'foo',
-        children: [
-          {
-            key: 'bar',
-            children: [
-              {
-                key: 'foo',
-                children: [
-                  {
-                    key: 'bar',
-                    value: {key: 'foo.bar.foo.bar', value: 'baz'},
-                  },
-                ],
-              },
-            ],
-          },
-        ],
-      },
-    ]);
-  });
-
-  test('handles value and children in same node', () => {
-    const args = [
-      {key: 'foo', value: 'foo'},
-      {key: 'foo.bar', value: 'bar'},
-    ];
-    expect(convertArgsToTree(args)).toEqual([
-      {
-        key: 'foo',
-        value: {key: 'foo', value: 'foo'},
-        children: [{key: 'bar', value: {key: 'foo.bar', value: 'bar'}}],
-      },
-    ]);
-  });
-
-  test('handles mixed key types', () => {
-    const args = [
-      {key: 'foo[0]', value: 'foo'},
-      {key: 'foo.bar', value: 'bar'},
-    ];
-    expect(convertArgsToTree(args)).toEqual([
-      {
-        key: 'foo',
-        children: [
-          {key: 0, value: {key: 'foo[0]', value: 'foo'}},
-          {key: 'bar', value: {key: 'foo.bar', value: 'bar'}},
-        ],
-      },
-    ]);
-  });
-
-  test('picks latest where duplicate keys exist', () => {
-    const args = [
-      {key: 'foo', value: 'foo'},
-      {key: 'foo', value: 'bar'},
-    ];
-    expect(convertArgsToTree(args)).toEqual([
-      {key: 'foo', value: {key: 'foo', value: 'bar'}},
-    ]);
-  });
-
-  test('handles sparse arrays', () => {
-    const args = [{key: 'foo[12]', value: 'foo'}];
-    expect(convertArgsToTree(args)).toEqual([
-      {
-        key: 'foo',
-        children: [{key: 12, value: {key: 'foo[12]', value: 'foo'}}],
-      },
-    ]);
-  });
-});
-
-describe('convertArgsToObject', () => {
-  it('converts example arg set', () => {
-    expect(convertArgsToObject(args)).toEqual({
-      simple_key: 'simple_value',
-      thing: {
-        key: 'value',
-        point: [
-          {x: 10, y: 20},
-          {x: 0, y: -10},
-        ],
-      },
-      foo: {bar: {foo: {bar: 'baz'}}},
-    });
-  });
-
-  test('throws on args containing a node with both value and children', () => {
-    expect(() => {
-      convertArgsToObject([
-        {key: 'foo', value: 'foo'},
-        {key: 'foo.bar', value: 'bar'},
-      ]);
-    }).toThrow();
-  });
-
-  test('throws on args containing mixed key types', () => {
-    expect(() => {
-      convertArgsToObject([
-        {key: 'foo[0]', value: 'foo'},
-        {key: 'foo.bar', value: 'bar'},
-      ]);
-    }).toThrow();
-  });
-
-  test('picks last one where duplicate keys exist', () => {
-    const args = [
-      {key: 'foo', value: 'foo'},
-      {key: 'foo', value: 'bar'},
-    ];
-    expect(convertArgsToObject(args)).toEqual({foo: 'bar'});
-  });
-
-  test('handles sparse arrays', () => {
-    const args = [{key: 'foo[3]', value: 'foo'}];
-    expect(convertArgsToObject(args)).toEqual({
-      foo: [undefined, undefined, undefined, 'foo'],
-    });
-  });
-});
diff --git a/ui/src/controller/chrome_proxy_record_controller.ts b/ui/src/controller/chrome_proxy_record_controller.ts
deleted file mode 100644
index 843798c..0000000
--- a/ui/src/controller/chrome_proxy_record_controller.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {binaryDecode, binaryEncode} from '../base/string_utils';
-import {TRACE_SUFFIX} from '../common/constants';
-
-import {
-  ConsumerPortResponse,
-  hasProperty,
-  isReadBuffersResponse,
-  Typed,
-} from './consumer_port_types';
-import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
-
-export interface ChromeExtensionError extends Typed {
-  error: string;
-}
-
-export interface ChromeExtensionStatus extends Typed {
-  status: string;
-}
-
-export interface GetCategoriesResponse extends Typed {
-  categories: string[];
-}
-
-export type ChromeExtensionMessage =
-  | ChromeExtensionError
-  | ChromeExtensionStatus
-  | ConsumerPortResponse
-  | GetCategoriesResponse;
-
-export function isChromeExtensionError(
-  obj: Typed,
-): obj is ChromeExtensionError {
-  return obj.type === 'ChromeExtensionError';
-}
-
-export function isChromeExtensionStatus(
-  obj: Typed,
-): obj is ChromeExtensionStatus {
-  return obj.type === 'ChromeExtensionStatus';
-}
-
-function isObject(obj: unknown): obj is object {
-  return typeof obj === 'object' && obj !== null;
-}
-
-export function isGetCategoriesResponse(
-  obj: unknown,
-): obj is GetCategoriesResponse {
-  if (
-    !(
-      isObject(obj) &&
-      hasProperty(obj, 'type') &&
-      obj.type === 'GetCategoriesResponse'
-    )
-  ) {
-    return false;
-  }
-
-  return hasProperty(obj, 'categories') && Array.isArray(obj.categories);
-}
-
-// This class acts as a proxy from the record controller (running in a worker),
-// to the frontend. This is needed because we can't directly talk with the
-// extension from a web-worker, so we use a MessagePort to communicate with the
-// frontend, that will consecutively forward it to the extension.
-
-// Rationale for the binaryEncode / binaryDecode calls below:
-// Messages to/from extensions need to be JSON serializable. ArrayBuffers are
-// not supported. For this reason here we use binaryEncode/Decode.
-// See https://developer.chrome.com/extensions/messaging#simple
-
-export class ChromeExtensionConsumerPort extends RpcConsumerPort {
-  private extensionPort: MessagePort;
-
-  constructor(extensionPort: MessagePort, consumer: Consumer) {
-    super(consumer);
-    this.extensionPort = extensionPort;
-    this.extensionPort.onmessage = this.onExtensionMessage.bind(this);
-  }
-
-  onExtensionMessage(message: {data: ChromeExtensionMessage}) {
-    if (isChromeExtensionError(message.data)) {
-      this.sendErrorMessage(message.data.error);
-      return;
-    }
-    if (isChromeExtensionStatus(message.data)) {
-      this.sendStatus(message.data.status);
-      return;
-    }
-
-    // In this else branch message.data will be a ConsumerPortResponse.
-    if (isReadBuffersResponse(message.data) && message.data.slices) {
-      const slice = message.data.slices[0].data as unknown as string;
-      message.data.slices[0].data = binaryDecode(slice);
-    }
-    this.sendMessage(message.data);
-  }
-
-  handleCommand(method: string, requestData: Uint8Array): void {
-    const reqEncoded = binaryEncode(requestData);
-    this.extensionPort.postMessage({method, requestData: reqEncoded});
-  }
-
-  getRecordedTraceSuffix(): string {
-    return `${TRACE_SUFFIX}.gz`;
-  }
-}
diff --git a/ui/src/controller/consumer_port_types.ts b/ui/src/controller/consumer_port_types.ts
deleted file mode 100644
index 973205f..0000000
--- a/ui/src/controller/consumer_port_types.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {
-  IDisableTracingResponse,
-  IEnableTracingResponse,
-  IFreeBuffersResponse,
-  IGetTraceStatsResponse,
-  IReadBuffersResponse,
-} from '../protos';
-
-export interface Typed {
-  type: string;
-}
-
-// A type guard that can be used in order to be able to access the property of
-// an object in a checked manner.
-export function hasProperty<T extends object, P extends string>(
-  obj: T,
-  prop: P,
-): obj is T & {[prop in P]: unknown} {
-  return obj.hasOwnProperty(prop);
-}
-
-export function isTyped(obj: object): obj is Typed {
-  return obj.hasOwnProperty('type');
-}
-
-export interface ReadBuffersResponse extends Typed, IReadBuffersResponse {}
-export interface EnableTracingResponse extends Typed, IEnableTracingResponse {}
-export interface GetTraceStatsResponse extends Typed, IGetTraceStatsResponse {}
-export interface FreeBuffersResponse extends Typed, IFreeBuffersResponse {}
-export interface GetCategoriesResponse extends Typed {}
-export interface DisableTracingResponse
-  extends Typed,
-    IDisableTracingResponse {}
-
-export type ConsumerPortResponse =
-  | EnableTracingResponse
-  | ReadBuffersResponse
-  | GetTraceStatsResponse
-  | GetCategoriesResponse
-  | FreeBuffersResponse
-  | DisableTracingResponse;
-
-export function isReadBuffersResponse(obj: Typed): obj is ReadBuffersResponse {
-  return obj.type === 'ReadBuffersResponse';
-}
-
-export function isEnableTracingResponse(
-  obj: Typed,
-): obj is EnableTracingResponse {
-  return obj.type === 'EnableTracingResponse';
-}
-
-export function isGetTraceStatsResponse(
-  obj: Typed,
-): obj is GetTraceStatsResponse {
-  return obj.type === 'GetTraceStatsResponse';
-}
-
-export function isFreeBuffersResponse(obj: Typed): obj is FreeBuffersResponse {
-  return obj.type === 'FreeBuffersResponse';
-}
-
-export function isDisableTracingResponse(
-  obj: Typed,
-): obj is DisableTracingResponse {
-  return obj.type === 'DisableTracingResponse';
-}
diff --git a/ui/src/controller/controller.ts b/ui/src/controller/controller.ts
deleted file mode 100644
index b70ed4f..0000000
--- a/ui/src/controller/controller.ts
+++ /dev/null
@@ -1,116 +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.
-
-export type ControllerAny = Controller</* StateType=*/ unknown>;
-
-export interface ControllerFactory<ConstructorArgs> {
-  new (args: ConstructorArgs): ControllerAny;
-}
-
-interface ControllerInitializer<ConstructorArgs> {
-  id: string;
-  factory: ControllerFactory<ConstructorArgs>;
-  args: ConstructorArgs;
-}
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type ControllerInitializerAny = ControllerInitializer<any>;
-
-export function Child<ConstructorArgs>(
-  id: string,
-  factory: ControllerFactory<ConstructorArgs>,
-  args: ConstructorArgs,
-): ControllerInitializer<ConstructorArgs> {
-  return {id, factory, args};
-}
-
-export type Children = ControllerInitializerAny[];
-
-export abstract class Controller<StateType> {
-  // This is about the local FSM state, has nothing to do with the global
-  // app state.
-  private _stateChanged = false;
-  private _inRunner = false;
-  private _state: StateType;
-  private _children = new Map<string, ControllerAny>();
-
-  constructor(initialState: StateType) {
-    this._state = initialState;
-  }
-
-  abstract run(): Children | void;
-  onDestroy(): void {}
-
-  // Invokes the current controller subtree, recursing into children.
-  // While doing so handles lifecycle of child controllers.
-  // This method should be called only by the runControllers() method in
-  // globals.ts. Exposed publicly for testing.
-  invoke(): boolean {
-    if (this._inRunner) throw new Error('Reentrancy in Controller');
-    this._stateChanged = false;
-    this._inRunner = true;
-    const resArray = this.run();
-    let triggerAnotherRun = this._stateChanged;
-    this._stateChanged = false;
-
-    const nextChildren = new Map<string, ControllerInitializerAny>();
-    if (resArray !== undefined) {
-      for (const childConfig of resArray) {
-        if (nextChildren.has(childConfig.id)) {
-          throw new Error(`Duplicate children controller ${childConfig.id}`);
-        }
-        nextChildren.set(childConfig.id, childConfig);
-      }
-    }
-    const dtors = new Array<() => void>();
-    const runners = new Array<() => boolean>();
-    for (const key of this._children.keys()) {
-      if (nextChildren.has(key)) continue;
-      const instance = this._children.get(key)!;
-      this._children.delete(key);
-      dtors.push(() => instance.onDestroy());
-    }
-    for (const nextChild of nextChildren.values()) {
-      if (!this._children.has(nextChild.id)) {
-        const instance = new nextChild.factory(nextChild.args);
-        this._children.set(nextChild.id, instance);
-      }
-      const instance = this._children.get(nextChild.id)!;
-      runners.push(() => instance.invoke());
-    }
-
-    for (const dtor of dtors) dtor(); // Invoke all onDestroy()s.
-
-    // Invoke all runner()s.
-    for (const runner of runners) {
-      const recursiveRes = runner();
-      triggerAnotherRun = triggerAnotherRun || recursiveRes;
-    }
-
-    this._inRunner = false;
-    return triggerAnotherRun;
-  }
-
-  setState(state: StateType) {
-    if (!this._inRunner) {
-      throw new Error('Cannot setState() outside of the run() method');
-    }
-    this._stateChanged = state !== this._state;
-    this._state = state;
-  }
-
-  get state(): StateType {
-    return this._state;
-  }
-}
diff --git a/ui/src/controller/controller_unittest.ts b/ui/src/controller/controller_unittest.ts
deleted file mode 100644
index ff5a558..0000000
--- a/ui/src/controller/controller_unittest.ts
+++ /dev/null
@@ -1,154 +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 {Child, Controller} from './controller';
-
-const _onCreate = jest.fn();
-const _onDestroy = jest.fn();
-const _run = jest.fn();
-
-type MockStates = 'idle' | 'state1' | 'state2' | 'state3';
-class MockController extends Controller<MockStates> {
-  constructor(public type: string) {
-    super('idle');
-    _onCreate(this.type);
-  }
-
-  run() {
-    return _run(this.type);
-  }
-
-  onDestroy() {
-    return _onDestroy(this.type);
-  }
-}
-
-function runControllerTree(rootController: MockController): void {
-  for (let runAgain = true, i = 0; runAgain; i++) {
-    if (i >= 100) throw new Error('Controller livelock');
-    runAgain = rootController.invoke();
-  }
-}
-
-beforeEach(() => {
-  _onCreate.mockClear();
-  _onCreate.mockReset();
-  _onDestroy.mockClear();
-  _onDestroy.mockReset();
-  _run.mockClear();
-  _run.mockReset();
-});
-
-test('singleControllerNoTransition', () => {
-  const rootCtl = new MockController('root');
-  runControllerTree(rootCtl);
-  expect(_run).toHaveBeenCalledTimes(1);
-  expect(_run).toHaveBeenCalledWith('root');
-});
-
-test('singleControllerThreeTransitions', () => {
-  const rootCtl = new MockController('root');
-  _run.mockImplementation(() => {
-    if (rootCtl.state === 'idle') {
-      rootCtl.setState('state1');
-    } else if (rootCtl.state === 'state1') {
-      rootCtl.setState('state2');
-    }
-  });
-  runControllerTree(rootCtl);
-  expect(_run).toHaveBeenCalledTimes(3);
-  expect(_run).toHaveBeenCalledWith('root');
-});
-
-test('nestedControllers', () => {
-  const rootCtl = new MockController('root');
-  let nextState: MockStates = 'idle';
-  _run.mockImplementation((type: string) => {
-    if (type !== 'root') return;
-    rootCtl.setState(nextState);
-    if (rootCtl.state === 'idle') return;
-
-    if (rootCtl.state === 'state1') {
-      return [Child('child1', MockController, 'child1')];
-    }
-    if (rootCtl.state === 'state2') {
-      return [
-        Child('child1', MockController, 'child1'),
-        Child('child2', MockController, 'child2'),
-      ];
-    }
-    if (rootCtl.state === 'state3') {
-      return [
-        Child('child1', MockController, 'child1'),
-        Child('child3', MockController, 'child3'),
-      ];
-    }
-    throw new Error('Not reached');
-  });
-  runControllerTree(rootCtl);
-  expect(_run).toHaveBeenCalledWith('root');
-  expect(_run).toHaveBeenCalledTimes(1);
-
-  // Transition the root controller to state1. This will create the first child
-  // and re-run both (because of the idle -> state1 transition).
-  _run.mockClear();
-  _onCreate.mockClear();
-  nextState = 'state1';
-  runControllerTree(rootCtl);
-  expect(_onCreate).toHaveBeenCalledWith('child1');
-  expect(_onCreate).toHaveBeenCalledTimes(1);
-  expect(_run).toHaveBeenCalledWith('root');
-  expect(_run).toHaveBeenCalledWith('child1');
-  expect(_run).toHaveBeenCalledTimes(4);
-
-  // Transition the root controller to state2. This will create the 2nd child
-  // and run the three of them (root + 2 chilren) two times.
-  _run.mockClear();
-  _onCreate.mockClear();
-  nextState = 'state2';
-  runControllerTree(rootCtl);
-  expect(_onCreate).toHaveBeenCalledWith('child2');
-  expect(_onCreate).toHaveBeenCalledTimes(1);
-  expect(_run).toHaveBeenCalledWith('root');
-  expect(_run).toHaveBeenCalledWith('child1');
-  expect(_run).toHaveBeenCalledWith('child2');
-  expect(_run).toHaveBeenCalledTimes(6);
-
-  // Transition the root controller to state3. This will create the 3rd child
-  // and remove the 2nd one.
-  _run.mockClear();
-  _onCreate.mockClear();
-  nextState = 'state3';
-  runControllerTree(rootCtl);
-  expect(_onCreate).toHaveBeenCalledWith('child3');
-  expect(_onDestroy).toHaveBeenCalledWith('child2');
-  expect(_onCreate).toHaveBeenCalledTimes(1);
-  expect(_run).toHaveBeenCalledWith('root');
-  expect(_run).toHaveBeenCalledWith('child1');
-  expect(_run).toHaveBeenCalledWith('child3');
-  expect(_run).toHaveBeenCalledTimes(6);
-
-  // Finally transition back to the idle state. All children should be removed.
-  _run.mockClear();
-  _onCreate.mockClear();
-  _onDestroy.mockClear();
-  nextState = 'idle';
-  runControllerTree(rootCtl);
-  expect(_onDestroy).toHaveBeenCalledWith('child1');
-  expect(_onDestroy).toHaveBeenCalledWith('child3');
-  expect(_onCreate).toHaveBeenCalledTimes(0);
-  expect(_onDestroy).toHaveBeenCalledTimes(2);
-  expect(_run).toHaveBeenCalledWith('root');
-  expect(_run).toHaveBeenCalledTimes(2);
-});
diff --git a/ui/src/controller/cpu_profile_controller.ts b/ui/src/controller/cpu_profile_controller.ts
deleted file mode 100644
index d658170..0000000
--- a/ui/src/controller/cpu_profile_controller.ts
+++ /dev/null
@@ -1,182 +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.
-
-import {CallsiteInfo} from '../common/legacy_flamegraph_util';
-import {CpuProfileSampleSelection, getLegacySelection} from '../common/state';
-import {CpuProfileDetails, globals} from '../frontend/globals';
-import {publishCpuProfileDetails} from '../frontend/publish';
-import {Engine} from '../trace_processor/engine';
-import {NUM, STR} from '../trace_processor/query_result';
-
-import {Controller} from './controller';
-
-export interface CpuProfileControllerArgs {
-  engine: Engine;
-}
-
-export class CpuProfileController extends Controller<'main'> {
-  private lastSelectedSample?: CpuProfileSampleSelection;
-  private requestingData = false;
-  private queuedRunRequest = false;
-
-  constructor(private args: CpuProfileControllerArgs) {
-    super('main');
-  }
-
-  run() {
-    const selection = getLegacySelection(globals.state);
-    if (!selection || selection.kind !== 'CPU_PROFILE_SAMPLE') {
-      return;
-    }
-
-    const selectedSample = selection as CpuProfileSampleSelection;
-    if (!this.shouldRequestData(selectedSample)) {
-      return;
-    }
-
-    if (this.requestingData) {
-      this.queuedRunRequest = true;
-      return;
-    }
-
-    this.requestingData = true;
-    publishCpuProfileDetails({});
-    this.lastSelectedSample = this.copyCpuProfileSample(selection);
-
-    this.getSampleData(selectedSample.id)
-      .then((sampleData) => {
-        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-        if (
-          sampleData !== undefined &&
-          selectedSample &&
-          /* eslint-enable */
-          this.lastSelectedSample &&
-          this.lastSelectedSample.id === selectedSample.id
-        ) {
-          const cpuProfileDetails: CpuProfileDetails = {
-            id: selectedSample.id,
-            ts: selectedSample.ts,
-            utid: selectedSample.utid,
-            stack: sampleData,
-          };
-
-          publishCpuProfileDetails(cpuProfileDetails);
-        }
-      })
-      .finally(() => {
-        this.requestingData = false;
-        if (this.queuedRunRequest) {
-          this.queuedRunRequest = false;
-          this.run();
-        }
-      });
-  }
-
-  private copyCpuProfileSample(
-    cpuProfileSample: CpuProfileSampleSelection,
-  ): CpuProfileSampleSelection {
-    return {
-      kind: cpuProfileSample.kind,
-      id: cpuProfileSample.id,
-      utid: cpuProfileSample.utid,
-      ts: cpuProfileSample.ts,
-    };
-  }
-
-  private shouldRequestData(selection: CpuProfileSampleSelection) {
-    return (
-      this.lastSelectedSample === undefined ||
-      (this.lastSelectedSample !== undefined &&
-        this.lastSelectedSample.id !== selection.id)
-    );
-  }
-
-  async getSampleData(id: number) {
-    // The goal of the query is to get all the frames of
-    // the callstack at the callsite given by |id|. To do this, it does
-    // the following:
-    // 1. Gets the leaf callsite id for the sample given by |id|.
-    // 2. For this callsite, get all the frame ids and depths
-    //    for the frame and all ancestors in the callstack.
-    // 3. For each frame, get the mapping name (i.e. library which
-    //    contains the frame).
-    // 4. Symbolize each frame using the symbol table if possible.
-    // 5. Sort the query by the depth of the callstack frames.
-    const sampleQuery = `
-      SELECT
-        samples.id as id,
-        IFNULL(
-          (
-            SELECT name
-            FROM stack_profile_symbol symbol
-            WHERE symbol.symbol_set_id = spf.symbol_set_id
-            LIMIT 1
-          ),
-          COALESCE(spf.deobfuscated_name, spf.name, "")
-        ) AS name,
-        spm.name AS mapping
-      FROM cpu_profile_stack_sample AS samples
-      LEFT JOIN (
-        SELECT
-          id,
-          frame_id,
-          depth
-        FROM stack_profile_callsite
-        UNION ALL
-        SELECT
-          leaf.id AS id,
-          callsite.frame_id AS frame_id,
-          callsite.depth AS depth
-        FROM stack_profile_callsite leaf
-        JOIN experimental_ancestor_stack_profile_callsite(leaf.id) AS callsite
-      ) AS callsites
-        ON samples.callsite_id = callsites.id
-      LEFT JOIN stack_profile_frame AS spf
-        ON callsites.frame_id = spf.id
-      LEFT JOIN stack_profile_mapping AS spm
-        ON spf.mapping = spm.id
-      WHERE samples.id = ${id}
-      ORDER BY callsites.depth;
-    `;
-
-    const callsites = await this.args.engine.query(sampleQuery);
-
-    if (callsites.numRows() === 0) {
-      return undefined;
-    }
-
-    const it = callsites.iter({
-      id: NUM,
-      name: STR,
-      mapping: STR,
-    });
-
-    const sampleData: CallsiteInfo[] = [];
-    for (; it.valid(); it.next()) {
-      sampleData.push({
-        id: it.id,
-        totalSize: 0,
-        depth: 0,
-        parentId: 0,
-        name: it.name,
-        selfSize: 0,
-        mapping: it.mapping,
-        merged: false,
-        highlighted: false,
-      });
-    }
-
-    return sampleData;
-  }
-}
diff --git a/ui/src/controller/flow_events_controller.ts b/ui/src/controller/flow_events_controller.ts
deleted file mode 100644
index b2e3d18..0000000
--- a/ui/src/controller/flow_events_controller.ts
+++ /dev/null
@@ -1,458 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size 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 {Time} from '../base/time';
-import {AreaSelection, getLegacySelection} from '../common/state';
-import {featureFlags} from '../core/feature_flags';
-import {Flow, globals} from '../frontend/globals';
-import {publishConnectedFlows, publishSelectedFlows} from '../frontend/publish';
-import {asSliceSqlId} from '../trace_processor/sql_utils/core_types';
-import {Engine} from '../trace_processor/engine';
-import {LONG, NUM, STR_NULL} from '../trace_processor/query_result';
-
-import {Controller} from './controller';
-import {Monitor} from '../base/monitor';
-import {
-  ACTUAL_FRAMES_SLICE_TRACK_KIND,
-  THREAD_SLICE_TRACK_KIND,
-} from '../core/track_kinds';
-
-export interface FlowEventsControllerArgs {
-  engine: Engine;
-}
-
-const SHOW_INDIRECT_PRECEDING_FLOWS_FLAG = featureFlags.register({
-  id: 'showIndirectPrecedingFlows',
-  name: 'Show indirect preceding flows',
-  description:
-    'Show indirect preceding flows (connected through ancestor ' +
-    'slices) when a slice is selected.',
-  defaultValue: false,
-});
-
-export class FlowEventsController extends Controller<'main'> {
-  private readonly monitor = new Monitor([() => globals.state.selection]);
-
-  constructor(private args: FlowEventsControllerArgs) {
-    super('main');
-
-    // Create |CHROME_CUSTOME_SLICE_NAME| helper, which combines slice name
-    // and args for some slices (scheduler tasks and mojo messages) for more
-    // helpful messages.
-    // In the future, it should be replaced with this a more scalable and
-    // customisable solution.
-    // Note that a function here is significantly faster than a join.
-    this.args.engine.query(`
-      SELECT CREATE_FUNCTION(
-        'CHROME_CUSTOM_SLICE_NAME(slice_id LONG)',
-        'STRING',
-        'select case
-           when name="Receive mojo message" then
-            printf("Receive mojo message (interface=%s, hash=%s)",
-              EXTRACT_ARG(arg_set_id,
-                          "chrome_mojo_event_info.mojo_interface_tag"),
-              EXTRACT_ARG(arg_set_id, "chrome_mojo_event_info.ipc_hash"))
-           when name="ThreadControllerImpl::RunTask" or
-                name="ThreadPool_RunTask" then
-            printf("RunTask(posted_from=%s:%s)",
-             EXTRACT_ARG(arg_set_id, "task.posted_from.file_name"),
-             EXTRACT_ARG(arg_set_id, "task.posted_from.function_name"))
-         end
-         from slice where id=$slice_id'
-    );`);
-  }
-
-  async queryFlowEvents(query: string, callback: (flows: Flow[]) => void) {
-    const result = await this.args.engine.query(query);
-    const flows: Flow[] = [];
-
-    const it = result.iter({
-      beginSliceId: NUM,
-      beginTrackId: NUM,
-      beginSliceName: STR_NULL,
-      beginSliceChromeCustomName: STR_NULL,
-      beginSliceCategory: STR_NULL,
-      beginSliceStartTs: LONG,
-      beginSliceEndTs: LONG,
-      beginDepth: NUM,
-      beginThreadName: STR_NULL,
-      beginProcessName: STR_NULL,
-      endSliceId: NUM,
-      endTrackId: NUM,
-      endSliceName: STR_NULL,
-      endSliceChromeCustomName: STR_NULL,
-      endSliceCategory: STR_NULL,
-      endSliceStartTs: LONG,
-      endSliceEndTs: LONG,
-      endDepth: NUM,
-      endThreadName: STR_NULL,
-      endProcessName: STR_NULL,
-      name: STR_NULL,
-      category: STR_NULL,
-      id: NUM,
-      flowToDescendant: NUM,
-    });
-
-    const nullToStr = (s: null | string): string => {
-      return s === null ? 'NULL' : s;
-    };
-
-    const nullToUndefined = (s: null | string): undefined | string => {
-      return s === null ? undefined : s;
-    };
-
-    const nodes = [];
-
-    for (; it.valid(); it.next()) {
-      // Category and name present only in version 1 flow events
-      // It is most likelly NULL for all other versions
-      const category = nullToUndefined(it.category);
-      const name = nullToUndefined(it.name);
-      const id = it.id;
-
-      const begin = {
-        trackId: it.beginTrackId,
-        sliceId: asSliceSqlId(it.beginSliceId),
-        sliceName: nullToStr(it.beginSliceName),
-        sliceChromeCustomName: nullToUndefined(it.beginSliceChromeCustomName),
-        sliceCategory: nullToStr(it.beginSliceCategory),
-        sliceStartTs: Time.fromRaw(it.beginSliceStartTs),
-        sliceEndTs: Time.fromRaw(it.beginSliceEndTs),
-        depth: it.beginDepth,
-        threadName: nullToStr(it.beginThreadName),
-        processName: nullToStr(it.beginProcessName),
-      };
-
-      const end = {
-        trackId: it.endTrackId,
-        sliceId: asSliceSqlId(it.endSliceId),
-        sliceName: nullToStr(it.endSliceName),
-        sliceChromeCustomName: nullToUndefined(it.endSliceChromeCustomName),
-        sliceCategory: nullToStr(it.endSliceCategory),
-        sliceStartTs: Time.fromRaw(it.endSliceStartTs),
-        sliceEndTs: Time.fromRaw(it.endSliceEndTs),
-        depth: it.endDepth,
-        threadName: nullToStr(it.endThreadName),
-        processName: nullToStr(it.endProcessName),
-      };
-
-      nodes.push(begin);
-      nodes.push(end);
-
-      flows.push({
-        id,
-        begin,
-        end,
-        dur: it.endSliceStartTs - it.beginSliceEndTs,
-        category,
-        name,
-        flowToDescendant: !!it.flowToDescendant,
-      });
-    }
-
-    // Everything below here is a horrible hack to support flows for
-    // async slice tracks.
-    // In short the issue is this:
-    // - For most slice tracks there is a one-to-one mapping between
-    //   the track in the UI and the track in the TP. n.b. Even in this
-    //   case the UI 'trackId' and the TP 'track.id' may not be the
-    //   same. In this case 'depth' in the TP is the exact depth in the
-    //   UI.
-    // - In the case of aysnc tracks however the mapping is
-    //   one-to-many. Each async slice track in the UI is 'backed' but
-    //   multiple TP tracks. In order to render this track we need
-    //   to adjust depth to avoid overlapping slices. In the render
-    //   path we use experimental_slice_layout for this purpose. This
-    //   is a virtual table in the TP which, for an arbitrary collection
-    //   of TP trackIds, computes for each slice a 'layout_depth'.
-    // - Everything above in this function and its callers doesn't
-    //   know anything about layout_depth.
-    //
-    // So if we stopped here we would have incorrect rendering for
-    // async slice tracks. Instead we want to 'fix' depth for these
-    // cases. We do this in two passes.
-    // - First we collect all the information we need in 'Info' POJOs
-    // - Secondly we loop over those Infos querying
-    //   the database to find the layout_depth for each sliceId
-    // TODO(hjd): This should not be needed after TracksV2 lands.
-
-    // We end up with one Info POJOs for each UI async slice track
-    // which has at least  one flow {begin,end}ing in one of its slices.
-    interface Info {
-      uiTrackId: string;
-      siblingTrackIds: number[];
-      sliceIds: number[];
-      nodes: Array<{
-        sliceId: number;
-        depth: number;
-      }>;
-    }
-
-    const uiTrackIdToInfo = new Map<string, null | Info>();
-    const trackIdToInfo = new Map<number, null | Info>();
-
-    const trackIdToUiTrackId = globals.trackManager.trackKeyByTrackId;
-    const tracks = globals.state.tracks;
-
-    const getInfo = (trackId: number): null | Info => {
-      let info = trackIdToInfo.get(trackId);
-      if (info !== undefined) {
-        return info;
-      }
-
-      const uiTrackId = trackIdToUiTrackId.get(trackId);
-      if (uiTrackId === undefined) {
-        trackIdToInfo.set(trackId, null);
-        return null;
-      }
-
-      const track = tracks[uiTrackId];
-      if (track === undefined) {
-        trackIdToInfo.set(trackId, null);
-        return null;
-      }
-
-      info = uiTrackIdToInfo.get(uiTrackId);
-      if (info !== undefined) {
-        return info;
-      }
-
-      // If 'trackIds' is undefined this is not an async slice track so
-      // we don't need to do anything. We also don't need to do
-      // anything if there is only one TP track in this async track. In
-      // that case experimental_slice_layout is just an expensive way
-      // to find out depth === layout_depth.
-      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-      const trackIds = trackInfo?.tags?.trackIds;
-      if (trackIds === undefined || trackIds.length <= 1) {
-        uiTrackIdToInfo.set(uiTrackId, null);
-        trackIdToInfo.set(trackId, null);
-        return null;
-      }
-
-      const newInfo = {
-        uiTrackId,
-        siblingTrackIds: [...trackIds],
-        sliceIds: [],
-        nodes: [],
-      };
-
-      uiTrackIdToInfo.set(uiTrackId, newInfo);
-      trackIdToInfo.set(trackId, newInfo);
-
-      return newInfo;
-    };
-
-    // First pass, collect:
-    // - all slices that belong to async slice track
-    // - grouped by the async slice track in question
-    for (const node of nodes) {
-      const info = getInfo(node.trackId);
-      if (info !== null) {
-        info.sliceIds.push(node.sliceId);
-        info.nodes.push(node);
-      }
-    }
-
-    // Second pass, for each async track:
-    // - Query to find the layout_depth for each relevant sliceId
-    // - Iterate through the nodes updating the depth in place
-    for (const info of uiTrackIdToInfo.values()) {
-      if (info === null) {
-        continue;
-      }
-      const r = await this.args.engine.query(`
-        SELECT
-          id,
-          layout_depth as depth
-        FROM
-          experimental_slice_layout
-        WHERE
-          filter_track_ids = '${info.siblingTrackIds.join(',')}'
-          AND id in (${info.sliceIds.join(', ')})
-      `);
-
-      // Create the sliceId -> new depth map:
-      const it = r.iter({
-        id: NUM,
-        depth: NUM,
-      });
-      const sliceIdToDepth = new Map<number, number>();
-      for (; it.valid(); it.next()) {
-        sliceIdToDepth.set(it.id, it.depth);
-      }
-
-      // For each begin/end from an async track update the depth:
-      for (const node of info.nodes) {
-        const newDepth = sliceIdToDepth.get(node.sliceId);
-        if (newDepth !== undefined) {
-          node.depth = newDepth;
-        }
-      }
-    }
-
-    callback(flows);
-  }
-
-  sliceSelected(sliceId: number) {
-    const connectedFlows = SHOW_INDIRECT_PRECEDING_FLOWS_FLAG.get()
-      ? `(
-           select * from directly_connected_flow(${sliceId})
-           union
-           select * from preceding_flow(${sliceId})
-         )`
-      : `directly_connected_flow(${sliceId})`;
-
-    const query = `
-    select
-      f.slice_out as beginSliceId,
-      t1.track_id as beginTrackId,
-      t1.name as beginSliceName,
-      CHROME_CUSTOM_SLICE_NAME(t1.slice_id) as beginSliceChromeCustomName,
-      t1.category as beginSliceCategory,
-      t1.ts as beginSliceStartTs,
-      (t1.ts+t1.dur) as beginSliceEndTs,
-      t1.depth as beginDepth,
-      (thread_out.name || ' ' || thread_out.tid) as beginThreadName,
-      (process_out.name || ' ' || process_out.pid) as beginProcessName,
-      f.slice_in as endSliceId,
-      t2.track_id as endTrackId,
-      t2.name as endSliceName,
-      CHROME_CUSTOM_SLICE_NAME(t2.slice_id) as endSliceChromeCustomName,
-      t2.category as endSliceCategory,
-      t2.ts as endSliceStartTs,
-      (t2.ts+t2.dur) as endSliceEndTs,
-      t2.depth as endDepth,
-      (thread_in.name || ' ' || thread_in.tid) as endThreadName,
-      (process_in.name || ' ' || process_in.pid) as endProcessName,
-      extract_arg(f.arg_set_id, 'cat') as category,
-      extract_arg(f.arg_set_id, 'name') as name,
-      f.id as id,
-      slice_is_ancestor(t1.slice_id, t2.slice_id) as flowToDescendant
-    from ${connectedFlows} f
-    join slice t1 on f.slice_out = t1.slice_id
-    join slice t2 on f.slice_in = t2.slice_id
-    left join thread_track track_out on track_out.id = t1.track_id
-    left join thread thread_out on thread_out.utid = track_out.utid
-    left join thread_track track_in on track_in.id = t2.track_id
-    left join thread thread_in on thread_in.utid = track_in.utid
-    left join process process_out on process_out.upid = thread_out.upid
-    left join process process_in on process_in.upid = thread_in.upid
-    `;
-    this.queryFlowEvents(query, (flows: Flow[]) =>
-      publishConnectedFlows(flows),
-    );
-  }
-
-  private areaSelected(area: AreaSelection) {
-    const trackIds: number[] = [];
-
-    for (const uiTrackId of area.tracks) {
-      const track = globals.state.tracks[uiTrackId];
-      if (track?.uri !== undefined) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        const kind = trackInfo?.tags?.kind;
-        if (
-          kind === THREAD_SLICE_TRACK_KIND ||
-          kind === ACTUAL_FRAMES_SLICE_TRACK_KIND
-        ) {
-          if (trackInfo?.tags?.trackIds) {
-            for (const trackId of trackInfo.tags.trackIds) {
-              trackIds.push(trackId);
-            }
-          }
-        }
-      }
-    }
-
-    const tracks = `(${trackIds.join(',')})`;
-
-    const startNs = area.start;
-    const endNs = area.end;
-
-    const query = `
-    select
-      f.slice_out as beginSliceId,
-      t1.track_id as beginTrackId,
-      t1.name as beginSliceName,
-      CHROME_CUSTOM_SLICE_NAME(t1.slice_id) as beginSliceChromeCustomName,
-      t1.category as beginSliceCategory,
-      t1.ts as beginSliceStartTs,
-      (t1.ts+t1.dur) as beginSliceEndTs,
-      t1.depth as beginDepth,
-      NULL as beginThreadName,
-      NULL as beginProcessName,
-      f.slice_in as endSliceId,
-      t2.track_id as endTrackId,
-      t2.name as endSliceName,
-      CHROME_CUSTOM_SLICE_NAME(t2.slice_id) as endSliceChromeCustomName,
-      t2.category as endSliceCategory,
-      t2.ts as endSliceStartTs,
-      (t2.ts+t2.dur) as endSliceEndTs,
-      t2.depth as endDepth,
-      NULL as endThreadName,
-      NULL as endProcessName,
-      extract_arg(f.arg_set_id, 'cat') as category,
-      extract_arg(f.arg_set_id, 'name') as name,
-      f.id as id,
-      slice_is_ancestor(t1.slice_id, t2.slice_id) as flowToDescendant
-    from flow f
-    join slice t1 on f.slice_out = t1.slice_id
-    join slice t2 on f.slice_in = t2.slice_id
-    where
-      (t1.track_id in ${tracks}
-        and (t1.ts+t1.dur <= ${endNs} and t1.ts+t1.dur >= ${startNs}))
-      or
-      (t2.track_id in ${tracks}
-        and (t2.ts <= ${endNs} and t2.ts >= ${startNs}))
-    `;
-    this.queryFlowEvents(query, (flows: Flow[]) => publishSelectedFlows(flows));
-  }
-
-  refreshVisibleFlows() {
-    if (!this.monitor.ifStateChanged()) {
-      return;
-    }
-
-    const selection = globals.state.selection;
-    if (selection.kind === 'empty') {
-      publishConnectedFlows([]);
-      publishSelectedFlows([]);
-      return;
-    }
-
-    const legacySelection = getLegacySelection(globals.state);
-    // TODO(b/155483804): This is a hack as annotation slices don't contain
-    // flows. We should tidy this up when fixing this bug.
-    if (
-      legacySelection &&
-      legacySelection.kind === 'SLICE' &&
-      legacySelection.table !== 'annotation'
-    ) {
-      this.sliceSelected(legacySelection.id);
-    } else {
-      publishConnectedFlows([]);
-    }
-
-    if (selection.kind === 'area') {
-      this.areaSelected(selection);
-    } else {
-      publishSelectedFlows([]);
-    }
-  }
-
-  run() {
-    this.refreshVisibleFlows();
-  }
-}
diff --git a/ui/src/controller/index.ts b/ui/src/controller/index.ts
deleted file mode 100644
index 8154c18..0000000
--- a/ui/src/controller/index.ts
+++ /dev/null
@@ -1,44 +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 '../common/recordingV2/target_factories';
-
-import {assertExists, assertTrue} from '../base/logging';
-
-import {AppController} from './app_controller';
-import {ControllerAny} from './controller';
-
-let rootController: ControllerAny;
-let runningControllers = false;
-
-export function initController(extensionPort: MessagePort) {
-  assertTrue(rootController === undefined);
-  rootController = new AppController(extensionPort);
-}
-
-export function runControllers() {
-  if (runningControllers) throw new Error('Re-entrant call detected');
-
-  // Run controllers locally until all state machines reach quiescence.
-  let runAgain = true;
-  for (let iter = 0; runAgain; iter++) {
-    if (iter > 100) throw new Error('Controllers are stuck in a livelock');
-    runningControllers = true;
-    try {
-      runAgain = assertExists(rootController).invoke();
-    } finally {
-      runningControllers = false;
-    }
-  }
-}
diff --git a/ui/src/controller/loading_manager.ts b/ui/src/controller/loading_manager.ts
deleted file mode 100644
index cfc1a46..0000000
--- a/ui/src/controller/loading_manager.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {publishLoading} from '../frontend/publish';
-import {LoadingTracker} from '../trace_processor/engine';
-
-// Used to keep track of whether the engine is currently querying.
-export class LoadingManager implements LoadingTracker {
-  private static _instance: LoadingManager;
-  private numQueuedQueries = 0;
-  private numLastUpdate = 0;
-
-  static get getInstance(): LoadingManager {
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    return this._instance || (this._instance = new this());
-  }
-
-  beginLoading() {
-    this.update(1);
-  }
-
-  endLoading() {
-    this.update(-1);
-  }
-
-  private update(change: number) {
-    this.numQueuedQueries += change;
-    if (
-      this.numQueuedQueries === 0 ||
-      Math.abs(this.numLastUpdate - this.numQueuedQueries) > 2
-    ) {
-      this.numLastUpdate = this.numQueuedQueries;
-      publishLoading(this.numQueuedQueries);
-    }
-  }
-}
diff --git a/ui/src/controller/pivot_table_controller.ts b/ui/src/controller/pivot_table_controller.ts
deleted file mode 100644
index bfa46ac..0000000
--- a/ui/src/controller/pivot_table_controller.ts
+++ /dev/null
@@ -1,313 +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 {arrayEquals} from '../base/array_utils';
-import {Actions} from '../common/actions';
-import {
-  Area,
-  AreaSelection,
-  PivotTableAreaState,
-  PivotTableQuery,
-  PivotTableQueryMetadata,
-  PivotTableResult,
-  PivotTableState,
-} from '../common/state';
-import {featureFlags} from '../core/feature_flags';
-import {globals} from '../frontend/globals';
-import {
-  aggregationIndex,
-  generateQueryFromState,
-} from '../frontend/pivot_table_query_generator';
-import {Aggregation, PivotTree} from '../frontend/pivot_table_types';
-import {Engine} from '../trace_processor/engine';
-import {ColumnType} from '../trace_processor/query_result';
-
-import {Controller} from './controller';
-
-export const PIVOT_TABLE_REDUX_FLAG = featureFlags.register({
-  id: 'pivotTable',
-  name: 'Pivot tables V2',
-  description: 'Second version of pivot table',
-  defaultValue: true,
-});
-
-function expectNumber(value: ColumnType): number {
-  if (typeof value === 'number') {
-    return value;
-  } else if (typeof value === 'bigint') {
-    return Number(value);
-  }
-  throw new Error(`number or bigint was expected, got ${typeof value}`);
-}
-
-// Auxiliary class to build the tree from query response.
-export class PivotTableTreeBuilder {
-  private readonly root: PivotTree;
-  queryMetadata: PivotTableQueryMetadata;
-
-  get pivotColumnsCount(): number {
-    return this.queryMetadata.pivotColumns.length;
-  }
-
-  get aggregateColumns(): Aggregation[] {
-    return this.queryMetadata.aggregationColumns;
-  }
-
-  constructor(queryMetadata: PivotTableQueryMetadata, firstRow: ColumnType[]) {
-    this.queryMetadata = queryMetadata;
-    this.root = this.createNode(firstRow);
-    let tree = this.root;
-    for (let i = 0; i + 1 < this.pivotColumnsCount; i++) {
-      const value = firstRow[i];
-      tree = this.insertChild(tree, value, this.createNode(firstRow));
-    }
-    tree.rows.push(firstRow);
-  }
-
-  // Add incoming row to the tree being built.
-  ingestRow(row: ColumnType[]) {
-    let tree = this.root;
-    this.updateAggregates(tree, row);
-    for (let i = 0; i + 1 < this.pivotColumnsCount; i++) {
-      const nextTree = tree.children.get(row[i]);
-      if (nextTree === undefined) {
-        // Insert the new node into the tree, and make variable `tree` point
-        // to the newly created node.
-        tree = this.insertChild(tree, row[i], this.createNode(row));
-      } else {
-        this.updateAggregates(nextTree, row);
-        tree = nextTree;
-      }
-    }
-    tree.rows.push(row);
-  }
-
-  build(): PivotTree {
-    return this.root;
-  }
-
-  updateAggregates(tree: PivotTree, row: ColumnType[]) {
-    const countIndex = this.queryMetadata.countIndex;
-    const treeCount =
-      countIndex >= 0 ? expectNumber(tree.aggregates[countIndex]) : 0;
-    const rowCount =
-      countIndex >= 0
-        ? expectNumber(
-            row[aggregationIndex(this.pivotColumnsCount, countIndex)],
-          )
-        : 0;
-
-    for (let i = 0; i < this.aggregateColumns.length; i++) {
-      const agg = this.aggregateColumns[i];
-
-      const currAgg = tree.aggregates[i];
-      const childAgg = row[aggregationIndex(this.pivotColumnsCount, i)];
-      if (typeof currAgg === 'number' && typeof childAgg === 'number') {
-        switch (agg.aggregationFunction) {
-          case 'SUM':
-          case 'COUNT':
-            tree.aggregates[i] = currAgg + childAgg;
-            break;
-          case 'MAX':
-            tree.aggregates[i] = Math.max(currAgg, childAgg);
-            break;
-          case 'MIN':
-            tree.aggregates[i] = Math.min(currAgg, childAgg);
-            break;
-          case 'AVG': {
-            const currSum = currAgg * treeCount;
-            const addSum = childAgg * rowCount;
-            tree.aggregates[i] = (currSum + addSum) / (treeCount + rowCount);
-            break;
-          }
-        }
-      }
-    }
-    tree.aggregates[this.aggregateColumns.length] = treeCount + rowCount;
-  }
-
-  // Helper method that inserts child node into the tree and returns it, used
-  // for more concise modification of local variable pointing to the current
-  // node being built.
-  insertChild(tree: PivotTree, key: ColumnType, child: PivotTree): PivotTree {
-    tree.children.set(key, child);
-
-    return child;
-  }
-
-  // Initialize PivotTree from a row.
-  createNode(row: ColumnType[]): PivotTree {
-    const aggregates = [];
-
-    for (let j = 0; j < this.aggregateColumns.length; j++) {
-      aggregates.push(row[aggregationIndex(this.pivotColumnsCount, j)]);
-    }
-    aggregates.push(
-      row[
-        aggregationIndex(this.pivotColumnsCount, this.aggregateColumns.length)
-      ],
-    );
-
-    return {
-      isCollapsed: false,
-      children: new Map(),
-      aggregates,
-      rows: [],
-    };
-  }
-}
-
-function createEmptyQueryResult(
-  metadata: PivotTableQueryMetadata,
-): PivotTableResult {
-  return {
-    tree: {
-      aggregates: [],
-      isCollapsed: false,
-      children: new Map(),
-      rows: [],
-    },
-    metadata,
-  };
-}
-
-// Controller responsible for showing the panel with pivot table, as well as
-// executing its queries and post-processing query results.
-export class PivotTableController extends Controller<{}> {
-  static detailsCount = 0;
-  engine: Engine;
-  lastQueryArea?: PivotTableAreaState;
-  lastQueryAreaTracks = new Set<string>();
-
-  constructor(args: {engine: Engine}) {
-    super({});
-    this.engine = args.engine;
-  }
-
-  sameTracks(tracks: Set<string>) {
-    if (this.lastQueryAreaTracks.size !== tracks.size) {
-      return false;
-    }
-
-    // ES6 Set does not have .every method, only Array does.
-    for (const track of tracks) {
-      if (!this.lastQueryAreaTracks.has(track)) {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-  shouldRerun(state: PivotTableState, selection: AreaSelection) {
-    if (state.selectionArea === undefined) {
-      return false;
-    }
-
-    const newTracks = new Set(selection.tracks);
-    if (
-      this.lastQueryArea !== state.selectionArea ||
-      !this.sameTracks(newTracks)
-    ) {
-      this.lastQueryArea = state.selectionArea;
-      this.lastQueryAreaTracks = newTracks;
-      return true;
-    }
-    return false;
-  }
-
-  async processQuery(query: PivotTableQuery) {
-    const result = await this.engine.query(query.text);
-    try {
-      await result.waitAllRows();
-    } catch {
-      // waitAllRows() frequently throws an exception, which is ignored in
-      // its other calls, so it's ignored here as well.
-    }
-
-    const columns = result.columns();
-
-    const it = result.iter({});
-    function nextRow(): ColumnType[] {
-      const row: ColumnType[] = [];
-      for (const column of columns) {
-        row.push(it.get(column));
-      }
-      it.next();
-      return row;
-    }
-
-    if (!it.valid()) {
-      // Iterator is invalid after creation; means that there are no rows
-      // satisfying filtering criteria. Return an empty tree.
-      globals.dispatch(
-        Actions.setPivotStateQueryResult({
-          queryResult: createEmptyQueryResult(query.metadata),
-        }),
-      );
-      return;
-    }
-
-    const treeBuilder = new PivotTableTreeBuilder(query.metadata, nextRow());
-    while (it.valid()) {
-      treeBuilder.ingestRow(nextRow());
-    }
-
-    globals.dispatch(
-      Actions.setPivotStateQueryResult({
-        queryResult: {tree: treeBuilder.build(), metadata: query.metadata},
-      }),
-    );
-  }
-
-  run() {
-    if (!PIVOT_TABLE_REDUX_FLAG.get()) {
-      return;
-    }
-
-    const pivotTableState = globals.state.nonSerializableState.pivotTable;
-    const selection = globals.state.selection;
-
-    if (
-      pivotTableState.queryRequested ||
-      (selection.kind === 'area' &&
-        this.shouldRerun(pivotTableState, selection))
-    ) {
-      globals.dispatch(
-        Actions.setPivotTableQueryRequested({queryRequested: false}),
-      );
-      // Need to re-run the existing query, clear the current result.
-      globals.dispatch(Actions.setPivotStateQueryResult({queryResult: null}));
-      this.processQuery(generateQueryFromState(pivotTableState));
-    }
-
-    if (
-      selection.kind === 'area' &&
-      (pivotTableState.selectionArea === undefined ||
-        !areasEqual(pivotTableState.selectionArea, selection))
-    ) {
-      globals.dispatch(Actions.togglePivotTable({area: selection}));
-    }
-  }
-}
-
-// Returns true if two areas and exactly equivalent, false otherwise
-function areasEqual(a: Area, b: Area): boolean {
-  if (a.start !== b.start) return false;
-  if (a.end !== b.end) return false;
-  if (!arrayEquals(a.tracks, b.tracks)) return false;
-  return true;
-}
diff --git a/ui/src/controller/pivot_table_tree_builder_unittest.ts b/ui/src/controller/pivot_table_tree_builder_unittest.ts
deleted file mode 100644
index 55f5358..0000000
--- a/ui/src/controller/pivot_table_tree_builder_unittest.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {PivotTableTreeBuilder} from './pivot_table_controller';
-
-describe('Pivot Table tree builder', () => {
-  test('aggregates averages correctly', () => {
-    const builder = new PivotTableTreeBuilder(
-      {
-        pivotColumns: [
-          {kind: 'regular', table: 'slice', column: 'category'},
-          {kind: 'regular', table: 'slice', column: 'name'},
-        ],
-        aggregationColumns: [
-          {
-            aggregationFunction: 'AVG',
-            column: {kind: 'regular', table: 'slice', column: 'dur'},
-          },
-        ],
-        countIndex: 1,
-      },
-      ['cat1', 'name1', 80.0, 2],
-    );
-
-    builder.ingestRow(['cat1', 'name2', 20.0, 1]);
-    builder.ingestRow(['cat2', 'name3', 20.0, 1]);
-
-    // With two rows of average value 80.0, and two of average value 20.0;
-    // the total sum is 80.0 * 2 + 20.0 + 20.0 = 200.0 over four slices. The
-    // average value should be 200.0 / 4 = 50.0
-    expect(builder.build().aggregates[0]).toBeCloseTo(50.0);
-  });
-});
diff --git a/ui/src/controller/record_config_types.ts b/ui/src/controller/record_config_types.ts
deleted file mode 100644
index e97bc0f..0000000
--- a/ui/src/controller/record_config_types.ts
+++ /dev/null
@@ -1,127 +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 {z} from 'zod';
-
-const recordModes = ['STOP_WHEN_FULL', 'RING_BUFFER', 'LONG_TRACE'] as const;
-export const RECORD_CONFIG_SCHEMA = z
-  .object({
-    mode: z.enum(recordModes).default('STOP_WHEN_FULL'),
-    durationMs: z.number().default(10000.0),
-    maxFileSizeMb: z.number().default(100),
-    fileWritePeriodMs: z.number().default(2500),
-    bufferSizeMb: z.number().default(64.0),
-
-    cpuSched: z.boolean().default(false),
-    cpuFreq: z.boolean().default(false),
-    cpuFreqPollMs: z.number().default(1000),
-    cpuSyscall: z.boolean().default(false),
-
-    gpuFreq: z.boolean().default(false),
-    gpuMemTotal: z.boolean().default(false),
-    gpuWorkPeriod: z.boolean().default(false),
-
-    ftrace: z.boolean().default(false),
-    atrace: z.boolean().default(false),
-    ftraceEvents: z.array(z.string()).default([]),
-    ftraceExtraEvents: z.string().default(''),
-    atraceCats: z.array(z.string()).default([]),
-    allAtraceApps: z.boolean().default(true),
-    atraceApps: z.string().default(''),
-    ftraceBufferSizeKb: z.number().default(0),
-    ftraceDrainPeriodMs: z.number().default(0),
-    androidLogs: z.boolean().default(false),
-    androidLogBuffers: z.array(z.string()).default([]),
-    androidFrameTimeline: z.boolean().default(false),
-    androidGameInterventionList: z.boolean().default(false),
-    androidNetworkTracing: z.boolean().default(false),
-    androidNetworkTracingPollMs: z.number().default(250),
-
-    cpuCoarse: z.boolean().default(false),
-    cpuCoarsePollMs: z.number().default(1000),
-
-    batteryDrain: z.boolean().default(false),
-    batteryDrainPollMs: z.number().default(1000),
-
-    boardSensors: z.boolean().default(false),
-
-    memHiFreq: z.boolean().default(false),
-    meminfo: z.boolean().default(false),
-    meminfoPeriodMs: z.number().default(1000),
-    meminfoCounters: z.array(z.string()).default([]),
-
-    vmstat: z.boolean().default(false),
-    vmstatPeriodMs: z.number().default(1000),
-    vmstatCounters: z.array(z.string()).default([]),
-
-    heapProfiling: z.boolean().default(false),
-    hpSamplingIntervalBytes: z.number().default(4096),
-    hpProcesses: z.string().default(''),
-    hpContinuousDumpsPhase: z.number().default(0),
-    hpContinuousDumpsInterval: z.number().default(0),
-    hpSharedMemoryBuffer: z.number().default(8 * 1048576),
-    hpBlockClient: z.boolean().default(true),
-    hpAllHeaps: z.boolean().default(false),
-
-    javaHeapDump: z.boolean().default(false),
-    jpProcesses: z.string().default(''),
-    jpContinuousDumpsPhase: z.number().default(0),
-    jpContinuousDumpsInterval: z.number().default(0),
-
-    memLmk: z.boolean().default(false),
-    procStats: z.boolean().default(false),
-    procStatsPeriodMs: z.number().default(1000),
-
-    chromeCategoriesSelected: z.array(z.string()).default([]),
-    chromeHighOverheadCategoriesSelected: z.array(z.string()).default([]),
-    chromePrivacyFiltering: z.boolean().default(false),
-
-    chromeLogs: z.boolean().default(false),
-    taskScheduling: z.boolean().default(false),
-    ipcFlows: z.boolean().default(false),
-    jsExecution: z.boolean().default(false),
-    webContentRendering: z.boolean().default(false),
-    uiRendering: z.boolean().default(false),
-    inputEvents: z.boolean().default(false),
-    navigationAndLoading: z.boolean().default(false),
-    audio: z.boolean().default(false),
-    video: z.boolean().default(false),
-
-    etwCSwitch: z.boolean().default(false),
-    etwThreadState: z.boolean().default(false),
-
-    symbolizeKsyms: z.boolean().default(false),
-
-    // Enabling stack sampling
-    tracePerf: z.boolean().default(false),
-    timebaseFrequency: z.number().default(100),
-    targetCmdLine: z.array(z.string()).default([]),
-
-    linuxDeviceRpm: z.boolean().default(false),
-  })
-  // .default({}) ensures that we can always default-construct a config and
-  // spots accidental missing .default(...)
-  .default({});
-
-export const NAMED_RECORD_CONFIG_SCHEMA = z.object({
-  title: z.string(),
-  key: z.string(),
-  config: RECORD_CONFIG_SCHEMA,
-});
-export type NamedRecordConfig = z.infer<typeof NAMED_RECORD_CONFIG_SCHEMA>;
-export type RecordConfig = z.infer<typeof RECORD_CONFIG_SCHEMA>;
-
-export function createEmptyRecordConfig(): RecordConfig {
-  return RECORD_CONFIG_SCHEMA.parse({});
-}
diff --git a/ui/src/controller/record_controller.ts b/ui/src/controller/record_controller.ts
deleted file mode 100644
index ba87a8b..0000000
--- a/ui/src/controller/record_controller.ts
+++ /dev/null
@@ -1,459 +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 {Message, Method, rpc, RPCImplCallback} from 'protobufjs';
-
-import {isString} from '../base/object_utils';
-import {base64Encode} from '../base/string_utils';
-import {Actions} from '../common/actions';
-import {TRACE_SUFFIX} from '../common/constants';
-import {genTraceConfig} from '../common/recordingV2/recording_config_utils';
-import {TargetInfo} from '../common/recordingV2/recording_interfaces_v2';
-import {
-  AdbRecordingTarget,
-  isAdbTarget,
-  isChromeTarget,
-  isWindowsTarget,
-  RecordingTarget,
-} from '../common/state';
-import {globals} from '../frontend/globals';
-import {publishBufferUsage, publishTrackData} from '../frontend/publish';
-import {ConsumerPort, TraceConfig} from '../protos';
-
-import {AdbOverWebUsb} from './adb';
-import {AdbConsumerPort} from './adb_shell_controller';
-import {AdbSocketConsumerPort} from './adb_socket_controller';
-import {ChromeExtensionConsumerPort} from './chrome_proxy_record_controller';
-import {
-  ConsumerPortResponse,
-  GetTraceStatsResponse,
-  isDisableTracingResponse,
-  isEnableTracingResponse,
-  isFreeBuffersResponse,
-  isGetTraceStatsResponse,
-  isReadBuffersResponse,
-} from './consumer_port_types';
-import {Controller} from './controller';
-import {RecordConfig} from './record_config_types';
-import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
-
-type RPCImplMethod = Method | rpc.ServiceMethod<Message<{}>, Message<{}>>;
-
-export function genConfigProto(
-  uiCfg: RecordConfig,
-  target: RecordingTarget,
-): Uint8Array {
-  return TraceConfig.encode(convertToRecordingV2Input(uiCfg, target)).finish();
-}
-
-// This method converts the 'RecordingTarget' to the 'TargetInfo' used by V2 of
-// the recording code. It is used so the logic is not duplicated and does not
-// diverge.
-// TODO(octaviant) delete this once we switch to RecordingV2.
-function convertToRecordingV2Input(
-  uiCfg: RecordConfig,
-  target: RecordingTarget,
-): TraceConfig {
-  let targetType: 'ANDROID' | 'CHROME' | 'CHROME_OS' | 'LINUX' | 'WINDOWS';
-  let androidApiLevel!: number;
-  switch (target.os) {
-    case 'L':
-      targetType = 'LINUX';
-      break;
-    case 'C':
-      targetType = 'CHROME';
-      break;
-    case 'CrOS':
-      targetType = 'CHROME_OS';
-      break;
-    case 'Win':
-      targetType = 'WINDOWS';
-      break;
-    case 'S':
-      androidApiLevel = 31;
-      targetType = 'ANDROID';
-      break;
-    case 'R':
-      androidApiLevel = 30;
-      targetType = 'ANDROID';
-      break;
-    case 'Q':
-      androidApiLevel = 29;
-      targetType = 'ANDROID';
-      break;
-    case 'P':
-      androidApiLevel = 28;
-      targetType = 'ANDROID';
-      break;
-    default:
-      androidApiLevel = 26;
-      targetType = 'ANDROID';
-  }
-
-  let targetInfo: TargetInfo;
-  if (targetType === 'ANDROID') {
-    targetInfo = {
-      targetType,
-      androidApiLevel,
-      dataSources: [],
-      name: '',
-    };
-  } else {
-    targetInfo = {
-      targetType,
-      dataSources: [],
-      name: '',
-    };
-  }
-
-  return genTraceConfig(uiCfg, targetInfo);
-}
-
-export function toPbtxt(configBuffer: Uint8Array): string {
-  const msg = TraceConfig.decode(configBuffer);
-  const json = msg.toJSON();
-  function snakeCase(s: string): string {
-    return s.replace(/[A-Z]/g, (c) => '_' + c.toLowerCase());
-  }
-  // With the ahead of time compiled protos we can't seem to tell which
-  // fields are enums.
-  function isEnum(value: string): boolean {
-    return (
-      value.startsWith('MEMINFO_') ||
-      value.startsWith('VMSTAT_') ||
-      value.startsWith('STAT_') ||
-      value.startsWith('LID_') ||
-      value.startsWith('BATTERY_COUNTER_') ||
-      value === 'DISCARD' ||
-      value === 'RING_BUFFER' ||
-      value === 'BACKGROUND' ||
-      value === 'USER_INITIATED' ||
-      value.startsWith('PERF_CLOCK_')
-    );
-  }
-  // Since javascript doesn't have 64 bit numbers when converting protos to
-  // json the proto library encodes them as strings. This is lossy since
-  // we can't tell which strings that look like numbers are actually strings
-  // and which are actually numbers. Ideally we would reflect on the proto
-  // definition somehow but for now we just hard code keys which have this
-  // problem in the config.
-  function is64BitNumber(key: string): boolean {
-    return [
-      'maxFileSizeBytes',
-      'pid',
-      'samplingIntervalBytes',
-      'shmemSizeBytes',
-      'timestampUnitMultiplier',
-      'frequency',
-    ].includes(key);
-  }
-  function* message(msg: {}, indent: number): IterableIterator<string> {
-    for (const [key, value] of Object.entries(msg)) {
-      const isRepeated = Array.isArray(value);
-      const isNested = typeof value === 'object' && !isRepeated;
-      for (const entry of isRepeated ? (value as Array<{}>) : [value]) {
-        yield ' '.repeat(indent) + `${snakeCase(key)}${isNested ? '' : ':'} `;
-        if (isString(entry)) {
-          if (isEnum(entry) || is64BitNumber(key)) {
-            yield entry;
-          } else {
-            yield `"${entry.replace(new RegExp('"', 'g'), '\\"')}"`;
-          }
-        } else if (typeof entry === 'number') {
-          yield entry.toString();
-        } else if (typeof entry === 'boolean') {
-          yield entry.toString();
-        } else if (typeof entry === 'object' && entry !== null) {
-          yield '{\n';
-          yield* message(entry, indent + 4);
-          yield ' '.repeat(indent) + '}';
-        } else {
-          throw new Error(
-            `Record proto entry "${entry}" with unexpected type ${typeof entry}`,
-          );
-        }
-        yield '\n';
-      }
-    }
-  }
-  return [...message(json, 0)].join('');
-}
-
-export class RecordController extends Controller<'main'> implements Consumer {
-  private config: RecordConfig | null = null;
-  private readonly extensionPort: MessagePort;
-  private recordingInProgress = false;
-  private consumerPort: ConsumerPort;
-  private traceBuffer: Uint8Array[] = [];
-  private bufferUpdateInterval: ReturnType<typeof setTimeout> | undefined;
-  private adb = new AdbOverWebUsb();
-  private recordedTraceSuffix = TRACE_SUFFIX;
-  private fetchedCategories = false;
-
-  // We have a different controller for each targetOS. The correct one will be
-  // created when needed, and stored here. When the key is a string, it is the
-  // serial of the target (used for android devices). When the key is a single
-  // char, it is the 'targetOS'
-  private controllerPromises = new Map<string, Promise<RpcConsumerPort>>();
-
-  constructor(args: {extensionPort: MessagePort}) {
-    super('main');
-    this.consumerPort = ConsumerPort.create(this.rpcImpl.bind(this));
-    this.extensionPort = args.extensionPort;
-  }
-
-  run() {
-    // TODO(eseckler): Use ConsumerPort's QueryServiceState instead
-    // of posting a custom extension message to retrieve the category list.
-    if (globals.state.fetchChromeCategories && !this.fetchedCategories) {
-      this.fetchedCategories = true;
-      if (globals.state.extensionInstalled) {
-        this.extensionPort.postMessage({method: 'GetCategories'});
-      }
-      globals.dispatch(Actions.setFetchChromeCategories({fetch: false}));
-    }
-    if (
-      globals.state.recordConfig === this.config &&
-      globals.state.recordingInProgress === this.recordingInProgress
-    ) {
-      return;
-    }
-    this.config = globals.state.recordConfig;
-
-    const configProto = genConfigProto(
-      this.config,
-      globals.state.recordingTarget,
-    );
-    const configProtoText = toPbtxt(configProto);
-    const configProtoBase64 = base64Encode(configProto);
-    const commandline = `
-      echo '${configProtoBase64}' |
-      base64 --decode |
-      adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace" &&
-      adb pull /data/misc/perfetto-traces/trace /tmp/trace
-    `;
-    const traceConfig = convertToRecordingV2Input(
-      this.config,
-      globals.state.recordingTarget,
-    );
-    // TODO(hjd): This should not be TrackData after we unify the stores.
-    publishTrackData({
-      id: 'config',
-      data: {
-        commandline,
-        pbBase64: configProtoBase64,
-        pbtxt: configProtoText,
-        traceConfig,
-      },
-    });
-
-    // If the recordingInProgress boolean state is different, it means that we
-    // have to start or stop recording a trace.
-    if (globals.state.recordingInProgress === this.recordingInProgress) return;
-    this.recordingInProgress = globals.state.recordingInProgress;
-
-    if (this.recordingInProgress) {
-      this.startRecordTrace(traceConfig);
-    } else {
-      this.stopRecordTrace();
-    }
-  }
-
-  startRecordTrace(traceConfig: TraceConfig) {
-    this.scheduleBufferUpdateRequests();
-    this.traceBuffer = [];
-    this.consumerPort.enableTracing({traceConfig});
-  }
-
-  stopRecordTrace() {
-    if (this.bufferUpdateInterval) clearInterval(this.bufferUpdateInterval);
-    this.consumerPort.disableTracing({});
-  }
-
-  scheduleBufferUpdateRequests() {
-    if (this.bufferUpdateInterval) clearInterval(this.bufferUpdateInterval);
-    this.bufferUpdateInterval = setInterval(() => {
-      this.consumerPort.getTraceStats({});
-    }, 200);
-  }
-
-  readBuffers() {
-    this.consumerPort.readBuffers({});
-  }
-
-  onConsumerPortResponse(data: ConsumerPortResponse) {
-    if (data === undefined) return;
-    if (isReadBuffersResponse(data)) {
-      if (!data.slices || data.slices.length === 0) return;
-      // TODO(nicomazz): handle this as intended by consumer_port.proto.
-      console.assert(data.slices.length === 1);
-      if (data.slices[0].data) this.traceBuffer.push(data.slices[0].data);
-      // The line underneath is 'misusing' the format ReadBuffersResponse.
-      // The boolean field 'lastSliceForPacket' is used as 'lastPacketInTrace'.
-      // See http://shortn/_53WB8A1aIr.
-      if (data.slices[0].lastSliceForPacket) this.onTraceComplete();
-    } else if (isEnableTracingResponse(data)) {
-      this.readBuffers();
-    } else if (isGetTraceStatsResponse(data)) {
-      const percentage = this.getBufferUsagePercentage(data);
-      if (percentage) {
-        publishBufferUsage({percentage});
-      }
-    } else if (isFreeBuffersResponse(data)) {
-      // No action required.
-    } else if (isDisableTracingResponse(data)) {
-      // No action required.
-    } else {
-      console.error('Unrecognized consumer port response:', data);
-    }
-  }
-
-  onTraceComplete() {
-    this.consumerPort.freeBuffers({});
-    globals.dispatch(Actions.setRecordingStatus({status: undefined}));
-    if (globals.state.recordingCancelled) {
-      globals.dispatch(
-        Actions.setLastRecordingError({error: 'Recording cancelled.'}),
-      );
-      this.traceBuffer = [];
-      return;
-    }
-    const trace = this.generateTrace();
-    globals.dispatch(
-      Actions.openTraceFromBuffer({
-        title: 'Recorded trace',
-        buffer: trace.buffer,
-        fileName: `recorded_trace${this.recordedTraceSuffix}`,
-      }),
-    );
-    this.traceBuffer = [];
-  }
-
-  // TODO(nicomazz): stream each chunk into the trace processor, instead of
-  // creating a big long trace.
-  generateTrace() {
-    let traceLen = 0;
-    for (const chunk of this.traceBuffer) traceLen += chunk.length;
-    const completeTrace = new Uint8Array(traceLen);
-    let written = 0;
-    for (const chunk of this.traceBuffer) {
-      completeTrace.set(chunk, written);
-      written += chunk.length;
-    }
-    return completeTrace;
-  }
-
-  getBufferUsagePercentage(data: GetTraceStatsResponse): number {
-    if (!data.traceStats || !data.traceStats.bufferStats) return 0.0;
-    let maximumUsage = 0;
-    for (const buffer of data.traceStats.bufferStats) {
-      const used = buffer.bytesWritten as number;
-      const total = buffer.bufferSize as number;
-      maximumUsage = Math.max(maximumUsage, used / total);
-    }
-    return maximumUsage;
-  }
-
-  onError(message: string) {
-    // TODO(octaviant): b/204998302
-    console.error('Error in record controller: ', message);
-    globals.dispatch(
-      Actions.setLastRecordingError({error: message.substr(0, 150)}),
-    );
-    globals.dispatch(Actions.stopRecording({}));
-  }
-
-  onStatus(message: string) {
-    globals.dispatch(Actions.setRecordingStatus({status: message}));
-  }
-
-  // Depending on the recording target, different implementation of the
-  // consumer_port will be used.
-  // - Chrome target: This forwards the messages that have to be sent
-  // to the extension to the frontend. This is necessary because this
-  // controller is running in a separate worker, that can't directly send
-  // messages to the extension.
-  // - Android device target: WebUSB is used to communicate using the adb
-  // protocol. Actually, there is no full consumer_port implementation, but
-  // only the support to start tracing and fetch the file.
-  async getTargetController(target: RecordingTarget): Promise<RpcConsumerPort> {
-    const identifier = RecordController.getTargetIdentifier(target);
-
-    // The reason why caching the target 'record controller' Promise is that
-    // multiple rcp calls can happen while we are trying to understand if an
-    // android device has a socket connection available or not.
-    const precedentPromise = this.controllerPromises.get(identifier);
-    if (precedentPromise) return precedentPromise;
-
-    const controllerPromise = new Promise<RpcConsumerPort>(
-      async (resolve, _) => {
-        let controller: RpcConsumerPort | undefined = undefined;
-        if (isChromeTarget(target) || isWindowsTarget(target)) {
-          controller = new ChromeExtensionConsumerPort(
-            this.extensionPort,
-            this,
-          );
-        } else if (isAdbTarget(target)) {
-          this.onStatus(`Please allow USB debugging on device.
-                 If you press cancel, reload the page.`);
-          const socketAccess = await this.hasSocketAccess(target);
-
-          controller = socketAccess
-            ? new AdbSocketConsumerPort(this.adb, this)
-            : new AdbConsumerPort(this.adb, this);
-        } else {
-          throw Error(`No device connected`);
-        }
-
-        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-        if (!controller) throw Error(`Unknown target: ${target}`);
-        /* eslint-enable */
-        resolve(controller);
-      },
-    );
-
-    this.controllerPromises.set(identifier, controllerPromise);
-    return controllerPromise;
-  }
-
-  private static getTargetIdentifier(target: RecordingTarget): string {
-    return isAdbTarget(target) ? target.serial : target.os;
-  }
-
-  private async hasSocketAccess(target: AdbRecordingTarget) {
-    const devices = await navigator.usb.getDevices();
-    const device = devices.find((d) => d.serialNumber === target.serial);
-    console.assert(device);
-    if (!device) return Promise.resolve(false);
-    return AdbSocketConsumerPort.hasSocketAccess(device, this.adb);
-  }
-
-  private async rpcImpl(
-    method: RPCImplMethod,
-    requestData: Uint8Array,
-    _callback: RPCImplCallback,
-  ) {
-    try {
-      const state = globals.state;
-      // TODO(hjd): This is a bit weird. We implicitly send each RPC message to
-      // whichever target is currently selected (creating that target if needed)
-      // it would be nicer if the setup/teardown was more explicit.
-      const target = await this.getTargetController(state.recordingTarget);
-      this.recordedTraceSuffix = target.getRecordedTraceSuffix();
-      target.handleCommand(method.name, requestData);
-    } catch (e) {
-      console.error(`error invoking ${method}: ${e.message}`);
-    }
-  }
-}
diff --git a/ui/src/controller/record_controller_interfaces.ts b/ui/src/controller/record_controller_interfaces.ts
deleted file mode 100644
index e9662fd..0000000
--- a/ui/src/controller/record_controller_interfaces.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (C) 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
-//
-//      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_SUFFIX} from '../common/constants';
-import {ConsumerPortResponse} from './consumer_port_types';
-
-export type ErrorCallback = (_: string) => void;
-export type StatusCallback = (_: string) => void;
-
-export abstract class RpcConsumerPort {
-  // The responses of the call invocations should be sent through this listener.
-  // This is done by the 3 "send" methods in this abstract class.
-  private consumerPortListener: Consumer;
-
-  protected constructor(consumerPortListener: Consumer) {
-    this.consumerPortListener = consumerPortListener;
-  }
-
-  // RequestData is the proto representing the arguments of the function call.
-  abstract handleCommand(methodName: string, requestData: Uint8Array): void;
-
-  sendMessage(data: ConsumerPortResponse) {
-    this.consumerPortListener.onConsumerPortResponse(data);
-  }
-
-  sendErrorMessage(message: string) {
-    this.consumerPortListener.onError(message);
-  }
-
-  sendStatus(status: string) {
-    this.consumerPortListener.onStatus(status);
-  }
-
-  // Allows the recording controller to customise the suffix added to recorded
-  // traces when they are downloaded. In the general case this will be
-  // .perfetto-trace however if the trace is recorded compressed if could be
-  // .perfetto-trace.gz etc.
-  getRecordedTraceSuffix(): string {
-    return TRACE_SUFFIX;
-  }
-}
-
-export interface Consumer {
-  onConsumerPortResponse(data: ConsumerPortResponse): void;
-  onError: ErrorCallback;
-  onStatus: StatusCallback;
-}
diff --git a/ui/src/controller/record_controller_jsdomtest.ts b/ui/src/controller/record_controller_jsdomtest.ts
deleted file mode 100644
index 2dbbc3f..0000000
--- a/ui/src/controller/record_controller_jsdomtest.ts
+++ /dev/null
@@ -1,464 +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 {assertExists} from '../base/logging';
-import {TraceConfig} from '../protos';
-
-import {createEmptyRecordConfig} from './record_config_types';
-import {genConfigProto, toPbtxt} from './record_controller';
-
-test('encodeConfig', () => {
-  const config = createEmptyRecordConfig();
-  config.durationMs = 20000;
-  const result = TraceConfig.decode(
-    genConfigProto(config, {os: 'Q', name: 'Android Q'}),
-  );
-  expect(result.durationMs).toBe(20000);
-});
-
-test('SysConfig', () => {
-  const config = createEmptyRecordConfig();
-  config.cpuSyscall = true;
-  const result = TraceConfig.decode(
-    genConfigProto(config, {os: 'Q', name: 'Android Q'}),
-  );
-  const sources = assertExists(result.dataSources);
-  // TODO(hjd): This is all bad. Should just match the whole config.
-  const srcConfig = assertExists(sources[2].config);
-  const ftraceConfig = assertExists(srcConfig.ftraceConfig);
-  const ftraceEvents = assertExists(ftraceConfig.ftraceEvents);
-  expect(ftraceEvents.includes('raw_syscalls/sys_enter')).toBe(true);
-  expect(ftraceEvents.includes('raw_syscalls/sys_exit')).toBe(true);
-});
-
-test('LinuxSystemInfo present', () => {
-  const config = createEmptyRecordConfig();
-  const result = TraceConfig.decode(
-    genConfigProto(config, {os: 'Q', name: 'Android Q'}),
-  );
-  const sources = assertExists(result.dataSources);
-  const sysInfoConfig = assertExists(sources[1].config);
-  expect(sysInfoConfig.name).toBe('linux.system_info');
-});
-
-test('cpu scheduling includes kSyms if OS >= S', () => {
-  const config = createEmptyRecordConfig();
-  config.cpuSched = true;
-  const result = TraceConfig.decode(
-    genConfigProto(config, {os: 'S', name: 'Android S'}),
-  );
-  const sources = assertExists(result.dataSources);
-  const srcConfig = assertExists(sources[3].config);
-  const ftraceConfig = assertExists(srcConfig.ftraceConfig);
-  const ftraceEvents = assertExists(ftraceConfig.ftraceEvents);
-  expect(ftraceConfig.symbolizeKsyms).toBe(true);
-  expect(ftraceEvents.includes('sched/sched_blocked_reason')).toBe(true);
-});
-
-test('cpu scheduling does not include kSyms if OS <= S', () => {
-  const config = createEmptyRecordConfig();
-  config.cpuSched = true;
-  const result = TraceConfig.decode(
-    genConfigProto(config, {os: 'Q', name: 'Android Q'}),
-  );
-  const sources = assertExists(result.dataSources);
-  const srcConfig = assertExists(sources[3].config);
-  const ftraceConfig = assertExists(srcConfig.ftraceConfig);
-  const ftraceEvents = assertExists(ftraceConfig.ftraceEvents);
-  expect(ftraceConfig.symbolizeKsyms).toBe(false);
-  expect(ftraceEvents.includes('sched/sched_blocked_reason')).toBe(false);
-});
-
-test('kSyms can be enabled individually', () => {
-  const config = createEmptyRecordConfig();
-  config.ftrace = true;
-  config.symbolizeKsyms = true;
-  const result = TraceConfig.decode(
-    genConfigProto(config, {os: 'Q', name: 'Android Q'}),
-  );
-  const sources = assertExists(result.dataSources);
-  const srcConfig = assertExists(sources[2].config);
-  const ftraceConfig = assertExists(srcConfig.ftraceConfig);
-  const ftraceEvents = assertExists(ftraceConfig.ftraceEvents);
-  expect(ftraceConfig.symbolizeKsyms).toBe(true);
-  expect(ftraceEvents.includes('sched/sched_blocked_reason')).toBe(true);
-});
-
-test('kSyms can be disabled individually', () => {
-  const config = createEmptyRecordConfig();
-  config.ftrace = true;
-  config.symbolizeKsyms = false;
-  const result = TraceConfig.decode(
-    genConfigProto(config, {os: 'Q', name: 'Android Q'}),
-  );
-  const sources = assertExists(result.dataSources);
-  const srcConfig = assertExists(sources[2].config);
-  const ftraceConfig = assertExists(srcConfig.ftraceConfig);
-  const ftraceEvents = assertExists(ftraceConfig.ftraceEvents);
-  expect(ftraceConfig.symbolizeKsyms).toBe(false);
-  expect(ftraceEvents.includes('sched/sched_blocked_reason')).toBe(false);
-});
-
-test('toPbtxt', () => {
-  const config = {
-    durationMs: 1000,
-    maxFileSizeBytes: 43,
-    buffers: [
-      {
-        sizeKb: 42,
-      },
-    ],
-    dataSources: [
-      {
-        config: {
-          name: 'linux.ftrace',
-          targetBuffer: 1,
-          ftraceConfig: {
-            ftraceEvents: ['sched_switch', 'print'],
-          },
-        },
-      },
-    ],
-    producers: [
-      {
-        producerName: 'perfetto.traced_probes',
-      },
-    ],
-  };
-
-  const text = toPbtxt(TraceConfig.encode(config).finish());
-
-  expect(text).toEqual(`buffers: {
-    size_kb: 42
-}
-data_sources: {
-    config {
-        name: "linux.ftrace"
-        target_buffer: 1
-        ftrace_config {
-            ftrace_events: "sched_switch"
-            ftrace_events: "print"
-        }
-    }
-}
-duration_ms: 1000
-producers: {
-    producer_name: "perfetto.traced_probes"
-}
-max_file_size_bytes: 43
-`);
-});
-
-test('ChromeConfig', () => {
-  const config = createEmptyRecordConfig();
-  config.ipcFlows = true;
-  config.jsExecution = true;
-  config.mode = 'STOP_WHEN_FULL';
-  const result = TraceConfig.decode(
-    genConfigProto(config, {os: 'C', name: 'Chrome'}),
-  );
-  const sources = assertExists(result.dataSources);
-
-  const traceConfigSource = assertExists(sources[0].config);
-  expect(traceConfigSource.name).toBe('org.chromium.trace_event');
-  const chromeConfig = assertExists(traceConfigSource.chromeConfig);
-  expect(chromeConfig.privacyFilteringEnabled).toBe(false);
-  const traceConfig = assertExists(chromeConfig.traceConfig);
-
-  const trackEventConfigSource = assertExists(sources[1].config);
-  expect(trackEventConfigSource.name).toBe('track_event');
-  const trackEventConfig = assertExists(
-    trackEventConfigSource.trackEventConfig,
-  );
-  expect(trackEventConfig.filterDynamicEventNames).toBe(false);
-  expect(trackEventConfig.filterDebugAnnotations).toBe(false);
-  const chromeConfigT = assertExists(trackEventConfigSource.chromeConfig);
-  const traceConfigT = assertExists(chromeConfigT.traceConfig);
-
-  const metadataConfigSource = assertExists(sources[2].config);
-  expect(metadataConfigSource.name).toBe('org.chromium.trace_metadata');
-  const chromeConfigM = assertExists(metadataConfigSource.chromeConfig);
-  const traceConfigM = assertExists(chromeConfigM.traceConfig);
-
-  const expectedTraceConfig =
-    '{"record_mode":"record-until-full",' +
-    '"included_categories":' +
-    '["toplevel","toplevel.flow","disabled-by-default-ipc.flow",' +
-    '"mojom","v8"],' +
-    '"excluded_categories":["*"],' +
-    '"memory_dump_config":{}}';
-  expect(traceConfig).toEqual(expectedTraceConfig);
-  expect(traceConfigT).toEqual(expectedTraceConfig);
-  expect(traceConfigM).toEqual(expectedTraceConfig);
-});
-
-test('ChromeConfig with privacy filtering', () => {
-  const config = createEmptyRecordConfig();
-  config.ipcFlows = true;
-  config.jsExecution = true;
-  config.mode = 'STOP_WHEN_FULL';
-  config.chromePrivacyFiltering = true;
-  const result = TraceConfig.decode(
-    genConfigProto(config, {os: 'C', name: 'Chrome'}),
-  );
-  const sources = assertExists(result.dataSources);
-
-  const traceConfigSource = assertExists(sources[0].config);
-  expect(traceConfigSource.name).toBe('org.chromium.trace_event');
-  const chromeConfig = assertExists(traceConfigSource.chromeConfig);
-  expect(chromeConfig.privacyFilteringEnabled).toBe(true);
-
-  const trackEventConfigSource = assertExists(sources[1].config);
-  expect(trackEventConfigSource.name).toBe('track_event');
-  const trackEventConfig = assertExists(
-    trackEventConfigSource.trackEventConfig,
-  );
-  expect(trackEventConfig.filterDynamicEventNames).toBe(true);
-  expect(trackEventConfig.filterDebugAnnotations).toBe(true);
-});
-
-test('ChromeMemoryConfig', () => {
-  const config = createEmptyRecordConfig();
-  config.chromeHighOverheadCategoriesSelected = [
-    'disabled-by-default-memory-infra',
-  ];
-  const result = TraceConfig.decode(
-    genConfigProto(config, {os: 'C', name: 'Chrome'}),
-  );
-  const sources = assertExists(result.dataSources);
-
-  const traceConfigSource = assertExists(sources[0].config);
-  expect(traceConfigSource.name).toBe('org.chromium.trace_event');
-  const chromeConfig = assertExists(traceConfigSource.chromeConfig);
-  const traceConfig = assertExists(chromeConfig.traceConfig);
-
-  const trackEventConfigSource = assertExists(sources[1].config);
-  expect(trackEventConfigSource.name).toBe('track_event');
-  const chromeConfigT = assertExists(trackEventConfigSource.chromeConfig);
-  const traceConfigT = assertExists(chromeConfigT.traceConfig);
-
-  const metadataConfigSource = assertExists(sources[2].config);
-  expect(metadataConfigSource.name).toBe('org.chromium.trace_metadata');
-  const chromeConfigM = assertExists(metadataConfigSource.chromeConfig);
-  const traceConfigM = assertExists(chromeConfigM.traceConfig);
-
-  const miConfigSource = assertExists(sources[3].config);
-  expect(miConfigSource.name).toBe('org.chromium.memory_instrumentation');
-  const chromeConfigI = assertExists(miConfigSource.chromeConfig);
-  const traceConfigI = assertExists(chromeConfigI.traceConfig);
-
-  const hpConfigSource = assertExists(sources[4].config);
-  expect(hpConfigSource.name).toBe('org.chromium.native_heap_profiler');
-  const chromeConfigH = assertExists(hpConfigSource.chromeConfig);
-  const traceConfigH = assertExists(chromeConfigH.traceConfig);
-
-  const expectedTraceConfig =
-    '{"record_mode":"record-until-full",' +
-    '"included_categories":["disabled-by-default-memory-infra"],' +
-    '"excluded_categories":["*"],' +
-    '"memory_dump_config":{"allowed_dump_modes":["background",' +
-    '"light","detailed"],"triggers":[{"min_time_between_dumps_ms":' +
-    '10000,"mode":"detailed","type":"periodic_interval"}]}}';
-  expect(traceConfig).toEqual(expectedTraceConfig);
-  expect(traceConfigT).toEqual(expectedTraceConfig);
-  expect(traceConfigM).toEqual(expectedTraceConfig);
-  expect(traceConfigI).toEqual(expectedTraceConfig);
-  expect(traceConfigH).toEqual(expectedTraceConfig);
-});
-
-test('ChromeCpuProfilerConfig', () => {
-  const config = createEmptyRecordConfig();
-  config.chromeHighOverheadCategoriesSelected = [
-    'disabled-by-default-cpu_profiler',
-  ];
-  const decoded = TraceConfig.decode(
-    genConfigProto(config, {os: 'C', name: 'Chrome'}),
-  );
-  const sources = assertExists(decoded.dataSources);
-
-  const traceConfigSource = assertExists(sources[0].config);
-  expect(traceConfigSource.name).toBe('org.chromium.trace_event');
-  const traceEventChromeConfig = assertExists(traceConfigSource.chromeConfig);
-  const traceEventConfig = assertExists(traceEventChromeConfig.traceConfig);
-
-  const trackEventConfigSource = assertExists(sources[1].config);
-  expect(trackEventConfigSource.name).toBe('track_event');
-  const chromeConfigT = assertExists(trackEventConfigSource.chromeConfig);
-  const traceConfigT = assertExists(chromeConfigT.traceConfig);
-
-  const metadataConfigSource = assertExists(sources[2].config);
-  expect(metadataConfigSource.name).toBe('org.chromium.trace_metadata');
-  const traceMetadataChromeConfig = assertExists(
-    metadataConfigSource.chromeConfig,
-  );
-  const traceMetadataConfig = assertExists(
-    traceMetadataChromeConfig.traceConfig,
-  );
-
-  const profilerConfigSource = assertExists(sources[3].config);
-  expect(profilerConfigSource.name).toBe('org.chromium.sampler_profiler');
-  const profilerChromeConfig = assertExists(profilerConfigSource.chromeConfig);
-  const profilerConfig = assertExists(profilerChromeConfig.traceConfig);
-
-  const expectedTraceConfig =
-    '{"record_mode":"record-until-full",' +
-    '"included_categories":["disabled-by-default-cpu_profiler"],' +
-    '"excluded_categories":["*"],"memory_dump_config":{}}';
-  expect(traceEventConfig).toEqual(expectedTraceConfig);
-  expect(traceConfigT).toEqual(expectedTraceConfig);
-  expect(traceMetadataConfig).toEqual(expectedTraceConfig);
-  expect(profilerConfig).toEqual(expectedTraceConfig);
-});
-
-test('ChromeCpuProfilerDebugConfig', () => {
-  const config = createEmptyRecordConfig();
-  config.chromeHighOverheadCategoriesSelected = [
-    'disabled-by-default-cpu_profiler.debug',
-  ];
-  const decoded = TraceConfig.decode(
-    genConfigProto(config, {os: 'C', name: 'Chrome'}),
-  );
-  const sources = assertExists(decoded.dataSources);
-
-  const traceConfigSource = assertExists(sources[0].config);
-  expect(traceConfigSource.name).toBe('org.chromium.trace_event');
-  const traceEventChromeConfig = assertExists(traceConfigSource.chromeConfig);
-  const traceEventConfig = assertExists(traceEventChromeConfig.traceConfig);
-
-  const trackEventConfigSource = assertExists(sources[1].config);
-  expect(trackEventConfigSource.name).toBe('track_event');
-  const chromeConfigT = assertExists(trackEventConfigSource.chromeConfig);
-  const traceConfigT = assertExists(chromeConfigT.traceConfig);
-
-  const metadataConfigSource = assertExists(sources[2].config);
-  expect(metadataConfigSource.name).toBe('org.chromium.trace_metadata');
-  const traceMetadataChromeConfig = assertExists(
-    metadataConfigSource.chromeConfig,
-  );
-  const traceMetadataConfig = assertExists(
-    traceMetadataChromeConfig.traceConfig,
-  );
-
-  const profilerConfigSource = assertExists(sources[3].config);
-  expect(profilerConfigSource.name).toBe('org.chromium.sampler_profiler');
-  const profilerChromeConfig = assertExists(profilerConfigSource.chromeConfig);
-  const profilerConfig = assertExists(profilerChromeConfig.traceConfig);
-
-  const expectedTraceConfig =
-    '{"record_mode":"record-until-full",' +
-    '"included_categories":["disabled-by-default-cpu_profiler.debug"],' +
-    '"excluded_categories":["*"],"memory_dump_config":{}}';
-  expect(traceConfigT).toEqual(expectedTraceConfig);
-  expect(traceEventConfig).toEqual(expectedTraceConfig);
-  expect(traceMetadataConfig).toEqual(expectedTraceConfig);
-  expect(profilerConfig).toEqual(expectedTraceConfig);
-});
-
-test('ChromeConfigRingBuffer', () => {
-  const config = createEmptyRecordConfig();
-  config.ipcFlows = true;
-  config.jsExecution = true;
-  config.mode = 'RING_BUFFER';
-  const result = TraceConfig.decode(
-    genConfigProto(config, {os: 'C', name: 'Chrome'}),
-  );
-  const sources = assertExists(result.dataSources);
-
-  const traceConfigSource = assertExists(sources[0].config);
-  expect(traceConfigSource.name).toBe('org.chromium.trace_event');
-  const chromeConfig = assertExists(traceConfigSource.chromeConfig);
-  const traceConfig = assertExists(chromeConfig.traceConfig);
-
-  const trackEventConfigSource = assertExists(sources[1].config);
-  expect(trackEventConfigSource.name).toBe('track_event');
-  const chromeConfigT = assertExists(trackEventConfigSource.chromeConfig);
-  const traceConfigT = assertExists(chromeConfigT.traceConfig);
-
-  const metadataConfigSource = assertExists(sources[2].config);
-  expect(metadataConfigSource.name).toBe('org.chromium.trace_metadata');
-  const chromeConfigM = assertExists(metadataConfigSource.chromeConfig);
-  const traceConfigM = assertExists(chromeConfigM.traceConfig);
-
-  const expectedTraceConfig =
-    '{"record_mode":"record-continuously",' +
-    '"included_categories":' +
-    '["toplevel","toplevel.flow","disabled-by-default-ipc.flow",' +
-    '"mojom","v8"],' +
-    '"excluded_categories":["*"],"memory_dump_config":{}}';
-  expect(traceConfig).toEqual(expectedTraceConfig);
-  expect(traceConfigT).toEqual(expectedTraceConfig);
-  expect(traceConfigM).toEqual(expectedTraceConfig);
-});
-
-test('ChromeConfigLongTrace', () => {
-  const config = createEmptyRecordConfig();
-  config.ipcFlows = true;
-  config.jsExecution = true;
-  config.mode = 'RING_BUFFER';
-  const result = TraceConfig.decode(
-    genConfigProto(config, {os: 'C', name: 'Chrome'}),
-  );
-  const sources = assertExists(result.dataSources);
-
-  const traceConfigSource = assertExists(sources[0].config);
-  expect(traceConfigSource.name).toBe('org.chromium.trace_event');
-  const chromeConfig = assertExists(traceConfigSource.chromeConfig);
-  const traceConfig = assertExists(chromeConfig.traceConfig);
-
-  const trackEventConfigSource = assertExists(sources[1].config);
-  expect(trackEventConfigSource.name).toBe('track_event');
-  const chromeConfigT = assertExists(trackEventConfigSource.chromeConfig);
-  const traceConfigT = assertExists(chromeConfigT.traceConfig);
-
-  const metadataConfigSource = assertExists(sources[2].config);
-  expect(metadataConfigSource.name).toBe('org.chromium.trace_metadata');
-  const chromeConfigM = assertExists(metadataConfigSource.chromeConfig);
-  const traceConfigM = assertExists(chromeConfigM.traceConfig);
-
-  const expectedTraceConfig =
-    '{"record_mode":"record-continuously",' +
-    '"included_categories":' +
-    '["toplevel","toplevel.flow","disabled-by-default-ipc.flow",' +
-    '"mojom","v8"],' +
-    '"excluded_categories":["*"],"memory_dump_config":{}}';
-  expect(traceConfig).toEqual(expectedTraceConfig);
-  expect(traceConfigT).toEqual(expectedTraceConfig);
-  expect(traceConfigM).toEqual(expectedTraceConfig);
-});
-
-test('ChromeConfigToPbtxt', () => {
-  const config = {
-    dataSources: [
-      {
-        config: {
-          name: 'org.chromium.trace_event',
-          chromeConfig: {
-            traceConfig: JSON.stringify({included_categories: ['v8']}),
-          },
-        },
-      },
-    ],
-  };
-  const text = toPbtxt(TraceConfig.encode(config).finish());
-
-  expect(text).toEqual(`data_sources: {
-    config {
-        name: "org.chromium.trace_event"
-        chrome_config {
-            trace_config: "{\\"included_categories\\":[\\"v8\\"]}"
-        }
-    }
-}
-`);
-});
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
deleted file mode 100644
index b7d2c45..0000000
--- a/ui/src/controller/search_controller.ts
+++ /dev/null
@@ -1,228 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {sqliteString} from '../base/string_utils';
-import {exists} from '../base/utils';
-import {CurrentSearchResults, SearchSource} from '../common/search_data';
-import {OmniboxState} from '../common/state';
-import {CPU_SLICE_TRACK_KIND} from '../core/track_kinds';
-import {globals} from '../frontend/globals';
-import {publishSearchResult} from '../frontend/publish';
-import {Engine} from '../trace_processor/engine';
-import {LONG, NUM, STR} from '../trace_processor/query_result';
-import {escapeSearchQuery} from '../trace_processor/query_utils';
-
-import {Controller} from './controller';
-
-export interface SearchControllerArgs {
-  engine: Engine;
-}
-
-export class SearchController extends Controller<'main'> {
-  private engine: Engine;
-  private previousOmniboxState?: OmniboxState;
-  private updateInProgress: boolean;
-
-  constructor(args: SearchControllerArgs) {
-    super('main');
-    this.engine = args.engine;
-    this.updateInProgress = false;
-  }
-
-  run() {
-    if (this.updateInProgress) {
-      return;
-    }
-
-    const omniboxState = globals.state.omniboxState;
-    if (omniboxState === undefined || omniboxState.mode === 'COMMAND') {
-      return;
-    }
-    const newOmniboxState = omniboxState;
-    if (this.previousOmniboxState === newOmniboxState) {
-      return;
-    }
-
-    // TODO(hjd): We should restrict this to the start of the trace but
-    // that is not easily available here.
-    // N.B. Timestamps can be negative.
-    this.previousOmniboxState = newOmniboxState;
-    const search = newOmniboxState.omnibox;
-    if (search === '' || (search.length < 4 && !newOmniboxState.force)) {
-      publishSearchResult({
-        eventIds: new Float64Array(0),
-        tses: new BigInt64Array(0),
-        utids: new Float64Array(0),
-        sources: [],
-        trackKeys: [],
-        totalResults: 0,
-      });
-      return;
-    }
-
-    const computeResults = this.specificSearch(search).then((searchResults) => {
-      publishSearchResult(searchResults);
-    });
-
-    Promise.all([computeResults]).finally(() => {
-      this.updateInProgress = false;
-      this.run();
-    });
-  }
-
-  onDestroy() {}
-
-  private async specificSearch(search: string) {
-    const searchLiteral = escapeSearchQuery(search);
-    // TODO(hjd): we should avoid recomputing this every time. This will be
-    // easier once the track table has entries for all the tracks.
-    const cpuToTrackId = new Map();
-    for (const track of Object.values(globals.state.tracks)) {
-      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-      if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-        exists(trackInfo.tags.cpu) &&
-          cpuToTrackId.set(trackInfo.tags.cpu, track.key);
-      }
-    }
-
-    const utidRes = await this.query(`select utid from thread join process
-    using(upid) where
-      thread.name glob ${searchLiteral} or
-      process.name glob ${searchLiteral}`);
-    const utids = [];
-    for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) {
-      utids.push(it.utid);
-    }
-
-    const res = await this.query(`
-      select
-        id as sliceId,
-        ts,
-        'cpu' as source,
-        cpu as sourceId,
-        utid
-      from sched where utid in (${utids.join(',')})
-      union all
-      select *
-      from (
-        select
-          slice_id as sliceId,
-          ts,
-          'slice' as source,
-          track_id as sourceId,
-          0 as utid
-          from slice
-          where slice.name glob ${searchLiteral}
-            or (
-              0 != CAST(${sqliteString(search)} AS INT) and
-              sliceId = CAST(${sqliteString(search)} AS INT)
-            )
-        union
-        select
-          slice_id as sliceId,
-          ts,
-          'slice' as source,
-          track_id as sourceId,
-          0 as utid
-        from slice
-        join args using(arg_set_id)
-        where string_value glob ${searchLiteral} or key glob ${searchLiteral}
-      )
-      union all
-      select
-        id as sliceId,
-        ts,
-        'log' as source,
-        0 as sourceId,
-        utid
-      from android_logs where msg glob ${searchLiteral}
-      order by ts
-    `);
-
-    const searchResults: CurrentSearchResults = {
-      eventIds: new Float64Array(0),
-      tses: new BigInt64Array(0),
-      utids: new Float64Array(0),
-      sources: [],
-      trackKeys: [],
-      totalResults: 0,
-    };
-
-    const lowerSearch = search.toLowerCase();
-    for (const track of Object.values(globals.state.tracks)) {
-      if (track.name.toLowerCase().indexOf(lowerSearch) === -1) {
-        continue;
-      }
-      searchResults.totalResults++;
-      searchResults.sources.push('track');
-      searchResults.trackKeys.push(track.key);
-    }
-
-    const rows = res.numRows();
-    searchResults.eventIds = new Float64Array(
-      searchResults.totalResults + rows,
-    );
-    searchResults.tses = new BigInt64Array(searchResults.totalResults + rows);
-    searchResults.utids = new Float64Array(searchResults.totalResults + rows);
-    for (let i = 0; i < searchResults.totalResults; ++i) {
-      searchResults.eventIds[i] = -1;
-      searchResults.tses[i] = -1n;
-      searchResults.utids[i] = -1;
-    }
-
-    const it = res.iter({
-      sliceId: NUM,
-      ts: LONG,
-      source: STR,
-      sourceId: NUM,
-      utid: NUM,
-    });
-    for (; it.valid(); it.next()) {
-      let trackId = undefined;
-      if (it.source === 'cpu') {
-        trackId = cpuToTrackId.get(it.sourceId);
-      } else if (it.source === 'slice') {
-        trackId = globals.trackManager.trackKeyByTrackId.get(it.sourceId);
-      } else if (it.source === 'log') {
-        const logTracks = Object.values(globals.state.tracks).filter(
-          (track) => {
-            const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
-            return trackDesc && trackDesc.tags?.kind === 'AndroidLogTrack';
-          },
-        );
-        if (logTracks.length > 0) {
-          trackId = logTracks[0].key;
-        }
-      }
-
-      // The .get() calls above could return undefined, this isn't just an else.
-      if (trackId === undefined) {
-        continue;
-      }
-
-      const i = searchResults.totalResults++;
-      searchResults.trackKeys.push(trackId);
-      searchResults.sources.push(it.source as SearchSource);
-      searchResults.eventIds[i] = it.sliceId;
-      searchResults.tses[i] = it.ts;
-      searchResults.utids[i] = it.utid;
-    }
-    return searchResults;
-  }
-
-  private async query(query: string) {
-    const result = await this.engine.query(query);
-    return result;
-  }
-}
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
deleted file mode 100644
index 4cf83b4..0000000
--- a/ui/src/controller/selection_controller.ts
+++ /dev/null
@@ -1,539 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {Time, time} from '../base/time';
-import {Optional} from '../base/utils';
-import {Args, ArgValue} from '../common/arg_types';
-import {
-  SelectionKind,
-  ThreadSliceSelection,
-  getLegacySelection,
-} from '../common/state';
-import {THREAD_SLICE_TRACK_KIND} from '../core/track_kinds';
-import {globals, SliceDetails, ThreadStateDetails} from '../frontend/globals';
-import {
-  publishSliceDetails,
-  publishThreadStateDetails,
-} from '../frontend/publish';
-import {Engine} from '../trace_processor/engine';
-import {
-  durationFromSql,
-  LONG,
-  NUM,
-  NUM_NULL,
-  STR,
-  STR_NULL,
-  timeFromSql,
-} from '../trace_processor/query_result';
-import {fromNumNull} from '../trace_processor/sql_utils';
-import {Controller} from './controller';
-
-export interface SelectionControllerArgs {
-  engine: Engine;
-}
-
-interface ThreadDetails {
-  tid: number;
-  threadName?: string;
-}
-
-interface ProcessDetails {
-  pid?: number;
-  processName?: string;
-  uid?: number;
-  packageName?: string;
-  versionCode?: number;
-}
-
-// This class queries the TP for the details on a specific slice that has
-// been clicked.
-export class SelectionController extends Controller<'main'> {
-  private lastSelectedId?: number | string;
-  private lastSelectedKind?: string;
-  constructor(private args: SelectionControllerArgs) {
-    super('main');
-  }
-
-  run() {
-    const selection = getLegacySelection(globals.state);
-    if (!selection) return;
-
-    const selectWithId: SelectionKind[] = [
-      'SLICE',
-      'SCHED_SLICE',
-      'HEAP_PROFILE',
-      'THREAD_STATE',
-    ];
-    if (
-      !selectWithId.includes(selection.kind) ||
-      (selectWithId.includes(selection.kind) &&
-        selection.id === this.lastSelectedId &&
-        selection.kind === this.lastSelectedKind)
-    ) {
-      return;
-    }
-    const selectedId = selection.id;
-    const selectedKind = selection.kind;
-    this.lastSelectedId = selectedId;
-    this.lastSelectedKind = selectedKind;
-
-    if (selectedId === undefined) return;
-
-    if (selection.kind === 'SCHED_SLICE') {
-      this.schedSliceDetails(selectedId as number);
-    } else if (selection.kind === 'THREAD_STATE') {
-      this.threadStateDetails(selection.id);
-    } else if (selection.kind === 'SLICE') {
-      this.sliceDetails(selection);
-    }
-  }
-
-  async sliceDetails(selection: ThreadSliceSelection) {
-    const selectedId = selection.id;
-    const table = selection.table;
-
-    let leafTable: string;
-    let promisedArgs: Promise<Args>;
-    // TODO(b/155483804): This is a hack to ensure annotation slices are
-    // selectable for now. We should tidy this up when improving this class.
-    if (table === 'annotation') {
-      leafTable = 'annotation_slice';
-      promisedArgs = Promise.resolve(new Map());
-    } else {
-      const result = await this.args.engine.query(`
-        SELECT
-          type as leafTable,
-          arg_set_id as argSetId
-        FROM slice WHERE id = ${selectedId}`);
-
-      if (result.numRows() === 0) {
-        return;
-      }
-
-      const row = result.firstRow({
-        leafTable: STR,
-        argSetId: NUM,
-      });
-
-      leafTable = row.leafTable;
-      const argSetId = row.argSetId;
-      promisedArgs = this.getArgs(argSetId);
-    }
-
-    const promisedDetails = this.args.engine.query(`
-      SELECT *, ABS_TIME_STR(ts) as absTime FROM ${leafTable} WHERE id = ${selectedId};
-    `);
-
-    const [details, args] = await Promise.all([promisedDetails, promisedArgs]);
-
-    if (details.numRows() <= 0) return;
-    const rowIter = details.iter({});
-    assertTrue(rowIter.valid());
-
-    // A few columns are hard coded as part of the SliceDetails interface.
-    // Long term these should be handled generically as args but for now
-    // handle them specially:
-    let ts = undefined;
-    let absTime = undefined;
-    let dur = undefined;
-    let name = undefined;
-    let category = undefined;
-    let threadDur = undefined;
-    let threadTs = undefined;
-    let trackId = undefined;
-
-    // We select all columns from the leafTable to ensure that we include
-    // additional fields from the child tables (like `thread_dur` from
-    // `thread_slice` or `frame_number` from `frame_slice`).
-    // However, this also includes some basic columns (especially from `slice`)
-    // that are not interesting (i.e. `arg_set_id`, which has already been used
-    // to resolve and show the arguments) and should not be shown to the user.
-    const ignoredColumns = [
-      'type',
-      'depth',
-      'parent_id',
-      'stack_id',
-      'parent_stack_id',
-      'arg_set_id',
-      'thread_instruction_count',
-      'thread_instruction_delta',
-    ];
-
-    for (const k of details.columns()) {
-      const v = rowIter.get(k);
-      switch (k) {
-        case 'id':
-          break;
-        case 'ts':
-          ts = timeFromSql(v);
-          break;
-        case 'thread_ts':
-          threadTs = timeFromSql(v);
-          break;
-        case 'absTime':
-          /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-          if (v) absTime = `${v}`;
-          /* eslint-enable */
-          break;
-        case 'name':
-          name = `${v}`;
-          break;
-        case 'dur':
-          dur = durationFromSql(v);
-          break;
-        case 'thread_dur':
-          threadDur = durationFromSql(v);
-          break;
-        case 'category':
-        case 'cat':
-          category = `${v}`;
-          break;
-        case 'track_id':
-          trackId = Number(v);
-          break;
-        default:
-          if (!ignoredColumns.includes(k)) args.set(k, `${v}`);
-      }
-    }
-
-    const selected: SliceDetails = {
-      id: selectedId,
-      ts,
-      threadTs,
-      absTime,
-      dur,
-      threadDur,
-      name,
-      category,
-      args,
-    };
-
-    if (trackId !== undefined) {
-      const columnInfo = (
-        await this.args.engine.query(`
-        WITH
-           leafTrackTable AS (SELECT type FROM track WHERE id = ${trackId}),
-           cols AS (
-                SELECT name
-                FROM pragma_table_info((SELECT type FROM leafTrackTable))
-            )
-        SELECT
-           type as leafTrackTable,
-          'upid' in cols AS hasUpid,
-          'utid' in cols AS hasUtid
-        FROM leafTrackTable
-      `)
-      ).firstRow({hasUpid: NUM, hasUtid: NUM, leafTrackTable: STR});
-      const hasUpid = columnInfo.hasUpid !== 0;
-      const hasUtid = columnInfo.hasUtid !== 0;
-
-      if (hasUtid) {
-        const utid = (
-          await this.args.engine.query(`
-            SELECT utid
-            FROM ${columnInfo.leafTrackTable}
-            WHERE id = ${trackId};
-        `)
-        ).firstRow({
-          utid: NUM,
-        }).utid;
-        Object.assign(selected, await this.computeThreadDetails(utid));
-      } else if (hasUpid) {
-        const upid = (
-          await this.args.engine.query(`
-            SELECT upid
-            FROM ${columnInfo.leafTrackTable}
-            WHERE id = ${trackId};
-        `)
-        ).firstRow({
-          upid: NUM,
-        }).upid;
-        Object.assign(selected, await this.computeProcessDetails(upid));
-      }
-    }
-
-    // Check selection is still the same on completion of query.
-    if (selection === getLegacySelection(globals.state)) {
-      publishSliceDetails(selected);
-    }
-  }
-
-  async getArgs(argId: number): Promise<Args> {
-    const args = new Map<string, ArgValue>();
-    const query = `
-      select
-        key AS name,
-        display_value AS value
-      FROM args
-      WHERE arg_set_id = ${argId}
-    `;
-    const result = await this.args.engine.query(query);
-    const it = result.iter({
-      name: STR,
-      value: STR_NULL,
-    });
-    for (; it.valid(); it.next()) {
-      const name = it.name;
-      const value = it.value ?? 'NULL';
-      if (name === 'destination slice id' && !isNaN(Number(value))) {
-        const destTrackId = await this.getDestTrackId(value);
-        args.set('Destination Slice', {
-          kind: 'SCHED_SLICE',
-          trackId: destTrackId,
-          sliceId: Number(value),
-          rawValue: value,
-        });
-      } else {
-        args.set(name, value);
-      }
-    }
-    return args;
-  }
-
-  async getDestTrackId(sliceId: string): Promise<string> {
-    const trackIdQuery = `select track_id as trackId from slice
-    where slice_id = ${sliceId}`;
-    const result = await this.args.engine.query(trackIdQuery);
-    const trackId = result.firstRow({trackId: NUM}).trackId;
-    // TODO(hjd): If we had a consistent mapping from TP track_id
-    // UI track id for slice tracks this would be unnecessary.
-    let trackKey = '';
-    for (const track of Object.values(globals.state.tracks)) {
-      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-      if (trackInfo?.tags?.kind === THREAD_SLICE_TRACK_KIND) {
-        const trackIds = trackInfo?.tags?.trackIds;
-        if (trackIds && trackIds.length > 0 && trackIds[0] === trackId) {
-          trackKey = track.key;
-          break;
-        }
-      }
-    }
-    return trackKey;
-  }
-
-  // TODO(altimin): We currently rely on the ThreadStateDetails for supporting
-  // marking the area (the rest goes is handled by ThreadStateTab
-  // directly. Refactor it to be plugin-friendly and remove this.
-  async threadStateDetails(id: number) {
-    const query = `
-      SELECT
-        ts,
-        thread_state.dur as dur
-      from thread_state
-      where thread_state.id = ${id}
-    `;
-    const result = await this.args.engine.query(query);
-
-    const selection = getLegacySelection(globals.state);
-    if (result.numRows() > 0 && selection) {
-      const row = result.firstRow({
-        ts: LONG,
-        dur: LONG,
-      });
-      const selected: ThreadStateDetails = {
-        ts: Time.fromRaw(row.ts),
-        dur: row.dur,
-      };
-      publishThreadStateDetails(selected);
-    }
-  }
-
-  async schedSliceDetails(id: number) {
-    const sqlQuery = `
-      SELECT
-        ts,
-        dur,
-        priority,
-        end_state as endState,
-        utid,
-        cpu
-      FROM sched
-      WHERE sched.id = ${id}
-    `;
-    const result = await this.args.engine.query(sqlQuery);
-    // Check selection is still the same on completion of query.
-    const selection = getLegacySelection(globals.state);
-    if (result.numRows() > 0 && selection) {
-      const row = result.firstRow({
-        ts: LONG,
-        dur: LONG,
-        priority: NUM,
-        endState: STR_NULL,
-        utid: NUM,
-        cpu: NUM,
-      });
-      const ts = Time.fromRaw(row.ts);
-      const dur = row.dur;
-      const priority = row.priority;
-      const endState = row.endState;
-      const utid = row.utid;
-      const cpu = row.cpu;
-      const selected: SliceDetails = {
-        ts,
-        dur,
-        priority,
-        endState,
-        cpu,
-        id,
-        utid,
-      };
-
-      selected.threadStateId = await getThreadStateForSchedSlice(
-        this.args.engine,
-        id,
-      );
-
-      Object.assign(selected, await this.computeThreadDetails(utid));
-
-      this.schedulingDetails(ts, utid)
-        .then((wakeResult) => {
-          Object.assign(selected, wakeResult);
-        })
-        .finally(() => {
-          publishSliceDetails(selected);
-        });
-    }
-  }
-
-  async schedulingDetails(ts: time, utid: number) {
-    // Find the ts of the first wakeup before the current slice.
-    const wakeResult = await this.args.engine.query(`
-      select ts, waker_utid as wakerUtid
-      from thread_state
-      where utid = ${utid} and ts < ${ts} and state = 'R'
-      order by ts desc
-      limit 1
-    `);
-    if (wakeResult.numRows() === 0) {
-      return undefined;
-    }
-
-    const wakeFirstRow = wakeResult.firstRow({ts: LONG, wakerUtid: NUM_NULL});
-    const wakeupTs = wakeFirstRow.ts;
-    const wakerUtid = wakeFirstRow.wakerUtid;
-    if (wakerUtid === null) {
-      return undefined;
-    }
-
-    // Find the previous sched slice for the current utid.
-    const prevSchedResult = await this.args.engine.query(`
-      select ts
-      from sched
-      where utid = ${utid} and ts < ${ts}
-      order by ts desc
-      limit 1
-    `);
-
-    // If this is the first sched slice for this utid or if the wakeup found
-    // was after the previous slice then we know the wakeup was for this slice.
-    if (
-      prevSchedResult.numRows() !== 0 &&
-      wakeupTs < prevSchedResult.firstRow({ts: LONG}).ts
-    ) {
-      return undefined;
-    }
-
-    // Find the sched slice with the utid of the waker running when the
-    // sched wakeup occurred. This is the waker.
-    const wakerResult = await this.args.engine.query(`
-      select cpu
-      from sched
-      where
-        utid = ${wakerUtid} and
-        ts < ${wakeupTs} and
-        ts + dur >= ${wakeupTs};
-    `);
-    if (wakerResult.numRows() === 0) {
-      return undefined;
-    }
-
-    const wakerRow = wakerResult.firstRow({cpu: NUM});
-    return {wakeupTs, wakerUtid, wakerCpu: wakerRow.cpu};
-  }
-
-  async computeThreadDetails(
-    utid: number,
-  ): Promise<ThreadDetails & ProcessDetails> {
-    const res = await this.args.engine.query(`
-      SELECT tid, name, upid
-      FROM thread
-      WHERE utid = ${utid};
-    `);
-    const threadInfo = res.firstRow({tid: NUM, name: STR_NULL, upid: NUM_NULL});
-    const threadDetails: ThreadDetails = {
-      tid: threadInfo.tid,
-      threadName: threadInfo.name ?? undefined,
-    };
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    if (threadInfo.upid) {
-      return Object.assign(
-        {},
-        threadDetails,
-        await this.computeProcessDetails(threadInfo.upid),
-      );
-    }
-    return threadDetails;
-  }
-
-  async computeProcessDetails(upid: number) {
-    const res = await this.args.engine.query(`
-      include perfetto module android.process_metadata;
-      select
-        p.pid,
-        p.name,
-        p.uid,
-        m.package_name as packageName,
-        m.version_code as versionCode
-      from process p
-      left join android_process_metadata m using (upid)
-      where p.upid = ${upid};
-    `);
-    const row = res.firstRow({
-      pid: NUM,
-      uid: NUM_NULL,
-      packageName: STR_NULL,
-      versionCode: NUM_NULL,
-    });
-    return {
-      pid: row.pid,
-      uid: fromNumNull(row.uid),
-      packageName: row.packageName ?? undefined,
-      versionCode: fromNumNull(row.versionCode),
-    };
-  }
-}
-
-// Get the corresponding thread state slice id for a given sched slice
-async function getThreadStateForSchedSlice(
-  engine: Engine,
-  id: number,
-): Promise<Optional<number>> {
-  const sqlQuery = `
-    SELECT
-      thread_state.id as threadStateId
-    FROM sched
-    JOIN thread_state USING(ts, utid, cpu)
-    WHERE sched.id = ${id}
-  `;
-  const threadStateResult = await engine.query(sqlQuery);
-  if (threadStateResult.numRows() === 1) {
-    const row = threadStateResult.firstRow({
-      threadStateId: NUM,
-    });
-    return row.threadStateId;
-  } else {
-    return undefined;
-  }
-}
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
deleted file mode 100644
index d66f747..0000000
--- a/ui/src/controller/trace_controller.ts
+++ /dev/null
@@ -1,1333 +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 {assertExists, assertTrue} from '../base/logging';
-import {Duration, time, Time, TimeSpan} from '../base/time';
-import {Actions, DeferredAction} from '../common/actions';
-import {cacheTrace} from '../common/cache_manager';
-import {
-  getEnabledMetatracingCategories,
-  isMetatracingEnabled,
-} from '../common/metatracing';
-import {pluginManager} from '../common/plugins';
-import {
-  EngineConfig,
-  EngineMode,
-  PendingDeeplinkState,
-  ProfileType,
-} from '../common/state';
-import {featureFlags, Flag} from '../core/feature_flags';
-import {globals, QuantizedLoad, ThreadDesc} from '../frontend/globals';
-import {
-  clearOverviewData,
-  publishHasFtrace,
-  publishMetricError,
-  publishOverviewData,
-  publishThreads,
-} from '../frontend/publish';
-import {addQueryResultsTab} from '../frontend/query_result_tab';
-import {Router} from '../frontend/router';
-import {Engine, EngineBase} from '../trace_processor/engine';
-import {HttpRpcEngine} from '../trace_processor/http_rpc_engine';
-import {
-  LONG,
-  LONG_NULL,
-  NUM,
-  NUM_NULL,
-  QueryError,
-  STR,
-  STR_NULL,
-} from '../trace_processor/query_result';
-import {
-  resetEngineWorker,
-  WasmEngineProxy,
-} from '../trace_processor/wasm_engine_proxy';
-import {showModal} from '../widgets/modal';
-
-import {CounterAggregationController} from './aggregation/counter_aggregation_controller';
-import {CpuAggregationController} from './aggregation/cpu_aggregation_controller';
-import {CpuByProcessAggregationController} from './aggregation/cpu_by_process_aggregation_controller';
-import {FrameAggregationController} from './aggregation/frame_aggregation_controller';
-import {SliceAggregationController} from './aggregation/slice_aggregation_controller';
-import {WattsonEstimateAggregationController} from './aggregation/wattson/estimate_aggregation_controller';
-import {WattsonThreadAggregationController} from './aggregation/wattson/thread_aggregation_controller';
-import {WattsonProcessAggregationController} from './aggregation/wattson/process_aggregation_controller';
-import {WattsonPackageAggregationController} from './aggregation/wattson/package_aggregation_controller';
-import {ThreadAggregationController} from './aggregation/thread_aggregation_controller';
-import {Child, Children, Controller} from './controller';
-import {
-  CpuProfileController,
-  CpuProfileControllerArgs,
-} from './cpu_profile_controller';
-import {
-  FlowEventsController,
-  FlowEventsControllerArgs,
-} from './flow_events_controller';
-import {LoadingManager} from './loading_manager';
-import {
-  PIVOT_TABLE_REDUX_FLAG,
-  PivotTableController,
-} from './pivot_table_controller';
-import {SearchController} from './search_controller';
-import {
-  SelectionController,
-  SelectionControllerArgs,
-} from './selection_controller';
-import {TraceErrorController} from './trace_error_controller';
-import {
-  TraceBufferStream,
-  TraceFileStream,
-  TraceHttpStream,
-  TraceStream,
-} from '../core/trace_stream';
-import {decideTracks} from './track_decider';
-import {profileType} from '../frontend/legacy_flamegraph_panel';
-import {LegacyFlamegraphCache} from '../core/legacy_flamegraph_cache';
-import {
-  deserializeAppStatePhase1,
-  deserializeAppStatePhase2,
-} from '../common/state_serialization';
-import {TraceContext} from '../frontend/trace_context';
-
-type States = 'init' | 'loading_trace' | 'ready';
-
-const METRICS = [
-  'android_ion',
-  'android_lmk',
-  'android_surfaceflinger',
-  'android_batt',
-  'android_other_traces',
-  'chrome_dropped_frames',
-  // TODO(289365196): Reenable:
-  // 'chrome_long_latency',
-  'android_trusty_workqueues',
-];
-const FLAGGED_METRICS: Array<[Flag, string]> = METRICS.map((m) => {
-  const id = `forceMetric${m}`;
-  let name = m.split('_').join(' ');
-  name = name[0].toUpperCase() + name.slice(1);
-  name = 'Metric: ' + name;
-  const flag = featureFlags.register({
-    id,
-    name,
-    description: `Overrides running the '${m}' metric at import time.`,
-    defaultValue: true,
-  });
-  return [flag, m];
-});
-
-const ENABLE_CHROME_RELIABLE_RANGE_ZOOM_FLAG = featureFlags.register({
-  id: 'enableChromeReliableRangeZoom',
-  name: 'Enable Chrome reliable range zoom',
-  description: 'Automatically zoom into the reliable range for Chrome traces',
-  defaultValue: false,
-});
-
-const ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG = featureFlags.register({
-  id: 'enableChromeReliableRangeAnnotation',
-  name: 'Enable Chrome reliable range annotation',
-  description: 'Automatically adds an annotation for the reliable range start',
-  defaultValue: false,
-});
-
-// The following flags control TraceProcessor Config.
-const CROP_TRACK_EVENTS_FLAG = featureFlags.register({
-  id: 'cropTrackEvents',
-  name: 'Crop track events',
-  description: 'Ignores track events outside of the range of interest',
-  defaultValue: false,
-});
-const INGEST_FTRACE_IN_RAW_TABLE_FLAG = featureFlags.register({
-  id: 'ingestFtraceInRawTable',
-  name: 'Ingest ftrace in raw table',
-  description: 'Enables ingestion of typed ftrace events into the raw table',
-  defaultValue: true,
-});
-const ANALYZE_TRACE_PROTO_CONTENT_FLAG = featureFlags.register({
-  id: 'analyzeTraceProtoContent',
-  name: 'Analyze trace proto content',
-  description:
-    'Enables trace proto content analysis (experimental_proto_content table)',
-  defaultValue: false,
-});
-const FTRACE_DROP_UNTIL_FLAG = featureFlags.register({
-  id: 'ftraceDropUntilAllCpusValid',
-  name: 'Crop ftrace events',
-  description:
-    'Drop ftrace events until all per-cpu data streams are known to be valid',
-  defaultValue: true,
-});
-
-// A local storage key where the indication that JSON warning has been shown is
-// stored.
-const SHOWN_JSON_WARNING_KEY = 'shownJsonWarning';
-
-function showJsonWarning() {
-  showModal({
-    title: 'Warning',
-    content: m(
-      'div',
-      m(
-        'span',
-        'Perfetto UI features are limited for JSON traces. ',
-        'We recommend recording ',
-        m(
-          'a',
-          {href: 'https://perfetto.dev/docs/quickstart/chrome-tracing'},
-          'proto-format traces',
-        ),
-        ' from Chrome.',
-      ),
-      m('br'),
-    ),
-    buttons: [],
-  });
-}
-
-// TODO(stevegolton): Move this into some global "SQL extensions" file and
-// ensure it's only run once.
-async function defineMaxLayoutDepthSqlFunction(engine: Engine): Promise<void> {
-  await engine.query(`
-    create perfetto function __max_layout_depth(track_count INT, track_ids STRING)
-    returns INT AS
-    select iif(
-      $track_count = 1,
-      (
-        select max_depth
-        from _slice_track_summary
-        where id = cast($track_ids AS int)
-      ),
-      (
-        select max(layout_depth)
-        from experimental_slice_layout($track_ids)
-      )
-    );
-  `);
-}
-
-// TraceController handles handshakes with the frontend for everything that
-// concerns a single trace. It owns the WASM trace processor engine, handles
-// tracks data and SQL queries. There is one TraceController instance for each
-// trace opened in the UI (for now only one trace is supported).
-export class TraceController extends Controller<States> {
-  private readonly engineId: string;
-  private engine?: EngineBase;
-
-  constructor(engineId: string) {
-    super('init');
-    this.engineId = engineId;
-  }
-
-  run() {
-    const engineCfg = assertExists(globals.state.engine);
-    switch (this.state) {
-      case 'init':
-        this.loadTrace()
-          .then((mode) => {
-            globals.dispatch(
-              Actions.setEngineReady({
-                engineId: this.engineId,
-                ready: true,
-                mode,
-              }),
-            );
-          })
-          .catch((err) => {
-            this.updateStatus(`${err}`);
-            throw err;
-          });
-        this.updateStatus('Opening trace');
-        this.setState('loading_trace');
-        break;
-
-      case 'loading_trace':
-        // Stay in this state until loadTrace() returns and marks the engine as
-        // ready.
-        if (this.engine === undefined || !engineCfg.ready) return;
-        this.setState('ready');
-        break;
-
-      case 'ready':
-        // At this point we are ready to serve queries and handle tracks.
-        const engine = assertExists(this.engine);
-        const childControllers: Children = [];
-
-        const selectionArgs: SelectionControllerArgs = {engine};
-        childControllers.push(
-          Child('selection', SelectionController, selectionArgs),
-        );
-
-        const flowEventsArgs: FlowEventsControllerArgs = {engine};
-        childControllers.push(
-          Child('flowEvents', FlowEventsController, flowEventsArgs),
-        );
-
-        const cpuProfileArgs: CpuProfileControllerArgs = {engine};
-        childControllers.push(
-          Child('cpuProfile', CpuProfileController, cpuProfileArgs),
-        );
-
-        childControllers.push(
-          Child('cpu_aggregation', CpuAggregationController, {
-            engine,
-            kind: 'cpu_aggregation',
-          }),
-        );
-        childControllers.push(
-          Child('thread_aggregation', ThreadAggregationController, {
-            engine,
-            kind: 'thread_state_aggregation',
-          }),
-        );
-        childControllers.push(
-          Child('cpu_process_aggregation', CpuByProcessAggregationController, {
-            engine,
-            kind: 'cpu_by_process_aggregation',
-          }),
-        );
-        if (!PIVOT_TABLE_REDUX_FLAG.get()) {
-          // Pivot table is supposed to handle the use cases the slice
-          // aggregation panel is used right now. When a flag to use pivot
-          // tables is enabled, do not add slice aggregation controller.
-          childControllers.push(
-            Child('slice_aggregation', SliceAggregationController, {
-              engine,
-              kind: 'slice_aggregation',
-            }),
-          );
-        }
-        childControllers.push(
-          Child('counter_aggregation', CounterAggregationController, {
-            engine,
-            kind: 'counter_aggregation',
-          }),
-        );
-        if (pluginManager.isActive('org.kernel.Wattson')) {
-          childControllers.push(
-            Child(
-              'wattson_estimate_aggregation',
-              WattsonEstimateAggregationController,
-              {
-                engine,
-                kind: 'wattson_estimate_aggregation',
-              },
-            ),
-          );
-          childControllers.push(
-            Child(
-              'wattson_thread_aggregation',
-              WattsonThreadAggregationController,
-              {
-                engine,
-                kind: 'wattson_thread_aggregation',
-              },
-            ),
-          );
-          childControllers.push(
-            Child(
-              'wattson_process_aggregation',
-              WattsonProcessAggregationController,
-              {
-                engine,
-                kind: 'wattson_process_aggregation',
-              },
-            ),
-          );
-          childControllers.push(
-            Child(
-              'wattson_package_aggregation',
-              WattsonPackageAggregationController,
-              {
-                engine,
-                kind: 'wattson_package_aggregation',
-              },
-            ),
-          );
-        }
-        childControllers.push(
-          Child('frame_aggregation', FrameAggregationController, {
-            engine,
-            kind: 'frame_aggregation',
-          }),
-        );
-        childControllers.push(
-          Child('search', SearchController, {
-            engine,
-            app: globals,
-          }),
-        );
-        childControllers.push(
-          Child('pivot_table', PivotTableController, {engine}),
-        );
-
-        childControllers.push(
-          Child('traceError', TraceErrorController, {engine}),
-        );
-
-        return childControllers;
-
-      default:
-        throw new Error(`unknown state ${this.state}`);
-    }
-    return;
-  }
-
-  onDestroy() {
-    pluginManager.onTraceClose();
-    globals.engines.delete(this.engineId);
-
-    // Invalidate the flamegraph cache.
-    // TODO(stevegolton): migrate this to the new system when it's ready.
-    globals.areaFlamegraphCache = new LegacyFlamegraphCache('area');
-  }
-
-  private async loadTrace(): Promise<EngineMode> {
-    this.updateStatus('Creating trace processor');
-    // Check if there is any instance of the trace_processor_shell running in
-    // HTTP RPC mode (i.e. trace_processor_shell -D).
-    let engineMode: EngineMode;
-    let useRpc = false;
-    if (globals.state.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE') {
-      useRpc = (await HttpRpcEngine.checkConnection()).connected;
-    }
-    let engine;
-    if (useRpc) {
-      console.log('Opening trace using native accelerator over HTTP+RPC');
-      engineMode = 'HTTP_RPC';
-      engine = new HttpRpcEngine(this.engineId, LoadingManager.getInstance);
-      engine.errorHandler = (err) => {
-        globals.dispatch(
-          Actions.setEngineFailed({mode: 'HTTP_RPC', failure: `${err}`}),
-        );
-        throw err;
-      };
-    } else {
-      console.log('Opening trace using built-in WASM engine');
-      engineMode = 'WASM';
-      const enginePort = resetEngineWorker();
-      engine = new WasmEngineProxy(
-        this.engineId,
-        enginePort,
-        LoadingManager.getInstance,
-      );
-      engine.resetTraceProcessor({
-        cropTrackEvents: CROP_TRACK_EVENTS_FLAG.get(),
-        ingestFtraceInRawTable: INGEST_FTRACE_IN_RAW_TABLE_FLAG.get(),
-        analyzeTraceProtoContent: ANALYZE_TRACE_PROTO_CONTENT_FLAG.get(),
-        ftraceDropUntilAllCpusValid: FTRACE_DROP_UNTIL_FLAG.get(),
-      });
-    }
-    this.engine = engine;
-
-    if (isMetatracingEnabled()) {
-      this.engine.enableMetatrace(
-        assertExists(getEnabledMetatracingCategories()),
-      );
-    }
-
-    globals.engines.set(this.engineId, engine);
-    globals.dispatch(
-      Actions.setEngineReady({
-        engineId: this.engineId,
-        ready: false,
-        mode: engineMode,
-      }),
-    );
-    const engineCfg = assertExists(globals.state.engine);
-    assertTrue(engineCfg.id === this.engineId);
-    let traceStream: TraceStream | undefined;
-    if (engineCfg.source.type === 'FILE') {
-      traceStream = new TraceFileStream(engineCfg.source.file);
-    } else if (engineCfg.source.type === 'ARRAY_BUFFER') {
-      traceStream = new TraceBufferStream(engineCfg.source.buffer);
-    } else if (engineCfg.source.type === 'URL') {
-      traceStream = new TraceHttpStream(engineCfg.source.url);
-    } else if (engineCfg.source.type === 'HTTP_RPC') {
-      traceStream = undefined;
-    } else {
-      throw new Error(`Unknown source: ${JSON.stringify(engineCfg.source)}`);
-    }
-
-    // |traceStream| can be undefined in the case when we are using the external
-    // HTTP+RPC endpoint and the trace processor instance has already loaded
-    // a trace (because it was passed as a cmdline argument to
-    // trace_processor_shell). In this case we don't want the UI to load any
-    // file/stream and we just want to jump to the loading phase.
-    if (traceStream !== undefined) {
-      const tStart = performance.now();
-      for (;;) {
-        const res = await traceStream.readChunk();
-        await this.engine.parse(res.data);
-        const elapsed = (performance.now() - tStart) / 1000;
-        let status = 'Loading trace ';
-        if (res.bytesTotal > 0) {
-          const progress = Math.round((res.bytesRead / res.bytesTotal) * 100);
-          status += `${progress}%`;
-        } else {
-          status += `${Math.round(res.bytesRead / 1e6)} MB`;
-        }
-        status += ` - ${Math.ceil(res.bytesRead / elapsed / 1e6)} MB/s`;
-        this.updateStatus(status);
-        if (res.eof) break;
-      }
-      await this.engine.notifyEof();
-    } else {
-      assertTrue(this.engine instanceof HttpRpcEngine);
-      await this.engine.restoreInitialTables();
-    }
-
-    // traceUuid will be '' if the trace is not cacheable (URL or RPC).
-    const traceUuid = await this.cacheCurrentTrace();
-
-    const traceDetails = await getTraceTimeDetails(this.engine, engineCfg);
-    if (traceDetails.traceTitle) {
-      document.title = `${traceDetails.traceTitle} - Perfetto UI`;
-    }
-    await globals.onTraceLoad(this.engine, traceDetails);
-
-    const shownJsonWarning =
-      window.localStorage.getItem(SHOWN_JSON_WARNING_KEY) !== null;
-
-    // Show warning if the trace is in JSON format.
-    const query = `select str_value from metadata where name = 'trace_type'`;
-    const result = await assertExists(this.engine).query(query);
-    const traceType = result.firstRow({str_value: STR}).str_value;
-    const isJsonTrace = traceType == 'json';
-    if (!shownJsonWarning) {
-      // When in embedded mode, the host app will control which trace format
-      // it passes to Perfetto, so we don't need to show this warning.
-      if (isJsonTrace && !globals.embeddedMode) {
-        showJsonWarning();
-        // Save that the warning has been shown. Value is irrelevant since only
-        // the presence of key is going to be checked.
-        window.localStorage.setItem(SHOWN_JSON_WARNING_KEY, 'true');
-      }
-    }
-
-    const emptyOmniboxState = {
-      omnibox: '',
-      mode: globals.state.omniboxState.mode || 'SEARCH',
-    };
-
-    const actions: DeferredAction[] = [
-      Actions.setOmnibox(emptyOmniboxState),
-      Actions.setTraceUuid({traceUuid}),
-    ];
-
-    const visibleTimeSpan = await computeVisibleTime(
-      traceDetails.start,
-      traceDetails.end,
-      isJsonTrace,
-      this.engine,
-    );
-
-    globals.timeline.updateVisibleTime(visibleTimeSpan);
-
-    globals.dispatchMultiple(actions);
-    Router.navigate(`#!/viewer?local_cache_key=${traceUuid}`);
-
-    // Make sure the helper views are available before we start adding tracks.
-    await this.initialiseHelperViews();
-    await this.includeSummaryTables();
-
-    await defineMaxLayoutDepthSqlFunction(engine);
-
-    if (globals.restoreAppStateAfterTraceLoad) {
-      deserializeAppStatePhase1(globals.restoreAppStateAfterTraceLoad);
-    }
-
-    await pluginManager.onTraceLoad(engine, (id) => {
-      this.updateStatus(`Running plugin: ${id}`);
-    });
-
-    {
-      // When we reload from a permalink don't create extra tracks:
-      const {pinnedTracks, tracks} = globals.state;
-      if (!pinnedTracks.length && !Object.keys(tracks).length) {
-        await this.listTracks();
-      }
-    }
-
-    this.decideTabs();
-
-    await this.listThreads();
-    await this.loadTimelineOverview(
-      new TimeSpan(traceDetails.start, traceDetails.end),
-    );
-
-    {
-      // Check if we have any ftrace events at all
-      const query = `
-        select
-          *
-        from ftrace_event
-        limit 1`;
-
-      const res = await engine.query(query);
-      publishHasFtrace(res.numRows() > 0);
-    }
-
-    globals.dispatch(Actions.sortThreadTracks({}));
-    globals.dispatch(Actions.maybeExpandOnlyTrackGroup({}));
-
-    await this.selectFirstHeapProfile();
-    await this.selectPerfSample(traceDetails);
-
-    const pendingDeeplink = globals.state.pendingDeeplink;
-    if (pendingDeeplink !== undefined) {
-      globals.dispatch(Actions.clearPendingDeeplink({}));
-      await this.selectPendingDeeplink(pendingDeeplink);
-      if (
-        pendingDeeplink.visStart !== undefined &&
-        pendingDeeplink.visEnd !== undefined
-      ) {
-        this.zoomPendingDeeplink(
-          pendingDeeplink.visStart,
-          pendingDeeplink.visEnd,
-        );
-      }
-      if (pendingDeeplink.query !== undefined) {
-        addQueryResultsTab({
-          query: pendingDeeplink.query,
-          title: 'Deeplink Query',
-        });
-      }
-    }
-
-    globals.dispatch(Actions.maybeExpandOnlyTrackGroup({}));
-
-    // Trace Processor doesn't support the reliable range feature for JSON
-    // traces.
-    if (!isJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG.get()) {
-      const reliableRangeStart = await computeTraceReliableRangeStart(engine);
-      if (reliableRangeStart > 0) {
-        globals.dispatch(
-          Actions.addNote({
-            timestamp: reliableRangeStart,
-            color: '#ff0000',
-            text: 'Reliable Range Start',
-          }),
-        );
-      }
-    }
-
-    if (globals.restoreAppStateAfterTraceLoad) {
-      // Wait that plugins have completed their actions and then proceed with
-      // the final phase of app state restore.
-      // TODO(primiano): this can probably be removed once we refactor tracks
-      // to be URI based and can deal with non-existing URIs.
-      deserializeAppStatePhase2(globals.restoreAppStateAfterTraceLoad);
-      globals.restoreAppStateAfterTraceLoad = undefined;
-    }
-
-    await pluginManager.onTraceReady();
-
-    return engineMode;
-  }
-
-  private async selectPerfSample(traceTime: {start: time; end: time}) {
-    const query = `select upid
-        from perf_sample
-        join thread using (utid)
-        where callsite_id is not null
-        order by ts desc limit 1`;
-    const profile = await assertExists(this.engine).query(query);
-    if (profile.numRows() !== 1) return;
-    const row = profile.firstRow({upid: NUM});
-    const upid = row.upid;
-    const leftTs = traceTime.start;
-    const rightTs = traceTime.end;
-    globals.dispatch(
-      Actions.selectPerfSamples({
-        id: 0,
-        upid,
-        leftTs,
-        rightTs,
-        type: ProfileType.PERF_SAMPLE,
-      }),
-    );
-  }
-
-  private async selectFirstHeapProfile() {
-    const query = `select * from (
-      select
-        min(ts) AS ts,
-        'heap_profile:' || group_concat(distinct heap_name) AS type,
-        upid
-      from heap_profile_allocation
-      group by upid
-      union
-      select distinct graph_sample_ts as ts, 'graph' as type, upid
-      from heap_graph_object)
-      order by ts limit 1`;
-    const profile = await assertExists(this.engine).query(query);
-    if (profile.numRows() !== 1) return;
-    const row = profile.firstRow({ts: LONG, type: STR, upid: NUM});
-    const ts = Time.fromRaw(row.ts);
-    let profType = row.type;
-    if (profType == 'heap_profile:libc.malloc,com.android.art') {
-      profType = 'heap_profile:com.android.art,libc.malloc';
-    }
-    const type = profileType(profType);
-    const upid = row.upid;
-    globals.dispatch(Actions.selectHeapProfile({id: 0, upid, ts, type}));
-  }
-
-  private async selectPendingDeeplink(link: PendingDeeplinkState) {
-    const conditions = [];
-    const {ts, dur} = link;
-
-    if (ts !== undefined) {
-      conditions.push(`ts = ${ts}`);
-    }
-    if (dur !== undefined) {
-      conditions.push(`dur = ${dur}`);
-    }
-
-    if (conditions.length === 0) {
-      return;
-    }
-
-    const query = `
-      select
-        id,
-        track_id as traceProcessorTrackId,
-        type
-      from slice
-      where ${conditions.join(' and ')}
-    ;`;
-
-    const result = await assertExists(this.engine).query(query);
-    if (result.numRows() > 0) {
-      const row = result.firstRow({
-        id: NUM,
-        traceProcessorTrackId: NUM,
-        type: STR,
-      });
-
-      const id = row.traceProcessorTrackId;
-      const trackKey = globals.trackManager.trackKeyByTrackId.get(id);
-      if (trackKey === undefined) {
-        return;
-      }
-      globals.setLegacySelection(
-        {
-          kind: 'SLICE',
-          id: row.id,
-          trackKey,
-          table: 'slice',
-        },
-        {
-          clearSearch: true,
-          pendingScrollId: row.id,
-          switchToCurrentSelectionTab: false,
-        },
-      );
-    }
-  }
-
-  private async listTracks() {
-    this.updateStatus('Loading tracks');
-    const engine = assertExists(this.engine);
-    const actions = await decideTracks(engine);
-    globals.dispatchMultiple(actions);
-  }
-
-  // Show the list of default tabs, but don't make them active!
-  private decideTabs() {
-    for (const tabUri of globals.tabManager.defaultTabs) {
-      globals.dispatch(Actions.showTab({uri: tabUri}));
-    }
-  }
-
-  private async listThreads() {
-    this.updateStatus('Reading thread list');
-    const query = `select
-        utid,
-        tid,
-        pid,
-        ifnull(thread.name, '') as threadName,
-        ifnull(
-          case when length(process.name) > 0 then process.name else null end,
-          thread.name) as procName,
-        process.cmdline as cmdline
-        from (select * from thread order by upid) as thread
-        left join (select * from process order by upid) as process
-        using(upid)`;
-    const result = await assertExists(this.engine).query(query);
-    const threads: ThreadDesc[] = [];
-    const it = result.iter({
-      utid: NUM,
-      tid: NUM,
-      pid: NUM_NULL,
-      threadName: STR,
-      procName: STR_NULL,
-      cmdline: STR_NULL,
-    });
-    for (; it.valid(); it.next()) {
-      const utid = it.utid;
-      const tid = it.tid;
-      const pid = it.pid === null ? undefined : it.pid;
-      const threadName = it.threadName;
-      const procName = it.procName === null ? undefined : it.procName;
-      const cmdline = it.cmdline === null ? undefined : it.cmdline;
-      threads.push({utid, tid, threadName, pid, procName, cmdline});
-    }
-    publishThreads(threads);
-  }
-
-  private async loadTimelineOverview(trace: TimeSpan) {
-    clearOverviewData();
-    const engine = assertExists<Engine>(this.engine);
-    const stepSize = Duration.max(1n, trace.duration / 100n);
-    const hasSchedSql = 'select ts from sched limit 1';
-    const hasSchedOverview = (await engine.query(hasSchedSql)).numRows() > 0;
-    if (hasSchedOverview) {
-      const stepPromises = [];
-      for (
-        let start = trace.start;
-        start < trace.end;
-        start = Time.add(start, stepSize)
-      ) {
-        const progress = start - trace.start;
-        const ratio = Number(progress) / Number(trace.duration);
-        this.updateStatus('Loading overview ' + `${Math.round(ratio * 100)}%`);
-        const end = Time.add(start, stepSize);
-        // The (async() => {})() queues all the 100 async promises in one batch.
-        // Without that, we would wait for each step to be rendered before
-        // kicking off the next one. That would interleave an animation frame
-        // between each step, slowing down significantly the overall process.
-        stepPromises.push(
-          (async () => {
-            const schedResult = await engine.query(
-              `select cast(sum(dur) as float)/${stepSize} as load, cpu from sched ` +
-                `where ts >= ${start} and ts < ${end} and utid != 0 ` +
-                'group by cpu order by cpu',
-            );
-            const schedData: {[key: string]: QuantizedLoad} = {};
-            const it = schedResult.iter({load: NUM, cpu: NUM});
-            for (; it.valid(); it.next()) {
-              const load = it.load;
-              const cpu = it.cpu;
-              schedData[cpu] = {start, end, load};
-            }
-            publishOverviewData(schedData);
-          })(),
-        );
-      } // for(start = ...)
-      await Promise.all(stepPromises);
-      return;
-    } // if (hasSchedOverview)
-
-    // Slices overview.
-    const sliceResult = await engine.query(`select
-           bucket,
-           upid,
-           ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load
-         from thread
-         inner join (
-           select
-             ifnull(cast((ts - ${trace.start})/${stepSize} as int), 0) as bucket,
-             sum(dur) as utid_sum,
-             utid
-           from slice
-           inner join thread_track on slice.track_id = thread_track.id
-           group by bucket, utid
-         ) using(utid)
-         where upid is not null
-         group by bucket, upid`);
-
-    const slicesData: {[key: string]: QuantizedLoad[]} = {};
-    const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM});
-    for (; it.valid(); it.next()) {
-      const bucket = it.bucket;
-      const upid = it.upid;
-      const load = it.load;
-
-      const start = Time.add(trace.start, stepSize * bucket);
-      const end = Time.add(start, stepSize);
-
-      const upidStr = upid.toString();
-      let loadArray = slicesData[upidStr];
-      if (loadArray === undefined) {
-        loadArray = slicesData[upidStr] = [];
-      }
-      loadArray.push({start, end, load});
-    }
-    publishOverviewData(slicesData);
-  }
-
-  private async cacheCurrentTrace(): Promise<string> {
-    const engine = assertExists(this.engine);
-    const result = await engine.query(`select str_value as uuid from metadata
-                  where name = 'trace_uuid'`);
-    if (result.numRows() === 0) {
-      // One of the cases covered is an empty trace.
-      return '';
-    }
-    const traceUuid = result.firstRow({uuid: STR}).uuid;
-    const engineConfig = assertExists(globals.state.engine);
-    assertTrue(engineConfig.id === this.engineId);
-    if (!(await cacheTrace(engineConfig.source, traceUuid))) {
-      // If the trace is not cacheable (cacheable means it has been opened from
-      // URL or RPC) only append '?local_cache_key' to the URL, without the
-      // local_cache_key value. Doing otherwise would cause an error if the tab
-      // is discarded or the user hits the reload button because the trace is
-      // not in the cache.
-      return '';
-    }
-    return traceUuid;
-  }
-
-  async initialiseHelperViews() {
-    const engine = assertExists(this.engine);
-
-    this.updateStatus('Creating annotation counter track table');
-    // Create the helper tables for all the annotations related data.
-    // NULL in min/max means "figure it out per track in the usual way".
-    await engine.query(`
-      CREATE TABLE annotation_counter_track(
-        id INTEGER PRIMARY KEY,
-        name STRING,
-        __metric_name STRING,
-        upid INTEGER,
-        min_value DOUBLE,
-        max_value DOUBLE
-      );
-    `);
-    this.updateStatus('Creating annotation slice track table');
-    await engine.query(`
-      CREATE TABLE annotation_slice_track(
-        id INTEGER PRIMARY KEY,
-        name STRING,
-        __metric_name STRING,
-        upid INTEGER,
-        group_name STRING
-      );
-    `);
-
-    this.updateStatus('Creating annotation counter table');
-    await engine.query(`
-      CREATE TABLE annotation_counter(
-        id BIGINT,
-        track_id INT,
-        ts BIGINT,
-        value DOUBLE,
-        PRIMARY KEY (track_id, ts)
-      ) WITHOUT ROWID;
-    `);
-    this.updateStatus('Creating annotation slice table');
-    await engine.query(`
-      CREATE TABLE annotation_slice(
-        id INTEGER PRIMARY KEY,
-        track_id INT,
-        ts BIGINT,
-        dur BIGINT,
-        thread_dur BIGINT,
-        depth INT,
-        cat STRING,
-        name STRING,
-        UNIQUE(track_id, ts)
-      );
-    `);
-
-    const availableMetrics = [];
-    const metricsResult = await engine.query('select name from trace_metrics');
-    for (const it = metricsResult.iter({name: STR}); it.valid(); it.next()) {
-      availableMetrics.push(it.name);
-    }
-
-    const availableMetricsSet = new Set<string>(availableMetrics);
-    for (const [flag, metric] of FLAGGED_METRICS) {
-      if (!flag.get() || !availableMetricsSet.has(metric)) {
-        continue;
-      }
-
-      this.updateStatus(`Computing ${metric} metric`);
-      try {
-        // We don't care about the actual result of metric here as we are just
-        // interested in the annotation tracks.
-        await engine.computeMetric([metric], 'proto');
-      } catch (e) {
-        if (e instanceof QueryError) {
-          publishMetricError('MetricError: ' + e.message);
-          continue;
-        } else {
-          throw e;
-        }
-      }
-
-      this.updateStatus(`Inserting data for ${metric} metric`);
-      try {
-        const result = await engine.query(`pragma table_info(${metric}_event)`);
-        let hasSliceName = false;
-        let hasDur = false;
-        let hasUpid = false;
-        let hasValue = false;
-        let hasGroupName = false;
-        const it = result.iter({name: STR});
-        for (; it.valid(); it.next()) {
-          const name = it.name;
-          hasSliceName = hasSliceName || name === 'slice_name';
-          hasDur = hasDur || name === 'dur';
-          hasUpid = hasUpid || name === 'upid';
-          hasValue = hasValue || name === 'value';
-          hasGroupName = hasGroupName || name === 'group_name';
-        }
-
-        const upidColumnSelect = hasUpid ? 'upid' : '0 AS upid';
-        const upidColumnWhere = hasUpid ? 'upid' : '0';
-        const groupNameColumn = hasGroupName
-          ? 'group_name'
-          : 'NULL AS group_name';
-        if (hasSliceName && hasDur) {
-          await engine.query(`
-            INSERT INTO annotation_slice_track(
-              name, __metric_name, upid, group_name)
-            SELECT DISTINCT
-              track_name,
-              '${metric}' as metric_name,
-              ${upidColumnSelect},
-              ${groupNameColumn}
-            FROM ${metric}_event
-            WHERE track_type = 'slice'
-          `);
-          await engine.query(`
-            INSERT INTO annotation_slice(
-              track_id, ts, dur, thread_dur, depth, cat, name
-            )
-            SELECT
-              t.id AS track_id,
-              ts,
-              dur,
-              NULL as thread_dur,
-              0 AS depth,
-              a.track_name as cat,
-              slice_name AS name
-            FROM ${metric}_event a
-            JOIN annotation_slice_track t
-            ON a.track_name = t.name AND t.__metric_name = '${metric}'
-            ORDER BY t.id, ts
-          `);
-        }
-
-        if (hasValue) {
-          const minMax = await engine.query(`
-            SELECT
-              IFNULL(MIN(value), 0) as minValue,
-              IFNULL(MAX(value), 0) as maxValue
-            FROM ${metric}_event
-            WHERE ${upidColumnWhere} != 0`);
-          const row = minMax.firstRow({minValue: NUM, maxValue: NUM});
-          await engine.query(`
-            INSERT INTO annotation_counter_track(
-              name, __metric_name, min_value, max_value, upid)
-            SELECT DISTINCT
-              track_name,
-              '${metric}' as metric_name,
-              CASE ${upidColumnWhere} WHEN 0 THEN NULL ELSE ${row.minValue} END,
-              CASE ${upidColumnWhere} WHEN 0 THEN NULL ELSE ${row.maxValue} END,
-              ${upidColumnSelect}
-            FROM ${metric}_event
-            WHERE track_type = 'counter'
-          `);
-          await engine.query(`
-            INSERT INTO annotation_counter(id, track_id, ts, value)
-            SELECT
-              -1 as id,
-              t.id AS track_id,
-              ts,
-              value
-            FROM ${metric}_event a
-            JOIN annotation_counter_track t
-            ON a.track_name = t.name AND t.__metric_name = '${metric}'
-            ORDER BY t.id, ts
-          `);
-        }
-      } catch (e) {
-        if (e instanceof QueryError) {
-          publishMetricError('MetricError: ' + e.message);
-        } else {
-          throw e;
-        }
-      }
-    }
-  }
-
-  async includeSummaryTables() {
-    const engine = assertExists<Engine>(this.engine);
-
-    this.updateStatus('Creating slice summaries');
-    await engine.query(`include perfetto module viz.summary.slices;`);
-
-    this.updateStatus('Creating counter summaries');
-    await engine.query(`include perfetto module viz.summary.counters;`);
-
-    this.updateStatus('Creating thread summaries');
-    await engine.query(`include perfetto module viz.summary.threads;`);
-
-    this.updateStatus('Creating processes summaries');
-    await engine.query(`include perfetto module viz.summary.processes;`);
-
-    this.updateStatus('Creating track summaries');
-    await engine.query(`include perfetto module viz.summary.tracks;`);
-  }
-
-  private updateStatus(msg: string): void {
-    globals.dispatch(
-      Actions.updateStatus({
-        msg,
-        timestamp: Date.now() / 1000,
-      }),
-    );
-  }
-
-  private zoomPendingDeeplink(visStart: string, visEnd: string) {
-    const visualStart = Time.fromRaw(BigInt(visStart));
-    const visualEnd = Time.fromRaw(BigInt(visEnd));
-    const traceContext = globals.traceContext;
-
-    if (
-      !(
-        visualStart < visualEnd &&
-        traceContext.start <= visualStart &&
-        visualEnd <= traceContext.end
-      )
-    ) {
-      return;
-    }
-
-    globals.timeline.updateVisibleTime(new TimeSpan(visualStart, visualEnd));
-  }
-}
-
-async function computeFtraceBounds(engine: Engine): Promise<TimeSpan | null> {
-  const result = await engine.query(`
-    SELECT min(ts) as start, max(ts) as end FROM ftrace_event;
-  `);
-  const {start, end} = result.firstRow({start: LONG_NULL, end: LONG_NULL});
-  if (start !== null && end !== null) {
-    return new TimeSpan(Time.fromRaw(start), Time.fromRaw(end));
-  }
-  return null;
-}
-
-async function computeTraceReliableRangeStart(engine: Engine): Promise<time> {
-  const result =
-    await engine.query(`SELECT RUN_METRIC('chrome/chrome_reliable_range.sql');
-       SELECT start FROM chrome_reliable_range`);
-  const bounds = result.firstRow({start: LONG});
-  return Time.fromRaw(bounds.start);
-}
-
-async function computeVisibleTime(
-  traceStart: time,
-  traceEnd: time,
-  isJsonTrace: boolean,
-  engine: Engine,
-): Promise<TimeSpan> {
-  // initialise visible time to the trace time bounds
-  let visibleStart = traceStart;
-  let visibleEnd = traceEnd;
-
-  // compare start and end with metadata computed by the trace processor
-  const mdTime = await getTracingMetadataTimeBounds(engine);
-  // make sure the bounds hold
-  if (Time.max(visibleStart, mdTime.start) < Time.min(visibleEnd, mdTime.end)) {
-    visibleStart = Time.max(visibleStart, mdTime.start);
-    visibleEnd = Time.min(visibleEnd, mdTime.end);
-  }
-
-  // Trace Processor doesn't support the reliable range feature for JSON
-  // traces.
-  if (!isJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ZOOM_FLAG.get()) {
-    const reliableRangeStart = await computeTraceReliableRangeStart(engine);
-    visibleStart = Time.max(visibleStart, reliableRangeStart);
-  }
-
-  // Move start of visible window to the first ftrace event
-  const ftraceBounds = await computeFtraceBounds(engine);
-  if (ftraceBounds !== null) {
-    // Avoid moving start of visible window past its end!
-    visibleStart = Time.min(ftraceBounds.start, visibleEnd);
-  }
-  return new TimeSpan(visibleStart, visibleEnd);
-}
-
-async function getTraceTimeDetails(
-  engine: Engine,
-  engineCfg: EngineConfig,
-): Promise<TraceContext> {
-  const traceTime = await getTraceTimeBounds(engine);
-
-  // Find the first REALTIME or REALTIME_COARSE clock snapshot.
-  // Prioritize REALTIME over REALTIME_COARSE.
-  const query = `select
-          ts,
-          clock_value as clockValue,
-          clock_name as clockName
-        from clock_snapshot
-        where
-          snapshot_id = 0 AND
-          clock_name in ('REALTIME', 'REALTIME_COARSE')
-        `;
-  const result = await engine.query(query);
-  const it = result.iter({
-    ts: LONG,
-    clockValue: LONG,
-    clockName: STR,
-  });
-
-  let snapshot = {
-    clockName: '',
-    ts: Time.ZERO,
-    clockValue: Time.ZERO,
-  };
-
-  // Find the most suitable snapshot
-  for (let row = 0; it.valid(); it.next(), row++) {
-    if (it.clockName === 'REALTIME') {
-      snapshot = {
-        clockName: it.clockName,
-        ts: Time.fromRaw(it.ts),
-        clockValue: Time.fromRaw(it.clockValue),
-      };
-      break;
-    } else if (it.clockName === 'REALTIME_COARSE') {
-      if (snapshot.clockName !== 'REALTIME') {
-        snapshot = {
-          clockName: it.clockName,
-          ts: Time.fromRaw(it.ts),
-          clockValue: Time.fromRaw(it.clockValue),
-        };
-      }
-    }
-  }
-
-  // The max() is so the query returns NULL if the tz info doesn't exist.
-  const queryTz = `select max(int_value) as tzOffMin from metadata
-        where name = 'timezone_off_mins'`;
-  const resTz = await assertExists(engine).query(queryTz);
-  const tzOffMin = resTz.firstRow({tzOffMin: NUM_NULL}).tzOffMin ?? 0;
-
-  // This is the offset between the unix epoch and ts in the ts domain.
-  // I.e. the value of ts at the time of the unix epoch - usually some large
-  // negative value.
-  const realtimeOffset = Time.sub(snapshot.ts, snapshot.clockValue);
-
-  // Find the previous closest midnight from the trace start time.
-  const utcOffset = Time.getLatestMidnight(traceTime.start, realtimeOffset);
-
-  const traceTzOffset = Time.getLatestMidnight(
-    traceTime.start,
-    Time.sub(realtimeOffset, Time.fromSeconds(tzOffMin * 60)),
-  );
-
-  let traceTitle = '';
-  let traceUrl = '';
-  switch (engineCfg.source.type) {
-    case 'FILE':
-      // Split on both \ and / (because C:\Windows\paths\are\like\this).
-      traceTitle = engineCfg.source.file.name.split(/[/\\]/).pop()!;
-      const fileSizeMB = Math.ceil(engineCfg.source.file.size / 1e6);
-      traceTitle += ` (${fileSizeMB} MB)`;
-      break;
-    case 'URL':
-      traceUrl = engineCfg.source.url;
-      traceTitle = traceUrl.split('/').pop()!;
-      break;
-    case 'ARRAY_BUFFER':
-      traceTitle = engineCfg.source.title;
-      traceUrl = engineCfg.source.url ?? '';
-      const arrayBufferSizeMB = Math.ceil(
-        engineCfg.source.buffer.byteLength / 1e6,
-      );
-      traceTitle += ` (${arrayBufferSizeMB} MB)`;
-      break;
-    case 'HTTP_RPC':
-      traceTitle = `RPC @ ${HttpRpcEngine.hostAndPort}`;
-      break;
-    default:
-      break;
-  }
-
-  return {
-    ...traceTime,
-    traceTitle,
-    traceUrl,
-    realtimeOffset,
-    utcOffset,
-    traceTzOffset,
-    cpus: await getCpus(engine),
-    gpuCount: await getNumberOfGpus(engine),
-  };
-}
-
-async function getTraceTimeBounds(engine: Engine): Promise<TimeSpan> {
-  const result = await engine.query(
-    `select start_ts as startTs, end_ts as endTs from trace_bounds`,
-  );
-  const bounds = result.firstRow({
-    startTs: LONG,
-    endTs: LONG,
-  });
-  return new TimeSpan(Time.fromRaw(bounds.startTs), Time.fromRaw(bounds.endTs));
-}
-
-// TODO(hjd): When streaming must invalidate this somehow.
-async function getCpus(engine: Engine): Promise<number[]> {
-  const cpus = [];
-  const queryRes = await engine.query(
-    'select distinct(cpu) as cpu from sched order by cpu;',
-  );
-  for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) {
-    cpus.push(it.cpu);
-  }
-  return cpus;
-}
-
-async function getNumberOfGpus(engine: Engine): Promise<number> {
-  const result = await engine.query(`
-    select count(distinct(gpu_id)) as gpuCount
-    from gpu_counter_track
-    where name = 'gpufreq';
-  `);
-  return result.firstRow({gpuCount: NUM}).gpuCount;
-}
-
-async function getTracingMetadataTimeBounds(engine: Engine): Promise<TimeSpan> {
-  const queryRes = await engine.query(`select
-       name,
-       int_value as intValue
-       from metadata
-       where name = 'tracing_started_ns' or name = 'tracing_disabled_ns'
-       or name = 'all_data_source_started_ns'`);
-  let startBound = Time.MIN;
-  let endBound = Time.MAX;
-  const it = queryRes.iter({name: STR, intValue: LONG_NULL});
-  for (; it.valid(); it.next()) {
-    const columnName = it.name;
-    const timestamp = it.intValue;
-    if (timestamp === null) continue;
-    if (columnName === 'tracing_disabled_ns') {
-      endBound = Time.min(endBound, Time.fromRaw(timestamp));
-    } else {
-      startBound = Time.max(startBound, Time.fromRaw(timestamp));
-    }
-  }
-
-  return new TimeSpan(startBound, endBound);
-}
diff --git a/ui/src/controller/trace_error_controller.ts b/ui/src/controller/trace_error_controller.ts
deleted file mode 100644
index 28bd327..0000000
--- a/ui/src/controller/trace_error_controller.ts
+++ /dev/null
@@ -1,46 +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.
-
-import {publishTraceErrors} from '../frontend/publish';
-import {Engine} from '../trace_processor/engine';
-import {NUM} from '../trace_processor/query_result';
-
-import {Controller} from './controller';
-
-export interface TraceErrorControllerArgs {
-  engine: Engine;
-}
-
-export class TraceErrorController extends Controller<'main'> {
-  private hasRun = false;
-  constructor(private args: TraceErrorControllerArgs) {
-    super('main');
-  }
-
-  run() {
-    if (this.hasRun) {
-      return;
-    }
-    this.hasRun = true;
-    const engine = this.args.engine;
-    engine
-      .query(
-        `SELECT sum(value) as sumValue FROM stats WHERE severity != 'info'`,
-      )
-      .then((result) => {
-        const errors = result.firstRow({sumValue: NUM}).sumValue;
-        publishTraceErrors(errors);
-      });
-  }
-}
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
deleted file mode 100644
index 194be21..0000000
--- a/ui/src/controller/track_decider.ts
+++ /dev/null
@@ -1,1261 +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.
-
-import {v4 as uuidv4} from 'uuid';
-
-import {assertExists} from '../base/logging';
-import {Actions, AddTrackArgs, DeferredAction} from '../common/actions';
-import {
-  InThreadTrackSortKey,
-  SCROLLING_TRACK_GROUP,
-  TrackSortKey,
-  UtidToTrackSortKey,
-} from '../common/state';
-import {globals} from '../frontend/globals';
-import {PrimaryTrackSortKey, TrackDescriptor} from '../public';
-import {getThreadOrProcUri, getTrackName} from '../public/utils';
-import {Engine, EngineBase} from '../trace_processor/engine';
-import {NUM, NUM_NULL, STR_NULL} from '../trace_processor/query_result';
-import {
-  ACTUAL_FRAMES_SLICE_TRACK_KIND,
-  ASYNC_SLICE_TRACK_KIND,
-  CHROME_EVENT_LATENCY_TRACK_KIND,
-  CHROME_SCROLL_JANK_TRACK_KIND,
-  CHROME_TOPLEVEL_SCROLLS_KIND,
-  COUNTER_TRACK_KIND,
-  CPU_FREQ_TRACK_KIND,
-  CPU_PROFILE_TRACK_KIND,
-  CPU_SLICE_TRACK_KIND,
-  EXPECTED_FRAMES_SLICE_TRACK_KIND,
-  HEAP_PROFILE_TRACK_KIND,
-  PERF_SAMPLES_PROFILE_TRACK_KIND,
-  SCROLL_JANK_V3_TRACK_KIND,
-  THREAD_SLICE_TRACK_KIND,
-  THREAD_STATE_TRACK_KIND,
-} from '../core/track_kinds';
-import {exists} from '../base/utils';
-import {sqliteString} from '../base/string_utils';
-
-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';
-const SCROLL_JANK_GROUP_ID = 'chrome-scroll-jank-track-group';
-
-export async function decideTracks(
-  engine: EngineBase,
-): Promise<DeferredAction[]> {
-  return new TrackDecider(engine).decideTracks();
-}
-
-class TrackDecider {
-  private engine: EngineBase;
-  private upidToUuid = new Map<number, string>();
-  private utidToUuid = new Map<number, string>();
-  private tracksToAdd: AddTrackArgs[] = [];
-  private tracksToPin: string[] = [];
-  private addTrackGroupActions: DeferredAction[] = [];
-
-  constructor(engine: EngineBase) {
-    this.engine = engine;
-  }
-
-  private groupGlobalIonTracks(): void {
-    const ionTracks: AddTrackArgs[] = [];
-    let hasSummary = false;
-    for (const track of this.tracksToAdd) {
-      const isIon = track.name.startsWith(MEM_ION);
-      const isIonCounter = track.name === MEM_ION;
-      const isDmaHeapCounter = track.name === MEM_DMA_COUNTER_NAME;
-      const isDmaBuffferSlices = track.name === MEM_DMA;
-      if (isIon || isIonCounter || isDmaHeapCounter || isDmaBuffferSlices) {
-        ionTracks.push(track);
-      }
-      hasSummary = hasSummary || isIonCounter;
-      hasSummary = hasSummary || isDmaHeapCounter;
-    }
-
-    if (ionTracks.length === 0 || !hasSummary) {
-      return;
-    }
-
-    const groupUuid = uuidv4();
-    const summaryTrackKey = uuidv4();
-    let foundSummary = false;
-
-    for (const track of ionTracks) {
-      if (
-        !foundSummary &&
-        [MEM_DMA_COUNTER_NAME, MEM_ION].includes(track.name)
-      ) {
-        foundSummary = true;
-        track.key = summaryTrackKey;
-        track.trackGroup = undefined;
-      } else {
-        track.trackGroup = groupUuid;
-      }
-    }
-
-    const addGroup = Actions.addTrackGroup({
-      summaryTrackKey,
-      name: MEM_DMA_COUNTER_NAME,
-      key: groupUuid,
-      collapsed: true,
-    });
-    this.addTrackGroupActions.push(addGroup);
-  }
-
-  private groupGlobalIostatTracks(tag: string, group: string): void {
-    const iostatTracks: AddTrackArgs[] = [];
-    const devMap = new Map<string, string>();
-
-    for (const track of this.tracksToAdd) {
-      if (track.name.startsWith(tag)) {
-        iostatTracks.push(track);
-      }
-    }
-
-    if (iostatTracks.length === 0) {
-      return;
-    }
-
-    for (const track of iostatTracks) {
-      const name = track.name.split('.', 3);
-
-      if (!devMap.has(name[1])) {
-        devMap.set(name[1], uuidv4());
-      }
-      track.name = name[2];
-      track.trackGroup = devMap.get(name[1]);
-    }
-
-    for (const [key, value] of devMap) {
-      const groupName = group + key;
-      const addGroup = Actions.addTrackGroup({
-        name: groupName,
-        key: value,
-        collapsed: true,
-      });
-      this.addTrackGroupActions.push(addGroup);
-    }
-  }
-
-  private groupGlobalBuddyInfoTracks(): void {
-    const buddyInfoTracks: AddTrackArgs[] = [];
-    const devMap = new Map<string, string>();
-
-    for (const track of this.tracksToAdd) {
-      if (track.name.startsWith(BUDDY_INFO_TAG)) {
-        buddyInfoTracks.push(track);
-      }
-    }
-
-    if (buddyInfoTracks.length === 0) {
-      return;
-    }
-
-    for (const track of buddyInfoTracks) {
-      const tokens = track.name.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)) {
-        devMap.set(groupName, uuidv4());
-      }
-      track.name = 'Chunk size: ' + size;
-      track.trackGroup = devMap.get(groupName);
-    }
-
-    for (const [key, value] of devMap) {
-      const groupName = key;
-      const addGroup = Actions.addTrackGroup({
-        name: groupName,
-        key: value,
-        collapsed: true,
-      });
-      this.addTrackGroupActions.push(addGroup);
-    }
-  }
-
-  private groupFrequencyTracks(groupName: string): void {
-    let groupUuid = undefined;
-    for (const track of this.tracksToAdd) {
-      // Group all the frequency tracks together (except the CPU and GPU
-      // frequency ones).
-      if (
-        track.name.endsWith('Frequency') &&
-        !track.name.startsWith('Cpu') &&
-        !track.name.startsWith('Gpu')
-      ) {
-        if (
-          track.trackGroup !== undefined &&
-          track.trackGroup !== SCROLLING_TRACK_GROUP
-        ) {
-          continue;
-        }
-        if (groupUuid === undefined) {
-          groupUuid = uuidv4();
-        }
-        track.trackGroup = groupUuid;
-      }
-    }
-
-    if (groupUuid !== undefined) {
-      const addGroup = Actions.addTrackGroup({
-        name: groupName,
-        key: groupUuid,
-        collapsed: true,
-      });
-      this.addTrackGroupActions.push(addGroup);
-    }
-  }
-
-  private groupMiscNonAllowlistedTracks(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$'),
-    ];
-
-    let groupUuid = undefined;
-    for (const track of this.tracksToAdd) {
-      if (
-        track.trackGroup !== undefined &&
-        track.trackGroup !== SCROLLING_TRACK_GROUP
-      ) {
-        continue;
-      }
-      let allowlisted = false;
-      for (const regex of ALLOWLIST_REGEXES) {
-        allowlisted = allowlisted || regex.test(track.name);
-      }
-      if (allowlisted) {
-        continue;
-      }
-      if (groupUuid === undefined) {
-        groupUuid = uuidv4();
-      }
-      track.trackGroup = groupUuid;
-    }
-
-    if (groupUuid !== undefined) {
-      const addGroup = Actions.addTrackGroup({
-        name: groupName,
-        key: groupUuid,
-        collapsed: true,
-      });
-      this.addTrackGroupActions.push(addGroup);
-    }
-  }
-
-  private groupTracksByRegex(regex: RegExp, groupName: string): void {
-    let groupUuid = undefined;
-
-    for (const track of this.tracksToAdd) {
-      if (regex.test(track.name)) {
-        if (
-          track.trackGroup !== undefined &&
-          track.trackGroup !== SCROLLING_TRACK_GROUP
-        ) {
-          continue;
-        }
-        if (groupUuid === undefined) {
-          groupUuid = uuidv4();
-        }
-        track.trackGroup = groupUuid;
-      }
-    }
-
-    if (groupUuid !== undefined) {
-      const addGroup = Actions.addTrackGroup({
-        name: groupName,
-        key: groupUuid,
-        collapsed: true,
-      });
-      this.addTrackGroupActions.push(addGroup);
-    }
-  }
-
-  private addAnnotationTracks(tracks: ReadonlyArray<TrackDescriptor>): void {
-    const annotationTracks = tracks.filter(
-      ({tags}) => tags?.scope === 'annotation',
-    );
-
-    interface GroupIds {
-      id: string;
-      summaryTrackKey: string;
-    }
-
-    const groupNameToKeys = new Map<string, GroupIds>();
-
-    annotationTracks
-      .filter(({tags}) => tags?.kind === THREAD_SLICE_TRACK_KIND)
-      .forEach((td) => {
-        const upid = assertExists(td.tags?.upid);
-        const groupName = td.tags?.groupName;
-
-        let summaryTrackKey = undefined;
-        let trackGroupId =
-          upid === 0 ? SCROLLING_TRACK_GROUP : this.upidToUuid.get(upid);
-
-        if (groupName !== undefined) {
-          // If this is the first track encountered for a certain group,
-          // create an id for the group and use this track as the group's
-          // summary track.
-          const groupKeys = groupNameToKeys.get(groupName);
-          if (groupKeys) {
-            trackGroupId = groupKeys.id;
-          } else {
-            trackGroupId = uuidv4();
-            summaryTrackKey = uuidv4();
-            groupNameToKeys.set(groupName, {
-              id: trackGroupId,
-              summaryTrackKey,
-            });
-          }
-        }
-
-        this.tracksToAdd.push({
-          uri: td.uri,
-          key: summaryTrackKey,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-          trackGroup: trackGroupId,
-        });
-      });
-
-    for (const [groupName, groupKeys] of groupNameToKeys) {
-      const addGroup = Actions.addTrackGroup({
-        summaryTrackKey: groupKeys.summaryTrackKey,
-        name: groupName,
-        key: groupKeys.id,
-        collapsed: true,
-      });
-      this.addTrackGroupActions.push(addGroup);
-    }
-
-    annotationTracks
-      .filter(({tags}) => tags?.kind === COUNTER_TRACK_KIND)
-      .forEach((td) => {
-        const upid = td.tags?.upid;
-
-        this.tracksToAdd.push({
-          uri: td.uri,
-          key: td.uri,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
-          trackGroup: exists(upid)
-            ? this.upidToUuid.get(upid)
-            : SCROLLING_TRACK_GROUP,
-        });
-      });
-  }
-
-  private addThreadStateTracks(tracks: ReadonlyArray<TrackDescriptor>): void {
-    tracks
-      .filter(
-        ({tags}) =>
-          tags?.kind === THREAD_STATE_TRACK_KIND && tags?.utid !== undefined,
-      )
-      .forEach((td) => {
-        const upid = td.tags?.upid ?? null;
-        const utid = assertExists(td.tags?.utid);
-
-        const groupId = this.getUuidUnchecked(utid, upid);
-        if (groupId === undefined) {
-          // If a thread has no scheduling activity (i.e. the sched table has zero
-          // rows for that uid) no track group will be created and we want to skip
-          // the track creation as well.
-          return;
-        }
-
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackGroup: groupId,
-          trackSortKey: {
-            utid,
-            priority: InThreadTrackSortKey.THREAD_SCHEDULING_STATE_TRACK,
-          },
-        });
-      });
-  }
-
-  private addThreadCpuSampleTracks(
-    tracks: ReadonlyArray<TrackDescriptor>,
-  ): void {
-    tracks
-      .filter(
-        ({tags}) =>
-          tags?.kind === CPU_PROFILE_TRACK_KIND && tags?.utid !== undefined,
-      )
-      .forEach((td) => {
-        const utid = assertExists(td.tags?.utid);
-        const upid = td.tags?.upid ?? null;
-        const groupId = this.getUuid(utid, upid);
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: {
-            utid,
-            priority: InThreadTrackSortKey.CPU_STACK_SAMPLES_TRACK,
-          },
-          trackGroup: groupId,
-        });
-      });
-  }
-
-  private addThreadCounterTracks(tracks: ReadonlyArray<TrackDescriptor>): void {
-    tracks
-      .filter(
-        ({tags}) =>
-          tags?.kind === COUNTER_TRACK_KIND &&
-          tags?.utid !== undefined &&
-          tags?.scope === 'thread',
-      )
-      .forEach((td) => {
-        const utid = assertExists(td.tags?.utid);
-        const upid = td.tags?.upid ?? null;
-        const groupId = this.getUuid(utid, upid);
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: {
-            utid,
-            priority: InThreadTrackSortKey.ORDINARY,
-          },
-          trackGroup: groupId,
-        });
-      });
-  }
-
-  private addProcessAsyncSliceTracks(
-    tracks: ReadonlyArray<TrackDescriptor>,
-  ): void {
-    tracks
-      .filter(
-        ({tags}) =>
-          tags?.kind === ASYNC_SLICE_TRACK_KIND &&
-          tags?.upid !== undefined &&
-          tags?.scope === 'process',
-      )
-      .forEach((td) => {
-        const upid = assertExists(td.tags?.upid);
-        const groupId = this.getUuid(null, upid);
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
-          trackGroup: groupId,
-        });
-      });
-  }
-
-  private addActualFramesTracks(tracks: ReadonlyArray<TrackDescriptor>): void {
-    tracks
-      .filter(
-        ({tags}) =>
-          tags?.kind === ACTUAL_FRAMES_SLICE_TRACK_KIND &&
-          tags?.upid !== undefined,
-      )
-      .forEach((td) => {
-        const upid = assertExists(td.tags?.upid);
-        const groupId = this.getUuid(null, upid);
-
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.ACTUAL_FRAMES_SLICE_TRACK,
-          trackGroup: groupId,
-        });
-      });
-  }
-
-  private addExpectedFramesTracks(
-    tracks: ReadonlyArray<TrackDescriptor>,
-  ): void {
-    tracks
-      .filter(
-        ({tags}) =>
-          tags?.kind === EXPECTED_FRAMES_SLICE_TRACK_KIND &&
-          tags?.upid !== undefined,
-      )
-      .forEach((td) => {
-        const upid = assertExists(td.tags?.upid);
-        const groupId = this.getUuid(null, upid);
-
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.EXPECTED_FRAMES_SLICE_TRACK,
-          trackGroup: groupId,
-        });
-      });
-  }
-
-  private addThreadSliceTracks(tracks: ReadonlyArray<TrackDescriptor>): void {
-    tracks
-      .filter(
-        ({tags}) =>
-          tags?.kind === THREAD_SLICE_TRACK_KIND && tags?.utid !== undefined,
-      )
-      .forEach((td) => {
-        const utid = assertExists(td.tags?.utid);
-        const upid = td.tags?.upid ?? null;
-        const isDefaultTrackForScope = Boolean(td.tags?.isDefaultTrackForScope);
-        const groupId = this.getUuid(utid, upid);
-
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackGroup: groupId,
-          trackSortKey: {
-            utid,
-            priority: isDefaultTrackForScope
-              ? InThreadTrackSortKey.DEFAULT_TRACK
-              : InThreadTrackSortKey.ORDINARY,
-          },
-        });
-      });
-  }
-
-  private async addProcessCounterTracks(
-    tracks: ReadonlyArray<TrackDescriptor>,
-  ): Promise<void> {
-    const processCounterTracks = tracks.filter(
-      ({tags}) =>
-        tags?.kind === COUNTER_TRACK_KIND &&
-        tags?.scope === 'process' &&
-        tags?.upid !== undefined,
-    );
-
-    for (const td of processCounterTracks) {
-      const upid = assertExists(td.tags?.upid);
-      const groupId = this.getUuid(null, upid);
-      const trackNameTag = td.tags?.trackName;
-      const trackName =
-        typeof trackNameTag === 'string' ? trackNameTag : undefined;
-
-      this.tracksToAdd.push({
-        key: td.uri,
-        uri: td.uri,
-        name: td.title,
-        trackSortKey: await this.resolveTrackSortKeyForProcessCounterTrack(
-          upid,
-          trackName,
-        ),
-        trackGroup: groupId,
-      });
-    }
-  }
-
-  private addProcessHeapProfileTracks(
-    tracks: ReadonlyArray<TrackDescriptor>,
-  ): void {
-    tracks
-      .filter(
-        ({tags}) =>
-          tags?.kind === HEAP_PROFILE_TRACK_KIND && tags?.upid !== undefined,
-      )
-      .forEach((td) => {
-        const upid = assertExists(td.tags?.upid);
-        const groupId = this.getUuid(null, upid);
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.HEAP_PROFILE_TRACK,
-          trackGroup: groupId,
-        });
-      });
-  }
-
-  private addProcessPerfSamplesTracks(
-    tracks: ReadonlyArray<TrackDescriptor>,
-  ): void {
-    tracks
-      .filter(
-        ({tags}) =>
-          tags?.kind === PERF_SAMPLES_PROFILE_TRACK_KIND &&
-          tags?.upid !== undefined,
-      )
-      .forEach((td) => {
-        const upid = assertExists(td.tags?.upid);
-        const groupId = this.getUuid(null, upid);
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.PERF_SAMPLES_PROFILE_TRACK,
-          trackGroup: groupId,
-        });
-      });
-  }
-
-  private addThreadPerfSamplesTracks(
-    tracks: ReadonlyArray<TrackDescriptor>,
-  ): void {
-    tracks
-      .filter(
-        ({tags}) =>
-          tags?.kind === PERF_SAMPLES_PROFILE_TRACK_KIND &&
-          tags?.utid !== undefined,
-      )
-      .forEach((td) => {
-        const upid = td.tags?.upid ?? null;
-        const utid = assertExists(td.tags?.utid);
-        const groupId = this.getUuid(utid, upid);
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.PERF_SAMPLES_PROFILE_TRACK,
-          trackGroup: groupId,
-        });
-      });
-  }
-
-  private getUuidUnchecked(utid: number | null, upid: number | null) {
-    return upid === null
-      ? this.utidToUuid.get(utid!)
-      : this.upidToUuid.get(upid);
-  }
-
-  private getUuid(utid: number | null, upid: number | null) {
-    return assertExists(this.getUuidUnchecked(utid, upid));
-  }
-
-  private getOrCreateUuid(utid: number | null, upid: number | null) {
-    let uuid = this.getUuidUnchecked(utid, upid);
-    if (uuid === undefined) {
-      uuid = uuidv4();
-      if (upid === null) {
-        this.utidToUuid.set(utid!, uuid);
-      } else {
-        this.upidToUuid.set(upid, uuid);
-      }
-    }
-    return uuid;
-  }
-
-  private setUuidForUpid(upid: number, uuid: string) {
-    this.upidToUuid.set(upid, uuid);
-  }
-
-  private async addKernelThreadGrouping(engine: Engine): Promise<void> {
-    // Identify kernel threads if this is a linux system trace, and sufficient
-    // process information is available. Kernel threads are identified by being
-    // children of kthreadd (always pid 2).
-    // The query will return the kthreadd process row first, which must exist
-    // for any other kthreads to be returned by the query.
-    // TODO(rsavitski): figure out how to handle the idle process (swapper),
-    // which has pid 0 but appears as a distinct process (with its own comm) on
-    // each cpu. It'd make sense to exclude its thread state track, but still
-    // put process-scoped tracks in this group.
-    const result = await engine.query(`
-      select
-        t.utid, p.upid, (case p.pid when 2 then 1 else 0 end) isKthreadd
-      from
-        thread t
-        join process p using (upid)
-        left join process parent on (p.parent_upid = parent.upid)
-        join
-          (select true from metadata m
-             where (m.name = 'system_name' and m.str_value = 'Linux')
-           union
-           select 1 from (select true from sched limit 1))
-      where
-        p.pid = 2 or parent.pid = 2
-      order by isKthreadd desc
-    `);
-
-    const it = result.iter({
-      utid: NUM,
-      upid: NUM,
-    });
-
-    // Not applying kernel thread grouping.
-    if (!it.valid()) {
-      return;
-    }
-
-    // Create the track group. Use kthreadd's PROCESS_SUMMARY_TRACK for the
-    // main track. It doesn't summarise the kernel threads within the group,
-    // but creating a dedicated track type is out of scope at the time of
-    // writing.
-    const kthreadGroupUuid = uuidv4();
-    const summaryTrackKey = uuidv4();
-    this.tracksToAdd.push({
-      uri: '/kernel',
-      key: summaryTrackKey,
-      trackSortKey: PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK,
-      name: `Kernel thread summary`,
-    });
-    const addTrackGroup = Actions.addTrackGroup({
-      summaryTrackKey,
-      name: `Kernel threads`,
-      key: kthreadGroupUuid,
-      collapsed: true,
-    });
-    this.addTrackGroupActions.push(addTrackGroup);
-
-    // Set the group for all kernel threads (including kthreadd itself).
-    for (; it.valid(); it.next()) {
-      this.setUuidForUpid(it.upid, kthreadGroupUuid);
-    }
-  }
-
-  private async addProcessTrackGroups(engine: Engine): Promise<void> {
-    // We want to create groups of tracks in a specific order.
-    // The tracks should be grouped:
-    //    by upid
-    //    or (if upid is null) by utid
-    // the groups should be sorted by:
-    //  Chrome-based process rank based on process names (e.g. Browser)
-    //  has a heap profile or not
-    //  total cpu time *for the whole parent process*
-    //  process name
-    //  upid
-    //  thread name
-    //  utid
-    const result = await engine.query(`
-      with processGroups as (
-        select
-          upid,
-          process.pid as pid,
-          process.name as processName,
-          sum_running_dur as sumRunningDur,
-          thread_slice_count + process_slice_count as sliceCount,
-          perf_sample_count as perfSampleCount,
-          allocation_count as heapProfileAllocationCount,
-          graph_object_count as heapGraphObjectCount,
-          (
-            select group_concat(string_value)
-            from args
-            where
-              process.arg_set_id is not null and
-              arg_set_id = process.arg_set_id and
-              flat_key = 'chrome.process_label'
-          ) chromeProcessLabels,
-          case process.name
-            when 'Browser' then 3
-            when 'Gpu' then 2
-            when 'Renderer' then 1
-            else 0
-          end as chromeProcessRank
-        from _process_available_info_summary
-        join process using(upid)
-      ),
-      threadGroups as (
-        select
-          utid,
-          tid,
-          thread.name as threadName,
-          sum_running_dur as sumRunningDur,
-          slice_count as sliceCount,
-          perf_sample_count as perfSampleCount
-        from _thread_available_info_summary
-        join thread using (utid)
-        where upid is null
-      )
-      select *
-      from (
-        select
-          upid,
-          null as utid,
-          pid,
-          null as tid,
-          processName,
-          null as threadName,
-          sumRunningDur > 0 as hasSched,
-          heapProfileAllocationCount > 0
-            or heapGraphObjectCount > 0 as hasHeapInfo,
-          ifnull(chromeProcessLabels, '') as chromeProcessLabels
-        from processGroups
-        order by
-          chromeProcessRank desc,
-          heapProfileAllocationCount desc,
-          heapGraphObjectCount desc,
-          perfSampleCount desc,
-          sumRunningDur desc,
-          sliceCount desc,
-          processName asc,
-          upid asc
-      )
-      union all
-      select *
-      from (
-        select
-          null,
-          utid,
-          null as pid,
-          tid,
-          null as processName,
-          threadName,
-          sumRunningDur > 0 as hasSched,
-          0 as hasHeapInfo,
-          '' as chromeProcessLabels
-        from threadGroups
-        order by
-          perfSampleCount desc,
-          sumRunningDur desc,
-          sliceCount desc,
-          threadName asc,
-          utid asc
-      )
-  `);
-
-    const it = result.iter({
-      upid: NUM_NULL,
-      utid: NUM_NULL,
-      pid: NUM_NULL,
-      tid: NUM_NULL,
-      processName: STR_NULL,
-      threadName: STR_NULL,
-      hasSched: NUM_NULL,
-      hasHeapInfo: NUM_NULL,
-    });
-    for (; it.valid(); it.next()) {
-      const utid = it.utid;
-      const upid = it.upid;
-      const pid = it.pid;
-      const tid = it.tid;
-      const threadName = it.threadName;
-      const processName = it.processName;
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      const hasSched = !!it.hasSched;
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      const hasHeapInfo = !!it.hasHeapInfo;
-
-      const summaryTrackKey = uuidv4();
-
-      const uri = getThreadOrProcUri(upid, utid);
-
-      // If previous groupings (e.g. kernel threads) picked up there tracks,
-      // don't try to regroup them.
-      const pUuid =
-        upid === null ? this.utidToUuid.get(utid!) : this.upidToUuid.get(upid);
-      if (pUuid !== undefined) {
-        continue;
-      }
-
-      this.tracksToAdd.push({
-        uri,
-        key: summaryTrackKey,
-        trackSortKey: hasSched
-          ? PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK
-          : PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK,
-        name: `${upid === null ? tid : pid} summary`,
-      });
-
-      const name = getTrackName({
-        utid,
-        processName,
-        pid,
-        threadName,
-        tid,
-        upid,
-      });
-
-      const addTrackGroup = Actions.addTrackGroup({
-        summaryTrackKey,
-        name: stripPathFromExecutable(name),
-        key: this.getOrCreateUuid(utid, upid),
-        // Perf profiling tracks remain collapsed, otherwise we would have too
-        // many expanded process tracks for some perf traces, leading to
-        // jankyness.
-        collapsed: !hasHeapInfo,
-      });
-      this.addTrackGroupActions.push(addTrackGroup);
-    }
-  }
-
-  private async computeThreadOrderingMetadata(): Promise<UtidToTrackSortKey> {
-    const result = await this.engine.query(`
-      select
-        utid,
-        tid,
-        (select pid from process p where t.upid = p.upid) as pid,
-        t.name as threadName
-      from thread t
-    `);
-
-    const it = result.iter({
-      utid: NUM,
-      tid: NUM_NULL,
-      pid: NUM_NULL,
-      threadName: STR_NULL,
-    });
-
-    const threadOrderingMetadata: UtidToTrackSortKey = {};
-    for (; it.valid(); it.next()) {
-      threadOrderingMetadata[it.utid] = {
-        tid: it.tid === null ? undefined : it.tid,
-        sortKey: TrackDecider.getThreadSortKey(it.threadName, it.tid, it.pid),
-      };
-    }
-    return threadOrderingMetadata;
-  }
-
-  private addPluginTracks(): void {
-    const groupNameToUuid = new Map<string, string>();
-    const tracks = globals.trackManager.findPotentialTracks();
-
-    for (const info of tracks) {
-      const groupName = info.groupName;
-
-      let groupUuid = SCROLLING_TRACK_GROUP;
-      if (groupName) {
-        const uuid = groupNameToUuid.get(groupName);
-        if (uuid) {
-          groupUuid = uuid;
-        } else {
-          // Add the group
-          groupUuid = uuidv4();
-          const addGroup = Actions.addTrackGroup({
-            name: groupName,
-            key: groupUuid,
-            collapsed: true,
-            fixedOrdering: true,
-          });
-          this.addTrackGroupActions.push(addGroup);
-
-          // Add group to the map
-          groupNameToUuid.set(groupName, groupUuid);
-        }
-      }
-
-      this.tracksToAdd.push({
-        uri: info.uri,
-        key: info.uri,
-        name: info.title,
-        // TODO(hjd): Fix how sorting works. Plugins should expose
-        // 'sort keys' which the user can use to choose a sort order.
-        trackSortKey: info.sortKey ?? PrimaryTrackSortKey.ORDINARY_TRACK,
-        trackGroup: groupUuid,
-      });
-
-      if (info.isPinned) {
-        this.tracksToPin.push(info.uri);
-      }
-    }
-  }
-
-  private addScrollJankPluginTracks(
-    tracks: ReadonlyArray<TrackDescriptor>,
-  ): void {
-    let scrollTracks = this.addTracks(
-      tracks,
-      ({tags}) => tags?.kind === CHROME_TOPLEVEL_SCROLLS_KIND,
-      SCROLL_JANK_GROUP_ID,
-    );
-    scrollTracks = scrollTracks.concat(
-      this.addTracks(
-        tracks,
-        ({tags}) => tags?.kind === SCROLL_JANK_V3_TRACK_KIND,
-        SCROLL_JANK_GROUP_ID,
-      ),
-    );
-    scrollTracks = scrollTracks.concat(
-      this.addTracks(
-        tracks,
-        ({tags}) => tags?.kind === CHROME_EVENT_LATENCY_TRACK_KIND,
-        SCROLL_JANK_GROUP_ID,
-      ),
-    );
-    if (scrollTracks.length > 0) {
-      this.addTrackGroupActions.push(
-        Actions.addTrackGroup({
-          name: 'Chrome Scroll Jank',
-          key: SCROLL_JANK_GROUP_ID,
-          collapsed: false,
-          fixedOrdering: true,
-        }),
-      );
-    }
-  }
-
-  private addChromeScrollJankTrack(
-    tracks: ReadonlyArray<TrackDescriptor>,
-  ): void {
-    tracks
-      .filter(({tags}) => tags?.kind === CHROME_SCROLL_JANK_TRACK_KIND)
-      .forEach((td) => {
-        const upid = assertExists(td.tags?.upid);
-        const utid = assertExists(td.tags?.utid);
-        const groupId = this.getUuid(utid, upid);
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: {
-            utid,
-            priority: InThreadTrackSortKey.ORDINARY,
-          },
-          trackGroup: groupId,
-        });
-      });
-  }
-
-  // Add an ordinary track from a track descriptor
-  private addTrack(track: TrackDescriptor, groupId?: string): void {
-    this.tracksToAdd.push({
-      key: track.uri,
-      uri: track.uri,
-      name: track.title,
-      trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-      trackGroup: groupId ?? SCROLLING_TRACK_GROUP,
-    });
-  }
-
-  // Add tracks that match some predicate
-  private addTracks(
-    source: ReadonlyArray<TrackDescriptor>,
-    predicate: (td: TrackDescriptor) => boolean,
-    groupId?: string,
-  ): ReadonlyArray<TrackDescriptor> {
-    const filteredTracks = source.filter(predicate);
-    filteredTracks.forEach((a) => this.addTrack(a, groupId));
-    return filteredTracks;
-  }
-
-  public async decideTracks(): Promise<DeferredAction[]> {
-    const tracks = globals.trackManager.getAllTracks();
-
-    // Add first the global tracks that don't require per-process track groups.
-    this.addTracks(tracks, ({uri}) => uri === 'screenshots');
-    this.addTracks(tracks, ({tags}) => tags?.kind === CPU_SLICE_TRACK_KIND);
-    this.addTracks(tracks, ({tags}) => tags?.kind === CPU_FREQ_TRACK_KIND);
-    this.addScrollJankPluginTracks(tracks);
-    this.addTracks(
-      tracks,
-      ({tags}) =>
-        tags?.kind === ASYNC_SLICE_TRACK_KIND && tags?.scope === 'global',
-    );
-    this.addTracks(
-      tracks,
-      ({tags}) =>
-        tags?.kind === COUNTER_TRACK_KIND && tags?.scope === 'gpuFreq',
-    );
-    this.addTracks(
-      tracks,
-      ({tags}) =>
-        tags?.kind === COUNTER_TRACK_KIND && tags?.scope === 'cpuFreqLimit',
-    );
-    this.addTracks(
-      tracks,
-      ({tags}) =>
-        tags?.kind === COUNTER_TRACK_KIND && tags?.scope === 'cpuPerf',
-    );
-    this.addPluginTracks();
-    this.addAnnotationTracks(tracks);
-
-    this.groupGlobalIonTracks();
-    this.groupGlobalIostatTracks(F2FS_IOSTAT_TAG, F2FS_IOSTAT_GROUP_NAME);
-    this.groupGlobalIostatTracks(
-      F2FS_IOSTAT_LAT_TAG,
-      F2FS_IOSTAT_LAT_GROUP_NAME,
-    );
-    this.groupGlobalIostatTracks(DISK_IOSTAT_TAG, DISK_IOSTAT_GROUP_NAME);
-    this.groupTracksByRegex(UFS_CMD_TAG_REGEX, UFS_CMD_TAG_GROUP);
-    this.groupGlobalBuddyInfoTracks();
-    this.groupTracksByRegex(KERNEL_WAKELOCK_REGEX, KERNEL_WAKELOCK_GROUP);
-    this.groupTracksByRegex(NETWORK_TRACK_REGEX, NETWORK_TRACK_GROUP);
-    this.groupTracksByRegex(ENTITY_RESIDENCY_REGEX, ENTITY_RESIDENCY_GROUP);
-    this.groupTracksByRegex(UCLAMP_REGEX, UCLAMP_GROUP);
-    this.groupFrequencyTracks(FREQUENCY_GROUP);
-    this.groupTracksByRegex(POWER_RAILS_REGEX, POWER_RAILS_GROUP);
-    this.groupTracksByRegex(TEMPERATURE_REGEX, TEMPERATURE_GROUP);
-    this.groupTracksByRegex(IRQ_REGEX, IRQ_GROUP);
-    this.groupTracksByRegex(CHROME_TRACK_REGEX, CHROME_TRACK_GROUP);
-    this.groupMiscNonAllowlistedTracks(MISC_GROUP);
-
-    // Add user slice tracks before listing the processes. These tracks will
-    // be listed with their user/package name only, and they will be grouped
-    // under on their original shared track names. E.g. "GPU Work Period"
-    this.addTracks(
-      tracks,
-      ({tags}) =>
-        tags?.kind === ASYNC_SLICE_TRACK_KIND && tags?.scope === 'user',
-    );
-
-    // Pre-group all kernel "threads" (actually processes) if this is a linux
-    // system trace. Below, addProcessTrackGroups will skip them due to an
-    // existing group uuid, and addThreadStateTracks will fill in the
-    // per-thread tracks. Quirk: since all threads will appear to be
-    // TrackKindPriority.MAIN_THREAD, any process-level tracks will end up
-    // pushed to the bottom of the group in the UI.
-    await this.addKernelThreadGrouping(
-      this.engine.getProxy('TrackDecider::addKernelThreadGrouping'),
-    );
-
-    // Create the per-process track groups. Note that this won't necessarily
-    // create a track per process. If a process has been completely idle and has
-    // no sched events, no track group will be emitted.
-    // Will populate this.addTrackGroupActions
-    await this.addProcessTrackGroups(
-      this.engine.getProxy('TrackDecider::addProcessTrackGroups'),
-    );
-
-    this.addProcessHeapProfileTracks(tracks);
-    this.addProcessPerfSamplesTracks(tracks);
-    this.addThreadPerfSamplesTracks(tracks);
-    await this.addProcessCounterTracks(tracks);
-    this.addProcessAsyncSliceTracks(tracks);
-    this.addActualFramesTracks(tracks);
-    this.addExpectedFramesTracks(tracks);
-    this.addThreadCounterTracks(tracks);
-    this.addThreadStateTracks(tracks);
-    this.addThreadSliceTracks(tracks);
-    this.addThreadCpuSampleTracks(tracks);
-
-    this.addChromeScrollJankTrack(tracks);
-
-    this.addTrackGroupActions.push(
-      Actions.addTracks({tracks: this.tracksToAdd}),
-    );
-
-    // Add the actions to pin any tracks we need to pin
-    for (const trackKey of this.tracksToPin) {
-      this.addTrackGroupActions.push(Actions.toggleTrackPinned({trackKey}));
-    }
-
-    const threadOrderingMetadata = await this.computeThreadOrderingMetadata();
-    this.addTrackGroupActions.push(
-      Actions.setUtidToTrackSortKey({threadOrderingMetadata}),
-    );
-
-    return this.addTrackGroupActions;
-  }
-
-  // Some process counter tracks are tied to specific threads based on their
-  // name.
-  private async resolveTrackSortKeyForProcessCounterTrack(
-    upid: number,
-    threadName?: string,
-  ): Promise<TrackSortKey> {
-    if (threadName !== 'GPU completion') {
-      return PrimaryTrackSortKey.COUNTER_TRACK;
-    }
-    const result = await this.engine.query(`
-      select utid
-      from thread
-      where upid=${upid} and name=${sqliteString(threadName)}
-    `);
-    const it = result.iter({
-      utid: NUM,
-    });
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    for (; it; it.next()) {
-      return {
-        utid: it.utid,
-        priority: InThreadTrackSortKey.THREAD_COUNTER_TRACK,
-      };
-    }
-    return PrimaryTrackSortKey.COUNTER_TRACK;
-  }
-
-  private static getThreadSortKey(
-    threadName?: string | null,
-    tid?: number | null,
-    pid?: number | null,
-  ): PrimaryTrackSortKey {
-    if (pid !== undefined && pid !== null && pid === tid) {
-      return PrimaryTrackSortKey.MAIN_THREAD;
-    }
-    if (threadName === undefined || threadName === null) {
-      return PrimaryTrackSortKey.ORDINARY_THREAD;
-    }
-
-    // Chrome main threads should always come first within their process.
-    if (
-      threadName === 'CrBrowserMain' ||
-      threadName === 'CrRendererMain' ||
-      threadName === 'CrGpuMain'
-    ) {
-      return PrimaryTrackSortKey.MAIN_THREAD;
-    }
-
-    // Chrome IO threads should always come immediately after the main thread.
-    if (
-      threadName === 'Chrome_ChildIOThread' ||
-      threadName === 'Chrome_IOThread'
-    ) {
-      return PrimaryTrackSortKey.CHROME_IO_THREAD;
-    }
-
-    // A Chrome process can have only one compositor thread, so we want to put
-    // it next to other named processes.
-    if (threadName === 'Compositor' || threadName === 'VizCompositorThread') {
-      return PrimaryTrackSortKey.CHROME_COMPOSITOR_THREAD;
-    }
-
-    switch (true) {
-      case /.*RenderThread.*/.test(threadName):
-        return PrimaryTrackSortKey.RENDER_THREAD;
-      case /.*GPU completion.*/.test(threadName):
-        return PrimaryTrackSortKey.GPU_COMPLETION_THREAD;
-      default:
-        return PrimaryTrackSortKey.ORDINARY_THREAD;
-    }
-  }
-}
-
-function stripPathFromExecutable(path: string) {
-  if (path[0] === '/') {
-    return path.split('/').slice(-1)[0];
-  } else {
-    return path;
-  }
-}
diff --git a/ui/src/core/analytics_impl.ts b/ui/src/core/analytics_impl.ts
new file mode 100644
index 0000000..be785ca
--- /dev/null
+++ b/ui/src/core/analytics_impl.ts
@@ -0,0 +1,193 @@
+// 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.
+
+import {ErrorDetails} from '../base/logging';
+import {getCurrentChannel} from './channels';
+import {VERSION} from '../gen/perfetto_version';
+import {Router} from './router';
+import {Analytics, TraceCategories} from '../public/analytics';
+
+const ANALYTICS_ID = 'G-BD89KT2P3C';
+const PAGE_TITLE = 'no-page-title';
+
+function isValidUrl(s: string) {
+  let url;
+  try {
+    url = new URL(s);
+  } catch (_) {
+    return false;
+  }
+  return url.protocol === 'http:' || url.protocol === 'https:';
+}
+
+function getReferrerOverride(): string | undefined {
+  const route = Router.parseUrl(window.location.href);
+  const referrer = route.args.referrer;
+  if (referrer) {
+    return referrer;
+  } else {
+    return undefined;
+  }
+}
+
+// Get the referrer from either:
+// - If present: the referrer argument if present
+// - document.referrer
+function getReferrer(): string {
+  const referrer = getReferrerOverride();
+  if (referrer) {
+    if (isValidUrl(referrer)) {
+      return referrer;
+    } else {
+      // Unclear if GA discards non-URL referrers. Lets try faking
+      // a URL to test.
+      const name = referrer.replaceAll('_', '-');
+      return `https://${name}.example.com/converted_non_url_referrer`;
+    }
+  } else {
+    return document.referrer.split('?')[0];
+  }
+}
+
+// Interface exposed only to core (for the initialize method).
+export interface AnalyticsInternal extends Analytics {
+  initialize(isInternalUser: boolean): void;
+}
+
+export function initAnalytics(
+  testingMode: boolean,
+  embeddedMode: boolean,
+): AnalyticsInternal {
+  // Only initialize logging on the official site and on localhost (to catch
+  // analytics bugs when testing locally).
+  // Skip analytics is the fragment has "testing=1", this is used by UI tests.
+  // Skip analytics in embeddedMode since iFrames do not have the same access to
+  // local storage.
+  if (
+    (window.location.origin.startsWith('http://localhost:') ||
+      window.location.origin.endsWith('.perfetto.dev')) &&
+    !testingMode &&
+    !embeddedMode
+  ) {
+    return new AnalyticsImpl();
+  }
+  return new NullAnalytics();
+}
+
+const gtagGlobals = window as {} as {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  dataLayer: any[];
+  gtag: (command: string, event: string | Date, args?: {}) => void;
+};
+
+class NullAnalytics implements AnalyticsInternal {
+  initialize(_: boolean) {}
+  logEvent(_category: TraceCategories | null, _event: string) {}
+  logError(_err: ErrorDetails) {}
+  isEnabled(): boolean {
+    return false;
+  }
+}
+
+class AnalyticsImpl implements AnalyticsInternal {
+  private initialized_ = false;
+
+  constructor() {
+    // The code below is taken from the official Google Analytics docs [1] and
+    // adapted to TypeScript. We have it here rather than as an inline script
+    // in index.html (as suggested by GA's docs) because inline scripts don't
+    // play nicely with the CSP policy, at least in Firefox (Firefox doesn't
+    // support all CSP 3 features we use).
+    // [1] https://developers.google.com/analytics/devguides/collection/gtagjs .
+    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+    gtagGlobals.dataLayer = gtagGlobals.dataLayer || [];
+
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    function gtagFunction(..._: any[]) {
+      // This needs to be a function and not a lambda. |arguments| behaves
+      // slightly differently in a lambda and breaks GA.
+      gtagGlobals.dataLayer.push(arguments);
+    }
+    gtagGlobals.gtag = gtagFunction;
+    gtagGlobals.gtag('js', new Date());
+  }
+
+  // This is callled only after the script that sets isInternalUser loads.
+  // It is fine to call updatePath() and log*() functions before initialize().
+  // The gtag() function internally enqueues all requests into |dataLayer|.
+  initialize(isInternalUser: boolean) {
+    if (this.initialized_) return;
+    this.initialized_ = true;
+    const script = document.createElement('script');
+    script.src = 'https://www.googletagmanager.com/gtag/js?id=' + ANALYTICS_ID;
+    script.defer = true;
+    document.head.appendChild(script);
+    const route = window.location.href;
+    console.log(
+      `GA initialized. route=${route}`,
+      `isInternalUser=${isInternalUser}`,
+    );
+    // GA's recommendation for SPAs is to disable automatic page views and
+    // manually send page_view events. See:
+    // https://developers.google.com/analytics/devguides/collection/gtagjs/pages#manual_pageviews
+    gtagGlobals.gtag('config', ANALYTICS_ID, {
+      allow_google_signals: false,
+      anonymize_ip: true,
+      page_location: route,
+      // Referrer as a URL including query string override.
+      page_referrer: getReferrer(),
+      send_page_view: false,
+      page_title: PAGE_TITLE,
+      perfetto_is_internal_user: isInternalUser ? '1' : '0',
+      perfetto_version: VERSION,
+      // Release channel (canary, stable, autopush)
+      perfetto_channel: getCurrentChannel(),
+      // Referrer *if overridden* via the query string else empty string.
+      perfetto_referrer_override: getReferrerOverride() ?? '',
+    });
+
+    gtagGlobals.gtag('event', 'page_view', {
+      page_path: route,
+      page_title: PAGE_TITLE,
+    });
+  }
+
+  logEvent(category: TraceCategories | null, event: string) {
+    gtagGlobals.gtag('event', event, {event_category: category});
+  }
+
+  logError(err: ErrorDetails) {
+    let stack = '';
+    for (const entry of err.stack) {
+      const shortLocation = entry.location.replace('frontend_bundle.js', '$');
+      stack += `${entry.name}(${shortLocation}),`;
+    }
+    // Strip trailing ',' (works also for empty strings without extra checks).
+    stack = stack.substring(0, stack.length - 1);
+
+    gtagGlobals.gtag('event', 'exception', {
+      description: err.message,
+      error_type: err.errType,
+
+      // As per GA4 all field are restrictred to 100 chars.
+      // page_title is the only one restricted to 1000 chars and we use that for
+      // the full crash report.
+      page_location: `http://crash?/${encodeURI(stack)}`,
+    });
+  }
+
+  isEnabled(): boolean {
+    return true;
+  }
+}
diff --git a/ui/src/core/app_impl.ts b/ui/src/core/app_impl.ts
new file mode 100644
index 0000000..3336045
--- /dev/null
+++ b/ui/src/core/app_impl.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 {assertExists, assertTrue} from '../base/logging';
+import {App} from '../public/app';
+import {TraceContext, TraceImpl} from './trace_impl';
+import {CommandManagerImpl} from './command_manager';
+import {OmniboxManagerImpl} from './omnibox_manager';
+import {raf} from './raf_scheduler';
+import {SidebarManagerImpl} from './sidebar_manager';
+import {PluginManagerImpl} from './plugin_manager';
+import {NewEngineMode} from '../trace_processor/engine';
+import {RouteArgs} from '../public/route_schema';
+import {SqlPackage} from '../public/extra_sql_packages';
+import {SerializedAppState} from './state_serialization_schema';
+import {PostedTrace, TraceSource} from './trace_source';
+import {loadTrace} from './load_trace';
+import {CORE_PLUGIN_ID} from './plugin_manager';
+import {Router} from './router';
+import {AnalyticsInternal, initAnalytics} from './analytics_impl';
+import {createProxy, getOrCreate} from '../base/utils';
+import {PageManagerImpl} from './page_manager';
+import {PageHandler} from '../public/page';
+import {setPerfHooks} from './perf';
+import {ServiceWorkerController} from '../frontend/service_worker_controller';
+import {FeatureFlagManager, FlagSettings} from '../public/feature_flag';
+import {featureFlags} from './feature_flags';
+
+// The args that frontend/index.ts passes when calling AppImpl.initialize().
+// This is to deal with injections that would otherwise cause circular deps.
+export interface AppInitArgs {
+  initialRouteArgs: RouteArgs;
+}
+
+/**
+ * Handles the global state of the ui, for anything that is not related to a
+ * specific trace. This is always available even before a trace is loaded (in
+ * contrast to TraceContext, which is bound to the lifetime of a trace).
+ * There is only one instance in total of this class (see instance()).
+ * This class is only exposed to TraceImpl, nobody else should refer to this
+ * and should use AppImpl instead.
+ */
+export class AppContext {
+  // The per-plugin instances of AppImpl (including the CORE_PLUGIN one).
+  private readonly pluginInstances = new Map<string, AppImpl>();
+  readonly commandMgr = new CommandManagerImpl();
+  readonly omniboxMgr = new OmniboxManagerImpl();
+  readonly pageMgr = new PageManagerImpl();
+  readonly sidebarMgr: SidebarManagerImpl;
+  readonly pluginMgr: PluginManagerImpl;
+  readonly analytics: AnalyticsInternal;
+  readonly serviceWorkerController: ServiceWorkerController;
+  httpRpc = {
+    newEngineMode: 'USE_HTTP_RPC_IF_AVAILABLE' as NewEngineMode,
+    httpRpcAvailable: false,
+  };
+  initialRouteArgs: RouteArgs;
+  isLoadingTrace = false; // Set when calling openTrace().
+  perfDebugging = false; // Enables performance debugging of tracks/panels.
+  readonly initArgs: AppInitArgs;
+  readonly embeddedMode: boolean;
+  readonly testingMode: boolean;
+
+  // This is normally empty and is injected with extra google-internal packages
+  // via is_internal_user.js
+  extraSqlPackages: SqlPackage[] = [];
+
+  // The currently open trace.
+  currentTrace?: TraceContext;
+
+  private static _instance: AppContext;
+
+  static initialize(initArgs: AppInitArgs): AppContext {
+    assertTrue(AppContext._instance === undefined);
+    return (AppContext._instance = new AppContext(initArgs));
+  }
+
+  static get instance(): AppContext {
+    return assertExists(AppContext._instance);
+  }
+
+  // This constructor is invoked only once, when frontend/index.ts invokes
+  // AppMainImpl.initialize().
+  private constructor(initArgs: AppInitArgs) {
+    this.initArgs = initArgs;
+    this.initialRouteArgs = initArgs.initialRouteArgs;
+    this.serviceWorkerController = new ServiceWorkerController();
+    this.embeddedMode = this.initialRouteArgs.mode === 'embedded';
+    this.testingMode =
+      self.location !== undefined &&
+      self.location.search.indexOf('testing=1') >= 0;
+    this.sidebarMgr = new SidebarManagerImpl({
+      disabled: this.embeddedMode,
+      hidden: this.initialRouteArgs.hideSidebar,
+    });
+    this.analytics = initAnalytics(this.testingMode, this.embeddedMode);
+    this.pluginMgr = new PluginManagerImpl({
+      forkForPlugin: (pluginId) => this.forPlugin(pluginId),
+      get trace() {
+        return AppImpl.instance.trace;
+      },
+    });
+  }
+
+  // Gets or creates an instance of AppImpl backed by the current AppContext
+  // for the given plugin.
+  forPlugin(pluginId: string) {
+    return getOrCreate(this.pluginInstances, pluginId, () => {
+      return new AppImpl(this, pluginId);
+    });
+  }
+
+  closeCurrentTrace() {
+    this.omniboxMgr.reset(/* focus= */ false);
+
+    if (this.currentTrace !== undefined) {
+      // This will trigger the unregistration of trace-scoped commands and
+      // sidebar menuitems (and few similar things).
+      this.currentTrace[Symbol.dispose]();
+      this.currentTrace = undefined;
+    }
+  }
+
+  // Called by trace_loader.ts soon after it has created a new TraceImpl.
+  setActiveTrace(traceCtx: TraceContext) {
+    // In 99% this closeCurrentTrace() call is not needed because the real one
+    // is performed by openTrace() in this file. However in some rare cases we
+    // might end up loading a trace while another one is still loading, and this
+    // covers races in that case.
+    this.closeCurrentTrace();
+    this.currentTrace = traceCtx;
+  }
+}
+
+/*
+ * Every plugin gets its own instance. This is how we keep track
+ * what each plugin is doing and how we can blame issues on particular
+ * plugins.
+ * The instance exists for the whole duration a plugin is active.
+ */
+
+export class AppImpl implements App {
+  readonly pluginId: string;
+  private readonly appCtx: AppContext;
+  private readonly pageMgrProxy: PageManagerImpl;
+
+  // Invoked by frontend/index.ts.
+  static initialize(args: AppInitArgs) {
+    AppContext.initialize(args).forPlugin(CORE_PLUGIN_ID);
+  }
+
+  // Gets access to the one instance that the core can use. Note that this is
+  // NOT the only instance, as other AppImpl instance will be created for each
+  // plugin.
+  static get instance(): AppImpl {
+    return AppContext.instance.forPlugin(CORE_PLUGIN_ID);
+  }
+
+  // Only called by AppContext.forPlugin().
+  constructor(appCtx: AppContext, pluginId: string) {
+    this.appCtx = appCtx;
+    this.pluginId = pluginId;
+
+    this.pageMgrProxy = createProxy(this.appCtx.pageMgr, {
+      registerPage(pageHandler: PageHandler): Disposable {
+        return appCtx.pageMgr.registerPage({
+          ...pageHandler,
+          pluginId,
+        });
+      },
+    });
+  }
+
+  forPlugin(pluginId: string): AppImpl {
+    return this.appCtx.forPlugin(pluginId);
+  }
+
+  get commands(): CommandManagerImpl {
+    return this.appCtx.commandMgr;
+  }
+
+  get sidebar(): SidebarManagerImpl {
+    return this.appCtx.sidebarMgr;
+  }
+
+  get omnibox(): OmniboxManagerImpl {
+    return this.appCtx.omniboxMgr;
+  }
+
+  get plugins(): PluginManagerImpl {
+    return this.appCtx.pluginMgr;
+  }
+
+  get analytics(): AnalyticsInternal {
+    return this.appCtx.analytics;
+  }
+
+  get pages(): PageManagerImpl {
+    return this.pageMgrProxy;
+  }
+
+  get trace(): TraceImpl | undefined {
+    return this.appCtx.currentTrace?.forPlugin(this.pluginId);
+  }
+
+  scheduleFullRedraw(): void {
+    raf.scheduleFullRedraw();
+  }
+
+  get httpRpc() {
+    return this.appCtx.httpRpc;
+  }
+
+  get initialRouteArgs(): RouteArgs {
+    return this.appCtx.initialRouteArgs;
+  }
+
+  get featureFlags(): FeatureFlagManager {
+    return {
+      register: (settings: FlagSettings) => featureFlags.register(settings),
+    };
+  }
+
+  openTraceFromFile(file: File): void {
+    this.openTrace({type: 'FILE', file});
+  }
+
+  openTraceFromUrl(url: string, serializedAppState?: SerializedAppState) {
+    this.openTrace({type: 'URL', url, serializedAppState});
+  }
+
+  openTraceFromBuffer(postMessageArgs: PostedTrace): void {
+    this.openTrace({type: 'ARRAY_BUFFER', ...postMessageArgs});
+  }
+
+  openTraceFromHttpRpc(): void {
+    this.openTrace({type: 'HTTP_RPC'});
+  }
+
+  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();
+    }
+  }
+
+  // Called by trace_loader.ts soon after it has created a new TraceImpl.
+  setActiveTrace(traceImpl: TraceImpl) {
+    this.appCtx.setActiveTrace(traceImpl.__traceCtxForApp);
+  }
+
+  get embeddedMode(): boolean {
+    return this.appCtx.embeddedMode;
+  }
+
+  get testingMode(): boolean {
+    return this.appCtx.testingMode;
+  }
+
+  get isLoadingTrace() {
+    return this.appCtx.isLoadingTrace;
+  }
+
+  get extraSqlPackages(): SqlPackage[] {
+    return this.appCtx.extraSqlPackages;
+  }
+
+  get perfDebugging(): boolean {
+    return this.appCtx.perfDebugging;
+  }
+
+  setPerfDebuggingEnabled(enabled: boolean) {
+    this.appCtx.perfDebugging = enabled;
+    setPerfHooks(
+      () => this.perfDebugging,
+      () => this.setPerfDebuggingEnabled(!this.perfDebugging),
+    );
+    raf.scheduleFullRedraw();
+  }
+
+  get serviceWorkerController(): ServiceWorkerController {
+    return this.appCtx.serviceWorkerController;
+  }
+
+  // Nothing other than TraceImpl's constructor should ever refer to this.
+  // This is necessary to avoid circular dependencies between trace_impl.ts
+  // and app_impl.ts.
+  get __appCtxForTrace() {
+    return this.appCtx;
+  }
+
+  navigate(newHash: string): void {
+    Router.navigate(newHash);
+  }
+}
diff --git a/ui/src/core/cache_manager.ts b/ui/src/core/cache_manager.ts
new file mode 100644
index 0000000..7dd9cd0
--- /dev/null
+++ b/ui/src/core/cache_manager.ts
@@ -0,0 +1,197 @@
+// 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.
+
+/**
+ * This file deals with caching traces in the browser's Cache storage. The
+ * traces are cached so that the UI can gracefully reload a trace when the tab
+ * containing it is discarded by Chrome (e.g. because the tab was not used for
+ * a long time) or when the user accidentally hits reload.
+ */
+import {TraceArrayBufferSource, TraceSource} from './trace_source';
+
+const TRACE_CACHE_NAME = 'cached_traces';
+const TRACE_CACHE_SIZE = 10;
+
+let LAZY_CACHE: Cache | undefined = undefined;
+
+async function getCache(): Promise<Cache | undefined> {
+  if (self.caches === undefined) {
+    // The browser doesn't support cache storage or the page is opened from
+    // a non-secure origin.
+    return undefined;
+  }
+  if (LAZY_CACHE !== undefined) {
+    return LAZY_CACHE;
+  }
+  LAZY_CACHE = await caches.open(TRACE_CACHE_NAME);
+  return LAZY_CACHE;
+}
+
+async function cacheDelete(key: Request): Promise<boolean> {
+  try {
+    const cache = await getCache();
+    if (cache === undefined) return false; // Cache storage not supported.
+    return await cache.delete(key);
+  } catch (_) {
+    // TODO(288483453): Reinstate:
+    // return ignoreCacheUnactionableErrors(e, false);
+    return false;
+  }
+}
+
+async function cachePut(key: string, value: Response): Promise<void> {
+  try {
+    const cache = await getCache();
+    if (cache === undefined) return; // Cache storage not supported.
+    await cache.put(key, value);
+  } catch (_) {
+    // TODO(288483453): Reinstate:
+    // ignoreCacheUnactionableErrors(e, undefined);
+  }
+}
+
+async function cacheMatch(
+  key: Request | string,
+): Promise<Response | undefined> {
+  try {
+    const cache = await getCache();
+    if (cache === undefined) return undefined; // Cache storage not supported.
+    return await cache.match(key);
+  } catch (_) {
+    // TODO(288483453): Reinstate:
+    // ignoreCacheUnactionableErrors(e, undefined);
+    return undefined;
+  }
+}
+
+async function cacheKeys(): Promise<readonly Request[]> {
+  try {
+    const cache = await getCache();
+    if (cache === undefined) return []; // Cache storage not supported.
+    return await cache.keys();
+  } catch (e) {
+    // TODO(288483453): Reinstate:
+    // return ignoreCacheUnactionableErrors(e, []);
+    return [];
+  }
+}
+
+export async function cacheTrace(
+  traceSource: TraceSource,
+  traceUuid: string,
+): Promise<boolean> {
+  let trace;
+  let title = '';
+  let fileName = '';
+  let url = '';
+  let contentLength = 0;
+  let localOnly = false;
+  switch (traceSource.type) {
+    case 'ARRAY_BUFFER':
+      trace = traceSource.buffer;
+      title = traceSource.title;
+      fileName = traceSource.fileName ?? '';
+      url = traceSource.url ?? '';
+      contentLength = traceSource.buffer.byteLength;
+      localOnly = traceSource.localOnly || false;
+      break;
+    case 'FILE':
+      trace = await traceSource.file.arrayBuffer();
+      title = traceSource.file.name;
+      contentLength = traceSource.file.size;
+      break;
+    default:
+      return false;
+  }
+
+  const headers = new Headers([
+    ['x-trace-title', encodeURI(title)],
+    ['x-trace-url', url],
+    ['x-trace-filename', fileName],
+    ['x-trace-local-only', `${localOnly}`],
+    ['content-type', 'application/octet-stream'],
+    ['content-length', `${contentLength}`],
+    [
+      'expires',
+      // Expires in a week from now (now = upload time)
+      new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 7).toUTCString(),
+    ],
+  ]);
+  await deleteStaleEntries();
+  await cachePut(
+    `/_${TRACE_CACHE_NAME}/${traceUuid}`,
+    new Response(trace, {headers}),
+  );
+  return true;
+}
+
+export async function tryGetTrace(
+  traceUuid: string,
+): Promise<TraceArrayBufferSource | undefined> {
+  await deleteStaleEntries();
+  const response = await cacheMatch(`/_${TRACE_CACHE_NAME}/${traceUuid}`);
+
+  if (!response) return undefined;
+  return {
+    type: 'ARRAY_BUFFER',
+    buffer: await response.arrayBuffer(),
+    title: decodeURI(response.headers.get('x-trace-title') ?? ''),
+    fileName: response.headers.get('x-trace-filename') ?? undefined,
+    url: response.headers.get('x-trace-url') ?? undefined,
+    uuid: traceUuid,
+    localOnly: response.headers.get('x-trace-local-only') === 'true',
+  };
+}
+
+async function deleteStaleEntries() {
+  // Loop through stored traces and invalidate all but the most recent
+  // TRACE_CACHE_SIZE.
+  const keys = await cacheKeys();
+  const storedTraces: Array<{key: Request; date: Date}> = [];
+  const now = new Date();
+  const deletions = [];
+  for (const key of keys) {
+    const existingTrace = await cacheMatch(key);
+    if (existingTrace === undefined) {
+      continue;
+    }
+    const expires = existingTrace.headers.get('expires');
+    if (expires === undefined || expires === null) {
+      // Missing `expires`, so give up and delete which is better than
+      // keeping it around forever.
+      deletions.push(cacheDelete(key));
+      continue;
+    }
+    const expiryDate = new Date(expires);
+    if (expiryDate < now) {
+      deletions.push(cacheDelete(key));
+    } else {
+      storedTraces.push({key, date: expiryDate});
+    }
+  }
+
+  // Sort the traces descending by time, such that most recent ones are placed
+  // at the beginning. Then, take traces from TRACE_CACHE_SIZE onwards and
+  // delete them from cache.
+  const oldTraces = storedTraces
+    .sort((a, b) => b.date.getTime() - a.date.getTime())
+    .slice(TRACE_CACHE_SIZE);
+  for (const oldTrace of oldTraces) {
+    deletions.push(cacheDelete(oldTrace.key));
+  }
+
+  // TODO(hjd): Wrong Promise.all here, should use the one that
+  // ignores failures but need to upgrade TypeScript for that.
+  await Promise.all(deletions);
+}
diff --git a/ui/src/core/channels.ts b/ui/src/core/channels.ts
new file mode 100644
index 0000000..22cf8e0
--- /dev/null
+++ b/ui/src/core/channels.ts
@@ -0,0 +1,49 @@
+// 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 {raf} from './raf_scheduler';
+
+export const DEFAULT_CHANNEL = 'stable';
+const CHANNEL_KEY = 'perfettoUiChannel';
+
+let currentChannel: string | undefined = undefined;
+let nextChannel: string | undefined = undefined;
+
+// This is the channel the UI is currently running. It doesn't change once the
+// UI has been loaded.
+export function getCurrentChannel(): string {
+  if (currentChannel === undefined) {
+    currentChannel = localStorage.getItem(CHANNEL_KEY) ?? DEFAULT_CHANNEL;
+  }
+  return currentChannel;
+}
+
+// This is the channel that will be applied on reload.
+export function getNextChannel(): string {
+  if (nextChannel !== undefined) {
+    return nextChannel;
+  }
+  return getCurrentChannel();
+}
+
+export function channelChanged(): boolean {
+  return getCurrentChannel() !== getNextChannel();
+}
+
+export function setChannel(channel: string): void {
+  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.ts b/ui/src/core/color.ts
deleted file mode 100644
index 9bfc65f..0000000
--- a/ui/src/core/color.ts
+++ /dev/null
@@ -1,290 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {hsluvToRgb} from 'hsluv';
-
-import {clamp} from '../base/math_utils';
-
-// This file contains a library for working with colors in various color spaces
-// and formats.
-
-const LIGHTNESS_MIN = 0;
-const LIGHTNESS_MAX = 100;
-
-const SATURATION_MIN = 0;
-const SATURATION_MAX = 100;
-
-// Most color formats can be defined using 3 numbers in a standardized order, so
-// this tuple serves as a compact way to store various color formats.
-// E.g. HSL, RGB
-type ColorTuple = [number, number, number];
-
-// Definition of an HSL color with named fields.
-interface HSL {
-  readonly h: number; // 0-360
-  readonly s: number; // 0-100
-  readonly l: number; // 0-100
-}
-
-// Defines an interface to an immutable color object, which can be defined in
-// any arbitrary format or color space and provides function to modify the color
-// and conversions to CSS compatible style strings.
-// Because this color object is effectively immutable, a new color object is
-// returned when modifying the color, rather than editing the current object
-// in-place.
-// Also, because these objects are immutable, it's expected that readonly
-// properties such as |cssString| are efficient, as they can be computed at
-// creation time, so they may be used in the hot path (render loop).
-export interface Color {
-  readonly cssString: string;
-
-  // The perceived brightness of the color using a weighted average of the
-  // r, g and b channels based on human perception.
-  readonly perceivedBrightness: number;
-
-  // Bring up the lightness by |percent| percent.
-  lighten(percent: number, max?: number): Color;
-
-  // Bring down the lightness by |percent| percent.
-  darken(percent: number, min?: number): Color;
-
-  // Bring up the saturation by |percent| percent.
-  saturate(percent: number, max?: number): Color;
-
-  // Bring down the saturation by |percent| percent.
-  desaturate(percent: number, min?: number): Color;
-
-  // Set one or more HSL values.
-  setHSL(hsl: Partial<HSL>): Color;
-
-  setAlpha(alpha: number | undefined): Color;
-}
-
-// Common base class for HSL colors. Avoids code duplication.
-abstract class HSLColorBase<T extends Color> {
-  readonly hsl: ColorTuple;
-  readonly alpha?: number;
-
-  // Values are in the range:
-  // Hue:        0-360
-  // Saturation: 0-100
-  // Lightness:  0-100
-  // Alpha:      0-1
-  constructor(init: ColorTuple | HSL | string, alpha?: number) {
-    if (Array.isArray(init)) {
-      this.hsl = init;
-    } else if (typeof init === 'string') {
-      const rgb = hexToRgb(init);
-      this.hsl = rgbToHsl(rgb);
-    } else {
-      this.hsl = [init.h, init.s, init.l];
-    }
-    this.alpha = alpha;
-  }
-
-  // Subclasses should implement this to teach the base class how to create a
-  // new object of the subclass type.
-  abstract create(hsl: ColorTuple | HSL, alpha?: number): T;
-
-  lighten(amount: number, max = LIGHTNESS_MAX): T {
-    const [h, s, l] = this.hsl;
-    const newLightness = clamp(l + amount, LIGHTNESS_MIN, max);
-    return this.create([h, s, newLightness], this.alpha);
-  }
-
-  darken(amount: number, min = LIGHTNESS_MIN): T {
-    const [h, s, l] = this.hsl;
-    const newLightness = clamp(l - amount, min, LIGHTNESS_MAX);
-    return this.create([h, s, newLightness], this.alpha);
-  }
-
-  saturate(amount: number, max = SATURATION_MAX): T {
-    const [h, s, l] = this.hsl;
-    const newSaturation = clamp(s + amount, SATURATION_MIN, max);
-    return this.create([h, newSaturation, l], this.alpha);
-  }
-
-  desaturate(amount: number, min = SATURATION_MIN): T {
-    const [h, s, l] = this.hsl;
-    const newSaturation = clamp(s - amount, min, SATURATION_MAX);
-    return this.create([h, newSaturation, l], this.alpha);
-  }
-
-  setHSL(hsl: Partial<HSL>): T {
-    const [h, s, l] = this.hsl;
-    return this.create({h, s, l, ...hsl}, this.alpha);
-  }
-
-  setAlpha(alpha: number | undefined): T {
-    return this.create(this.hsl, alpha);
-  }
-}
-
-// Describes a color defined in standard HSL color space.
-export class HSLColor extends HSLColorBase<HSLColor> implements Color {
-  readonly cssString: string;
-  readonly perceivedBrightness: number;
-
-  // Values are in the range:
-  // Hue:        0-360
-  // Saturation: 0-100
-  // Lightness:  0-100
-  // Alpha:      0-1
-  constructor(hsl: ColorTuple | HSL | string, alpha?: number) {
-    super(hsl, alpha);
-
-    const [r, g, b] = hslToRgb(...this.hsl);
-
-    this.perceivedBrightness = perceivedBrightness(r, g, b);
-
-    if (this.alpha === undefined) {
-      this.cssString = `rgb(${r} ${g} ${b})`;
-    } else {
-      this.cssString = `rgb(${r} ${g} ${b} / ${this.alpha})`;
-    }
-  }
-
-  create(values: ColorTuple | HSL, alpha?: number | undefined): HSLColor {
-    return new HSLColor(values, alpha);
-  }
-}
-
-// Describes a color defined in HSLuv color space.
-// See: https://www.hsluv.org/
-export class HSLuvColor extends HSLColorBase<HSLuvColor> implements Color {
-  readonly cssString: string;
-  readonly perceivedBrightness: number;
-
-  constructor(hsl: ColorTuple | HSL, alpha?: number) {
-    super(hsl, alpha);
-
-    const rgb = hsluvToRgb(this.hsl);
-    const r = Math.floor(rgb[0] * 255);
-    const g = Math.floor(rgb[1] * 255);
-    const b = Math.floor(rgb[2] * 255);
-
-    this.perceivedBrightness = perceivedBrightness(r, g, b);
-
-    if (this.alpha === undefined) {
-      this.cssString = `rgb(${r} ${g} ${b})`;
-    } else {
-      this.cssString = `rgb(${r} ${g} ${b} / ${this.alpha})`;
-    }
-  }
-
-  create(raw: ColorTuple | HSL, alpha?: number | undefined): HSLuvColor {
-    return new HSLuvColor(raw, alpha);
-  }
-}
-
-// Hue: 0-360
-// Saturation: 0-100
-// Lightness: 0-100
-// RGB: 0-255
-export function hslToRgb(h: number, s: number, l: number): ColorTuple {
-  h = h;
-  s = s / SATURATION_MAX;
-  l = l / LIGHTNESS_MAX;
-
-  const c = (1 - Math.abs(2 * l - 1)) * s;
-  const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
-  const m = l - c / 2;
-
-  let [r, g, b] = [0, 0, 0];
-
-  if (0 <= h && h < 60) {
-    [r, g, b] = [c, x, 0];
-  } else if (60 <= h && h < 120) {
-    [r, g, b] = [x, c, 0];
-  } else if (120 <= h && h < 180) {
-    [r, g, b] = [0, c, x];
-  } else if (180 <= h && h < 240) {
-    [r, g, b] = [0, x, c];
-  } else if (240 <= h && h < 300) {
-    [r, g, b] = [x, 0, c];
-  } else if (300 <= h && h < 360) {
-    [r, g, b] = [c, 0, x];
-  }
-
-  // Convert to 0-255 range
-  r = Math.round((r + m) * 255);
-  g = Math.round((g + m) * 255);
-  b = Math.round((b + m) * 255);
-
-  return [r, g, b];
-}
-
-export function hexToRgb(hex: string): ColorTuple {
-  // Convert hex to RGB first
-  let r: number = 0;
-  let g: number = 0;
-  let b: number = 0;
-
-  if (hex.length === 4) {
-    r = parseInt(hex[1] + hex[1], 16);
-    g = parseInt(hex[2] + hex[2], 16);
-    b = parseInt(hex[3] + hex[3], 16);
-  } else if (hex.length === 7) {
-    r = parseInt(hex.substring(1, 3), 16);
-    g = parseInt(hex.substring(3, 5), 16);
-    b = parseInt(hex.substring(5, 7), 16);
-  }
-
-  return [r, g, b];
-}
-
-export function rgbToHsl(rgb: ColorTuple): ColorTuple {
-  let [r, g, b] = rgb;
-  r /= 255;
-  g /= 255;
-  b /= 255;
-  const max = Math.max(r, g, b);
-  const min = Math.min(r, g, b);
-  let h: number = (max + min) / 2;
-  let s: number = (max + min) / 2;
-  const l: number = (max + min) / 2;
-
-  if (max === min) {
-    h = s = 0; // achromatic
-  } else {
-    const d = max - min;
-    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
-    switch (max) {
-      case r:
-        h = (g - b) / d + (g < b ? 6 : 0);
-        break;
-      case g:
-        h = (b - r) / d + 2;
-        break;
-      case b:
-        h = (r - g) / d + 4;
-        break;
-    }
-    h /= 6;
-  }
-
-  return [h * 360, s * 100, l * 100];
-}
-
-// Return the perceived brightness of a color using a weighted average of the
-// r, g and b channels based on human perception.
-function perceivedBrightness(r: number, g: number, b: number): number {
-  // YIQ calculation from https://24ways.org/2010/calculating-color-contrast
-  return (r * 299 + g * 587 + b * 114) / 1000;
-}
-
-// Comparison function used for sorting colors.
-export function colorCompare(a: Color, b: Color): number {
-  return a.cssString.localeCompare(b.cssString);
-}
diff --git a/ui/src/core/color_unittest.ts b/ui/src/core/color_unittest.ts
index 1b6d67c..acc5cb5 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 './color';
+import {HSLColor, hslToRgb, HSLuvColor} from '../public/color';
 
 describe('HSLColor', () => {
   const col = new HSLColor({h: 123, s: 66, l: 45});
diff --git a/ui/src/core/colorizer.ts b/ui/src/core/colorizer.ts
deleted file mode 100644
index 3f0d1b3..0000000
--- a/ui/src/core/colorizer.ts
+++ /dev/null
@@ -1,250 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {hsl} from 'color-convert';
-
-import {hash} from './hash';
-import {featureFlags} from './feature_flags';
-
-import {Color, HSLColor, HSLuvColor} from './color';
-
-// 128 would provide equal weighting between dark and light text.
-// However, we want to prefer light text for stylistic reasons.
-// A higher value means color must be brighter before switching to dark text.
-const PERCEIVED_BRIGHTNESS_LIMIT = 180;
-
-// This file defines some opinionated colors and provides functions to access
-// random but predictable colors based on a seed, as well as standardized ways
-// to access colors for core objects such as slices and thread states.
-
-// We have, over the years, accumulated a number of different color palettes
-// which are used for different parts of the UI.
-// It would be nice to combine these into a single palette in the future, but
-// changing colors is difficult especially for slice colors, as folks get used
-// to certain slices being certain colors and are resistant to change.
-// However we do it, we should make it possible for folks to switch back the a
-// previous palette, or define their own.
-
-const USE_CONSISTENT_COLORS = featureFlags.register({
-  id: 'useConsistentColors',
-  name: 'Use common color palette for timeline elements',
-  description: 'Use the same color palette for all timeline elements.',
-  defaultValue: false,
-});
-
-// |ColorScheme| defines a collection of colors which can be used for various UI
-// elements. In the future we would expand this interface to include light and
-// dark variants.
-export interface ColorScheme {
-  // The base color to be used for the bulk of the element.
-  readonly base: Color;
-
-  // A variant on the base color, commonly used for highlighting.
-  readonly variant: Color;
-
-  // Grayed out color to represent a disabled state.
-  readonly disabled: Color;
-
-  // Appropriate colors for text to be displayed on top of the above colors.
-  readonly textBase: Color;
-  readonly textVariant: Color;
-  readonly textDisabled: Color;
-}
-
-const MD_PALETTE_RAW: Color[] = [
-  new HSLColor({h: 4, s: 90, l: 58}),
-  new HSLColor({h: 340, s: 82, l: 52}),
-  new HSLColor({h: 291, s: 64, l: 42}),
-  new HSLColor({h: 262, s: 52, l: 47}),
-  new HSLColor({h: 231, s: 48, l: 48}),
-  new HSLColor({h: 207, s: 90, l: 54}),
-  new HSLColor({h: 199, s: 98, l: 48}),
-  new HSLColor({h: 187, s: 100, l: 42}),
-  new HSLColor({h: 174, s: 100, l: 29}),
-  new HSLColor({h: 122, s: 39, l: 49}),
-  new HSLColor({h: 88, s: 50, l: 53}),
-  new HSLColor({h: 66, s: 70, l: 54}),
-  new HSLColor({h: 45, s: 100, l: 51}),
-  new HSLColor({h: 36, s: 100, l: 50}),
-  new HSLColor({h: 14, s: 100, l: 57}),
-  new HSLColor({h: 16, s: 25, l: 38}),
-  new HSLColor({h: 200, s: 18, l: 46}),
-  new HSLColor({h: 54, s: 100, l: 62}),
-];
-
-const WHITE_COLOR = new HSLColor([0, 0, 100]);
-const BLACK_COLOR = new HSLColor([0, 0, 0]);
-const GRAY_COLOR = new HSLColor([0, 0, 90]);
-
-const MD_PALETTE: ColorScheme[] = MD_PALETTE_RAW.map((color): ColorScheme => {
-  const base = color.lighten(10, 60).desaturate(20);
-  const variant = base.lighten(30, 80).desaturate(20);
-
-  return {
-    base,
-    variant,
-    disabled: GRAY_COLOR,
-    textBase: WHITE_COLOR, // White text suits MD colors quite well
-    textVariant: WHITE_COLOR,
-    textDisabled: WHITE_COLOR, // Low contrast is on purpose
-  };
-});
-
-// Create a color scheme based on a single color, which defines the variant
-// color as a slightly darker and more saturated version of the base color.
-export function makeColorScheme(base: Color, variant?: Color): ColorScheme {
-  variant = variant ?? base.darken(15).saturate(15);
-
-  return {
-    base,
-    variant,
-    disabled: GRAY_COLOR,
-    textBase:
-      base.perceivedBrightness >= PERCEIVED_BRIGHTNESS_LIMIT
-        ? BLACK_COLOR
-        : WHITE_COLOR,
-    textVariant:
-      variant.perceivedBrightness >= PERCEIVED_BRIGHTNESS_LIMIT
-        ? BLACK_COLOR
-        : WHITE_COLOR,
-    textDisabled: WHITE_COLOR, // Low contrast is on purpose
-  };
-}
-
-const GRAY = makeColorScheme(new HSLColor([0, 0, 62]));
-const DESAT_RED = makeColorScheme(new HSLColor([3, 30, 49]));
-const DARK_GREEN = makeColorScheme(new HSLColor([120, 44, 34]));
-const LIME_GREEN = makeColorScheme(new HSLColor([75, 55, 47]));
-const TRANSPARENT_WHITE = makeColorScheme(new HSLColor([0, 1, 97], 0.55));
-const ORANGE = makeColorScheme(new HSLColor([36, 100, 50]));
-const INDIGO = makeColorScheme(new HSLColor([231, 48, 48]));
-
-// A piece of wisdom from a long forgotten blog post: "Don't make
-// colors you want to change something normal like grey."
-export const UNEXPECTED_PINK = makeColorScheme(new HSLColor([330, 100, 70]));
-
-// Selects a predictable color scheme from a palette of material design colors,
-// based on a string seed.
-function materialColorScheme(seed: string): ColorScheme {
-  const colorIdx = hash(seed, MD_PALETTE.length);
-  return MD_PALETTE[colorIdx];
-}
-
-const proceduralColorCache = new Map<string, ColorScheme>();
-
-// Procedurally generates a predictable color scheme based on a string seed.
-function proceduralColorScheme(seed: string): ColorScheme {
-  const colorScheme = proceduralColorCache.get(seed);
-  if (colorScheme) {
-    return colorScheme;
-  } else {
-    const hue = hash(seed, 360);
-    // Saturation 100 would give the most differentiation between colors, but
-    // it's garish.
-    const saturation = 80;
-
-    // Prefer using HSLuv, not the browser's built-in vanilla HSL handling. This
-    // is because this function chooses hue/lightness uniform at random, but HSL
-    // is not perceptually uniform.
-    // See https://www.boronine.com/2012/03/26/Color-Spaces-for-Human-Beings/.
-    const base = new HSLuvColor({
-      h: hue,
-      s: saturation,
-      l: hash(seed + 'x', 40) + 40,
-    });
-    const variant = new HSLuvColor({h: hue, s: saturation, l: 30});
-    const colorScheme = makeColorScheme(base, variant);
-
-    proceduralColorCache.set(seed, colorScheme);
-
-    return colorScheme;
-  }
-}
-
-export function colorForState(state: string): ColorScheme {
-  if (state === 'Running') {
-    return DARK_GREEN;
-  } else if (state.startsWith('Runnable')) {
-    return LIME_GREEN;
-  } else if (state.includes('Uninterruptible Sleep')) {
-    if (state.includes('non-IO')) {
-      return DESAT_RED;
-    }
-    return ORANGE;
-  } else if (state.includes('Dead')) {
-    return GRAY;
-  } else if (state.includes('Sleeping') || state.includes('Idle')) {
-    return TRANSPARENT_WHITE;
-  }
-  return INDIGO;
-}
-
-export function colorForTid(tid: number): ColorScheme {
-  return materialColorScheme(tid.toString());
-}
-
-export function colorForThread(thread?: {
-  pid?: number;
-  tid: number;
-}): ColorScheme {
-  if (thread === undefined) {
-    return GRAY;
-  }
-  const tid = thread.pid ?? thread.tid;
-  return colorForTid(tid);
-}
-
-export function colorForCpu(cpu: number): Color {
-  if (USE_CONSISTENT_COLORS.get()) {
-    return materialColorScheme(cpu.toString()).base;
-  } else {
-    const hue = (128 + 32 * cpu) % 256;
-    return new HSLColor({h: hue, s: 50, l: 50});
-  }
-}
-
-export function randomColor(): string {
-  const rand = Math.random();
-  if (USE_CONSISTENT_COLORS.get()) {
-    return materialColorScheme(rand.toString()).base.cssString;
-  } else {
-    // 40 different random hues 9 degrees apart.
-    const hue = Math.floor(rand * 40) * 9;
-    return '#' + hsl.hex([hue, 90, 30]);
-  }
-}
-
-export function getColorForSlice(sliceName: string): ColorScheme {
-  const name = sliceName.replace(/( )?\d+/g, '');
-  if (USE_CONSISTENT_COLORS.get()) {
-    return materialColorScheme(name);
-  } else {
-    return proceduralColorScheme(name);
-  }
-}
-
-export function colorForFtrace(name: string): ColorScheme {
-  return materialColorScheme(name);
-}
-
-export function colorForSample(callsiteId: number, isHovered: boolean): string {
-  let colorScheme;
-  if (USE_CONSISTENT_COLORS.get()) {
-    colorScheme = materialColorScheme(String(callsiteId));
-  } else {
-    colorScheme = proceduralColorScheme(String(callsiteId));
-  }
-
-  return isHovered ? colorScheme.variant.cssString : colorScheme.base.cssString;
-}
diff --git a/ui/src/core/command_manager.ts b/ui/src/core/command_manager.ts
new file mode 100644
index 0000000..fdf6ee6
--- /dev/null
+++ b/ui/src/core/command_manager.ts
@@ -0,0 +1,57 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {FuzzyFinder, FuzzySegment} from '../base/fuzzy';
+import {Registry} from '../base/registry';
+import {Command, CommandManager} from '../public/command';
+
+export interface CommandWithMatchInfo extends Command {
+  segments: FuzzySegment[];
+}
+
+export class CommandManagerImpl implements CommandManager {
+  private readonly registry = new Registry<Command>((cmd) => cmd.id);
+
+  getCommand(commandId: string): Command {
+    return this.registry.get(commandId);
+  }
+
+  hasCommand(commandId: string): boolean {
+    return this.registry.has(commandId);
+  }
+
+  get commands(): Command[] {
+    return Array.from(this.registry.values());
+  }
+
+  registerCommand(cmd: Command): Disposable {
+    return this.registry.register(cmd);
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  runCommand(id: string, ...args: any[]): any {
+    const cmd = this.registry.get(id);
+    return cmd.callback(...args);
+  }
+
+  // Returns a list of commands that match the search term, along with a list
+  // of segments which describe which parts of the command name match and
+  // which don't.
+  fuzzyFilterCommands(searchTerm: string): CommandWithMatchInfo[] {
+    const finder = new FuzzyFinder(this.commands, ({name}) => name);
+    return finder.find(searchTerm).map((result) => {
+      return {segments: result.segments, ...result.item};
+    });
+  }
+}
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index ab57814..4791e8e 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -21,45 +21,56 @@
 // - Not directly rely on any other plugins.
 // - Be approved by one of Perfetto UI owners.
 export const defaultPlugins = [
+  'com.android.GpuWorkPeriod',
+  'com.google.PixelCpmTrace',
   'com.google.PixelMemory',
   'dev.perfetto.AndroidBinderVizPlugin',
   'dev.perfetto.AndroidClientServer',
   'dev.perfetto.AndroidCujs',
+  'dev.perfetto.AndroidDmabuf',
+  'dev.perfetto.AndroidLog',
   'dev.perfetto.AndroidLongBatteryTracing',
   'dev.perfetto.AndroidNetwork',
   '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',
+  'dev.perfetto.CriticalPath',
+  'dev.perfetto.DebugTracks',
+  'dev.perfetto.DeeplinkQuerystring',
+  'dev.perfetto.FlagsPage',
+  'dev.perfetto.Frames',
+  'dev.perfetto.Ftrace',
+  'dev.perfetto.HeapProfile',
   'dev.perfetto.LargeScreensPerf',
+  'dev.perfetto.MetricsPage',
+  'dev.perfetto.PerfSamplesProfile',
   'dev.perfetto.PinAndroidPerfMetrics',
   'dev.perfetto.PinSysUITracks',
+  'dev.perfetto.Process',
+  'dev.perfetto.ProcessSummary',
+  'dev.perfetto.ProcessThreadGroups',
+  'dev.perfetto.QueryPage',
+  'dev.perfetto.RecordTrace',
   'dev.perfetto.RestorePinnedTrack',
+  'dev.perfetto.Sched',
+  'dev.perfetto.Screenshots',
+  'dev.perfetto.Thread',
+  'dev.perfetto.ThreadState',
   'dev.perfetto.TimelineSync',
+  'dev.perfetto.TraceInfoPage',
   'dev.perfetto.TraceMetadata',
-  'org.kernel.LinuxKernelDevices',
-  'perfetto.AndroidLog',
-  'perfetto.Annotation',
-  'perfetto.AsyncSlices',
-  'perfetto.ChromeScrollJank',
+  'dev.perfetto.VizPage',
+  'org.chromium.CriticalUserInteraction',
+  'org.kernel.LinuxKernelSubsystems',
+  'org.kernel.SuspendResumeLatency',
   'perfetto.CoreCommands',
-  'perfetto.Counter',
-  'perfetto.CpuFreq',
-  'perfetto.CpuProfile',
-  'perfetto.CpuSlices',
-  'perfetto.CriticalUserInteraction',
-  'perfetto.DebugTracks',
   'perfetto.ExampleTraces',
-  'perfetto.Flows',
-  'perfetto.Frames',
-  'perfetto.FtraceRaw',
-  'perfetto.HeapProfile',
-  'perfetto.PerfSamplesProfile',
-  'perfetto.PivotTable',
-  'perfetto.ProcessSummary',
-  'perfetto.Sched',
-  'perfetto.Screenshots',
-  'perfetto.ThreadSlices',
-  'perfetto.ThreadState',
+  'perfetto.GlobalGroups',
   'perfetto.TrackUtils',
 ];
diff --git a/ui/src/core/fake_trace_impl.ts b/ui/src/core/fake_trace_impl.ts
new file mode 100644
index 0000000..fc944db
--- /dev/null
+++ b/ui/src/core/fake_trace_impl.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 {Time} from '../base/time';
+import {EngineBase} from '../trace_processor/engine';
+import {AppImpl} from './app_impl';
+import {TraceImpl} from './trace_impl';
+import {TraceInfoImpl} from './trace_info_impl';
+
+export interface FakeTraceImplArgs {
+  // If true suppresses exceptions when trying to issue a query. This is to
+  // catch bugs where we are trying to query an empty instance. However some
+  // unittests need to do so. Default: false.
+  allowQueries?: boolean;
+}
+
+let appImplInitialized = false;
+
+export function initializeAppImplForTesting(): AppImpl {
+  if (!appImplInitialized) {
+    appImplInitialized = true;
+    AppImpl.initialize({initialRouteArgs: {}});
+  }
+  return AppImpl.instance;
+}
+
+// For testing purposes only.
+export function createFakeTraceImpl(args: FakeTraceImplArgs = {}) {
+  initializeAppImplForTesting();
+  const fakeTraceInfo: TraceInfoImpl = {
+    source: {type: 'URL', url: ''},
+    traceTitle: '',
+    traceUrl: '',
+    start: Time.fromSeconds(0),
+    end: Time.fromSeconds(10),
+    realtimeOffset: Time.ZERO,
+    utcOffset: Time.ZERO,
+    traceTzOffset: Time.ZERO,
+    cpus: [],
+    importErrors: 0,
+    traceType: 'proto',
+    hasFtrace: false,
+    uuid: '',
+    cached: false,
+    downloadable: false,
+  };
+  return TraceImpl.createInstanceForCore(
+    AppImpl.instance,
+    new FakeEngine(args.allowQueries ?? false),
+    fakeTraceInfo,
+  );
+}
+
+class FakeEngine extends EngineBase {
+  readonly mode = 'WASM';
+  id: string = 'TestEngine';
+
+  constructor(private allowQueries: boolean) {
+    super();
+  }
+
+  rpcSendRequestBytes(_data: Uint8Array) {
+    if (!this.allowQueries) {
+      throw new Error(
+        'FakeEngine.query() should never be reached. ' +
+          'If this is a unittest, try adding {allowQueries: true} to the ' +
+          'createFakeTraceImpl() call.',
+      );
+    }
+  }
+
+  [Symbol.dispose]() {}
+}
diff --git a/ui/src/core/feature_flags.ts b/ui/src/core/feature_flags.ts
index 0a45c9a..4e60a61 100644
--- a/ui/src/core/feature_flags.ts
+++ b/ui/src/core/feature_flags.ts
@@ -16,20 +16,7 @@
 // ~everywhere and the are "statically" initialized (i.e. files construct Flags
 // at import time) if this file starts importing anything we will quickly run
 // into issues with initialization order which will be a pain.
-
-interface FlagSettings {
-  id: string;
-  defaultValue: boolean;
-  description: string;
-  name?: string;
-  devOnly?: boolean;
-}
-
-export enum OverrideState {
-  DEFAULT = 'DEFAULT',
-  TRUE = 'OVERRIDE_TRUE',
-  FALSE = 'OVERRIDE_FALSE',
-}
+import {Flag, FlagSettings, OverrideState} from '../public/feature_flag';
 
 export interface FlagStore {
   load(): object;
@@ -118,39 +105,21 @@
 
     this.store.save(this.overrides);
   }
-}
 
-export interface Flag {
-  // A unique identifier for this flag ("magicSorting")
-  readonly id: string;
-
-  // The name of the flag the user sees ("New track sorting algorithm")
-  readonly name: string;
-
-  // A longer description which is displayed to the user.
-  // "Sort tracks using an embedded tfLite model based on your expression
-  // while waiting for the trace to load."
-  readonly description: string;
-
-  // Whether the flag defaults to true or false.
-  // If !flag.isOverridden() then flag.get() === flag.defaultValue
-  readonly defaultValue: boolean;
-
-  // Get the current value of the flag.
-  get(): boolean;
-
-  // Override the flag and persist the new value.
-  set(value: boolean): void;
-
-  // If the flag has been overridden.
-  // Note: A flag can be overridden to its default value.
-  isOverridden(): boolean;
-
-  // Reset the flag to its default setting.
-  reset(): void;
-
-  // Get the current state of the flag.
-  overriddenState(): OverrideState;
+  /**
+   * Modify an override at runtime and save it back to local storage.
+   *
+   * This method operates on the raw JSON in local storage and doesn't require
+   * the presence of a flag to work. Thus, it may be called at any point in the
+   * lifecycle of the flags object.
+   *
+   * @param key - The key of the flag to modify.
+   * @param state - The desired state of the flag override.
+   */
+  patchOverride(key: string, state: OverrideState): void {
+    this.overrides[key] = state;
+    this.save();
+  }
 }
 
 class FlagImpl implements Flag {
@@ -233,10 +202,3 @@
 
 export const FlagsForTesting = Flags;
 export const featureFlags = new Flags(new LocalStorageStore());
-
-export const RECORDING_V2_FLAG = featureFlags.register({
-  id: 'recordingv2',
-  name: 'Recording V2',
-  description: 'Record using V2 interface',
-  defaultValue: false,
-});
diff --git a/ui/src/core/flow_manager.ts b/ui/src/core/flow_manager.ts
new file mode 100644
index 0000000..efa35bd
--- /dev/null
+++ b/ui/src/core/flow_manager.ts
@@ -0,0 +1,588 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use size 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 {Time} from '../base/time';
+import {featureFlags} from './feature_flags';
+import {FlowDirection, Flow} from './flow_types';
+import {asSliceSqlId} from '../trace_processor/sql_utils/core_types';
+import {LONG, NUM, STR_NULL} from '../trace_processor/query_result';
+import {
+  ACTUAL_FRAMES_SLICE_TRACK_KIND,
+  SLICE_TRACK_KIND,
+} 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({
+  id: 'showIndirectPrecedingFlows',
+  name: 'Show indirect preceding flows',
+  description:
+    'Show indirect preceding flows (connected through ancestor ' +
+    'slices) when a slice is selected.',
+  defaultValue: false,
+});
+
+export class FlowManager {
+  private _connectedFlows: Flow[] = [];
+  private _selectedFlows: Flow[] = [];
+  private _curSelection?: Selection;
+  private _focusedFlowIdLeft = -1;
+  private _focusedFlowIdRight = -1;
+  private _visibleCategories = new Map<string, boolean>();
+  private _initialized = false;
+
+  constructor(
+    private engine: Engine,
+    private trackMgr: TrackManager,
+    private selectionMgr: SelectionManager,
+  ) {}
+
+  // TODO(primiano): the only reason why this is not done in the constructor is
+  // because when loading the UI with no trace, we initialize globals with a
+  // FakeTraceImpl with a FakeEngine, which crashes when issuing queries.
+  // This can be moved in the ctor once globals go away.
+  private initialize() {
+    if (this._initialized) return;
+    this._initialized = true;
+    // Create |CHROME_CUSTOME_SLICE_NAME| helper, which combines slice name
+    // and args for some slices (scheduler tasks and mojo messages) for more
+    // helpful messages.
+    // In the future, it should be replaced with this a more scalable and
+    // customisable solution.
+    // Note that a function here is significantly faster than a join.
+    this.engine.query(`
+      SELECT CREATE_FUNCTION(
+        'CHROME_CUSTOM_SLICE_NAME(slice_id LONG)',
+        'STRING',
+        'select case
+           when name="Receive mojo message" then
+            printf("Receive mojo message (interface=%s, hash=%s)",
+              EXTRACT_ARG(arg_set_id,
+                          "chrome_mojo_event_info.mojo_interface_tag"),
+              EXTRACT_ARG(arg_set_id, "chrome_mojo_event_info.ipc_hash"))
+           when name="ThreadControllerImpl::RunTask" or
+                name="ThreadPool_RunTask" then
+            printf("RunTask(posted_from=%s:%s)",
+             EXTRACT_ARG(arg_set_id, "task.posted_from.file_name"),
+             EXTRACT_ARG(arg_set_id, "task.posted_from.function_name"))
+         end
+         from slice where id=$slice_id'
+    );`);
+  }
+
+  async queryFlowEvents(query: string, callback: (flows: Flow[]) => void) {
+    const result = await this.engine.query(query);
+    const flows: Flow[] = [];
+
+    const it = result.iter({
+      beginSliceId: NUM,
+      beginTrackId: NUM,
+      beginSliceName: STR_NULL,
+      beginSliceChromeCustomName: STR_NULL,
+      beginSliceCategory: STR_NULL,
+      beginSliceStartTs: LONG,
+      beginSliceEndTs: LONG,
+      beginDepth: NUM,
+      beginThreadName: STR_NULL,
+      beginProcessName: STR_NULL,
+      endSliceId: NUM,
+      endTrackId: NUM,
+      endSliceName: STR_NULL,
+      endSliceChromeCustomName: STR_NULL,
+      endSliceCategory: STR_NULL,
+      endSliceStartTs: LONG,
+      endSliceEndTs: LONG,
+      endDepth: NUM,
+      endThreadName: STR_NULL,
+      endProcessName: STR_NULL,
+      name: STR_NULL,
+      category: STR_NULL,
+      id: NUM,
+      flowToDescendant: NUM,
+    });
+
+    const nullToStr = (s: null | string): string => {
+      return s === null ? 'NULL' : s;
+    };
+
+    const nullToUndefined = (s: null | string): undefined | string => {
+      return s === null ? undefined : s;
+    };
+
+    const nodes = [];
+
+    for (; it.valid(); it.next()) {
+      // Category and name present only in version 1 flow events
+      // It is most likelly NULL for all other versions
+      const category = nullToUndefined(it.category);
+      const name = nullToUndefined(it.name);
+      const id = it.id;
+
+      const begin = {
+        trackId: it.beginTrackId,
+        sliceId: asSliceSqlId(it.beginSliceId),
+        sliceName: nullToStr(it.beginSliceName),
+        sliceChromeCustomName: nullToUndefined(it.beginSliceChromeCustomName),
+        sliceCategory: nullToStr(it.beginSliceCategory),
+        sliceStartTs: Time.fromRaw(it.beginSliceStartTs),
+        sliceEndTs: Time.fromRaw(it.beginSliceEndTs),
+        depth: it.beginDepth,
+        threadName: nullToStr(it.beginThreadName),
+        processName: nullToStr(it.beginProcessName),
+      };
+
+      const end = {
+        trackId: it.endTrackId,
+        sliceId: asSliceSqlId(it.endSliceId),
+        sliceName: nullToStr(it.endSliceName),
+        sliceChromeCustomName: nullToUndefined(it.endSliceChromeCustomName),
+        sliceCategory: nullToStr(it.endSliceCategory),
+        sliceStartTs: Time.fromRaw(it.endSliceStartTs),
+        sliceEndTs: Time.fromRaw(it.endSliceEndTs),
+        depth: it.endDepth,
+        threadName: nullToStr(it.endThreadName),
+        processName: nullToStr(it.endProcessName),
+      };
+
+      nodes.push(begin);
+      nodes.push(end);
+
+      flows.push({
+        id,
+        begin,
+        end,
+        dur: it.endSliceStartTs - it.beginSliceEndTs,
+        category,
+        name,
+        flowToDescendant: !!it.flowToDescendant,
+      });
+    }
+
+    // Everything below here is a horrible hack to support flows for
+    // async slice tracks.
+    // In short the issue is this:
+    // - For most slice tracks there is a one-to-one mapping between
+    //   the track in the UI and the track in the TP. n.b. Even in this
+    //   case the UI 'trackId' and the TP 'track.id' may not be the
+    //   same. In this case 'depth' in the TP is the exact depth in the
+    //   UI.
+    // - In the case of aysnc tracks however the mapping is
+    //   one-to-many. Each async slice track in the UI is 'backed' but
+    //   multiple TP tracks. In order to render this track we need
+    //   to adjust depth to avoid overlapping slices. In the render
+    //   path we use experimental_slice_layout for this purpose. This
+    //   is a virtual table in the TP which, for an arbitrary collection
+    //   of TP trackIds, computes for each slice a 'layout_depth'.
+    // - Everything above in this function and its callers doesn't
+    //   know anything about layout_depth.
+    //
+    // So if we stopped here we would have incorrect rendering for
+    // async slice tracks. Instead we want to 'fix' depth for these
+    // cases. We do this in two passes.
+    // - First we collect all the information we need in 'Info' POJOs
+    // - Secondly we loop over those Infos querying
+    //   the database to find the layout_depth for each sliceId
+    // TODO(hjd): This should not be needed after TracksV2 lands.
+
+    // We end up with one Info POJOs for each UI async slice track
+    // which has at least  one flow {begin,end}ing in one of its slices.
+    interface Info {
+      siblingTrackIds: number[];
+      sliceIds: number[];
+      nodes: Array<{
+        sliceId: number;
+        depth: number;
+      }>;
+    }
+
+    const trackUriToInfo = new Map<string, null | Info>();
+    const trackIdToInfo = new Map<number, null | Info>();
+
+    const trackIdToTrack = new Map<number, TrackDescriptor>();
+    this.trackMgr
+      .getAllTracks()
+      .forEach((trackDescriptor) =>
+        trackDescriptor.tags?.trackIds?.forEach((trackId) =>
+          trackIdToTrack.set(trackId, trackDescriptor),
+        ),
+      );
+
+    const getInfo = (trackId: number): null | Info => {
+      let info = trackIdToInfo.get(trackId);
+      if (info !== undefined) {
+        return info;
+      }
+
+      const trackDescriptor = trackIdToTrack.get(trackId);
+      if (trackDescriptor === undefined) {
+        trackIdToInfo.set(trackId, null);
+        return null;
+      }
+
+      info = trackUriToInfo.get(trackDescriptor.uri);
+      if (info !== undefined) {
+        return info;
+      }
+
+      // If 'trackIds' is undefined this is not an async slice track so
+      // we don't need to do anything. We also don't need to do
+      // anything if there is only one TP track in this async track. In
+      // that case experimental_slice_layout is just an expensive way
+      // to find out depth === layout_depth.
+      const trackIds = trackDescriptor?.tags?.trackIds;
+      if (trackIds === undefined || trackIds.length <= 1) {
+        trackUriToInfo.set(trackDescriptor.uri, null);
+        trackIdToInfo.set(trackId, null);
+        return null;
+      }
+
+      const newInfo = {
+        siblingTrackIds: [...trackIds],
+        sliceIds: [],
+        nodes: [],
+      };
+
+      trackUriToInfo.set(trackDescriptor.uri, newInfo);
+      trackIdToInfo.set(trackId, newInfo);
+
+      return newInfo;
+    };
+
+    // First pass, collect:
+    // - all slices that belong to async slice track
+    // - grouped by the async slice track in question
+    for (const node of nodes) {
+      const info = getInfo(node.trackId);
+      if (info !== null) {
+        info.sliceIds.push(node.sliceId);
+        info.nodes.push(node);
+      }
+    }
+
+    // Second pass, for each async track:
+    // - Query to find the layout_depth for each relevant sliceId
+    // - Iterate through the nodes updating the depth in place
+    for (const info of trackUriToInfo.values()) {
+      if (info === null) {
+        continue;
+      }
+      const r = await this.engine.query(`
+        SELECT
+          id,
+          layout_depth as depth
+        FROM
+          experimental_slice_layout
+        WHERE
+          filter_track_ids = '${info.siblingTrackIds.join(',')}'
+          AND id in (${info.sliceIds.join(', ')})
+      `);
+
+      // Create the sliceId -> new depth map:
+      const it = r.iter({
+        id: NUM,
+        depth: NUM,
+      });
+      const sliceIdToDepth = new Map<number, number>();
+      for (; it.valid(); it.next()) {
+        sliceIdToDepth.set(it.id, it.depth);
+      }
+
+      // For each begin/end from an async track update the depth:
+      for (const node of info.nodes) {
+        const newDepth = sliceIdToDepth.get(node.sliceId);
+        if (newDepth !== undefined) {
+          node.depth = newDepth;
+        }
+      }
+    }
+
+    callback(flows);
+  }
+
+  sliceSelected(sliceId: number) {
+    const connectedFlows = SHOW_INDIRECT_PRECEDING_FLOWS_FLAG.get()
+      ? `(
+           select * from directly_connected_flow(${sliceId})
+           union
+           select * from preceding_flow(${sliceId})
+         )`
+      : `directly_connected_flow(${sliceId})`;
+
+    const query = `
+    -- Include slices.flow to initialise indexes on 'flow.slice_in' and 'flow.slice_out'.
+    INCLUDE PERFETTO MODULE slices.flow;
+
+    select
+      f.slice_out as beginSliceId,
+      t1.track_id as beginTrackId,
+      t1.name as beginSliceName,
+      CHROME_CUSTOM_SLICE_NAME(t1.slice_id) as beginSliceChromeCustomName,
+      t1.category as beginSliceCategory,
+      t1.ts as beginSliceStartTs,
+      (t1.ts+t1.dur) as beginSliceEndTs,
+      t1.depth as beginDepth,
+      (thread_out.name || ' ' || thread_out.tid) as beginThreadName,
+      (process_out.name || ' ' || process_out.pid) as beginProcessName,
+      f.slice_in as endSliceId,
+      t2.track_id as endTrackId,
+      t2.name as endSliceName,
+      CHROME_CUSTOM_SLICE_NAME(t2.slice_id) as endSliceChromeCustomName,
+      t2.category as endSliceCategory,
+      t2.ts as endSliceStartTs,
+      (t2.ts+t2.dur) as endSliceEndTs,
+      t2.depth as endDepth,
+      (thread_in.name || ' ' || thread_in.tid) as endThreadName,
+      (process_in.name || ' ' || process_in.pid) as endProcessName,
+      extract_arg(f.arg_set_id, 'cat') as category,
+      extract_arg(f.arg_set_id, 'name') as name,
+      f.id as id,
+      slice_is_ancestor(t1.slice_id, t2.slice_id) as flowToDescendant
+    from ${connectedFlows} f
+    join slice t1 on f.slice_out = t1.slice_id
+    join slice t2 on f.slice_in = t2.slice_id
+    left join thread_track track_out on track_out.id = t1.track_id
+    left join thread thread_out on thread_out.utid = track_out.utid
+    left join thread_track track_in on track_in.id = t2.track_id
+    left join thread thread_in on thread_in.utid = track_in.utid
+    left join process process_out on process_out.upid = thread_out.upid
+    left join process process_in on process_in.upid = thread_in.upid
+    `;
+    this.queryFlowEvents(query, (flows: Flow[]) =>
+      this.setConnectedFlows(flows),
+    );
+  }
+
+  private areaSelected(area: AreaSelection) {
+    const trackIds: number[] = [];
+
+    for (const trackInfo of area.tracks) {
+      const kind = trackInfo?.tags?.kind;
+      if (
+        kind === SLICE_TRACK_KIND ||
+        kind === ACTUAL_FRAMES_SLICE_TRACK_KIND
+      ) {
+        if (trackInfo?.tags?.trackIds) {
+          for (const trackId of trackInfo.tags.trackIds) {
+            trackIds.push(trackId);
+          }
+        }
+      }
+    }
+
+    const tracks = `(${trackIds.join(',')})`;
+
+    const startNs = area.start;
+    const endNs = area.end;
+
+    const query = `
+    select
+      f.slice_out as beginSliceId,
+      t1.track_id as beginTrackId,
+      t1.name as beginSliceName,
+      CHROME_CUSTOM_SLICE_NAME(t1.slice_id) as beginSliceChromeCustomName,
+      t1.category as beginSliceCategory,
+      t1.ts as beginSliceStartTs,
+      (t1.ts+t1.dur) as beginSliceEndTs,
+      t1.depth as beginDepth,
+      NULL as beginThreadName,
+      NULL as beginProcessName,
+      f.slice_in as endSliceId,
+      t2.track_id as endTrackId,
+      t2.name as endSliceName,
+      CHROME_CUSTOM_SLICE_NAME(t2.slice_id) as endSliceChromeCustomName,
+      t2.category as endSliceCategory,
+      t2.ts as endSliceStartTs,
+      (t2.ts+t2.dur) as endSliceEndTs,
+      t2.depth as endDepth,
+      NULL as endThreadName,
+      NULL as endProcessName,
+      extract_arg(f.arg_set_id, 'cat') as category,
+      extract_arg(f.arg_set_id, 'name') as name,
+      f.id as id,
+      slice_is_ancestor(t1.slice_id, t2.slice_id) as flowToDescendant
+    from flow f
+    join slice t1 on f.slice_out = t1.slice_id
+    join slice t2 on f.slice_in = t2.slice_id
+    where
+      (t1.track_id in ${tracks}
+        and (t1.ts+t1.dur <= ${endNs} and t1.ts+t1.dur >= ${startNs}))
+      or
+      (t2.track_id in ${tracks}
+        and (t2.ts <= ${endNs} and t2.ts >= ${startNs}))
+    `;
+    this.queryFlowEvents(query, (flows: Flow[]) =>
+      this.setSelectedFlows(flows),
+    );
+  }
+
+  private setConnectedFlows(connectedFlows: Flow[]) {
+    this._connectedFlows = connectedFlows;
+    // If a chrome slice is selected and we have any flows in connectedFlows
+    // we will find the flows on the right and left of that slice to set a default
+    // focus. In all other cases the focusedFlowId(Left|Right) will be set to -1.
+    this._focusedFlowIdLeft = -1;
+    this._focusedFlowIdRight = -1;
+    if (this._curSelection?.kind === 'track_event') {
+      const sliceId = this._curSelection.eventId;
+      for (const flow of connectedFlows) {
+        if (flow.begin.sliceId === sliceId) {
+          this._focusedFlowIdRight = flow.id;
+        }
+        if (flow.end.sliceId === sliceId) {
+          this._focusedFlowIdLeft = flow.id;
+        }
+      }
+    }
+    raf.scheduleFullRedraw();
+  }
+
+  private setSelectedFlows(selectedFlows: Flow[]) {
+    this._selectedFlows = selectedFlows;
+    raf.scheduleFullRedraw();
+  }
+
+  updateFlows(selection: Selection) {
+    this.initialize();
+    this._curSelection = selection;
+
+    if (selection.kind === 'empty') {
+      this.setConnectedFlows([]);
+      this.setSelectedFlows([]);
+      return;
+    }
+
+    // TODO(b/155483804): This is a hack as annotation slices don't contain
+    // flows. We should tidy this up when fixing this bug.
+    if (selection.kind === 'track_event' && selection.tableName === 'slice') {
+      this.sliceSelected(selection.eventId);
+    } else {
+      this.setConnectedFlows([]);
+    }
+
+    if (selection.kind === 'area') {
+      this.areaSelected(selection);
+    } else {
+      this.setConnectedFlows([]);
+    }
+  }
+
+  // Change focus to the next flow event (matching the direction)
+  focusOtherFlow(direction: FlowDirection) {
+    const currentSelection = this._curSelection;
+    if (!currentSelection || currentSelection.kind !== 'track_event') {
+      return;
+    }
+    const sliceId = currentSelection.eventId;
+    if (sliceId === -1) {
+      return;
+    }
+
+    const boundFlows = this._connectedFlows.filter(
+      (flow) =>
+        (flow.begin.sliceId === sliceId && direction === 'Forward') ||
+        (flow.end.sliceId === sliceId && direction === 'Backward'),
+    );
+
+    if (direction === 'Backward') {
+      const nextFlowId = findAnotherFlowExcept(
+        boundFlows,
+        this._focusedFlowIdLeft,
+      );
+      this._focusedFlowIdLeft = nextFlowId;
+    } else {
+      const nextFlowId = findAnotherFlowExcept(
+        boundFlows,
+        this._focusedFlowIdRight,
+      );
+      this._focusedFlowIdRight = nextFlowId;
+    }
+    raf.scheduleFullRedraw();
+  }
+
+  // Select the slice connected to the flow in focus
+  moveByFocusedFlow(direction: FlowDirection): void {
+    const currentSelection = this._curSelection;
+    if (!currentSelection || currentSelection.kind !== 'track_event') {
+      return;
+    }
+
+    const sliceId = currentSelection.eventId;
+    const flowId =
+      direction === 'Backward'
+        ? this._focusedFlowIdLeft
+        : this._focusedFlowIdRight;
+
+    if (sliceId === -1 || flowId === -1) {
+      return;
+    }
+
+    // Find flow that is in focus and select corresponding slice
+    for (const flow of this._connectedFlows) {
+      if (flow.id === flowId) {
+        const flowPoint = direction === 'Backward' ? flow.begin : flow.end;
+        this.selectionMgr.selectSqlEvent('slice', flowPoint.sliceId, {
+          scrollToSelection: true,
+        });
+      }
+    }
+  }
+
+  get connectedFlows() {
+    return this._connectedFlows;
+  }
+
+  get selectedFlows() {
+    return this._selectedFlows;
+  }
+
+  get focusedFlowIdLeft() {
+    return this._focusedFlowIdLeft;
+  }
+  get focusedFlowIdRight() {
+    return this._focusedFlowIdRight;
+  }
+
+  get visibleCategories(): ReadonlyMap<string, boolean> {
+    return this._visibleCategories;
+  }
+
+  setCategoryVisible(name: string, value: boolean) {
+    this._visibleCategories.set(name, value);
+    raf.scheduleFullRedraw();
+  }
+}
+
+// Search |boundFlows| for |flowId| and return the id following it.
+// Returns the first flow id if nothing was found or |flowId| was the last flow
+// in |boundFlows|, and -1 if |boundFlows| is empty
+function findAnotherFlowExcept(boundFlows: Flow[], flowId: number): number {
+  let selectedFlowFound = false;
+
+  if (boundFlows.length === 0) {
+    return -1;
+  }
+
+  for (const flow of boundFlows) {
+    if (selectedFlowFound) {
+      return flow.id;
+    }
+
+    if (flow.id === flowId) {
+      selectedFlowFound = true;
+    }
+  }
+  return boundFlows[0].id;
+}
diff --git a/ui/src/core/flow_types.ts b/ui/src/core/flow_types.ts
new file mode 100644
index 0000000..bed9dae
--- /dev/null
+++ b/ui/src/core/flow_types.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 {time, duration} from '../base/time';
+import {SliceSqlId} from '../trace_processor/sql_utils/core_types';
+
+export interface Flow {
+  id: number;
+
+  begin: FlowPoint;
+  end: FlowPoint;
+  dur: duration;
+
+  // Whether this flow connects a slice with its descendant.
+  flowToDescendant: boolean;
+
+  category?: string;
+  name?: string;
+}
+
+export interface FlowPoint {
+  trackId: number;
+
+  sliceName: string;
+  sliceCategory: string;
+  sliceId: SliceSqlId;
+  sliceStartTs: time;
+  sliceEndTs: time;
+  // Thread and process info. Only set in sliceSelected not in areaSelected as
+  // the latter doesn't display per-flow info and it'd be a waste to join
+  // additional tables for undisplayed info in that case. Nothing precludes
+  // adding this in a future iteration however.
+  threadName: string;
+  processName: string;
+
+  depth: number;
+
+  // TODO(altimin): Ideally we should have a generic mechanism for allowing to
+  // customise the name here, but for now we are hardcording a few
+  // Chrome-specific bits in the query here.
+  sliceChromeCustomName?: string;
+}
+
+export type FlowDirection = 'Forward' | 'Backward';
diff --git a/ui/src/core/generic_slice_details_types.ts b/ui/src/core/generic_slice_details_types.ts
deleted file mode 100644
index 186f6e5..0000000
--- a/ui/src/core/generic_slice_details_types.ts
+++ /dev/null
@@ -1,32 +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.
-
-export interface ColumnConfig {
-  displayName?: string;
-}
-
-export type Columns = {
-  [columnName: string]: ColumnConfig;
-};
-
-export interface GenericSliceDetailsTabConfigBase {
-  sqlTableName: string;
-  title: string;
-  // All columns are rendered if |columns| is undefined.
-  columns?: Columns;
-}
-
-export type GenericSliceDetailsTabConfig = GenericSliceDetailsTabConfigBase & {
-  id: number;
-};
diff --git a/ui/src/core/legacy_flamegraph_cache.ts b/ui/src/core/legacy_flamegraph_cache.ts
deleted file mode 100644
index ae37877..0000000
--- a/ui/src/core/legacy_flamegraph_cache.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size 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 {Engine} from '../trace_processor/engine';
-
-export class LegacyFlamegraphCache {
-  private cache: Map<string, string>;
-  private prefix: string;
-  private tableId: number;
-  private cacheSizeLimit: number;
-
-  constructor(prefix: string) {
-    this.cache = new Map<string, string>();
-    this.prefix = prefix;
-    this.tableId = 0;
-    this.cacheSizeLimit = 10;
-  }
-
-  async getTableName(engine: Engine, query: string): Promise<string> {
-    let tableName = this.cache.get(query);
-    if (tableName === undefined) {
-      // TODO(hjd): This should be LRU.
-      if (this.cache.size > this.cacheSizeLimit) {
-        for (const name of this.cache.values()) {
-          await engine.query(`drop table ${name}`);
-        }
-        this.cache.clear();
-      }
-      tableName = `${this.prefix}_${this.tableId++}`;
-      await engine.query(
-        `create temp table if not exists ${tableName} as ${query}`,
-      );
-      this.cache.set(query, tableName);
-    }
-    return tableName;
-  }
-
-  hasQuery(query: string): boolean {
-    return this.cache.get(query) !== undefined;
-  }
-}
diff --git a/ui/src/core/load_trace.ts b/ui/src/core/load_trace.ts
new file mode 100644
index 0000000..c39245f
--- /dev/null
+++ b/ui/src/core/load_trace.ts
@@ -0,0 +1,544 @@
+// 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 {assertExists, assertTrue} from '../base/logging';
+import {time, Time, TimeSpan} from '../base/time';
+import {cacheTrace} from './cache_manager';
+import {
+  getEnabledMetatracingCategories,
+  isMetatracingEnabled,
+} from './metatracing';
+import {featureFlags} from './feature_flags';
+import {Engine, EngineBase} from '../trace_processor/engine';
+import {HttpRpcEngine} from '../trace_processor/http_rpc_engine';
+import {
+  LONG,
+  LONG_NULL,
+  NUM,
+  NUM_NULL,
+  STR,
+} from '../trace_processor/query_result';
+import {WasmEngineProxy} from '../trace_processor/wasm_engine_proxy';
+import {
+  TraceBufferStream,
+  TraceFileStream,
+  TraceHttpStream,
+  TraceStream,
+} from '../core/trace_stream';
+import {
+  deserializeAppStatePhase1,
+  deserializeAppStatePhase2,
+} from './state_serialization';
+import {AppImpl} from './app_impl';
+import {raf} from './raf_scheduler';
+import {TraceImpl} from './trace_impl';
+import {SerializedAppState} from './state_serialization_schema';
+import {TraceSource} from './trace_source';
+import {Router} from '../core/router';
+import {TraceInfoImpl} from './trace_info_impl';
+
+const ENABLE_CHROME_RELIABLE_RANGE_ZOOM_FLAG = featureFlags.register({
+  id: 'enableChromeReliableRangeZoom',
+  name: 'Enable Chrome reliable range zoom',
+  description: 'Automatically zoom into the reliable range for Chrome traces',
+  defaultValue: false,
+});
+
+const ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG = featureFlags.register({
+  id: 'enableChromeReliableRangeAnnotation',
+  name: 'Enable Chrome reliable range annotation',
+  description: 'Automatically adds an annotation for the reliable range start',
+  defaultValue: false,
+});
+
+// The following flags control TraceProcessor Config.
+const CROP_TRACK_EVENTS_FLAG = featureFlags.register({
+  id: 'cropTrackEvents',
+  name: 'Crop track events',
+  description: 'Ignores track events outside of the range of interest',
+  defaultValue: false,
+});
+const INGEST_FTRACE_IN_RAW_TABLE_FLAG = featureFlags.register({
+  id: 'ingestFtraceInRawTable',
+  name: 'Ingest ftrace in raw table',
+  description: 'Enables ingestion of typed ftrace events into the raw table',
+  defaultValue: true,
+});
+const ANALYZE_TRACE_PROTO_CONTENT_FLAG = featureFlags.register({
+  id: 'analyzeTraceProtoContent',
+  name: 'Analyze trace proto content',
+  description:
+    'Enables trace proto content analysis (experimental_proto_content table)',
+  defaultValue: false,
+});
+const FTRACE_DROP_UNTIL_FLAG = featureFlags.register({
+  id: 'ftraceDropUntilAllCpusValid',
+  name: 'Crop ftrace events',
+  description:
+    'Drop ftrace events until all per-cpu data streams are known to be valid',
+  defaultValue: true,
+});
+
+// TODO(stevegolton): Move this into some global "SQL extensions" file and
+// ensure it's only run once.
+async function defineMaxLayoutDepthSqlFunction(engine: Engine): Promise<void> {
+  await engine.query(`
+    create perfetto function __max_layout_depth(track_count INT, track_ids STRING)
+    returns INT AS
+    select iif(
+      $track_count = 1,
+      (
+        select max_depth
+        from _slice_track_summary
+        where id = cast($track_ids AS int)
+      ),
+      (
+        select max(layout_depth)
+        from experimental_slice_layout($track_ids)
+      )
+    );
+  `);
+}
+
+let lastEngineId = 0;
+
+export async function loadTrace(
+  app: AppImpl,
+  traceSource: TraceSource,
+): Promise<TraceImpl> {
+  updateStatus(app, 'Opening trace');
+  const engineId = `${++lastEngineId}`;
+  const engine = await createEngine(app, engineId);
+  return await loadTraceIntoEngine(app, traceSource, engine);
+}
+
+async function createEngine(
+  app: AppImpl,
+  engineId: string,
+): Promise<EngineBase> {
+  // Check if there is any instance of the trace_processor_shell running in
+  // HTTP RPC mode (i.e. trace_processor_shell -D).
+  let useRpc = false;
+  if (app.httpRpc.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE') {
+    useRpc = (await HttpRpcEngine.checkConnection()).connected;
+  }
+  let engine;
+  if (useRpc) {
+    console.log('Opening trace using native accelerator over HTTP+RPC');
+    engine = new HttpRpcEngine(engineId);
+  } else {
+    console.log('Opening trace using built-in WASM engine');
+    engine = new WasmEngineProxy(engineId);
+    engine.resetTraceProcessor({
+      cropTrackEvents: CROP_TRACK_EVENTS_FLAG.get(),
+      ingestFtraceInRawTable: INGEST_FTRACE_IN_RAW_TABLE_FLAG.get(),
+      analyzeTraceProtoContent: ANALYZE_TRACE_PROTO_CONTENT_FLAG.get(),
+      ftraceDropUntilAllCpusValid: FTRACE_DROP_UNTIL_FLAG.get(),
+    });
+  }
+  engine.onResponseReceived = () => raf.scheduleFullRedraw();
+
+  if (isMetatracingEnabled()) {
+    engine.enableMetatrace(assertExists(getEnabledMetatracingCategories()));
+  }
+  return engine;
+}
+
+async function loadTraceIntoEngine(
+  app: AppImpl,
+  traceSource: TraceSource,
+  engine: EngineBase,
+): Promise<TraceImpl> {
+  let traceStream: TraceStream | undefined;
+  let serializedAppState: SerializedAppState | undefined;
+  if (traceSource.type === 'FILE') {
+    traceStream = new TraceFileStream(traceSource.file);
+  } else if (traceSource.type === 'ARRAY_BUFFER') {
+    traceStream = new TraceBufferStream(traceSource.buffer);
+  } else if (traceSource.type === 'URL') {
+    traceStream = new TraceHttpStream(traceSource.url);
+    serializedAppState = traceSource.serializedAppState;
+  } else if (traceSource.type === 'HTTP_RPC') {
+    traceStream = undefined;
+  } else {
+    throw new Error(`Unknown source: ${JSON.stringify(traceSource)}`);
+  }
+
+  // |traceStream| can be undefined in the case when we are using the external
+  // HTTP+RPC endpoint and the trace processor instance has already loaded
+  // a trace (because it was passed as a cmdline argument to
+  // trace_processor_shell). In this case we don't want the UI to load any
+  // file/stream and we just want to jump to the loading phase.
+  if (traceStream !== undefined) {
+    const tStart = performance.now();
+    for (;;) {
+      const res = await traceStream.readChunk();
+      await engine.parse(res.data);
+      const elapsed = (performance.now() - tStart) / 1000;
+      let status = 'Loading trace ';
+      if (res.bytesTotal > 0) {
+        const progress = Math.round((res.bytesRead / res.bytesTotal) * 100);
+        status += `${progress}%`;
+      } else {
+        status += `${Math.round(res.bytesRead / 1e6)} MB`;
+      }
+      status += ` - ${Math.ceil(res.bytesRead / elapsed / 1e6)} MB/s`;
+      updateStatus(app, status);
+      if (res.eof) break;
+    }
+    await engine.notifyEof();
+  } else {
+    assertTrue(engine instanceof HttpRpcEngine);
+    await engine.restoreInitialTables();
+  }
+  for (const p of app.extraSqlPackages) {
+    await engine.registerSqlPackages(p);
+  }
+
+  const traceDetails = await getTraceInfo(engine, traceSource);
+  const trace = TraceImpl.createInstanceForCore(app, engine, traceDetails);
+  app.setActiveTrace(trace);
+
+  const visibleTimeSpan = await computeVisibleTime(
+    traceDetails.start,
+    traceDetails.end,
+    trace.traceInfo.traceType === 'json',
+    engine,
+  );
+
+  trace.timeline.updateVisibleTime(visibleTimeSpan);
+
+  const cacheUuid = traceDetails.cached ? traceDetails.uuid : '';
+  Router.navigate(`#!/viewer?local_cache_key=${cacheUuid}`);
+
+  // Make sure the helper views are available before we start adding tracks.
+  await includeSummaryTables(trace);
+
+  await defineMaxLayoutDepthSqlFunction(engine);
+
+  if (serializedAppState !== undefined) {
+    deserializeAppStatePhase1(serializedAppState, trace);
+  }
+
+  await app.plugins.onTraceLoad(trace, (id) => {
+    updateStatus(app, `Running plugin: ${id}`);
+  });
+
+  decideTabs(trace);
+
+  // Trace Processor doesn't support the reliable range feature for JSON
+  // traces.
+  if (
+    trace.traceInfo.traceType !== 'json' &&
+    ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG.get()
+  ) {
+    const reliableRangeStart = await computeTraceReliableRangeStart(engine);
+    if (reliableRangeStart > 0) {
+      trace.notes.addNote({
+        timestamp: reliableRangeStart,
+        color: '#ff0000',
+        text: 'Reliable Range Start',
+      });
+    }
+  }
+
+  for (const callback of trace.getEventListeners('traceready')) {
+    await callback();
+  }
+
+  if (serializedAppState !== undefined) {
+    // Wait that plugins have completed their actions and then proceed with
+    // the final phase of app state restore.
+    // TODO(primiano): this can probably be removed once we refactor tracks
+    // to be URI based and can deal with non-existing URIs.
+    deserializeAppStatePhase2(serializedAppState, trace);
+  }
+
+  return trace;
+}
+
+function decideTabs(trace: TraceImpl) {
+  // Show the list of default tabs, but don't make them active!
+  for (const tabUri of trace.tabs.defaultTabs) {
+    trace.tabs.showTab(tabUri);
+  }
+}
+
+async function includeSummaryTables(trace: TraceImpl) {
+  const engine = trace.engine;
+  updateStatus(trace, 'Creating slice summaries');
+  await engine.query(`include perfetto module viz.summary.slices;`);
+
+  updateStatus(trace, 'Creating counter summaries');
+  await engine.query(`include perfetto module viz.summary.counters;`);
+
+  updateStatus(trace, 'Creating thread summaries');
+  await engine.query(`include perfetto module viz.summary.threads;`);
+
+  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 {
+  const showUntilDismissed = 0;
+  traceOrApp.omnibox.showStatusMessage(msg, showUntilDismissed);
+}
+
+async function computeFtraceBounds(engine: Engine): Promise<TimeSpan | null> {
+  const result = await engine.query(`
+    SELECT min(ts) as start, max(ts) as end FROM ftrace_event;
+  `);
+  const {start, end} = result.firstRow({start: LONG_NULL, end: LONG_NULL});
+  if (start !== null && end !== null) {
+    return new TimeSpan(Time.fromRaw(start), Time.fromRaw(end));
+  }
+  return null;
+}
+
+async function computeTraceReliableRangeStart(engine: Engine): Promise<time> {
+  const result =
+    await engine.query(`SELECT RUN_METRIC('chrome/chrome_reliable_range.sql');
+       SELECT start FROM chrome_reliable_range`);
+  const bounds = result.firstRow({start: LONG});
+  return Time.fromRaw(bounds.start);
+}
+
+async function computeVisibleTime(
+  traceStart: time,
+  traceEnd: time,
+  isJsonTrace: boolean,
+  engine: Engine,
+): Promise<TimeSpan> {
+  // initialise visible time to the trace time bounds
+  let visibleStart = traceStart;
+  let visibleEnd = traceEnd;
+
+  // compare start and end with metadata computed by the trace processor
+  const mdTime = await getTracingMetadataTimeBounds(engine);
+  // make sure the bounds hold
+  if (Time.max(visibleStart, mdTime.start) < Time.min(visibleEnd, mdTime.end)) {
+    visibleStart = Time.max(visibleStart, mdTime.start);
+    visibleEnd = Time.min(visibleEnd, mdTime.end);
+  }
+
+  // Trace Processor doesn't support the reliable range feature for JSON
+  // traces.
+  if (!isJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ZOOM_FLAG.get()) {
+    const reliableRangeStart = await computeTraceReliableRangeStart(engine);
+    visibleStart = Time.max(visibleStart, reliableRangeStart);
+  }
+
+  // Move start of visible window to the first ftrace event
+  const ftraceBounds = await computeFtraceBounds(engine);
+  if (ftraceBounds !== null) {
+    // Avoid moving start of visible window past its end!
+    visibleStart = Time.min(ftraceBounds.start, visibleEnd);
+  }
+  return new TimeSpan(visibleStart, visibleEnd);
+}
+
+async function getTraceInfo(
+  engine: Engine,
+  traceSource: TraceSource,
+): Promise<TraceInfoImpl> {
+  const traceTime = await getTraceTimeBounds(engine);
+
+  // Find the first REALTIME or REALTIME_COARSE clock snapshot.
+  // Prioritize REALTIME over REALTIME_COARSE.
+  const query = `select
+          ts,
+          clock_value as clockValue,
+          clock_name as clockName
+        from clock_snapshot
+        where
+          snapshot_id = 0 AND
+          clock_name in ('REALTIME', 'REALTIME_COARSE')
+        `;
+  const result = await engine.query(query);
+  const it = result.iter({
+    ts: LONG,
+    clockValue: LONG,
+    clockName: STR,
+  });
+
+  let snapshot = {
+    clockName: '',
+    ts: Time.ZERO,
+    clockValue: Time.ZERO,
+  };
+
+  // Find the most suitable snapshot
+  for (let row = 0; it.valid(); it.next(), row++) {
+    if (it.clockName === 'REALTIME') {
+      snapshot = {
+        clockName: it.clockName,
+        ts: Time.fromRaw(it.ts),
+        clockValue: Time.fromRaw(it.clockValue),
+      };
+      break;
+    } else if (it.clockName === 'REALTIME_COARSE') {
+      if (snapshot.clockName !== 'REALTIME') {
+        snapshot = {
+          clockName: it.clockName,
+          ts: Time.fromRaw(it.ts),
+          clockValue: Time.fromRaw(it.clockValue),
+        };
+      }
+    }
+  }
+
+  // The max() is so the query returns NULL if the tz info doesn't exist.
+  const queryTz = `select max(int_value) as tzOffMin from metadata
+        where name = 'timezone_off_mins'`;
+  const resTz = await assertExists(engine).query(queryTz);
+  const tzOffMin = resTz.firstRow({tzOffMin: NUM_NULL}).tzOffMin ?? 0;
+
+  // This is the offset between the unix epoch and ts in the ts domain.
+  // I.e. the value of ts at the time of the unix epoch - usually some large
+  // negative value.
+  const realtimeOffset = Time.sub(snapshot.ts, snapshot.clockValue);
+
+  // Find the previous closest midnight from the trace start time.
+  const utcOffset = Time.getLatestMidnight(traceTime.start, realtimeOffset);
+
+  const traceTzOffset = Time.getLatestMidnight(
+    traceTime.start,
+    Time.sub(realtimeOffset, Time.fromSeconds(tzOffMin * 60)),
+  );
+
+  let traceTitle = '';
+  let traceUrl = '';
+  switch (traceSource.type) {
+    case 'FILE':
+      // Split on both \ and / (because C:\Windows\paths\are\like\this).
+      traceTitle = traceSource.file.name.split(/[/\\]/).pop()!;
+      const fileSizeMB = Math.ceil(traceSource.file.size / 1e6);
+      traceTitle += ` (${fileSizeMB} MB)`;
+      break;
+    case 'URL':
+      traceUrl = traceSource.url;
+      traceTitle = traceUrl.split('/').pop()!;
+      break;
+    case 'ARRAY_BUFFER':
+      traceTitle = traceSource.title;
+      traceUrl = traceSource.url ?? '';
+      const arrayBufferSizeMB = Math.ceil(traceSource.buffer.byteLength / 1e6);
+      traceTitle += ` (${arrayBufferSizeMB} MB)`;
+      break;
+    case 'HTTP_RPC':
+      traceTitle = `RPC @ ${HttpRpcEngine.hostAndPort}`;
+      break;
+    default:
+      break;
+  }
+
+  const traceType = await getTraceType(engine);
+
+  const hasFtrace =
+    (await engine.query(`select * from ftrace_event limit 1`)).numRows() > 0;
+
+  const uuidRes = await engine.query(`select str_value as uuid from metadata
+    where name = 'trace_uuid'`);
+  // trace_uuid can be missing from the TP tables if the trace is empty or in
+  // other similar edge cases.
+  const uuid = uuidRes.numRows() > 0 ? uuidRes.firstRow({uuid: STR}).uuid : '';
+  const cached = await cacheTrace(traceSource, uuid);
+
+  const downloadable =
+    (traceSource.type === 'ARRAY_BUFFER' && !traceSource.localOnly) ||
+    traceSource.type === 'FILE' ||
+    traceSource.type === 'URL';
+
+  return {
+    ...traceTime,
+    traceTitle,
+    traceUrl,
+    realtimeOffset,
+    utcOffset,
+    traceTzOffset,
+    cpus: await getCpus(engine),
+    importErrors: await getTraceErrors(engine),
+    source: traceSource,
+    traceType,
+    hasFtrace,
+    uuid,
+    cached,
+    downloadable,
+  };
+}
+
+async function getTraceType(engine: Engine) {
+  const result = await engine.query(
+    `select str_value from metadata where name = 'trace_type'`,
+  );
+
+  if (result.numRows() === 0) return undefined;
+  return result.firstRow({str_value: STR}).str_value;
+}
+
+async function getTraceTimeBounds(engine: Engine): Promise<TimeSpan> {
+  const result = await engine.query(
+    `select start_ts as startTs, end_ts as endTs from trace_bounds`,
+  );
+  const bounds = result.firstRow({
+    startTs: LONG,
+    endTs: LONG,
+  });
+  return new TimeSpan(Time.fromRaw(bounds.startTs), Time.fromRaw(bounds.endTs));
+}
+
+// TODO(hjd): When streaming must invalidate this somehow.
+async function getCpus(engine: Engine): Promise<number[]> {
+  const cpus = [];
+  const queryRes = await engine.query(
+    'select distinct(cpu) as cpu from sched order by cpu;',
+  );
+  for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) {
+    cpus.push(it.cpu);
+  }
+  return cpus;
+}
+
+async function getTraceErrors(engine: Engine): Promise<number> {
+  const sql = `SELECT sum(value) as errs FROM stats WHERE severity != 'info'`;
+  const result = await engine.query(sql);
+  return result.firstRow({errs: NUM}).errs;
+}
+
+async function getTracingMetadataTimeBounds(engine: Engine): Promise<TimeSpan> {
+  const queryRes = await engine.query(`select
+       name,
+       int_value as intValue
+       from metadata
+       where name = 'tracing_started_ns' or name = 'tracing_disabled_ns'
+       or name = 'all_data_source_started_ns'`);
+  let startBound = Time.MIN;
+  let endBound = Time.MAX;
+  const it = queryRes.iter({name: STR, intValue: LONG_NULL});
+  for (; it.valid(); it.next()) {
+    const columnName = it.name;
+    const timestamp = it.intValue;
+    if (timestamp === null) continue;
+    if (columnName === 'tracing_disabled_ns') {
+      endBound = Time.min(endBound, Time.fromRaw(timestamp));
+    } else {
+      startBound = Time.max(startBound, Time.fromRaw(timestamp));
+    }
+  }
+
+  return new TimeSpan(startBound, endBound);
+}
diff --git a/ui/src/core/metatracing.ts b/ui/src/core/metatracing.ts
new file mode 100644
index 0000000..3567468
--- /dev/null
+++ b/ui/src/core/metatracing.ts
@@ -0,0 +1,221 @@
+// 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 {featureFlags} from './feature_flags';
+import {MetatraceCategories, PerfettoMetatrace} from '../protos';
+import protobuf from 'protobufjs/minimal';
+
+const METATRACING_BUFFER_SIZE = 100000;
+
+export enum MetatraceTrackId {
+  // 1 is reserved for the Trace Processor track.
+  // Events emitted by the JS main thread.
+  kMainThread = 2,
+  // Async track for the status (e.g. "loading tracks") shown to the user
+  // in the omnibox.
+  kOmniboxStatus = 3,
+}
+
+const AOMT_FLAG = featureFlags.register({
+  id: 'alwaysOnMetatracing',
+  name: 'Enable always-on-metatracing',
+  description: 'Enables trace events in the UI and trace processor',
+  defaultValue: false,
+});
+
+const AOMT_DETAILED_FLAG = featureFlags.register({
+  id: 'alwaysOnMetatracing_detailed',
+  name: 'Detailed always-on-metatracing',
+  description: 'Enables recording additional events for trace event',
+  defaultValue: false,
+});
+
+function getInitialCategories(): MetatraceCategories | undefined {
+  if (!AOMT_FLAG.get()) return undefined;
+  if (AOMT_DETAILED_FLAG.get()) return MetatraceCategories.ALL;
+  return MetatraceCategories.QUERY_TIMELINE | MetatraceCategories.API_TIMELINE;
+}
+
+let enabledCategories: MetatraceCategories | undefined = getInitialCategories();
+
+export function enableMetatracing(categories?: MetatraceCategories) {
+  enabledCategories =
+    categories === undefined || categories === MetatraceCategories.NONE
+      ? MetatraceCategories.ALL
+      : categories;
+}
+
+export function disableMetatracingAndGetTrace(): Uint8Array {
+  enabledCategories = undefined;
+  return readMetatrace();
+}
+
+export function isMetatracingEnabled(): boolean {
+  return enabledCategories !== undefined;
+}
+
+export function getEnabledMetatracingCategories():
+  | MetatraceCategories
+  | undefined {
+  return enabledCategories;
+}
+
+interface TraceEvent {
+  eventName: string;
+  startNs: number;
+  durNs: number;
+  track: MetatraceTrackId;
+  args?: {[key: string]: string};
+}
+
+const traceEvents: TraceEvent[] = [];
+
+function readMetatrace(): Uint8Array {
+  const eventToPacket = (e: TraceEvent): Uint8Array => {
+    const metatraceEvent = PerfettoMetatrace.create({
+      eventName: e.eventName,
+      threadId: e.track,
+      eventDurationNs: e.durNs,
+    });
+    for (const [key, value] of Object.entries(e.args ?? {})) {
+      metatraceEvent.args.push(
+        PerfettoMetatrace.Arg.create({
+          key,
+          value,
+        }),
+      );
+    }
+    const PROTO_VARINT_TYPE = 0;
+    const PROTO_LEN_DELIMITED_WIRE_TYPE = 2;
+    const TRACE_PACKET_PROTO_TAG = (1 << 3) | PROTO_LEN_DELIMITED_WIRE_TYPE;
+    const TRACE_PACKET_TIMESTAMP_TAG = (8 << 3) | PROTO_VARINT_TYPE;
+    const TRACE_PACKET_CLOCK_ID_TAG = (58 << 3) | PROTO_VARINT_TYPE;
+    const TRACE_PACKET_METATRACE_TAG =
+      (49 << 3) | PROTO_LEN_DELIMITED_WIRE_TYPE;
+
+    const wri = protobuf.Writer.create();
+    wri.uint32(TRACE_PACKET_PROTO_TAG);
+    wri.fork(); // Start of Trace Packet.
+    wri.uint32(TRACE_PACKET_TIMESTAMP_TAG).int64(e.startNs);
+    wri.uint32(TRACE_PACKET_CLOCK_ID_TAG).int32(1);
+    wri
+      .uint32(TRACE_PACKET_METATRACE_TAG)
+      .bytes(PerfettoMetatrace.encode(metatraceEvent).finish());
+    wri.ldelim();
+    return wri.finish();
+  };
+  const packets: Uint8Array[] = [];
+  for (const event of traceEvents) {
+    packets.push(eventToPacket(event));
+  }
+  const totalLength = packets.reduce((acc, arr) => acc + arr.length, 0);
+  const trace = new Uint8Array(totalLength);
+  let offset = 0;
+  for (const packet of packets) {
+    trace.set(packet, offset);
+    offset += packet.length;
+  }
+  return trace;
+}
+
+interface TraceEventParams {
+  track?: MetatraceTrackId;
+  args?: {[key: string]: string};
+}
+
+export type TraceEventScope = {
+  startNs: number;
+  eventName: string;
+  params?: TraceEventParams;
+};
+
+const correctedTimeOrigin = new Date().getTime() - performance.now();
+
+function msToNs(ms: number) {
+  return Math.round(ms * 1e6);
+}
+
+function now(): number {
+  return msToNs(correctedTimeOrigin + performance.now());
+}
+
+export function traceEvent<T>(
+  name: string,
+  event: () => T,
+  params?: TraceEventParams,
+): T {
+  const scope = traceEventBegin(name, params);
+  try {
+    const result = event();
+    return result;
+  } finally {
+    traceEventEnd(scope);
+  }
+}
+
+export function traceEventBegin(
+  eventName: string,
+  params?: TraceEventParams,
+): TraceEventScope {
+  return {
+    eventName,
+    startNs: now(),
+    params: params,
+  };
+}
+
+export function traceEventEnd(traceEvent: TraceEventScope) {
+  if (!isMetatracingEnabled()) return;
+
+  traceEvents.push({
+    eventName: traceEvent.eventName,
+    startNs: traceEvent.startNs,
+    durNs: now() - traceEvent.startNs,
+    track: traceEvent.params?.track ?? MetatraceTrackId.kMainThread,
+    args: traceEvent.params?.args,
+  });
+  while (traceEvents.length > METATRACING_BUFFER_SIZE) {
+    traceEvents.shift();
+  }
+}
+
+// Flatten arbitrary values so they can be used as args in traceEvent() et al.
+export function flattenArgs(
+  input: unknown,
+  parentKey = '',
+): {[key: string]: string} {
+  if (typeof input !== 'object' || input === null) {
+    return {[parentKey]: String(input)};
+  }
+
+  if (Array.isArray(input)) {
+    const result: Record<string, string> = {};
+
+    (input as Array<unknown>).forEach((item, index) => {
+      const arrayKey = `${parentKey}[${index}]`;
+      Object.assign(result, flattenArgs(item, arrayKey));
+    });
+
+    return result;
+  }
+
+  const result: Record<string, string> = {};
+
+  Object.entries(input as Record<string, unknown>).forEach(([key, value]) => {
+    const newKey = parentKey ? `${parentKey}.${key}` : key;
+    Object.assign(result, flattenArgs(value, newKey));
+  });
+
+  return result;
+}
diff --git a/ui/src/common/metatracing_unittest.ts b/ui/src/core/metatracing_unittest.ts
similarity index 100%
rename from ui/src/common/metatracing_unittest.ts
rename to ui/src/core/metatracing_unittest.ts
diff --git a/ui/src/core/note_manager.ts b/ui/src/core/note_manager.ts
new file mode 100644
index 0000000..ab2249a
--- /dev/null
+++ b/ui/src/core/note_manager.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 {
+  AddNoteArgs,
+  AddSpanNoteArgs,
+  Note,
+  NoteManager,
+  SpanNote,
+} from '../public/note';
+import {randomColor} from '../public/lib/colorizer';
+import {raf} from './raf_scheduler';
+
+export class NoteManagerImpl implements NoteManager {
+  private _lastNodeId = 0;
+  private _notes = new Map<string, Note | SpanNote>();
+
+  // This function is wired up to clear the SelectionManager state if the
+  // current selection is a note.
+  // TODO(primiano): figure out some better (de-)coupling here.
+  // We cannot pass SelectionManager in our constructor because doing so would
+  // create a cyclic ctor dependency (SelectionManager requires NoteManager in
+  // its ctor). There is a 2way logical dependency between NoteManager and
+  // SelectionManager:
+  // 1. SM needs NM to handle SM.findTimeRangeOfSelection(), for [M]ark.
+  // 2. NM needs SM to tell it that a note has been delete and should be
+  //   deselected if it was currently selected.
+  onNoteDeleted?: (nodeId: string) => void;
+
+  get notes(): ReadonlyMap<string, Note | SpanNote> {
+    return this._notes;
+  }
+
+  getNote(id: string): Note | SpanNote | undefined {
+    return this._notes.get(id);
+  }
+
+  addNote(args: AddNoteArgs): string {
+    const id = args.id ?? `note_${++this._lastNodeId}`;
+    this._notes.set(id, {
+      ...args,
+      noteType: 'DEFAULT',
+      id,
+      color: args.color ?? randomColor(),
+      text: args.text ?? '',
+    });
+    raf.scheduleFullRedraw();
+    return id;
+  }
+
+  addSpanNote(args: AddSpanNoteArgs): string {
+    const id = args.id ?? `note_${++this._lastNodeId}`;
+    this._notes.set(id, {
+      ...args,
+      noteType: 'SPAN',
+      id,
+      color: args.color ?? randomColor(),
+      text: args.text ?? '',
+    });
+    raf.scheduleFullRedraw();
+    return id;
+  }
+
+  changeNote(id: string, args: {color?: string; text?: string}) {
+    const note = this._notes.get(id);
+    if (note === undefined) return;
+
+    this._notes.set(id, {
+      ...note,
+      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
new file mode 100644
index 0000000..8a9dfe0
--- /dev/null
+++ b/ui/src/core/omnibox_manager.ts
@@ -0,0 +1,165 @@
+// 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 {OmniboxManager, PromptOption} from '../public/omnibox';
+import {raf} from './raf_scheduler';
+
+export enum OmniboxMode {
+  Search,
+  Query,
+  Command,
+  Prompt,
+}
+
+interface Prompt {
+  text: string;
+  options?: PromptOption[];
+  resolve(result: string | undefined): void;
+}
+
+const defaultMode = OmniboxMode.Search;
+
+export class OmniboxManagerImpl implements OmniboxManager {
+  private _mode = defaultMode;
+  private _focusOmniboxNextRender = false;
+  private _pendingCursorPlacement?: number;
+  private _pendingPrompt?: Prompt;
+  private _omniboxSelectionIndex = 0;
+  private _forceShortTextSearch = false;
+  private _textForMode = new Map<OmniboxMode, string>();
+  private _statusMessageContainer: {msg?: string} = {};
+
+  get mode(): OmniboxMode {
+    return this._mode;
+  }
+
+  get pendingPrompt(): Prompt | undefined {
+    return this._pendingPrompt;
+  }
+
+  get text(): string {
+    return this._textForMode.get(this._mode) ?? '';
+  }
+
+  get selectionIndex(): number {
+    return this._omniboxSelectionIndex;
+  }
+
+  get focusOmniboxNextRender(): boolean {
+    return this._focusOmniboxNextRender;
+  }
+
+  get pendingCursorPlacement(): number | undefined {
+    return this._pendingCursorPlacement;
+  }
+
+  get forceShortTextSearch() {
+    return this._forceShortTextSearch;
+  }
+
+  setText(value: string): void {
+    this._textForMode.set(this._mode, value);
+  }
+
+  setSelectionIndex(index: number): void {
+    this._omniboxSelectionIndex = index;
+  }
+
+  focus(cursorPlacement?: number): void {
+    this._focusOmniboxNextRender = true;
+    this._pendingCursorPlacement = cursorPlacement;
+    raf.scheduleFullRedraw();
+  }
+
+  clearFocusFlag(): void {
+    this._focusOmniboxNextRender = false;
+    this._pendingCursorPlacement = undefined;
+  }
+
+  setMode(mode: OmniboxMode, focus = true): void {
+    this._mode = mode;
+    this._focusOmniboxNextRender = focus;
+    this._omniboxSelectionIndex = 0;
+    this.rejectPendingPrompt();
+    raf.scheduleFullRedraw();
+  }
+
+  showStatusMessage(msg: string, durationMs = 2000) {
+    const statusMessageContainer: {msg?: string} = {msg};
+    if (durationMs > 0) {
+      setTimeout(() => {
+        statusMessageContainer.msg = undefined;
+        raf.scheduleFullRedraw();
+      }, durationMs);
+    }
+    this._statusMessageContainer = statusMessageContainer;
+    raf.scheduleFullRedraw();
+  }
+
+  get statusMessage(): string | undefined {
+    return this._statusMessageContainer.msg;
+  }
+
+  // Start a prompt. If options are supplied, the user must pick one from the
+  // list, otherwise the input is free-form text.
+  prompt(text: string, options?: PromptOption[]): Promise<string | undefined> {
+    this._mode = OmniboxMode.Prompt;
+    this._omniboxSelectionIndex = 0;
+    this.rejectPendingPrompt();
+
+    const promise = new Promise<string | undefined>((resolve) => {
+      this._pendingPrompt = {
+        text,
+        options,
+        resolve,
+      };
+    });
+
+    this._focusOmniboxNextRender = true;
+    raf.scheduleFullRedraw();
+
+    return promise;
+  }
+
+  // Resolve the pending prompt with a value to return to the prompter.
+  resolvePrompt(value: string): void {
+    if (this._pendingPrompt) {
+      this._pendingPrompt.resolve(value);
+      this._pendingPrompt = undefined;
+    }
+    this.setMode(OmniboxMode.Search);
+  }
+
+  // Reject the prompt outright. Doing this will force the owner of the prompt
+  // promise to catch, so only do this when things go seriously wrong.
+  // Use |resolvePrompt(null)| to indicate cancellation.
+  rejectPrompt(): void {
+    this.rejectPendingPrompt();
+    this.setMode(OmniboxMode.Search);
+  }
+
+  reset(focus = true): void {
+    this.setMode(defaultMode, focus);
+    this._omniboxSelectionIndex = 0;
+    this._statusMessageContainer = {};
+    raf.scheduleFullRedraw();
+  }
+
+  private rejectPendingPrompt() {
+    if (this._pendingPrompt) {
+      this._pendingPrompt.resolve(undefined);
+      this._pendingPrompt = undefined;
+    }
+  }
+}
diff --git a/ui/src/core/page_manager.ts b/ui/src/core/page_manager.ts
new file mode 100644
index 0000000..18e6858
--- /dev/null
+++ b/ui/src/core/page_manager.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 {assertExists, assertTrue} from '../base/logging';
+import {Registry} from '../base/registry';
+import {PageAttrs, PageHandler, PageWithTraceAttrs} from '../public/page';
+import {Router} from './router';
+import {TraceImpl} from './trace_impl';
+
+export interface PageWithTraceImplAttrs extends PageAttrs {
+  trace: TraceImpl;
+}
+
+// This is to allow internal core classes to get a TraceImpl injected rather
+// than just a Trace.
+type PageHandlerInternal = PageHandler<
+  | m.ComponentTypes<PageWithTraceAttrs>
+  | m.ComponentTypes<PageWithTraceImplAttrs>
+>;
+
+export class PageManagerImpl {
+  private readonly registry = new Registry<PageHandlerInternal>((x) => x.route);
+
+  registerPage(pageHandler: PageHandlerInternal): Disposable {
+    assertTrue(/^\/\w*$/.exec(pageHandler.route) !== null);
+    // The pluginId is injected by the proxy in AppImpl / TraceImpl. If this is
+    // undefined somebody (tests) managed to call this method without proxy.
+    assertExists(pageHandler.pluginId);
+    return this.registry.register(pageHandler);
+  }
+
+  // Called by index.ts upon the main frame redraw callback.
+  renderPageForCurrentRoute(
+    trace: TraceImpl | undefined,
+  ): m.Vnode<PageAttrs> | m.Vnode<PageWithTraceImplAttrs> {
+    const route = Router.parseFragment(location.hash);
+    const res = this.renderPageForRoute(trace, route.page, route.subpage);
+    if (res !== undefined) {
+      return res;
+    }
+    // If either the route doesn't exist or requires a trace but the trace is
+    // not loaded, fall back on the default route /.
+    return assertExists(this.renderPageForRoute(trace, '/', ''));
+  }
+
+  // Will return undefined if either: (1) the route does not exist; (2) the
+  // route exists, it requires a trace, but there is no trace loaded.
+  private renderPageForRoute(
+    coreTrace: TraceImpl | undefined,
+    page: string,
+    subpage: string,
+  ) {
+    const handler = this.registry.tryGet(page);
+    if (handler === undefined) {
+      return undefined;
+    }
+    const pluginId = assertExists(handler?.pluginId);
+    const trace = coreTrace?.forkForPlugin(pluginId);
+    const traceRequired = !handler?.traceless;
+    if (traceRequired && trace === undefined) {
+      return undefined;
+    }
+    if (traceRequired) {
+      return m(handler.page as m.ComponentTypes<PageWithTraceImplAttrs>, {
+        subpage,
+        trace: assertExists(trace),
+      });
+    }
+    return m(handler.page, {subpage, trace});
+  }
+}
diff --git a/ui/src/core/pivot_table_dragndrop_logic_unittest.ts b/ui/src/core/pivot_table_dragndrop_logic_unittest.ts
new file mode 100644
index 0000000..0bb34fa
--- /dev/null
+++ b/ui/src/core/pivot_table_dragndrop_logic_unittest.ts
@@ -0,0 +1,46 @@
+// 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 {computeIntervals, performReordering} from './pivot_table_manager';
+
+describe('performReordering', () => {
+  test('has the same elements in the result', () => {
+    const arr = [1, 2, 3, 4, 5, 6];
+    const arrSet = new Set(arr);
+
+    for (let i = 0; i < arr.length; i++) {
+      for (let j = 0; j < arr.length; j++) {
+        if (i === j) {
+          // The function has a precondition that two indices have to be
+          // different.
+          continue;
+        }
+
+        const permutedLeft = performReordering(
+          computeIntervals(arr.length, i, j, 'left'),
+          arr,
+        );
+        expect(new Set(permutedLeft)).toEqual(arrSet);
+        expect(permutedLeft.length).toEqual(arr.length);
+
+        const permutedRight = performReordering(
+          computeIntervals(arr.length, i, j, 'right'),
+          arr,
+        );
+        expect(new Set(permutedRight)).toEqual(arrSet);
+        expect(permutedRight.length).toEqual(arr.length);
+      }
+    }
+  });
+});
diff --git a/ui/src/core/pivot_table_manager.ts b/ui/src/core/pivot_table_manager.ts
new file mode 100644
index 0000000..551125d
--- /dev/null
+++ b/ui/src/core/pivot_table_manager.ts
@@ -0,0 +1,401 @@
+// 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 {
+  PivotTableQuery,
+  PivotTableQueryMetadata,
+  PivotTableResult,
+  PivotTableState,
+  COUNT_AGGREGATION,
+  TableColumn,
+  toggleEnabled,
+  tableColumnEquals,
+  AggregationFunction,
+} from './pivot_table_types';
+import {AreaSelection} from '../public/selection';
+import {
+  aggregationIndex,
+  generateQueryFromState,
+} from './pivot_table_query_generator';
+import {Aggregation, PivotTree} from './pivot_table_types';
+import {Engine} from '../trace_processor/engine';
+import {ColumnType} from '../trace_processor/query_result';
+import {SortDirection} from '../base/comparison_utils';
+import {assertTrue} from '../base/logging';
+import {featureFlags} from './feature_flags';
+
+export const PIVOT_TABLE_REDUX_FLAG = featureFlags.register({
+  id: 'pivotTable',
+  name: 'Pivot tables V2',
+  description: 'Second version of pivot table',
+  defaultValue: true,
+});
+
+function expectNumber(value: ColumnType): number {
+  if (typeof value === 'number') {
+    return value;
+  } else if (typeof value === 'bigint') {
+    return Number(value);
+  }
+  throw new Error(`number or bigint was expected, got ${typeof value}`);
+}
+
+// Auxiliary class to build the tree from query response.
+export class PivotTableTreeBuilder {
+  private readonly root: PivotTree;
+  queryMetadata: PivotTableQueryMetadata;
+
+  get pivotColumnsCount(): number {
+    return this.queryMetadata.pivotColumns.length;
+  }
+
+  get aggregateColumns(): Aggregation[] {
+    return this.queryMetadata.aggregationColumns;
+  }
+
+  constructor(queryMetadata: PivotTableQueryMetadata, firstRow: ColumnType[]) {
+    this.queryMetadata = queryMetadata;
+    this.root = this.createNode(firstRow);
+    let tree = this.root;
+    for (let i = 0; i + 1 < this.pivotColumnsCount; i++) {
+      const value = firstRow[i];
+      tree = this.insertChild(tree, value, this.createNode(firstRow));
+    }
+    tree.rows.push(firstRow);
+  }
+
+  // Add incoming row to the tree being built.
+  ingestRow(row: ColumnType[]) {
+    let tree = this.root;
+    this.updateAggregates(tree, row);
+    for (let i = 0; i + 1 < this.pivotColumnsCount; i++) {
+      const nextTree = tree.children.get(row[i]);
+      if (nextTree === undefined) {
+        // Insert the new node into the tree, and make variable `tree` point
+        // to the newly created node.
+        tree = this.insertChild(tree, row[i], this.createNode(row));
+      } else {
+        this.updateAggregates(nextTree, row);
+        tree = nextTree;
+      }
+    }
+    tree.rows.push(row);
+  }
+
+  build(): PivotTree {
+    return this.root;
+  }
+
+  updateAggregates(tree: PivotTree, row: ColumnType[]) {
+    const countIndex = this.queryMetadata.countIndex;
+    const treeCount =
+      countIndex >= 0 ? expectNumber(tree.aggregates[countIndex]) : 0;
+    const rowCount =
+      countIndex >= 0
+        ? expectNumber(
+            row[aggregationIndex(this.pivotColumnsCount, countIndex)],
+          )
+        : 0;
+
+    for (let i = 0; i < this.aggregateColumns.length; i++) {
+      const agg = this.aggregateColumns[i];
+
+      const currAgg = tree.aggregates[i];
+      const childAgg = row[aggregationIndex(this.pivotColumnsCount, i)];
+      if (typeof currAgg === 'number' && typeof childAgg === 'number') {
+        switch (agg.aggregationFunction) {
+          case 'SUM':
+          case 'COUNT':
+            tree.aggregates[i] = currAgg + childAgg;
+            break;
+          case 'MAX':
+            tree.aggregates[i] = Math.max(currAgg, childAgg);
+            break;
+          case 'MIN':
+            tree.aggregates[i] = Math.min(currAgg, childAgg);
+            break;
+          case 'AVG': {
+            const currSum = currAgg * treeCount;
+            const addSum = childAgg * rowCount;
+            tree.aggregates[i] = (currSum + addSum) / (treeCount + rowCount);
+            break;
+          }
+        }
+      }
+    }
+    tree.aggregates[this.aggregateColumns.length] = treeCount + rowCount;
+  }
+
+  // Helper method that inserts child node into the tree and returns it, used
+  // for more concise modification of local variable pointing to the current
+  // node being built.
+  insertChild(tree: PivotTree, key: ColumnType, child: PivotTree): PivotTree {
+    tree.children.set(key, child);
+
+    return child;
+  }
+
+  // Initialize PivotTree from a row.
+  createNode(row: ColumnType[]): PivotTree {
+    const aggregates = [];
+
+    for (let j = 0; j < this.aggregateColumns.length; j++) {
+      aggregates.push(row[aggregationIndex(this.pivotColumnsCount, j)]);
+    }
+    aggregates.push(
+      row[
+        aggregationIndex(this.pivotColumnsCount, this.aggregateColumns.length)
+      ],
+    );
+
+    return {
+      isCollapsed: false,
+      children: new Map(),
+      aggregates,
+      rows: [],
+    };
+  }
+}
+
+function createEmptyQueryResult(
+  metadata: PivotTableQueryMetadata,
+): PivotTableResult {
+  return {
+    tree: {
+      aggregates: [],
+      isCollapsed: false,
+      children: new Map(),
+      rows: [],
+    },
+    metadata,
+  };
+}
+
+// Controller responsible for showing the panel with pivot table, as well as
+// executing its queries and post-processing query results.
+export class PivotTableManager {
+  state: PivotTableState = createEmptyPivotTableState();
+
+  constructor(private engine: Engine) {}
+
+  setSelectionArea(area: AreaSelection) {
+    if (!PIVOT_TABLE_REDUX_FLAG.get()) {
+      return;
+    }
+    this.state.selectionArea = area;
+    this.refresh();
+  }
+
+  addAggregation(aggregation: Aggregation, after: number) {
+    this.state.selectedAggregations.splice(after, 0, aggregation);
+    this.refresh();
+  }
+
+  removeAggregation(index: number) {
+    this.state.selectedAggregations.splice(index, 1);
+    this.refresh();
+  }
+
+  setPivotSelected(args: {column: TableColumn; selected: boolean}) {
+    toggleEnabled(
+      tableColumnEquals,
+      this.state.selectedPivots,
+      args.column,
+      args.selected,
+    );
+    this.refresh();
+  }
+
+  setAggregationFunction(index: number, fn: AggregationFunction) {
+    this.state.selectedAggregations[index].aggregationFunction = fn;
+    this.refresh();
+  }
+
+  setSortColumn(aggregationIndex: number, order: SortDirection) {
+    this.state.selectedAggregations = this.state.selectedAggregations.map(
+      (agg, index) => ({
+        column: agg.column,
+        aggregationFunction: agg.aggregationFunction,
+        sortDirection: index === aggregationIndex ? order : undefined,
+      }),
+    );
+    this.refresh();
+  }
+
+  setOrder(from: number, to: number, direction: DropDirection) {
+    const pivots = this.state.selectedPivots;
+    this.state.selectedPivots = performReordering(
+      computeIntervals(pivots.length, from, to, direction),
+      pivots,
+    );
+    this.refresh();
+  }
+
+  setAggregationOrder(from: number, to: number, direction: DropDirection) {
+    const aggregations = this.state.selectedAggregations;
+    this.state.selectedAggregations = performReordering(
+      computeIntervals(aggregations.length, from, to, direction),
+      aggregations,
+    );
+    this.refresh();
+  }
+
+  setConstrainedToArea(constrain: boolean) {
+    this.state.constrainToArea = constrain;
+    this.refresh();
+  }
+
+  private refresh() {
+    this.state.queryResult = undefined;
+    if (!PIVOT_TABLE_REDUX_FLAG.get()) {
+      return;
+    }
+    this.processQuery(generateQueryFromState(this.state));
+  }
+
+  private async processQuery(query: PivotTableQuery) {
+    const result = await this.engine.query(query.text);
+    try {
+      await result.waitAllRows();
+    } catch {
+      // waitAllRows() frequently throws an exception, which is ignored in
+      // its other calls, so it's ignored here as well.
+    }
+
+    const columns = result.columns();
+
+    const it = result.iter({});
+    function nextRow(): ColumnType[] {
+      const row: ColumnType[] = [];
+      for (const column of columns) {
+        row.push(it.get(column));
+      }
+      it.next();
+      return row;
+    }
+
+    if (!it.valid()) {
+      // Iterator is invalid after creation; means that there are no rows
+      // satisfying filtering criteria. Return an empty tree.
+      this.state.queryResult = createEmptyQueryResult(query.metadata);
+      return;
+    }
+
+    const treeBuilder = new PivotTableTreeBuilder(query.metadata, nextRow());
+    while (it.valid()) {
+      treeBuilder.ingestRow(nextRow());
+    }
+    this.state.queryResult = {
+      tree: treeBuilder.build(),
+      metadata: query.metadata,
+    };
+  }
+}
+
+function createEmptyPivotTableState(): PivotTableState {
+  return {
+    queryResult: undefined,
+    selectedPivots: [
+      {
+        kind: 'regular',
+        table: '_slice_with_thread_and_process_info',
+        column: 'name',
+      },
+    ],
+    selectedAggregations: [
+      {
+        aggregationFunction: 'SUM',
+        column: {
+          kind: 'regular',
+          table: '_slice_with_thread_and_process_info',
+          column: 'dur',
+        },
+        sortDirection: 'DESC',
+      },
+      {
+        aggregationFunction: 'SUM',
+        column: {
+          kind: 'regular',
+          table: '_slice_with_thread_and_process_info',
+          column: 'thread_dur',
+        },
+      },
+      COUNT_AGGREGATION,
+    ],
+    constrainToArea: true,
+  };
+}
+
+// Drag&Drop logic
+
+export type DropDirection = 'left' | 'right';
+
+export interface Interval {
+  from: number;
+  to: number;
+}
+
+/*
+ * When a drag'n'drop is performed in a linear sequence, the resulting reordered
+ * array will consist of several contiguous subarrays of the original glued
+ * together.
+ *
+ * This function implements the computation of these intervals.
+ *
+ * The drag'n'drop operation performed is as follows: in the sequence with given
+ * length, the element with index `dragFrom` is dropped on the `direction` to
+ * the element `dragTo`.
+ */
+
+export function computeIntervals(
+  length: number,
+  dragFrom: number,
+  dragTo: number,
+  direction: DropDirection,
+): Interval[] {
+  assertTrue(dragFrom !== dragTo);
+
+  if (dragTo < dragFrom) {
+    const prefixLen = direction == 'left' ? dragTo : dragTo + 1;
+    return [
+      // First goes unchanged prefix.
+      {from: 0, to: prefixLen},
+      // Then goes dragged element.
+      {from: dragFrom, to: dragFrom + 1},
+      // Then goes suffix up to dragged element (which has already been moved).
+      {from: prefixLen, to: dragFrom},
+      // Then the rest of an array.
+      {from: dragFrom + 1, to: length},
+    ];
+  }
+
+  // Other case: dragTo > dragFrom
+  const prefixLen = direction == 'left' ? dragTo : dragTo + 1;
+  return [
+    {from: 0, to: dragFrom},
+    {from: dragFrom + 1, to: prefixLen},
+    {from: dragFrom, to: dragFrom + 1},
+    {from: prefixLen, to: length},
+  ];
+}
+
+export function performReordering<T>(intervals: Interval[], arr: T[]): T[] {
+  const result: T[] = [];
+
+  for (const interval of intervals) {
+    result.push(...arr.slice(interval.from, interval.to));
+  }
+
+  return result;
+}
diff --git a/ui/src/core/pivot_table_query_generator.ts b/ui/src/core/pivot_table_query_generator.ts
new file mode 100644
index 0000000..dfaa919
--- /dev/null
+++ b/ui/src/core/pivot_table_query_generator.ts
@@ -0,0 +1,183 @@
+// 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 {sqliteString} from '../base/string_utils';
+import {
+  PivotTableQuery,
+  PivotTableState,
+  Aggregation,
+  TableColumn,
+} from './pivot_table_types';
+import {AreaSelection} from '../public/selection';
+import {SLICE_TRACK_KIND} from '../public/track_kinds';
+
+interface Table {
+  name: string;
+  displayName: string;
+  columns: string[];
+}
+
+const sliceTable = {
+  name: '_slice_with_thread_and_process_info',
+  displayName: 'slice',
+  columns: [
+    'type',
+    'ts',
+    'dur',
+    'category',
+    'name',
+    'depth',
+    'pid',
+    'process_name',
+    'tid',
+    'thread_name',
+  ],
+};
+
+// Columns of `slice` table available for aggregation.
+export const sliceAggregationColumns = [
+  'ts',
+  'dur',
+  'depth',
+  'thread_ts',
+  'thread_dur',
+  'thread_instruction_count',
+  'thread_instruction_delta',
+];
+
+// List of available tables to query, used to populate selectors of pivot
+// columns in the UI.
+export const tables: Table[] = [sliceTable];
+
+// Exception thrown by query generator in case incoming parameters are not
+// suitable in order to build a correct query; these are caught by the UI and
+// displayed to the user.
+export class QueryGeneratorError extends Error {}
+
+// Internal column name for different rollover levels of aggregate columns.
+function aggregationAlias(aggregationIndex: number): string {
+  return `agg_${aggregationIndex}`;
+}
+
+export function areaFilters(
+  area: AreaSelection,
+): {op: (cols: string[]) => string; columns: string[]}[] {
+  return [
+    {
+      op: (cols) => `${cols[0]} + ${cols[1]} > ${area.start}`,
+      columns: ['ts', 'dur'],
+    },
+    {op: (cols) => `${cols[0]} < ${area.end}`, columns: ['ts']},
+    {
+      op: (cols) =>
+        `${cols[0]} in (${getSelectedTrackSqlIds(area).join(', ')})`,
+      columns: ['track_id'],
+    },
+  ];
+}
+
+function expression(column: TableColumn): string {
+  switch (column.kind) {
+    case 'regular':
+      return `${column.table}.${column.column}`;
+    case 'argument':
+      return extractArgumentExpression(column.argument, sliceTable.name);
+  }
+}
+
+function aggregationExpression(aggregation: Aggregation): string {
+  if (aggregation.aggregationFunction === 'COUNT') {
+    return 'COUNT()';
+  }
+  return `${aggregation.aggregationFunction}(${expression(
+    aggregation.column,
+  )})`;
+}
+
+function extractArgumentExpression(argument: string, table?: string) {
+  const prefix = table === undefined ? '' : `${table}.`;
+  return `extract_arg(${prefix}arg_set_id, ${sqliteString(argument)})`;
+}
+
+export function aggregationIndex(pivotColumns: number, aggregationNo: number) {
+  return pivotColumns + aggregationNo;
+}
+
+export function generateQueryFromState(
+  state: PivotTableState,
+): PivotTableQuery {
+  if (state.selectionArea === undefined) {
+    throw new QueryGeneratorError('Should not be called without area');
+  }
+
+  const sliceTableAggregations = [...state.selectedAggregations.values()];
+  if (sliceTableAggregations.length === 0) {
+    throw new QueryGeneratorError('No aggregations selected');
+  }
+
+  const pivots = state.selectedPivots;
+
+  const aggregations = sliceTableAggregations.map(
+    (agg, index) =>
+      `${aggregationExpression(agg)} as ${aggregationAlias(index)}`,
+  );
+  const countIndex = aggregations.length;
+  // Extra count aggregation, needed in order to compute combined averages.
+  aggregations.push('COUNT() as hidden_count');
+
+  const renderedPivots = pivots.map(expression);
+  const sortClauses: string[] = [];
+  for (let i = 0; i < sliceTableAggregations.length; i++) {
+    const sortDirection = sliceTableAggregations[i].sortDirection;
+    if (sortDirection !== undefined) {
+      sortClauses.push(`${aggregationAlias(i)} ${sortDirection}`);
+    }
+  }
+
+  const whereClause = state.constrainToArea
+    ? `where ${areaFilters(state.selectionArea)
+        .map((f) => f.op(f.columns))
+        .join(' and\n')}`
+    : '';
+  const text = `
+    INCLUDE PERFETTO MODULE slices.slices;
+
+    select
+      ${renderedPivots.concat(aggregations).join(',\n')}
+    from ${sliceTable.name}
+    ${whereClause}
+    group by ${renderedPivots.join(', ')}
+    ${sortClauses.length > 0 ? 'order by ' + sortClauses.join(', ') : ''}
+  `;
+
+  return {
+    text,
+    metadata: {
+      pivotColumns: pivots,
+      aggregationColumns: sliceTableAggregations,
+      countIndex,
+    },
+  };
+}
+
+function getSelectedTrackSqlIds(area: AreaSelection): number[] {
+  const selectedTrackKeys: number[] = [];
+  for (const trackInfo of area.tracks) {
+    if (trackInfo?.tags?.kind === SLICE_TRACK_KIND) {
+      trackInfo.tags.trackIds &&
+        selectedTrackKeys.push(...trackInfo.tags.trackIds);
+    }
+  }
+  return selectedTrackKeys;
+}
diff --git a/ui/src/core/pivot_table_tree_builder_unittest.ts b/ui/src/core/pivot_table_tree_builder_unittest.ts
new file mode 100644
index 0000000..527c19c
--- /dev/null
+++ b/ui/src/core/pivot_table_tree_builder_unittest.ts
@@ -0,0 +1,44 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {PivotTableTreeBuilder} from './pivot_table_manager';
+
+describe('Pivot Table tree builder', () => {
+  test('aggregates averages correctly', () => {
+    const builder = new PivotTableTreeBuilder(
+      {
+        pivotColumns: [
+          {kind: 'regular', table: 'slice', column: 'category'},
+          {kind: 'regular', table: 'slice', column: 'name'},
+        ],
+        aggregationColumns: [
+          {
+            aggregationFunction: 'AVG',
+            column: {kind: 'regular', table: 'slice', column: 'dur'},
+          },
+        ],
+        countIndex: 1,
+      },
+      ['cat1', 'name1', 80.0, 2],
+    );
+
+    builder.ingestRow(['cat1', 'name2', 20.0, 1]);
+    builder.ingestRow(['cat2', 'name3', 20.0, 1]);
+
+    // With two rows of average value 80.0, and two of average value 20.0;
+    // the total sum is 80.0 * 2 + 20.0 + 20.0 = 200.0 over four slices. The
+    // average value should be 200.0 / 4 = 50.0
+    expect(builder.build().aggregates[0]).toBeCloseTo(50.0);
+  });
+});
diff --git a/ui/src/core/pivot_table_types.ts b/ui/src/core/pivot_table_types.ts
new file mode 100644
index 0000000..ef7fb57
--- /dev/null
+++ b/ui/src/core/pivot_table_types.ts
@@ -0,0 +1,206 @@
+// 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 {SortDirection} from '../base/comparison_utils';
+import {AreaSelection} from '../public/selection';
+import {ColumnType} from '../trace_processor/query_result';
+
+// Auxiliary metadata needed to parse the query result, as well as to render it
+// correctly. Generated together with the text of query and passed without the
+// change to the query response.
+
+export interface PivotTableQueryMetadata {
+  pivotColumns: TableColumn[];
+  aggregationColumns: Aggregation[];
+  countIndex: number;
+}
+// Everything that's necessary to run the query for pivot table
+
+export interface PivotTableQuery {
+  text: string;
+  metadata: PivotTableQueryMetadata;
+}
+// Pivot table query result
+
+export interface PivotTableResult {
+  // Hierarchical pivot structure on top of rows
+  tree: PivotTree;
+  // Copy of the query metadata from the request, bundled up with the query
+  // result to ensure the correct rendering.
+  metadata: PivotTableQueryMetadata;
+}
+// Input parameters to check whether the pivot table needs to be re-queried.
+
+export interface PivotTableState {
+  // Currently selected area, if null, pivot table is not going to be visible.
+  selectionArea?: AreaSelection;
+
+  // Query response
+  queryResult: PivotTableResult | undefined;
+
+  // Selected pivots for tables other than slice.
+  // Because of the query generation, pivoting happens first on non-slice
+  // pivots; therefore, those can't be put after slice pivots. In order to
+  // maintain the separation more clearly, slice and non-slice pivots are
+  // located in separate arrays.
+  selectedPivots: TableColumn[];
+
+  // Selected aggregation columns. Stored same way as pivots.
+  selectedAggregations: Aggregation[];
+
+  // Whether the pivot table results should be constrained to the selected area.
+  constrainToArea: boolean;
+}
+
+// Node in the hierarchical pivot tree. Only leaf nodes contain data from the
+// query result.
+
+export interface PivotTree {
+  // Whether the node should be collapsed in the UI, false by default and can
+  // be toggled with the button.
+  isCollapsed: boolean;
+
+  // Non-empty only in internal nodes.
+  children: Map<ColumnType, PivotTree>;
+  aggregates: ColumnType[];
+
+  // Non-empty only in leaf nodes.
+  rows: ColumnType[][];
+}
+
+export type AggregationFunction = 'COUNT' | 'SUM' | 'MIN' | 'MAX' | 'AVG';
+// Queried "table column" is either:
+// 1. A real one, represented as object with table and column name.
+// 2. Pseudo-column 'count' that's rendered as '1' in SQL to use in queries like
+// `select sum(1), name from slice group by name`.
+
+export interface RegularColumn {
+  kind: 'regular';
+  table: string;
+  column: string;
+}
+
+export interface ArgumentColumn {
+  kind: 'argument';
+  argument: string;
+}
+
+export type TableColumn = RegularColumn | ArgumentColumn;
+
+export function tableColumnEquals(t1: TableColumn, t2: TableColumn): boolean {
+  switch (t1.kind) {
+    case 'argument': {
+      return t2.kind === 'argument' && t1.argument === t2.argument;
+    }
+    case 'regular': {
+      return (
+        t2.kind === 'regular' &&
+        t1.table === t2.table &&
+        t1.column === t2.column
+      );
+    }
+  }
+}
+
+export function toggleEnabled<T>(
+  compare: (fst: T, snd: T) => boolean,
+  arr: T[],
+  column: T,
+  enabled: boolean,
+): void {
+  if (enabled && arr.find((value) => compare(column, value)) === undefined) {
+    arr.push(column);
+  }
+  if (!enabled) {
+    const index = arr.findIndex((value) => compare(column, value));
+    if (index !== -1) {
+      arr.splice(index, 1);
+    }
+  }
+}
+
+export interface Aggregation {
+  aggregationFunction: AggregationFunction;
+  column: TableColumn;
+
+  // If the aggregation is sorted, the field contains a sorting direction.
+  sortDirection?: SortDirection;
+}
+
+export function aggregationEquals(agg1: Aggregation, agg2: Aggregation) {
+  return new EqualsBuilder(agg1, agg2)
+    .comparePrimitive((agg) => agg.aggregationFunction)
+    .compare(tableColumnEquals, (agg) => agg.column)
+    .equals();
+}
+
+// Used to convert TableColumn to a string in order to store it in a Map, as
+// ES6 does not support compound Set/Map keys. This function should only be used
+// for interning keys, and does not have any requirements beyond different
+// TableColumn objects mapping to different strings.
+
+export function columnKey(tableColumn: TableColumn): string {
+  switch (tableColumn.kind) {
+    case 'argument': {
+      return `argument:${tableColumn.argument}`;
+    }
+    case 'regular': {
+      return `${tableColumn.table}.${tableColumn.column}`;
+    }
+  }
+}
+
+export function aggregationKey(aggregation: Aggregation): string {
+  return `${aggregation.aggregationFunction}:${columnKey(aggregation.column)}`;
+}
+
+export const COUNT_AGGREGATION: Aggregation = {
+  aggregationFunction: 'COUNT',
+  // Exact column is ignored for count aggregation because it does not matter
+  // what to count, use empty strings.
+  column: {kind: 'regular', table: '', column: ''},
+};
+
+// Simple builder-style class to implement object equality more succinctly.
+class EqualsBuilder<T> {
+  result = true;
+  first: T;
+  second: T;
+
+  constructor(first: T, second: T) {
+    this.first = first;
+    this.second = second;
+  }
+
+  comparePrimitive(getter: (arg: T) => string | number): EqualsBuilder<T> {
+    if (this.result) {
+      this.result = getter(this.first) === getter(this.second);
+    }
+    return this;
+  }
+
+  compare<S>(
+    comparator: (first: S, second: S) => boolean,
+    getter: (arg: T) => S,
+  ): EqualsBuilder<T> {
+    if (this.result) {
+      this.result = comparator(getter(this.first), getter(this.second));
+    }
+    return this;
+  }
+
+  equals(): boolean {
+    return this.result;
+  }
+}
diff --git a/ui/src/core/plugin_manager.ts b/ui/src/core/plugin_manager.ts
new file mode 100644
index 0000000..f444618
--- /dev/null
+++ b/ui/src/core/plugin_manager.ts
@@ -0,0 +1,206 @@
+// 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 {Registry} from '../base/registry';
+import {App} from '../public/app';
+import {
+  MetricVisualisation,
+  PerfettoPlugin,
+  PerfettoPluginStatic,
+} from '../public/plugin';
+import {Trace} from '../public/trace';
+import {defaultPlugins} from './default_plugins';
+import {featureFlags} from './feature_flags';
+import {Flag} from '../public/feature_flag';
+import {TraceImpl} from './trace_impl';
+
+// The pseudo plugin id used for the core instance of AppImpl.
+export const CORE_PLUGIN_ID = '__core__';
+
+function makePlugin(
+  desc: PerfettoPluginStatic<PerfettoPlugin>,
+  trace: Trace,
+): PerfettoPlugin {
+  const PluginClass = desc;
+  return new PluginClass(trace);
+}
+
+// This interface injects AppImpl's methods into PluginManager to avoid
+// circular dependencies between PluginManager and AppImpl.
+export interface PluginAppInterface {
+  forkForPlugin(pluginId: string): App;
+  get trace(): TraceImpl | undefined;
+}
+
+// Contains information about a plugin.
+export interface PluginWrapper {
+  // A reference to the plugin descriptor
+  readonly desc: PerfettoPluginStatic<PerfettoPlugin>;
+
+  // The feature flag used to allow users to change whether this plugin should
+  // be enabled or not.
+  readonly enableFlag: Flag;
+
+  // Keeps track of whether the plugin has been activated or not.
+  active?: boolean;
+
+  // If a trace has been loaded, this object stores the relevant trace-scoped
+  // plugin data
+  traceContext?: {
+    // The concrete plugin instance, created on trace load.
+    readonly instance: PerfettoPlugin;
+
+    // How long it took for the plugin's onTraceLoad() function to run.
+    readonly loadTimeMs: number;
+  };
+}
+
+export class PluginManagerImpl {
+  private readonly registry = new Registry<PluginWrapper>((x) => x.desc.id);
+  private orderedPlugins: Array<PluginWrapper> = [];
+
+  constructor(private readonly app: PluginAppInterface) {}
+
+  registerPlugin(desc: PerfettoPluginStatic<PerfettoPlugin>) {
+    const flagId = `plugin_${desc.id}`;
+    const name = `Plugin: ${desc.id}`;
+    const flag = featureFlags.register({
+      id: flagId,
+      name,
+      description: `Overrides '${desc.id}' plugin.`,
+      defaultValue: defaultPlugins.includes(desc.id),
+    });
+    this.registry.register({
+      desc,
+      enableFlag: flag,
+    });
+  }
+
+  /**
+   * Activates all registered plugins that have not already been registered.
+   *
+   * @param enableOverrides - The list of plugins that are enabled regardless of
+   * the current flag setting.
+   */
+  activatePlugins(enableOverrides: ReadonlyArray<string> = []) {
+    const enabledPlugins = this.registry
+      .valuesAsArray()
+      .filter((p) => p.enableFlag.get() || enableOverrides.includes(p.desc.id));
+
+    this.orderedPlugins = this.sortPluginsTopologically(enabledPlugins);
+
+    this.orderedPlugins.forEach((p) => {
+      if (p.active) return;
+      const app = this.app.forkForPlugin(p.desc.id);
+      p.desc.onActivate?.(app);
+      p.active = true;
+    });
+  }
+
+  async onTraceLoad(
+    traceCore: TraceImpl,
+    beforeEach?: (id: string) => void,
+  ): Promise<void> {
+    // Awaiting all plugins in parallel will skew timing data as later plugins
+    // will spend most of their time waiting for earlier plugins to load.
+    // Running in parallel will have very little performance benefit assuming
+    // most plugins use the same engine, which can only process one query at a
+    // time.
+    for (const p of this.orderedPlugins) {
+      if (p.active) {
+        beforeEach?.(p.desc.id);
+        const trace = traceCore.forkForPlugin(p.desc.id);
+        const before = performance.now();
+        const instance = makePlugin(p.desc, trace);
+        await instance.onTraceLoad?.(trace);
+        const loadTimeMs = performance.now() - before;
+        p.traceContext = {
+          instance,
+          loadTimeMs,
+        };
+        traceCore.trash.defer(() => {
+          p.traceContext = undefined;
+        });
+      }
+    }
+  }
+
+  metricVisualisations(): MetricVisualisation[] {
+    return this.registry.valuesAsArray().flatMap((plugin) => {
+      if (!plugin.active) return [];
+      return plugin.desc.metricVisualisations?.() ?? [];
+    });
+  }
+
+  getAllPlugins() {
+    return this.registry.valuesAsArray();
+  }
+
+  getPluginContainer(id: string): PluginWrapper | undefined {
+    return this.registry.tryGet(id);
+  }
+
+  getPlugin<T extends PerfettoPlugin>(
+    pluginDescriptor: PerfettoPluginStatic<T>,
+  ): T {
+    const plugin = this.registry.get(pluginDescriptor.id);
+    return assertExists(plugin.traceContext).instance as T;
+  }
+
+  /**
+   * Sort plugins in dependency order, ensuring that if a plugin depends on
+   * other plugins, those plugins will appear fist in the list.
+   */
+  private sortPluginsTopologically(
+    plugins: ReadonlyArray<PluginWrapper>,
+  ): Array<PluginWrapper> {
+    const orderedPlugins = new Array<PluginWrapper>();
+    const visiting = new Set<string>();
+
+    const visit = (p: PluginWrapper) => {
+      // Continue if we've already added this plugin, there's no need to add it
+      // again
+      if (orderedPlugins.includes(p)) {
+        return;
+      }
+
+      // Detect circular dependencies
+      if (visiting.has(p.desc.id)) {
+        const cycle = Array.from(visiting).concat(p.desc.id);
+        throw new Error(
+          `Cyclic plugin dependency detected: ${cycle.join(' -> ')}`,
+        );
+      }
+
+      // Temporarily push this plugin onto the visiting stack while visiting
+      // dependencies, to allow circular dependencies to be detected
+      visiting.add(p.desc.id);
+
+      // Recursively visit dependencies
+      p.desc.dependencies?.forEach((d) => {
+        visit(this.registry.get(d.id));
+      });
+
+      visiting.delete(p.desc.id);
+
+      // Finally add this plugin to the ordered list
+      orderedPlugins.push(p);
+    };
+
+    plugins.forEach((p) => visit(p));
+
+    return orderedPlugins;
+  }
+}
diff --git a/ui/src/core/query_flamegraph.ts b/ui/src/core/query_flamegraph.ts
deleted file mode 100644
index 8e600af..0000000
--- a/ui/src/core/query_flamegraph.ts
+++ /dev/null
@@ -1,459 +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 {AsyncLimiter} from '../base/async_limiter';
-import {AsyncDisposableStack} from '../base/disposable_stack';
-import {assertExists} from '../base/logging';
-import {Monitor} from '../base/monitor';
-import {uuidv4Sql} from '../base/uuid';
-import {Engine} from '../trace_processor/engine';
-import {
-  createPerfettoIndex,
-  createPerfettoTable,
-} from '../trace_processor/sql_utils';
-import {NUM, NUM_NULL, STR, STR_NULL} from '../trace_processor/query_result';
-import {
-  Flamegraph,
-  FlamegraphFilters,
-  FlamegraphQueryData,
-  FlamegraphView,
-} from '../widgets/flamegraph';
-
-import {featureFlags} from './feature_flags';
-
-export interface QueryFlamegraphColumn {
-  // The name of the column in SQL.
-  readonly name: string;
-
-  // The human readable name describing the contents of the column.
-  readonly displayName: string;
-}
-
-export interface QueryFlamegraphMetric {
-  // The human readable name of the metric: will be shown to the user to change
-  // between metrics.
-  readonly name: string;
-
-  // The human readable SI-style unit of `selfValue`. Values will be shown to
-  // the user suffixed with this.
-  readonly unit: string;
-
-  // SQL statement which need to be run in preparation for being able to execute
-  // `statement`.
-  readonly dependencySql?: string;
-
-  // A single SQL statement which returns the columns `id`, `parentId`, `name`
-  // `selfValue`, all columns specified by `unaggregatableProperties` and
-  // `aggregatableProperties`.
-  readonly statement: string;
-
-  // Additional contextual columns containing data which should not be merged
-  // between sibling nodes, even if they have the same name.
-  //
-  // Examples include the mapping that a name comes from, the heap graph root
-  // type etc.
-  //
-  // Note: the name is always unaggregatable and should not be specified here.
-  readonly unaggregatableProperties?: ReadonlyArray<QueryFlamegraphColumn>;
-
-  // Additional contextual columns containing data which will be displayed to
-  // the user if there is no merging. If there is merging, currently the value
-  // will not be shown.
-  //
-  // Examples include the source file and line number.
-  //
-  // TODO(lalitm): reconsider the decision to show nothing, instead maybe show
-  // the top 5 options etc.
-  readonly aggregatableProperties?: ReadonlyArray<QueryFlamegraphColumn>;
-}
-
-// Given a table and columns on those table (corresponding to metrics),
-// returns an array of `QueryFlamegraphMetric` structs which can be passed
-// in QueryFlamegraph's attrs.
-//
-// `tableOrSubquery` should have the columns `id`, `parentId`, `name` and all
-// columns specified by `tableMetrics[].name`, `unaggregatableProperties` and
-// `aggregatableProperties`.
-export function metricsFromTableOrSubquery(
-  tableOrSubquery: string,
-  tableMetrics: ReadonlyArray<{name: string; unit: string; columnName: string}>,
-  dependencySql?: string,
-  unaggregatableProperties?: ReadonlyArray<QueryFlamegraphColumn>,
-  aggregatableProperties?: ReadonlyArray<QueryFlamegraphColumn>,
-): QueryFlamegraphMetric[] {
-  const metrics = [];
-  for (const {name, unit, columnName} of tableMetrics) {
-    metrics.push({
-      name,
-      unit,
-      dependencySql,
-      statement: `
-        select *, ${columnName} as value
-        from ${tableOrSubquery}
-      `,
-      unaggregatableProperties,
-      aggregatableProperties,
-    });
-  }
-  return metrics;
-}
-
-export interface QueryFlamegraphAttrs {
-  readonly engine: Engine;
-  readonly metrics: ReadonlyArray<QueryFlamegraphMetric>;
-}
-
-// A Mithril component which wraps the `Flamegraph` widget and fetches the data
-// for the widget by querying an `Engine`.
-export class QueryFlamegraph implements m.ClassComponent<QueryFlamegraphAttrs> {
-  private selectedMetricName;
-  private data?: FlamegraphQueryData;
-  private filters: FlamegraphFilters = {
-    showStack: [],
-    hideStack: [],
-    showFromFrame: [],
-    hideFrame: [],
-    view: {kind: 'TOP_DOWN'},
-  };
-  private attrs: QueryFlamegraphAttrs;
-  private selMonitor = new Monitor([() => this.attrs.metrics]);
-  private queryLimiter = new AsyncLimiter();
-
-  constructor({attrs}: m.Vnode<QueryFlamegraphAttrs>) {
-    this.attrs = attrs;
-    this.selectedMetricName = attrs.metrics[0].name;
-  }
-
-  view({attrs}: m.Vnode<QueryFlamegraphAttrs>) {
-    this.attrs = attrs;
-    if (this.selMonitor.ifStateChanged()) {
-      this.selectedMetricName = attrs.metrics[0].name;
-      this.data = undefined;
-      this.fetchData(attrs);
-    }
-    return m(Flamegraph, {
-      metrics: attrs.metrics,
-      selectedMetricName: this.selectedMetricName,
-      data: this.data,
-      onMetricChange: (name) => {
-        this.selectedMetricName = name;
-        this.data = undefined;
-        this.fetchData(attrs);
-      },
-      onFiltersChanged: (filters) => {
-        this.filters = filters;
-        this.data = undefined;
-        this.fetchData(attrs);
-      },
-    });
-  }
-
-  private async fetchData(attrs: QueryFlamegraphAttrs) {
-    const metric = assertExists(
-      attrs.metrics.find((metric) => metric.name === this.selectedMetricName),
-    );
-    const engine = attrs.engine;
-    const filters = this.filters;
-    this.queryLimiter.schedule(async () => {
-      this.data = await computeFlamegraphTree(engine, metric, filters);
-    });
-  }
-}
-
-async function computeFlamegraphTree(
-  engine: Engine,
-  {
-    dependencySql,
-    statement,
-    unaggregatableProperties,
-    aggregatableProperties,
-  }: QueryFlamegraphMetric,
-  {showStack, hideStack, showFromFrame, hideFrame, view}: FlamegraphFilters,
-): Promise<FlamegraphQueryData> {
-  // Pivot also essentially acts as a "show stack" filter so treat it like one.
-  const showStackAndPivot = [...showStack];
-  if (view.kind === 'PIVOT') {
-    showStackAndPivot.push(view.pivot);
-  }
-
-  const showStackFilter =
-    showStackAndPivot.length === 0
-      ? '0'
-      : showStackAndPivot
-          .map((x, i) => `((name like '${makeSqlFilter(x)}') << ${i})`)
-          .join(' | ');
-  const showStackBits = (1 << showStackAndPivot.length) - 1;
-
-  const hideStackFilter =
-    hideStack.length === 0
-      ? 'false'
-      : hideStack.map((x) => `name like '${makeSqlFilter(x)}'`).join(' OR ');
-
-  const showFromFrameFilter =
-    showFromFrame.length === 0
-      ? '0'
-      : showFromFrame
-          .map((x, i) => `((name like '${makeSqlFilter(x)}') << ${i})`)
-          .join(' | ');
-  const showFromFrameBits = (1 << showFromFrame.length) - 1;
-
-  const hideFrameFilter =
-    hideFrame.length === 0
-      ? 'false'
-      : hideFrame.map((x) => `name like '${makeSqlFilter(x)}'`).join(' OR ');
-
-  const pivotFilter = getPivotFilter(view);
-
-  const unagg = unaggregatableProperties ?? [];
-  const unaggCols = unagg.map((x) => x.name);
-
-  const agg = aggregatableProperties ?? [];
-  const aggCols = agg.map((x) => x.name);
-
-  const groupingColumns = `(${(unaggCols.length === 0 ? ['groupingColumn'] : unaggCols).join()})`;
-  const groupedColumns = `(${(aggCols.length === 0 ? ['groupedColumn'] : aggCols).join()})`;
-
-  if (dependencySql !== undefined) {
-    await engine.query(dependencySql);
-  }
-  await engine.query(`include perfetto module viz.flamegraph;`);
-
-  const uuid = uuidv4Sql();
-  await using disposable = new AsyncDisposableStack();
-
-  disposable.use(
-    await createPerfettoTable(
-      engine,
-      `_flamegraph_materialized_statement_${uuid}`,
-      statement,
-    ),
-  );
-  disposable.use(
-    await createPerfettoIndex(
-      engine,
-      `_flamegraph_materialized_statement_${uuid}_index`,
-      `_flamegraph_materialized_statement_${uuid}(parentId)`,
-    ),
-  );
-
-  // TODO(lalitm): this doesn't need to be called unless we have
-  // a non-empty set of filters.
-  disposable.use(
-    await createPerfettoTable(
-      engine,
-      `_flamegraph_source_${uuid}`,
-      `
-        select *
-        from _viz_flamegraph_prepare_filter!(
-          (
-            select
-              s.id,
-              s.parentId,
-              s.name,
-              s.value,
-              ${(unaggCols.length === 0
-                ? [`'' as groupingColumn`]
-                : unaggCols.map((x) => `s.${x}`)
-              ).join()},
-              ${(aggCols.length === 0
-                ? [`'' as groupedColumn`]
-                : aggCols.map((x) => `s.${x}`)
-              ).join()}
-            from _flamegraph_materialized_statement_${uuid} s
-          ),
-          (${showStackFilter}),
-          (${hideStackFilter}),
-          (${showFromFrameFilter}),
-          (${hideFrameFilter}),
-          (${pivotFilter}),
-          ${1 << showStackAndPivot.length},
-          ${groupingColumns}
-        )
-      `,
-    ),
-  );
-  // TODO(lalitm): this doesn't need to be called unless we have
-  // a non-empty set of filters.
-  disposable.use(
-    await createPerfettoTable(
-      engine,
-      `_flamegraph_filtered_${uuid}`,
-      `
-        select *
-        from _viz_flamegraph_filter_frames!(
-          _flamegraph_source_${uuid},
-          ${showFromFrameBits}
-        )
-      `,
-    ),
-  );
-  disposable.use(
-    await createPerfettoTable(
-      engine,
-      `_flamegraph_accumulated_${uuid}`,
-      `
-        select *
-        from _viz_flamegraph_accumulate!(
-          _flamegraph_filtered_${uuid},
-          ${showStackBits}
-        )
-      `,
-    ),
-  );
-  disposable.use(
-    await createPerfettoTable(
-      engine,
-      `_flamegraph_hash_${uuid}`,
-      `
-        select *
-        from _viz_flamegraph_downwards_hash!(
-          _flamegraph_source_${uuid},
-          _flamegraph_filtered_${uuid},
-          _flamegraph_accumulated_${uuid},
-          ${groupingColumns},
-          ${groupedColumns},
-          ${view.kind === 'BOTTOM_UP' ? 'FALSE' : 'TRUE'}
-        )
-        union all
-        select *
-        from _viz_flamegraph_upwards_hash!(
-          _flamegraph_source_${uuid},
-          _flamegraph_filtered_${uuid},
-          _flamegraph_accumulated_${uuid},
-          ${groupingColumns},
-          ${groupedColumns}
-        )
-        order by hash
-      `,
-    ),
-  );
-  disposable.use(
-    await createPerfettoTable(
-      engine,
-      `_flamegraph_merged_${uuid}`,
-      `
-        select *
-        from _viz_flamegraph_merge_hashes!(
-          _flamegraph_hash_${uuid},
-          ${groupingColumns},
-          ${groupedColumns}
-        )
-      `,
-    ),
-  );
-  disposable.use(
-    await createPerfettoTable(
-      engine,
-      `_flamegraph_layout_${uuid}`,
-      `
-        select *
-        from _viz_flamegraph_local_layout!(
-          _flamegraph_merged_${uuid}
-        );
-      `,
-    ),
-  );
-  const res = await engine.query(`
-    select *
-    from _viz_flamegraph_global_layout!(
-      _flamegraph_merged_${uuid},
-      _flamegraph_layout_${uuid},
-      ${groupingColumns},
-      ${groupedColumns}
-    )
-  `);
-
-  const it = res.iter({
-    id: NUM,
-    parentId: NUM,
-    depth: NUM,
-    name: STR,
-    selfValue: NUM,
-    cumulativeValue: NUM,
-    xStart: NUM,
-    xEnd: NUM,
-    ...Object.fromEntries(unaggCols.map((m) => [m, STR_NULL])),
-    ...Object.fromEntries(aggCols.map((m) => [m, STR_NULL])),
-  });
-  let postiveRootsValue = 0;
-  let negativeRootsValue = 0;
-  let minDepth = 0;
-  let maxDepth = 0;
-  const nodes = [];
-  for (; it.valid(); it.next()) {
-    const properties = new Map<string, string>();
-    for (const a of [...agg, ...unagg]) {
-      const r = it.get(a.name);
-      if (r !== null) {
-        properties.set(a.displayName, r as string);
-      }
-    }
-    nodes.push({
-      id: it.id,
-      parentId: it.parentId,
-      depth: it.depth,
-      name: it.name,
-      selfValue: it.selfValue,
-      cumulativeValue: it.cumulativeValue,
-      xStart: it.xStart,
-      xEnd: it.xEnd,
-      properties,
-    });
-    if (it.depth === 1) {
-      postiveRootsValue += it.cumulativeValue;
-    } else if (it.depth === -1) {
-      negativeRootsValue += it.cumulativeValue;
-    }
-    minDepth = Math.min(minDepth, it.depth);
-    maxDepth = Math.max(maxDepth, it.depth);
-  }
-  const sumQuery = await engine.query(
-    `select sum(value) v from _flamegraph_source_${uuid}`,
-  );
-  const unfilteredCumulativeValue = sumQuery.firstRow({v: NUM_NULL}).v ?? 0;
-  return {
-    nodes,
-    allRootsCumulativeValue:
-      view.kind === 'BOTTOM_UP' ? negativeRootsValue : postiveRootsValue,
-    unfilteredCumulativeValue,
-    minDepth,
-    maxDepth,
-  };
-}
-
-function makeSqlFilter(x: string) {
-  if (x.startsWith('^') && x.endsWith('$')) {
-    return x.slice(1, -1);
-  }
-  return `%${x}%`;
-}
-
-function getPivotFilter(view: FlamegraphView) {
-  if (view.kind === 'PIVOT') {
-    return `name like '${makeSqlFilter(view.pivot)}'`;
-  }
-  if (view.kind === 'BOTTOM_UP') {
-    return 'value > 0';
-  }
-  return '0';
-}
-
-export const USE_NEW_FLAMEGRAPH_IMPL = featureFlags.register({
-  id: 'useNewFlamegraphImpl',
-  name: 'Use new flamegraph implementation',
-  description: 'Use new flamgraph implementation in details panels.',
-  defaultValue: true,
-});
diff --git a/ui/src/core/raf_scheduler.ts b/ui/src/core/raf_scheduler.ts
index be3f546..c6ca0fc 100644
--- a/ui/src/core/raf_scheduler.ts
+++ b/ui/src/core/raf_scheduler.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {
   debugNow,
   measure,
diff --git a/ui/src/core/router.ts b/ui/src/core/router.ts
new file mode 100644
index 0000000..259ed6d
--- /dev/null
+++ b/ui/src/core/router.ts
@@ -0,0 +1,234 @@
+// 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 {assertTrue} from '../base/logging';
+import {RouteArgs, ROUTE_SCHEMA} from '../public/route_schema';
+import {PageAttrs} from '../public/page';
+
+export const ROUTE_PREFIX = '#!';
+
+// The set of args that can be set on the route via #!/page?a=1&b2.
+// Route args are orthogonal to pages (i.e. should NOT make sense only in a
+// only within a specific page, use /page/subpages for that).
+// Args are !== the querystring (location.search) which is sent to the
+// server. The route args are NOT sent to the HTTP server.
+// Given this URL:
+// http://host/?foo=1&bar=2#!/page/subpage?local_cache_key=a0b1&baz=3.
+//
+// location.search = 'foo=1&bar=2'.
+//   This is seen by the HTTP server. We really don't use querystrings as the
+//   perfetto UI is client only.
+//
+// location.hash = '#!/page/subpage?local_cache_key=a0b1'.
+//   This is client-only. All the routing logic in the Perfetto UI uses only
+//   this.
+
+function safeParseRoute(rawRoute: unknown): RouteArgs {
+  const res = ROUTE_SCHEMA.safeParse(rawRoute);
+  return res.success ? res.data : {};
+}
+
+// A broken down representation of a route.
+// For instance: #!/record/gpu?local_cache_key=a0b1
+// becomes: {page: '/record', subpage: '/gpu', args: {local_cache_key: 'a0b1'}}
+export interface Route {
+  page: string;
+  subpage: string;
+  fragment: string;
+  args: RouteArgs;
+}
+
+export interface RoutesMap {
+  [key: string]: m.ComponentTypes<PageAttrs>;
+}
+
+// This router does two things:
+// 1) Maps fragment paths (#!/page/subpage) to Mithril components.
+// The route map is passed to the ctor and is later used when calling the
+// resolve() method.
+//
+// 2) Handles the (optional) args, e.g. #!/page?arg=1&arg2=2.
+// Route args are carry information that is orthogonal to the page (e.g. the
+// trace id).
+// local_cache_key has some special treatment: once a URL has a local_cache_key,
+// it gets automatically appended to further navigations that don't have one.
+// For instance if the current url is #!/viewer?local_cache_key=1234 and a later
+// action (either user-initiated or code-initited) navigates to #!/info, the
+// rotuer will automatically replace the history entry with
+// #!/info?local_cache_key=1234.
+// This is to keep propagating the trace id across page changes, for handling
+// tab discards (b/175041881).
+//
+// This class does NOT deal with the "load a trace when the url contains ?url=
+// or ?local_cache_key=". That logic lives in trace_url_handler.ts, which is
+// triggered by Router.onRouteChanged().
+export class Router {
+  private readonly recentChanges: number[] = [];
+
+  // frontend/index.ts calls maybeOpenTraceFromRoute() + redraw here.
+  // This event is decoupled for testing and to avoid circular deps.
+  onRouteChanged: (route: Route) => void = () => {};
+
+  constructor() {
+    window.onhashchange = (e: HashChangeEvent) => this.onHashChange(e);
+    const route = Router.parseUrl(window.location.href);
+    this.onRouteChanged(route);
+  }
+
+  private onHashChange(e: HashChangeEvent) {
+    this.crashIfLivelock();
+
+    const oldRoute = Router.parseUrl(e.oldURL);
+    const newRoute = Router.parseUrl(e.newURL);
+
+    if (
+      newRoute.args.local_cache_key === undefined &&
+      oldRoute.args.local_cache_key
+    ) {
+      // Propagate `local_cache_key across` navigations. When a trace is loaded,
+      // the URL becomes #!/viewer?local_cache_key=123. `local_cache_key` allows
+      // reopening the trace from cache in the case of a reload or discard.
+      // When using the UI we can hit "bare" links (e.g. just '#!/info') which
+      // don't have the trace_uuid:
+      // - When clicking on an <a> element from the sidebar.
+      // - When the code calls Router.navigate().
+      // - When the user pastes a URL from docs page.
+      // In all these cases we want to keep propagating the `local_cache_key`.
+      // We do so by re-setting the `local_cache_key` and doing a
+      // location.replace which overwrites the history entry (note
+      // location.replace is NOT just a String.replace operation).
+      newRoute.args.local_cache_key = oldRoute.args.local_cache_key;
+    }
+
+    const args = m.buildQueryString(newRoute.args);
+    let normalizedFragment = `#!${newRoute.page}${newRoute.subpage}`;
+    if (args.length) {
+      normalizedFragment += `?${args}`;
+    }
+    if (newRoute.fragment) {
+      normalizedFragment += `#${newRoute.fragment}`;
+    }
+
+    if (!e.newURL.endsWith(normalizedFragment)) {
+      location.replace(normalizedFragment);
+      return;
+    }
+
+    this.onRouteChanged(newRoute);
+  }
+
+  static navigate(newHash: string) {
+    assertTrue(newHash.startsWith(ROUTE_PREFIX));
+    window.location.hash = newHash;
+  }
+
+  // Breaks down a fragment into a Route object.
+  // Sample input:
+  // '#!/record/gpu?local_cache_key=abcd-1234#myfragment'
+  // Sample output:
+  // {
+  //  page: '/record',
+  //  subpage: '/gpu',
+  //  fragment: 'myfragment',
+  //  args: {local_cache_key: 'abcd-1234'}
+  // }
+  static parseFragment(hash: string): Route {
+    if (hash.startsWith(ROUTE_PREFIX)) {
+      hash = hash.substring(ROUTE_PREFIX.length);
+    } else {
+      hash = '';
+    }
+
+    const url = new URL(`https://example.com${hash}`);
+
+    const path = url.pathname;
+    let page = path;
+    let subpage = '';
+    const splittingPoint = path.indexOf('/', 1);
+    if (splittingPoint > 0) {
+      page = path.substring(0, splittingPoint);
+      subpage = path.substring(splittingPoint);
+    }
+    if (page === '/') {
+      page = '';
+    }
+
+    let rawArgs = {};
+    if (url.search) {
+      rawArgs = Router.parseQueryString(url.search);
+    }
+
+    const args = safeParseRoute(rawArgs);
+
+    // Javascript sadly distinguishes between foo[bar] === undefined
+    // and foo[bar] is not set at all. Here we need the second case to
+    // avoid making the URL ugly.
+    for (const key of Object.keys(args)) {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      if ((args as any)[key] === undefined) {
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        delete (args as any)[key];
+      }
+    }
+
+    let fragment = url.hash;
+    if (fragment.startsWith('#')) {
+      fragment = fragment.substring(1);
+    }
+
+    return {page, subpage, args, fragment};
+  }
+
+  private static parseQueryString(query: string) {
+    query = query.replaceAll('+', ' ');
+    return m.parseQueryString(query);
+  }
+
+  private static parseSearchParams(url: string): RouteArgs {
+    const query = new URL(url).search;
+    const rawArgs = Router.parseQueryString(query);
+    const args = safeParseRoute(rawArgs);
+    return args;
+  }
+
+  // Like parseFragment() but takes a full URL.
+  static parseUrl(url: string): Route {
+    const searchArgs = Router.parseSearchParams(url);
+
+    const hashPos = url.indexOf('#');
+    const fragment = hashPos < 0 ? '' : url.substring(hashPos);
+    const route = Router.parseFragment(fragment);
+    route.args = Object.assign({}, searchArgs, route.args);
+
+    return route;
+  }
+
+  // Throws if EVENT_LIMIT onhashchange events occur within WINDOW_MS.
+  private crashIfLivelock() {
+    const WINDOW_MS = 1000;
+    const EVENT_LIMIT = 20;
+    const now = Date.now();
+    while (
+      this.recentChanges.length > 0 &&
+      now - this.recentChanges[0] > WINDOW_MS
+    ) {
+      this.recentChanges.shift();
+    }
+    this.recentChanges.push(now);
+    if (this.recentChanges.length > EVENT_LIMIT) {
+      throw new Error('History rewriting livelock');
+    }
+  }
+}
diff --git a/ui/src/core/router_unittest.ts b/ui/src/core/router_unittest.ts
new file mode 100644
index 0000000..89887d0
--- /dev/null
+++ b/ui/src/core/router_unittest.ts
@@ -0,0 +1,201 @@
+// 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 {PageManagerImpl} from './page_manager';
+import {CORE_PLUGIN_ID} from './plugin_manager';
+import {Router} from './router';
+
+const mockComponent = {
+  view() {},
+};
+
+describe('Router#resolve', () => {
+  beforeEach(() => {
+    window.location.hash = '';
+  });
+
+  const pluginId = CORE_PLUGIN_ID;
+  const traceless = true;
+
+  test('Resolves empty route to default component', () => {
+    const pages = new PageManagerImpl();
+    pages.registerPage({route: '/', page: mockComponent, traceless, pluginId});
+    window.location.hash = '';
+    expect(pages.renderPageForCurrentRoute(undefined).tag).toBe(mockComponent);
+  });
+
+  test('Resolves subpage route to component of main page', () => {
+    const nonDefaultComponent = {view() {}};
+    const pages = new PageManagerImpl();
+    pages.registerPage({route: '/', page: mockComponent, traceless, pluginId});
+    pages.registerPage({
+      route: '/a',
+      page: nonDefaultComponent,
+      traceless,
+      pluginId,
+    });
+    window.location.hash = '#!/a/subpage';
+    expect(pages.renderPageForCurrentRoute(undefined).tag).toBe(
+      nonDefaultComponent,
+    );
+    expect(pages.renderPageForCurrentRoute(undefined).attrs.subpage).toBe(
+      '/subpage',
+    );
+  });
+
+  test('Pass empty subpage if not found in URL', () => {
+    const nonDefaultComponent = {view() {}};
+    const pages = new PageManagerImpl();
+    pages.registerPage({route: '/', page: mockComponent, traceless, pluginId});
+    pages.registerPage({
+      route: '/a',
+      page: nonDefaultComponent,
+      traceless,
+      pluginId,
+    });
+    window.location.hash = '#!/a';
+    expect(pages.renderPageForCurrentRoute(undefined).tag).toBe(
+      nonDefaultComponent,
+    );
+    expect(pages.renderPageForCurrentRoute(undefined).attrs.subpage).toBe('');
+  });
+});
+
+describe('Router.parseUrl', () => {
+  // Can parse arguments from the search string.
+  test('Search parsing', () => {
+    const url = 'http://localhost?p=123&s=42&url=a?b?c';
+    const route = Router.parseUrl(url);
+    const args = route.args;
+    expect(args.p).toBe('123');
+    expect(args.s).toBe('42');
+    expect(args.url).toBe('a?b?c');
+    expect(route.fragment).toBe('');
+  });
+
+  // Or from the fragment string.
+  test('Fragment parsing', () => {
+    const url = 'http://localhost/#!/foo?p=123&s=42&url=a?b?c';
+    const route = Router.parseUrl(url);
+    const args = route.args;
+    expect(args.p).toBe('123');
+    expect(args.s).toBe('42');
+    expect(args.url).toBe('a?b?c');
+    expect(route.fragment).toBe('');
+  });
+
+  // Or both in which case fragment overrides the search.
+  test('Fragment parsing', () => {
+    const url =
+      'http://localhost/?p=1&s=2&hideSidebar=true#!/foo?s=3&url=4&hideSidebar=false';
+    const route = Router.parseUrl(url);
+    const args = route.args;
+    expect(args.p).toBe('1');
+    expect(args.s).toBe('3');
+    expect(args.url).toBe('4');
+    expect(args.hideSidebar).toBe(false);
+    expect(route.fragment).toBe('');
+  });
+
+  // + is also space
+  test('plus is space query', () => {
+    const url = 'http://localhost?query=(foo+%2B+bar),';
+    const route = Router.parseUrl(url);
+    const args = route.args;
+    expect(args.query).toBe('(foo + bar),');
+  });
+
+  // + is also space
+  test('plus is space hash', () => {
+    const url = 'http://localhost#!/foo?query=(foo+%2B+bar),';
+    const route = Router.parseUrl(url);
+    const args = route.args;
+    expect(args.query).toBe('(foo + bar),');
+  });
+
+  test('Nested fragment', () => {
+    const url =
+      'http://localhost/?p=1&s=2&hideSidebar=true#!/foo?s=3&url=4&hideSidebar=false#myfragment';
+    const route = Router.parseUrl(url);
+    expect(route.fragment).toBe('myfragment');
+  });
+});
+
+describe('Router.parseFragment', () => {
+  test('empty route broken into empty components', () => {
+    const {page, subpage, args} = Router.parseFragment('');
+    expect(page).toBe('');
+    expect(subpage).toBe('');
+    expect(args.mode).toBe(undefined);
+  });
+
+  test('by default args are undefined', () => {
+    // This prevents the url from becoming messy.
+    const {args} = Router.parseFragment('');
+    expect(args).toEqual({});
+  });
+
+  test('invalid route broken into empty components', () => {
+    const {page, subpage} = Router.parseFragment('/bla');
+    expect(page).toBe('');
+    expect(subpage).toBe('');
+  });
+
+  test('simple route has page defined', () => {
+    const {page, subpage} = Router.parseFragment('#!/record');
+    expect(page).toBe('/record');
+    expect(subpage).toBe('');
+  });
+
+  test('simple route has both components defined', () => {
+    const {page, subpage} = Router.parseFragment('#!/record/memory');
+    expect(page).toBe('/record');
+    expect(subpage).toBe('/memory');
+  });
+
+  test('route broken at first slash', () => {
+    const {page, subpage} = Router.parseFragment('#!/record/memory/stuff');
+    expect(page).toBe('/record');
+    expect(subpage).toBe('/memory/stuff');
+  });
+
+  test('parameters separated from route', () => {
+    const {page, subpage, args} = Router.parseFragment(
+      '#!/record/memory?url=http://localhost:1234/aaaa',
+    );
+    expect(page).toBe('/record');
+    expect(subpage).toBe('/memory');
+    expect(args.url).toEqual('http://localhost:1234/aaaa');
+  });
+
+  test('openFromAndroidBugTool can be false', () => {
+    const {args} = Router.parseFragment('#!/?openFromAndroidBugTool=false');
+    expect(args.openFromAndroidBugTool).toEqual(false);
+  });
+
+  test('openFromAndroidBugTool can be true', () => {
+    const {args} = Router.parseFragment('#!/?openFromAndroidBugTool=true');
+    expect(args.openFromAndroidBugTool).toEqual(true);
+  });
+
+  test('bad modes are coerced to default', () => {
+    const {args} = Router.parseFragment('#!/?mode=1234');
+    expect(args.mode).toEqual(undefined);
+  });
+
+  test('bad hideSidebar is coerced to default', () => {
+    const {args} = Router.parseFragment('#!/?hideSidebar=helloworld!');
+    expect(args.hideSidebar).toEqual(undefined);
+  });
+});
diff --git a/ui/src/core/scroll_helper.ts b/ui/src/core/scroll_helper.ts
new file mode 100644
index 0000000..59b7b11
--- /dev/null
+++ b/ui/src/core/scroll_helper.ts
@@ -0,0 +1,146 @@
+// 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 {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
+import {time} from '../base/time';
+import {ScrollToArgs} from '../public/scroll_helper';
+import {TraceInfo} from '../public/trace_info';
+import {Workspace} from '../public/workspace';
+import {raf} from './raf_scheduler';
+import {TimelineImpl} from './timeline';
+import {TrackManagerImpl} from './track_manager';
+
+// A helper class to help jumping to tracks and time ranges.
+// This class must NOT alter in any way the selection status. That
+// responsibility belongs to SelectionManager (which uses this).
+export class ScrollHelper {
+  constructor(
+    private traceInfo: TraceInfo,
+    private timeline: TimelineImpl,
+    private workspace: Workspace,
+    private trackManager: TrackManagerImpl,
+  ) {}
+
+  // See comments in ScrollToArgs for the intended semantics.
+  scrollTo(args: ScrollToArgs) {
+    const {time, track} = args;
+    raf.scheduleRedraw();
+
+    if (time !== undefined) {
+      if (time.end === undefined) {
+        this.timeline.panToTimestamp(time.start);
+      } else if (time.viewPercentage !== undefined) {
+        this.focusHorizontalRangePercentage(
+          time.start,
+          time.end,
+          time.viewPercentage,
+        );
+      } else {
+        this.focusHorizontalRange(time.start, time.end);
+      }
+    }
+
+    if (track !== undefined) {
+      this.verticalScrollToTrack(track.uri, track.expandGroup ?? false);
+    }
+  }
+
+  private focusHorizontalRangePercentage(
+    start: time,
+    end: time,
+    viewPercentage: number,
+  ): void {
+    const aoi = HighPrecisionTimeSpan.fromTime(start, end);
+
+    if (viewPercentage <= 0.0 || viewPercentage > 1.0) {
+      console.warn(
+        'Invalid value for [viewPercentage]. ' +
+          'Value must be between 0.0 (exclusive) and 1.0 (inclusive).',
+      );
+      // Default to 50%.
+      viewPercentage = 0.5;
+    }
+    const paddingPercentage = 1.0 - viewPercentage;
+    const halfPaddingTime = (aoi.duration * paddingPercentage) / 2;
+    this.timeline.updateVisibleTimeHP(aoi.pad(halfPaddingTime));
+  }
+
+  private focusHorizontalRange(start: time, end: time): void {
+    const visible = this.timeline.visibleWindow;
+    const aoi = HighPrecisionTimeSpan.fromTime(start, end);
+    const fillRatio = 5; // Default amount to make the AOI fill the viewport
+    const padRatio = (fillRatio - 1) / 2;
+
+    // If the area of interest already fills more than half the viewport, zoom
+    // out so that the AOI fills 20% of the viewport
+    if (aoi.duration * 2 > visible.duration) {
+      const padded = aoi.pad(aoi.duration * padRatio);
+      this.timeline.updateVisibleTimeHP(padded);
+    } else {
+      // Center visible window on the middle of the AOI, preserving zoom level.
+      const newStart = aoi.midpoint.subNumber(visible.duration / 2);
+
+      // Adjust the new visible window if it intersects with the trace boundaries.
+      // It's needed to make the "update the zoom level if visible window doesn't
+      // change" logic reliable.
+      const newVisibleWindow = new HighPrecisionTimeSpan(
+        newStart,
+        visible.duration,
+      ).fitWithin(this.traceInfo.start, this.traceInfo.end);
+
+      // If preserving the zoom doesn't change the visible window, consider this
+      // to be the "second" hotkey press, so just make the AOI fill 20% of the
+      // viewport
+      if (newVisibleWindow.equals(visible)) {
+        const padded = aoi.pad(aoi.duration * padRatio);
+        this.timeline.updateVisibleTimeHP(padded);
+      } else {
+        this.timeline.updateVisibleTimeHP(newVisibleWindow);
+      }
+    }
+  }
+
+  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);
+    if (!trackNode) return;
+
+    // Try finding the track directly.
+    const element = document.getElementById(trackNode.id);
+    if (element) {
+      // block: 'nearest' means that it will only scroll if the track is not
+      // currently in view.
+      element.scrollIntoView({behavior: 'smooth', block: 'nearest'});
+      return;
+    }
+
+    // If we get here, the element for this track was not present in the DOM,
+    // which might be because it's inside a collapsed group.
+    if (openGroup) {
+      // Try to reveal the track node in the workspace by opening up all
+      // ancestor groups, and mark the track URI to be scrolled to in the
+      // future.
+      trackNode.reveal();
+      this.trackManager.scrollToTrackNodeId = trackNode.id;
+    } else {
+      // Find the closest visible ancestor of our target track and scroll to
+      // that instead.
+      const container = trackNode.findClosestVisibleAncestor();
+      document
+        .getElementById(container.id)
+        ?.scrollIntoView({behavior: 'smooth', block: 'nearest'});
+    }
+  }
+}
diff --git a/ui/src/core/search_data.ts b/ui/src/core/search_data.ts
new file mode 100644
index 0000000..cd5856b
--- /dev/null
+++ b/ui/src/core/search_data.ts
@@ -0,0 +1,30 @@
+// Copyright (C) 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
+//
+//      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 SearchSource = 'cpu' | 'log' | 'slice' | 'track';
+
+export interface SearchSummary {
+  tsStarts: BigInt64Array;
+  tsEnds: BigInt64Array;
+  count: Uint8Array;
+}
+
+export interface CurrentSearchResults {
+  eventIds: Float64Array;
+  tses: BigInt64Array;
+  utids: Float64Array;
+  trackUris: string[];
+  sources: SearchSource[];
+  totalResults: number;
+}
diff --git a/ui/src/core/search_manager.ts b/ui/src/core/search_manager.ts
new file mode 100644
index 0000000..bc56498
--- /dev/null
+++ b/ui/src/core/search_manager.ts
@@ -0,0 +1,353 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {AsyncLimiter} from '../base/async_limiter';
+import {searchSegment} from '../base/binary_search';
+import {assertExists, assertTrue} from '../base/logging';
+import {sqliteString} from '../base/string_utils';
+import {Time, TimeSpan} from '../base/time';
+import {exists} from '../base/utils';
+import {ResultStepEventHandler} from '../public/search';
+import {
+  ANDROID_LOGS_TRACK_KIND,
+  CPU_SLICE_TRACK_KIND,
+} from '../public/track_kinds';
+import {Workspace} from '../public/workspace';
+import {Engine} from '../trace_processor/engine';
+import {LONG, NUM, STR} from '../trace_processor/query_result';
+import {escapeSearchQuery} from '../trace_processor/query_utils';
+import {raf} from './raf_scheduler';
+import {SearchSource} from './search_data';
+import {TimelineImpl} from './timeline';
+import {TrackManagerImpl} from './track_manager';
+
+export interface SearchResults {
+  eventIds: Float64Array;
+  tses: BigInt64Array;
+  utids: Float64Array;
+  trackUris: string[];
+  sources: SearchSource[];
+  totalResults: number;
+}
+
+export class SearchManagerImpl {
+  private _searchGeneration = 0;
+  private _searchText = '';
+  private _searchWindow?: TimeSpan;
+  private _results?: SearchResults;
+  private _resultIndex = -1;
+  private _searchInProgress = false;
+
+  // TODO(primiano): once we get rid of globals, these below can be made always
+  // defined. the ?: is to deal with globals-before-trace-load.
+  private _timeline?: TimelineImpl;
+  private _trackManager?: TrackManagerImpl;
+  private _workspace?: Workspace;
+  private _engine?: Engine;
+  private _limiter = new AsyncLimiter();
+  private _onResultStep?: ResultStepEventHandler;
+
+  constructor(args?: {
+    timeline: TimelineImpl;
+    trackManager: TrackManagerImpl;
+    workspace: Workspace;
+    engine: Engine;
+    onResultStep: ResultStepEventHandler;
+  }) {
+    this._timeline = args?.timeline;
+    this._trackManager = args?.trackManager;
+    this._engine = args?.engine;
+    this._workspace = args?.workspace;
+    this._onResultStep = args?.onResultStep;
+  }
+
+  search(text: string) {
+    if (text === this._searchText) {
+      return;
+    }
+    this._searchText = text;
+    this._searchGeneration++;
+    this._results = undefined;
+    this._resultIndex = -1;
+    this._searchInProgress = false;
+    if (text !== '') {
+      this._searchInProgress = true;
+      this._searchWindow = this._timeline?.visibleWindow.toTimeSpan();
+      this._limiter.schedule(async () => {
+        await this.executeSearch();
+        this._searchInProgress = false;
+        raf.scheduleFullRedraw();
+      });
+    }
+    raf.scheduleFullRedraw();
+  }
+
+  reset() {
+    this.search('');
+  }
+
+  stepForward() {
+    this.stepInternal(false);
+  }
+
+  stepBackwards() {
+    this.stepInternal(true);
+  }
+
+  private stepInternal(reverse = false) {
+    if (this._searchWindow === undefined) return;
+    if (this._results === undefined) return;
+
+    const index = this._resultIndex;
+    const {start: startNs, end: endNs} = this._searchWindow;
+    const currentTs = this._results.tses[index];
+
+    // If the value of |this._results.totalResults| is 0,
+    // it means that the query is in progress or no results are found.
+    if (this._results.totalResults === 0) {
+      return;
+    }
+
+    // If this is a new search or the currentTs is not in the viewport,
+    // select the first/last item in the viewport.
+    if (
+      index === -1 ||
+      (currentTs !== -1n && (currentTs < startNs || currentTs > endNs))
+    ) {
+      if (reverse) {
+        const [smaller] = searchSegment(this._results.tses, endNs);
+        // If there is no item in the viewport just go to the previous.
+        if (smaller === -1) {
+          this.setResultIndexWithSaturation(index - 1);
+        } else {
+          this._resultIndex = smaller;
+        }
+      } else {
+        const [, larger] = searchSegment(this._results.tses, startNs);
+        // If there is no item in the viewport just go to the next.
+        if (larger === -1) {
+          this.setResultIndexWithSaturation(index + 1);
+        } else {
+          this._resultIndex = larger;
+        }
+      }
+    } else {
+      // If the currentTs is in the viewport, increment the index.
+      if (reverse) {
+        this.setResultIndexWithSaturation(index - 1);
+      } else {
+        this.setResultIndexWithSaturation(index + 1);
+      }
+    }
+    if (this._onResultStep) {
+      this._onResultStep({
+        eventId: this._results.eventIds[this._resultIndex],
+        ts: Time.fromRaw(this._results.tses[this._resultIndex]),
+        trackUri: this._results.trackUris[this._resultIndex],
+        source: this._results.sources[this._resultIndex],
+      });
+    }
+    raf.scheduleFullRedraw();
+  }
+
+  private setResultIndexWithSaturation(nextIndex: number) {
+    const tot = assertExists(this._results).totalResults;
+    assertTrue(tot !== 0); // The early out in step() guarantees this.
+    // This is a modulo operation that works also for negative numbers.
+    this._resultIndex = ((nextIndex % tot) + tot) % tot;
+  }
+
+  get hasResults() {
+    return this._results !== undefined;
+  }
+
+  get searchResults() {
+    return this._results;
+  }
+
+  get resultIndex() {
+    return this._resultIndex;
+  }
+
+  get searchText() {
+    return this._searchText;
+  }
+
+  get searchGeneration() {
+    return this._searchGeneration;
+  }
+
+  get searchInProgress(): boolean {
+    return this._searchInProgress;
+  }
+
+  private async executeSearch() {
+    const search = this._searchText;
+    const window = this._searchWindow;
+    const searchLiteral = escapeSearchQuery(this._searchText);
+    const generation = this._searchGeneration;
+
+    const engine = this._engine;
+    const trackManager = this._trackManager;
+    const workspace = this._workspace;
+    if (!engine || !trackManager || !workspace || !window) {
+      return;
+    }
+
+    // TODO(stevegolton): Avoid recomputing these indexes each time.
+    const trackUrisByCpu = new Map<number, string>();
+    const allTracks = trackManager.getAllTracks();
+    allTracks.forEach((td) => {
+      const tags = td?.tags;
+      const cpu = tags?.cpu;
+      const kind = tags?.kind;
+      exists(cpu) &&
+        kind === CPU_SLICE_TRACK_KIND &&
+        trackUrisByCpu.set(cpu, td.uri);
+    });
+
+    const trackUrisByTrackId = new Map<number, string>();
+    allTracks.forEach((td) => {
+      const trackIds = td?.tags?.trackIds ?? [];
+      trackIds.forEach((trackId) => trackUrisByTrackId.set(trackId, td.uri));
+    });
+
+    const utidRes = await engine.query(`select utid from thread join process
+    using(upid) where
+      thread.name glob ${searchLiteral} or
+      process.name glob ${searchLiteral}`);
+    const utids = [];
+    for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) {
+      utids.push(it.utid);
+    }
+
+    const res = await engine.query(`
+      select
+        id as sliceId,
+        ts,
+        'cpu' as source,
+        cpu as sourceId,
+        utid
+      from sched where utid in (${utids.join(',')})
+      union all
+      select *
+      from (
+        select
+          slice_id as sliceId,
+          ts,
+          'slice' as source,
+          track_id as sourceId,
+          0 as utid
+          from slice
+          where slice.name glob ${searchLiteral}
+            or (
+              0 != CAST(${sqliteString(search)} AS INT) and
+              sliceId = CAST(${sqliteString(search)} AS INT)
+            )
+        union
+        select
+          slice_id as sliceId,
+          ts,
+          'slice' as source,
+          track_id as sourceId,
+          0 as utid
+        from slice
+        join args using(arg_set_id)
+        where string_value glob ${searchLiteral} or key glob ${searchLiteral}
+      )
+      union all
+      select
+        id as sliceId,
+        ts,
+        'log' as source,
+        0 as sourceId,
+        utid
+      from android_logs where msg glob ${searchLiteral}
+      order by ts
+    `);
+
+    const searchResults: SearchResults = {
+      eventIds: new Float64Array(0),
+      tses: new BigInt64Array(0),
+      utids: new Float64Array(0),
+      sources: [],
+      trackUris: [],
+      totalResults: 0,
+    };
+
+    const lowerSearch = search.toLowerCase();
+    for (const track of workspace.flatTracks) {
+      // We don't support searching for tracks that don't have a URI.
+      if (!track.uri) continue;
+      if (track.title.toLowerCase().indexOf(lowerSearch) === -1) {
+        continue;
+      }
+      searchResults.totalResults++;
+      searchResults.sources.push('track');
+      searchResults.trackUris.push(track.uri);
+    }
+
+    const rows = res.numRows();
+    searchResults.eventIds = new Float64Array(
+      searchResults.totalResults + rows,
+    );
+    searchResults.tses = new BigInt64Array(searchResults.totalResults + rows);
+    searchResults.utids = new Float64Array(searchResults.totalResults + rows);
+    for (let i = 0; i < searchResults.totalResults; ++i) {
+      searchResults.eventIds[i] = -1;
+      searchResults.tses[i] = -1n;
+      searchResults.utids[i] = -1;
+    }
+
+    const it = res.iter({
+      sliceId: NUM,
+      ts: LONG,
+      source: STR,
+      sourceId: NUM,
+      utid: NUM,
+    });
+    for (; it.valid(); it.next()) {
+      let track: string | undefined = undefined;
+
+      if (it.source === 'cpu') {
+        track = trackUrisByCpu.get(it.sourceId);
+      } else if (it.source === 'slice') {
+        track = trackUrisByTrackId.get(it.sourceId);
+      } else if (it.source === 'log') {
+        track = trackManager
+          .getAllTracks()
+          .find((td) => td.tags?.kind === ANDROID_LOGS_TRACK_KIND)?.uri;
+      }
+      // The .get() calls above could return undefined, this isn't just an else.
+      if (track === undefined) {
+        continue;
+      }
+
+      const i = searchResults.totalResults++;
+      searchResults.trackUris.push(track);
+      searchResults.sources.push(it.source as SearchSource);
+      searchResults.eventIds[i] = it.sliceId;
+      searchResults.tses[i] = it.ts;
+      searchResults.utids[i] = it.utid;
+    }
+
+    if (generation !== this._searchGeneration) {
+      // We arrived too late. By the time we computed results the user issued
+      // another search.
+      return;
+    }
+    this._results = searchResults;
+    this._resultIndex = -1;
+  }
+}
diff --git a/ui/src/core/selection_aggregation_manager.ts b/ui/src/core/selection_aggregation_manager.ts
new file mode 100644
index 0000000..cba366f
--- /dev/null
+++ b/ui/src/core/selection_aggregation_manager.ts
@@ -0,0 +1,202 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {AsyncLimiter} from '../base/async_limiter';
+import {isString} from '../base/object_utils';
+import {AggregateData, Column, ColumnDef, Sorting} from '../public/aggregation';
+import {AreaSelection, AreaSelectionAggregator} from '../public/selection';
+import {Engine} from '../trace_processor/engine';
+import {NUM} from '../trace_processor/query_result';
+import {raf} from './raf_scheduler';
+
+export class SelectionAggregationManager {
+  private engine: Engine;
+  private readonly limiter = new AsyncLimiter();
+  private _aggregators = new Array<AreaSelectionAggregator>();
+  private _aggregatedData = new Map<string, AggregateData>();
+  private _sorting = new Map<string, Sorting>();
+  private _currentArea: AreaSelection | undefined = undefined;
+
+  constructor(engine: Engine) {
+    this.engine = engine;
+  }
+
+  registerAggregator(aggr: AreaSelectionAggregator) {
+    this._aggregators.push(aggr);
+  }
+
+  aggregateArea(area: AreaSelection) {
+    this.limiter.schedule(async () => {
+      this._currentArea = area;
+      this._aggregatedData.clear();
+      for (const aggr of this._aggregators) {
+        const data = await this.runAggregator(aggr, area);
+        if (data !== undefined) {
+          this._aggregatedData.set(aggr.id, data);
+        }
+      }
+      raf.scheduleFullRedraw();
+    });
+  }
+
+  clear() {
+    // This is wrapped in the async limiter to make sure that an aggregateArea()
+    // followed by a clear() (e.g., because selection changes) doesn't end up
+    // with the aggregation being displayed anyways once the promise completes.
+    this.limiter.schedule(async () => {
+      this._currentArea = undefined;
+      this._aggregatedData.clear();
+      this._sorting.clear();
+      raf.scheduleFullRedraw();
+    });
+  }
+
+  getSortingPrefs(aggregatorId: string): Sorting | undefined {
+    return this._sorting.get(aggregatorId);
+  }
+
+  toggleSortingColumn(aggregatorId: string, column: string) {
+    const sorting = this._sorting.get(aggregatorId);
+
+    if (sorting === undefined || sorting.column !== column) {
+      // No sorting set for current column.
+      this._sorting.set(aggregatorId, {
+        column,
+        direction: 'DESC',
+      });
+    } else if (sorting.direction === 'DESC') {
+      // Toggle the direction if the column is currently sorted.
+      this._sorting.set(aggregatorId, {
+        column,
+        direction: 'ASC',
+      });
+    } else {
+      // If direction is currently 'ASC' toggle to no sorting.
+      this._sorting.delete(aggregatorId);
+    }
+
+    // Re-run the aggregation.
+    if (this._currentArea) {
+      this.aggregateArea(this._currentArea);
+    }
+  }
+
+  get aggregators(): ReadonlyArray<AreaSelectionAggregator> {
+    return this._aggregators;
+  }
+
+  getAggregatedData(aggregatorId: string): AggregateData | undefined {
+    return this._aggregatedData.get(aggregatorId);
+  }
+
+  private async runAggregator(
+    aggr: AreaSelectionAggregator,
+    area: AreaSelection,
+  ): Promise<AggregateData | undefined> {
+    const viewExists = await aggr.createAggregateView(this.engine, area);
+    if (!viewExists) {
+      return undefined;
+    }
+
+    const defs = aggr.getColumnDefinitions();
+    const colIds = defs.map((col) => col.columnId);
+    const sorting = this._sorting.get(aggr.id);
+    let sortClause = `${aggr.getDefaultSorting().column} ${
+      aggr.getDefaultSorting().direction
+    }`;
+    if (sorting) {
+      sortClause = `${sorting.column} ${sorting.direction}`;
+    }
+    const query = `select ${colIds} from ${aggr.id} order by ${sortClause}`;
+    const result = await this.engine.query(query);
+
+    const numRows = result.numRows();
+    const columns = defs.map((def) => columnFromColumnDef(def, numRows));
+    const columnSums = await Promise.all(
+      defs.map((def) => this.getSum(aggr.id, def)),
+    );
+    const extraData = await aggr.getExtra(this.engine, area);
+    const extra = extraData ? extraData : undefined;
+    const data: AggregateData = {
+      tabName: aggr.getTabName(),
+      columns,
+      columnSums,
+      strings: [],
+      extra,
+    };
+
+    const stringIndexes = new Map<string, number>();
+    function internString(str: string) {
+      let idx = stringIndexes.get(str);
+      if (idx !== undefined) return idx;
+      idx = data.strings.length;
+      data.strings.push(str);
+      stringIndexes.set(str, idx);
+      return idx;
+    }
+
+    const it = result.iter({});
+    for (let i = 0; it.valid(); it.next(), ++i) {
+      for (const column of data.columns) {
+        const item = it.get(column.columnId);
+        if (item === null) {
+          column.data[i] = isStringColumn(column) ? internString('NULL') : 0;
+        } else if (isString(item)) {
+          column.data[i] = internString(item);
+        } else if (item instanceof Uint8Array) {
+          column.data[i] = internString('<Binary blob>');
+        } else if (typeof item === 'bigint') {
+          // TODO(stevegolton) It would be nice to keep bigints as bigints for
+          // the purposes of aggregation, however the aggregation infrastructure
+          // is likely to be significantly reworked when we introduce EventSet,
+          // and the complexity of supporting bigints throughout the aggregation
+          // panels in its current form is not worth it. Thus, we simply
+          // convert bigints to numbers.
+          column.data[i] = Number(item);
+        } else {
+          column.data[i] = item;
+        }
+      }
+    }
+
+    return data;
+  }
+
+  private async getSum(tableName: string, def: ColumnDef): Promise<string> {
+    if (!def.sum) return '';
+    const result = await this.engine.query(
+      `select ifnull(sum(${def.columnId}), 0) as s from ${tableName}`,
+    );
+    let sum = result.firstRow({s: NUM}).s;
+    if (def.kind === 'TIMESTAMP_NS') {
+      sum = sum / 1e6;
+    }
+    return `${sum}`;
+  }
+}
+
+function columnFromColumnDef(def: ColumnDef, numRows: number): Column {
+  // TODO(hjd): The Column type should be based on the
+  // ColumnDef type or vice versa to avoid this cast.
+  return {
+    title: def.title,
+    kind: def.kind,
+    data: new def.columnConstructor(numRows),
+    columnId: def.columnId,
+  } as Column;
+}
+
+function isStringColumn(column: Column): boolean {
+  return column.kind === 'STRING' || column.kind === 'STATE';
+}
diff --git a/ui/src/core/selection_manager.ts b/ui/src/core/selection_manager.ts
index 2ddb75b..8644301 100644
--- a/ui/src/core/selection_manager.ts
+++ b/ui/src/core/selection_manager.ts
@@ -12,234 +12,404 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {duration, time} from '../base/time';
-import {Store} from '../base/store';
-import {assertUnreachable} from '../base/logging';
-import {GenericSliceDetailsTabConfigBase} from './generic_slice_details_types';
+import {assertTrue, assertUnreachable} from '../base/logging';
+import {
+  Selection,
+  Area,
+  SelectionOpts,
+  SelectionManager,
+  AreaSelectionAggregator,
+  SqlSelectionResolver,
+  TrackEventSelection,
+} from '../public/selection';
+import {TimeSpan} from '../base/time';
+import {raf} from './raf_scheduler';
+import {exists} from '../base/utils';
+import {TrackManagerImpl} from './track_manager';
+import {Engine} from '../trace_processor/engine';
+import {ScrollHelper} from './scroll_helper';
+import {NoteManagerImpl} from './note_manager';
+import {SearchResult} from '../public/search';
+import {SelectionAggregationManager} from './selection_aggregation_manager';
+import {AsyncLimiter} from '../base/async_limiter';
+import m from 'mithril';
+import {SerializedSelection} from './state_serialization_schema';
 
-export enum ProfileType {
-  HEAP_PROFILE = 'heap_profile',
-  MIXED_HEAP_PROFILE = 'heap_profile:com.android.art,libc.malloc',
-  NATIVE_HEAP_PROFILE = 'heap_profile:libc.malloc',
-  JAVA_HEAP_SAMPLES = 'heap_profile:com.android.art',
-  JAVA_HEAP_GRAPH = 'graph',
-  PERF_SAMPLE = 'perf',
+const INSTANT_FOCUS_DURATION = 1n;
+const INCOMPLETE_SLICE_DURATION = 30_000n;
+
+interface SelectionDetailsPanel {
+  isLoading: boolean;
+  render(): m.Children;
+  serializatonState(): unknown;
 }
 
-// LEGACY Selection types:
-export interface SliceSelection {
-  kind: 'SCHED_SLICE';
-  id: number;
-}
+// There are two selection-related states in this class.
+// 1. _selection: This is the "input" / locator of the selection, what other
+//    parts of the codebase specify (e.g., a tuple of trackUri + eventId) to say
+//    "please select this object if it exists".
+// 2. _selected{Slice,ThreadState}: This is the resolved selection, that is, the
+//    rich details about the object that has been selected. If the input
+//    `_selection` is valid, this is filled in the near future. Doing so
+//    requires querying the SQL engine, which is an async operation.
+export class SelectionManagerImpl implements SelectionManager {
+  private readonly detailsPanelLimiter = new AsyncLimiter();
+  private _selection: Selection = {kind: 'empty'};
+  private _aggregationManager: SelectionAggregationManager;
+  // Incremented every time _selection changes.
+  private readonly selectionResolvers = new Array<SqlSelectionResolver>();
+  private readonly detailsPanels = new WeakMap<
+    Selection,
+    SelectionDetailsPanel
+  >();
 
-export interface HeapProfileSelection {
-  kind: 'HEAP_PROFILE';
-  id: number;
-  upid: number;
-  ts: time;
-  type: ProfileType;
-}
-
-export interface PerfSamplesSelection {
-  kind: 'PERF_SAMPLES';
-  id: number;
-  utid?: number;
-  upid?: number;
-  leftTs: time;
-  rightTs: time;
-  type: ProfileType;
-}
-
-export interface CpuProfileSampleSelection {
-  kind: 'CPU_PROFILE_SAMPLE';
-  id: number;
-  utid: number;
-  ts: time;
-}
-
-export interface ThreadSliceSelection {
-  kind: 'SLICE';
-  id: number;
-  table?: string;
-}
-
-export interface ThreadStateSelection {
-  kind: 'THREAD_STATE';
-  id: number;
-}
-
-export interface LogSelection {
-  kind: 'LOG';
-  id: number;
-  trackKey: string;
-}
-
-export interface GenericSliceSelection {
-  kind: 'GENERIC_SLICE';
-  id: number;
-  sqlTableName: string;
-  start: time;
-  duration: duration;
-  // NOTE: this config can be expanded for multiple details panel types.
-  detailsPanelConfig: {kind: string; config: GenericSliceDetailsTabConfigBase};
-}
-
-export type LegacySelection = (
-  | SliceSelection
-  | HeapProfileSelection
-  | CpuProfileSampleSelection
-  | ThreadSliceSelection
-  | ThreadStateSelection
-  | PerfSamplesSelection
-  | LogSelection
-  | GenericSliceSelection
-) & {trackKey?: string};
-export type SelectionKind = LegacySelection['kind']; // 'THREAD_STATE' | 'SLICE' ...
-
-// New Selection types:
-export interface LegacySelectionWrapper {
-  kind: 'legacy';
-  legacySelection: LegacySelection;
-}
-
-export interface SingleSelection {
-  kind: 'single';
-  trackKey: string;
-  eventId: number;
-}
-
-export interface AreaSelection {
-  kind: 'area';
-  tracks: string[];
-  start: time;
-  end: time;
-}
-
-export interface NoteSelection {
-  kind: 'note';
-  id: string;
-}
-
-export interface UnionSelection {
-  kind: 'union';
-  selections: Selection[];
-}
-
-export interface EmptySelection {
-  kind: 'empty';
-}
-
-export type Selection =
-  | SingleSelection
-  | AreaSelection
-  | NoteSelection
-  | UnionSelection
-  | EmptySelection
-  | LegacySelectionWrapper;
-
-export function selectionToLegacySelection(
-  selection: Selection,
-): LegacySelection | null {
-  switch (selection.kind) {
-    case 'area':
-    case 'single':
-    case 'empty':
-    case 'note':
-      return null;
-    case 'union':
-      for (const child of selection.selections) {
-        const result = selectionToLegacySelection(child);
-        if (result !== null) {
-          return result;
-        }
-      }
-      return null;
-    case 'legacy':
-      return selection.legacySelection;
-    default:
-      assertUnreachable(selection);
-      return null;
+  constructor(
+    engine: Engine,
+    private trackManager: TrackManagerImpl,
+    private noteManager: NoteManagerImpl,
+    private scrollHelper: ScrollHelper,
+    private onSelectionChange: (s: Selection, opts: SelectionOpts) => void,
+  ) {
+    this._aggregationManager = new SelectionAggregationManager(
+      engine.getProxy('SelectionAggregationManager'),
+    );
   }
-}
 
-interface SelectionState {
-  selection: Selection;
-}
-
-export class SelectionManager {
-  private store: Store<SelectionState>;
-
-  constructor(store: Store<SelectionState>) {
-    this.store = store;
+  registerAreaSelectionAggreagtor(aggr: AreaSelectionAggregator): void {
+    this._aggregationManager.registerAggregator(aggr);
   }
 
   clear(): void {
-    this.store.edit((draft) => {
-      draft.selection = {
-        kind: 'empty',
-      };
-    });
+    this.setSelection({kind: 'empty'});
   }
 
-  private addSelection(selection: Selection): void {
-    this.store.edit((draft) => {
-      switch (draft.selection.kind) {
-        case 'empty':
-          draft.selection = selection;
-          break;
-        case 'union':
-          draft.selection.selections.push(selection);
-          break;
-        case 'single':
-        case 'legacy':
-        case 'area':
-        case 'note':
-          draft.selection = {
-            kind: 'union',
-            selections: [draft.selection, selection],
-          };
-          break;
-        default:
-          assertUnreachable(draft.selection);
-          break;
-      }
-    });
-  }
-
-  // There is no matching addLegacy as we did not support multi-single
-  // selection with the legacy selection system.
-  setLegacy(legacySelection: LegacySelection): void {
-    this.clear();
-    this.addSelection({
-      kind: 'legacy',
-      legacySelection,
-    });
-  }
-
-  setEvent(
-    trackKey: string,
+  async selectTrackEvent(
+    trackUri: string,
     eventId: number,
-    legacySelection?: LegacySelection,
+    opts?: SelectionOpts,
   ) {
-    this.clear();
-    this.addEvent(trackKey, eventId, legacySelection);
+    this.selectTrackEventInternal(trackUri, eventId, opts);
   }
 
-  addEvent(
-    trackKey: string,
-    eventId: number,
-    legacySelection?: LegacySelection,
-  ) {
-    this.addSelection({
-      kind: 'single',
-      trackKey,
-      eventId,
+  selectTrack(trackUri: string, opts?: SelectionOpts) {
+    this.setSelection({kind: 'track', trackUri}, opts);
+  }
+
+  selectNote(args: {id: string}, opts?: SelectionOpts) {
+    this.setSelection(
+      {
+        kind: 'note',
+        id: args.id,
+      },
+      opts,
+    );
+  }
+
+  selectArea(area: Area, opts?: SelectionOpts): void {
+    const {start, end} = area;
+    assertTrue(start <= end);
+
+    // In the case of area selection, the caller provides a list of trackUris.
+    // However, all the consumer want to access the resolved TrackDescriptor.
+    // Rather than delegating this to the various consumers, we resolve them
+    // now once and for all and place them in the selection object.
+    const tracks = [];
+    for (const uri of area.trackUris) {
+      const trackDescr = this.trackManager.getTrack(uri);
+      if (trackDescr === undefined) continue;
+      tracks.push(trackDescr);
+    }
+
+    this.setSelection(
+      {
+        ...area,
+        kind: 'area',
+        tracks,
+      },
+      opts,
+    );
+  }
+
+  deserialize(serialized: SerializedSelection | undefined) {
+    if (serialized === undefined) {
+      return;
+    }
+    switch (serialized.kind) {
+      case 'TRACK_EVENT':
+        this.selectTrackEventInternal(
+          serialized.trackKey,
+          parseInt(serialized.eventId),
+          undefined,
+          serialized.detailsPanel,
+        );
+        break;
+      case 'AREA':
+        this.selectArea({
+          start: serialized.start,
+          end: serialized.end,
+          trackUris: serialized.trackUris,
+        });
+    }
+  }
+
+  toggleTrackAreaSelection(trackUri: string) {
+    const curSelection = this._selection;
+    if (curSelection.kind !== 'area') return;
+
+    let trackUris = curSelection.trackUris.slice();
+    if (!trackUris.includes(trackUri)) {
+      trackUris.push(trackUri);
+    } else {
+      trackUris = trackUris.filter((t) => t !== trackUri);
+    }
+    this.selectArea({
+      ...curSelection,
+      trackUris,
     });
-    if (legacySelection) {
-      this.addSelection({
-        kind: 'legacy',
-        legacySelection,
+  }
+
+  toggleGroupAreaSelection(trackUris: string[]) {
+    const curSelection = this._selection;
+    if (curSelection.kind !== 'area') return;
+
+    const allTracksSelected = trackUris.every((t) =>
+      curSelection.trackUris.includes(t),
+    );
+
+    let newTrackUris: string[];
+    if (allTracksSelected) {
+      // Deselect all tracks in the list
+      newTrackUris = curSelection.trackUris.filter(
+        (t) => !trackUris.includes(t),
+      );
+    } else {
+      newTrackUris = curSelection.trackUris.slice();
+      trackUris.forEach((t) => {
+        if (!newTrackUris.includes(t)) {
+          newTrackUris.push(t);
+        }
       });
     }
+    this.selectArea({
+      ...curSelection,
+      trackUris: newTrackUris,
+    });
+  }
+
+  get selection(): Selection {
+    return this._selection;
+  }
+
+  getDetailsPanelForSelection(): SelectionDetailsPanel | undefined {
+    return this.detailsPanels.get(this._selection);
+  }
+
+  registerSqlSelectionResolver(resolver: SqlSelectionResolver): void {
+    this.selectionResolvers.push(resolver);
+  }
+
+  async resolveSqlEvent(
+    sqlTableName: string,
+    id: number,
+  ): Promise<{eventId: number; trackUri: string} | undefined> {
+    const matchingResolvers = this.selectionResolvers.filter(
+      (r) => r.sqlTableName === sqlTableName,
+    );
+
+    for (const resolver of matchingResolvers) {
+      const result = await resolver.callback(id, sqlTableName);
+      if (result) {
+        // If we have multiple resolvers for the same table, just return the first one.
+        return result;
+      }
+    }
+
+    return undefined;
+  }
+
+  selectSqlEvent(sqlTableName: string, id: number, opts?: SelectionOpts): void {
+    this.resolveSqlEvent(sqlTableName, id).then((selection) => {
+      selection &&
+        this.selectTrackEvent(selection.trackUri, selection.eventId, opts);
+    });
+  }
+
+  private setSelection(selection: Selection, opts?: SelectionOpts) {
+    this._selection = selection;
+    this.onSelectionChange(selection, opts ?? {});
+    raf.scheduleFullRedraw();
+
+    if (opts?.scrollToSelection) {
+      this.scrollToCurrentSelection();
+    }
+
+    if (this._selection.kind === 'area') {
+      this._aggregationManager.aggregateArea(this._selection);
+    } else {
+      this._aggregationManager.clear();
+    }
+  }
+
+  selectSearchResult(searchResult: SearchResult) {
+    const {source, eventId, trackUri} = searchResult;
+    if (eventId === undefined) {
+      return;
+    }
+    switch (source) {
+      case 'track':
+        this.selectTrack(trackUri, {
+          clearSearch: false,
+          scrollToSelection: true,
+        });
+        break;
+      case 'cpu':
+        this.selectSqlEvent('sched_slice', eventId, {
+          clearSearch: false,
+          scrollToSelection: true,
+          switchToCurrentSelectionTab: true,
+        });
+        break;
+      case 'log':
+        // TODO(stevegolton): Get log selection working.
+        break;
+      case 'slice':
+        // Search results only include slices from the slice table for now.
+        // When we include annotations we need to pass the correct table.
+        this.selectSqlEvent('slice', eventId, {
+          clearSearch: false,
+          scrollToSelection: true,
+          switchToCurrentSelectionTab: true,
+        });
+        break;
+      default:
+        assertUnreachable(source);
+    }
+  }
+
+  scrollToCurrentSelection() {
+    const uri = (() => {
+      switch (this.selection.kind) {
+        case 'track_event':
+        case 'track':
+          return this.selection.trackUri;
+        // TODO(stevegolton): Handle scrolling to area and note selections.
+        default:
+          return undefined;
+      }
+    })();
+    const range = this.findFocusRangeOfSelection();
+    this.scrollHelper.scrollTo({
+      time: range ? {...range} : undefined,
+      track: uri ? {uri: uri, expandGroup: true} : undefined,
+    });
+  }
+
+  // Finds the time range range that we should actually focus on - using dummy
+  // values for instant and incomplete slices, so we don't end up super zoomed
+  // in.
+  private findFocusRangeOfSelection(): TimeSpan | undefined {
+    const sel = this.selection;
+    if (sel.kind === 'track_event') {
+      // The focus range of slices is different to that of the actual span
+      if (sel.dur === -1n) {
+        return TimeSpan.fromTimeAndDuration(sel.ts, INCOMPLETE_SLICE_DURATION);
+      } else if (sel.dur === 0n) {
+        return TimeSpan.fromTimeAndDuration(sel.ts, INSTANT_FOCUS_DURATION);
+      } else {
+        return TimeSpan.fromTimeAndDuration(sel.ts, sel.dur);
+      }
+    } else {
+      return this.findTimeRangeOfSelection();
+    }
+  }
+
+  private async selectTrackEventInternal(
+    trackUri: string,
+    eventId: number,
+    opts?: SelectionOpts,
+    serializedDetailsPanel?: unknown,
+  ) {
+    const details = await this.trackManager
+      .getTrack(trackUri)
+      ?.track.getSelectionDetails?.(eventId);
+
+    if (!exists(details)) {
+      throw new Error('Unable to resolve selection details');
+    }
+
+    const selection: TrackEventSelection = {
+      ...details,
+      kind: 'track_event',
+      trackUri,
+      eventId,
+    };
+    this.createTrackEventDetailsPanel(selection, serializedDetailsPanel);
+    this.setSelection(selection, opts);
+  }
+
+  private createTrackEventDetailsPanel(
+    selection: TrackEventSelection,
+    serializedState: unknown,
+  ) {
+    const td = this.trackManager.getTrack(selection.trackUri);
+    if (!td) {
+      return;
+    }
+    const panel = td.track.detailsPanel?.(selection);
+    if (!panel) {
+      return;
+    }
+
+    if (panel.serialization && serializedState !== undefined) {
+      const res = panel.serialization.schema.safeParse(serializedState);
+      if (res.success) {
+        panel.serialization.state = res.data;
+      }
+    }
+
+    const detailsPanel: SelectionDetailsPanel = {
+      render: () => panel.render(),
+      serializatonState: () => panel.serialization?.state,
+      isLoading: true,
+    };
+    // Associate this details panel with this selection object
+    this.detailsPanels.set(selection, detailsPanel);
+
+    this.detailsPanelLimiter.schedule(async () => {
+      await panel?.load?.(selection);
+      detailsPanel.isLoading = false;
+      raf.scheduleFullRedraw();
+    });
+  }
+
+  findTimeRangeOfSelection(): TimeSpan | undefined {
+    const sel = this.selection;
+    if (sel.kind === 'area') {
+      return new TimeSpan(sel.start, sel.end);
+    } else if (sel.kind === 'note') {
+      const selectedNote = this.noteManager.getNote(sel.id);
+      if (selectedNote !== undefined) {
+        const kind = selectedNote.noteType;
+        switch (kind) {
+          case 'SPAN':
+            return new TimeSpan(selectedNote.start, selectedNote.end);
+          case 'DEFAULT':
+            return TimeSpan.fromTimeAndDuration(
+              selectedNote.timestamp,
+              INSTANT_FOCUS_DURATION,
+            );
+          default:
+            assertUnreachable(kind);
+        }
+      }
+    } else if (sel.kind === 'track_event') {
+      return TimeSpan.fromTimeAndDuration(sel.ts, sel.dur);
+    }
+
+    return undefined;
+  }
+
+  get aggregation() {
+    return this._aggregationManager;
   }
 }
diff --git a/ui/src/core/sidebar_manager.ts b/ui/src/core/sidebar_manager.ts
new file mode 100644
index 0000000..9de9b90
--- /dev/null
+++ b/ui/src/core/sidebar_manager.ts
@@ -0,0 +1,52 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+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.
+};
+
+export class SidebarManagerImpl implements SidebarManager {
+  readonly enabled: boolean;
+  private _visible: boolean;
+  private lastId = 0;
+
+  readonly menuItems = new Registry<SidebarMenuItemInternal>((m) => m.id);
+
+  constructor(args: {disabled?: boolean; hidden?: boolean}) {
+    this.enabled = !args.disabled;
+    this._visible = !args.hidden;
+  }
+
+  addMenuItem(item: SidebarMenuItem): Disposable {
+    // Assign a unique id to every item. This simplifies the job of the mithril
+    // component that renders the sidebar.
+    const id = `sidebar_${++this.lastId}`;
+    const itemInt: SidebarMenuItemInternal = {...item, id};
+    return this.menuItems.register(itemInt);
+  }
+
+  public get visible() {
+    return this._visible;
+  }
+
+  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
new file mode 100644
index 0000000..0ad4872
--- /dev/null
+++ b/ui/src/core/state_serialization.ts
@@ -0,0 +1,224 @@
+// 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 {
+  SERIALIZED_STATE_VERSION,
+  APP_STATE_SCHEMA,
+  SerializedNote,
+  SerializedPluginState,
+  SerializedSelection,
+  SerializedAppState,
+} from './state_serialization_schema';
+import {TimeSpan} from '../base/time';
+import {TraceImpl} from './trace_impl';
+
+// When it comes to serialization & permalinks there are two different use cases
+// 1. Uploading the current trace in a Cloud Storage (GCS) file AND serializing
+//    the app state into a different GCS JSON file. This is what happens when
+//    clicking on "share trace" on a local file manually opened.
+// 2. [future use case] Uploading the current state in a GCS JSON file, but
+//    letting the trace file come from a deep-link via postMessage().
+//    This is the case when traces are opened via Dashboards (e.g. APC) and we
+//    want to persist only the state itself, not the trace file.
+//
+// In order to do so, we have two layers of serialization
+// 1. Serialization of the app state (This file):
+//    This is a JSON object that represents the visual app state (pinned tracks,
+//    visible viewport bounds, etc) BUT not the trace source.
+// 2. An outer layer that contains the app state AND a link to the trace file.
+//    (permalink.ts)
+//
+// In a nutshell:
+//   AppState:  {viewport: {...}, pinnedTracks: {...}, notes: {...}}
+//   Permalink: {appState: {see above}, traceUrl: 'https://gcs/trace/file'}
+//
+// This file deals with the app state. permalink.ts deals with the outer layer.
+
+/**
+ * Serializes the current app state into a JSON-friendly POJO that can be stored
+ * in a permalink (@see permalink.ts).
+ * @returns A @type {SerializedAppState} object, @see state_serialization_schema.ts
+ */
+export function serializeAppState(trace: TraceImpl): SerializedAppState {
+  const vizWindow = trace.timeline.visibleWindow.toTimeSpan();
+
+  const notes = new Array<SerializedNote>();
+  for (const [id, note] of trace.notes.notes.entries()) {
+    if (note.noteType === 'DEFAULT') {
+      notes.push({
+        noteType: 'DEFAULT',
+        id,
+        start: note.timestamp,
+        color: note.color,
+        text: note.text,
+      });
+    } else if (note.noteType === 'SPAN') {
+      notes.push({
+        noteType: 'SPAN',
+        id,
+        start: note.start,
+        end: note.end,
+        color: note.color,
+        text: note.text,
+      });
+    }
+  }
+
+  const selection = new Array<SerializedSelection>();
+  const stateSel = trace.selection.selection;
+  if (stateSel.kind === 'track_event') {
+    selection.push({
+      kind: 'TRACK_EVENT',
+      trackKey: stateSel.trackUri,
+      eventId: stateSel.eventId.toString(),
+      detailsPanel: trace.selection
+        .getDetailsPanelForSelection()
+        ?.serializatonState(),
+    });
+  } else if (stateSel.kind === 'area') {
+    selection.push({
+      kind: 'AREA',
+      trackUris: stateSel.trackUris,
+      start: stateSel.start,
+      end: stateSel.end,
+    });
+  }
+
+  const plugins = new Array<SerializedPluginState>();
+  const pluginsStore = trace.getPluginStoreForSerialization();
+
+  for (const [id, pluginState] of Object.entries(pluginsStore)) {
+    plugins.push({id, state: pluginState});
+  }
+
+  return {
+    version: SERIALIZED_STATE_VERSION,
+    pinnedTracks: trace.workspace.pinnedTracks
+      .map((t) => t.uri)
+      .filter((uri) => uri !== undefined),
+    viewport: {
+      start: vizWindow.start,
+      end: vizWindow.end,
+    },
+    notes,
+    selection,
+    plugins,
+  };
+}
+
+export type ParseStateResult =
+  | {success: true; data: SerializedAppState}
+  | {success: false; error: string};
+
+/**
+ * Parses the app state from a JSON blob.
+ * @param jsonDecodedObj the output of JSON.parse() that needs validation
+ * @returns Either a @type {SerializedAppState} object or an error.
+ */
+export function parseAppState(jsonDecodedObj: unknown): ParseStateResult {
+  const parseRes = APP_STATE_SCHEMA.safeParse(jsonDecodedObj);
+  if (parseRes.success) {
+    if (parseRes.data.version == SERIALIZED_STATE_VERSION) {
+      return {success: true, data: parseRes.data};
+    } else {
+      return {
+        success: false,
+        error:
+          `SERIALIZED_STATE_VERSION mismatch ` +
+          `(actual: ${parseRes.data.version}, ` +
+          `expected: ${SERIALIZED_STATE_VERSION})`,
+      };
+    }
+  }
+  return {success: false, error: parseRes.error.toString()};
+}
+
+/**
+ * This function gets invoked after the trace is loaded, but before plugins,
+ * track decider and initial selections are run.
+ * @param appState the .data object returned by parseAppState() when successful.
+ */
+export function deserializeAppStatePhase1(
+  appState: SerializedAppState,
+  trace: TraceImpl,
+): void {
+  // Restore the plugin state.
+  trace.getPluginStoreForSerialization().edit((draft) => {
+    for (const p of appState.plugins ?? []) {
+      draft[p.id] = p.state ?? {};
+    }
+  });
+}
+
+/**
+ * This function gets invoked after the trace controller has run and all plugins
+ * have executed.
+ * @param appState the .data object returned by parseAppState() when successful.
+ * @param trace the target trace object to manipulate.
+ */
+export function deserializeAppStatePhase2(
+  appState: SerializedAppState,
+  trace: TraceImpl,
+): void {
+  if (appState.viewport !== undefined) {
+    trace.timeline.updateVisibleTime(
+      new TimeSpan(appState.viewport.start, appState.viewport.end),
+    );
+  }
+
+  // Restore the pinned tracks, if they exist.
+  for (const uri of appState.pinnedTracks) {
+    const track = trace.workspace.findTrackByUri(uri);
+    if (track) {
+      track.pin();
+    }
+  }
+
+  // Restore notes.
+  for (const note of appState.notes) {
+    const commonArgs = {
+      id: note.id,
+      timestamp: note.start,
+      color: note.color,
+      text: note.text,
+    };
+    if (note.noteType === 'DEFAULT') {
+      trace.notes.addNote({...commonArgs});
+    } else if (note.noteType === 'SPAN') {
+      trace.notes.addSpanNote({
+        ...commonArgs,
+        start: commonArgs.timestamp,
+        end: note.end,
+      });
+    }
+  }
+
+  // Restore the selection
+  trace.selection.deserialize(appState.selection[0]);
+}
+
+/**
+ * Performs JSON serialization, taking care of also serializing BigInt->string.
+ * For the matching deserializer see zType in state_serialization_schema.ts.
+ * @param obj A POJO, typically a SerializedAppState or PermalinkState.
+ * @returns JSON-encoded string.
+ */
+export function JsonSerialize(obj: Object): string {
+  return JSON.stringify(obj, (_key, value) => {
+    if (typeof value === 'bigint') {
+      return value.toString();
+    }
+    return value;
+  });
+}
diff --git a/ui/src/core/state_serialization_schema.ts b/ui/src/core/state_serialization_schema.ts
new file mode 100644
index 0000000..c42772a
--- /dev/null
+++ b/ui/src/core/state_serialization_schema.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 {z} from 'zod';
+import {Time} from '../base/time';
+
+// This should be bumped only in case of breaking changes that cannot be
+// addressed using zod's z.optional(), z.default() or z.coerce.xxx().
+// Ideally these cases should be extremely rare.
+export const SERIALIZED_STATE_VERSION = 1;
+
+// At deserialization time this takes a string as input and converts it into a
+// BigInt. The serialization side of this is handled by JsonSerialize(), which
+// converts BigInt into strings when invoking JSON.stringify.
+const zTime = z
+  .string()
+  .regex(/[-]?\d+/)
+  .transform((s) => Time.fromRaw(BigInt(s)));
+
+const SELECTION_SCHEMA = z.discriminatedUnion('kind', [
+  z.object({
+    kind: z.literal('TRACK_EVENT'),
+    // This is actually the track URI but let's not rename for backwards compat
+    trackKey: z.string(),
+    eventId: z.string(),
+    detailsPanel: z.unknown(),
+  }),
+  z.object({
+    kind: z.literal('AREA'),
+    start: zTime,
+    end: zTime,
+    trackUris: z.array(z.string()),
+  }),
+]);
+
+export type SerializedSelection = z.infer<typeof SELECTION_SCHEMA>;
+
+const NOTE_SCHEMA = z
+  .object({
+    id: z.string(),
+    start: zTime,
+    color: z.string(),
+    text: z.string(),
+  })
+  .and(
+    z.discriminatedUnion('noteType', [
+      z.object({noteType: z.literal('DEFAULT')}),
+      z.object({noteType: z.literal('SPAN'), end: zTime}),
+    ]),
+  );
+
+export type SerializedNote = z.infer<typeof NOTE_SCHEMA>;
+
+const PLUGIN_SCHEMA = z.object({
+  id: z.string(),
+  state: z.any(),
+});
+
+export type SerializedPluginState = z.infer<typeof PLUGIN_SCHEMA>;
+
+export const APP_STATE_SCHEMA = z.object({
+  version: z.number(),
+  pinnedTracks: z.array(z.string()).default([]),
+  viewport: z
+    .object({
+      start: zTime,
+      end: zTime,
+    })
+    .optional(),
+  selection: z.array(SELECTION_SCHEMA).default([]),
+  notes: z.array(NOTE_SCHEMA).default([]),
+  plugins: z.array(PLUGIN_SCHEMA).default([]),
+});
+
+export type SerializedAppState = z.infer<typeof APP_STATE_SCHEMA>;
diff --git a/ui/src/core/tab_manager.ts b/ui/src/core/tab_manager.ts
new file mode 100644
index 0000000..3179bd1
--- /dev/null
+++ b/ui/src/core/tab_manager.ts
@@ -0,0 +1,243 @@
+// 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 {DetailsPanel} from '../public/details_panel';
+import {TabDescriptor, TabManager} from '../public/tab';
+import {raf} from './raf_scheduler';
+
+export interface ResolvedTab {
+  uri: string;
+  tab?: TabDescriptor;
+}
+
+export type TabPanelVisibility = 'COLLAPSED' | 'VISIBLE' | 'FULLSCREEN';
+
+/**
+ * Stores tab & current selection section registries.
+ * Keeps track of tab lifecycles.
+ */
+export class TabManagerImpl implements TabManager, Disposable {
+  private _registry = new Map<string, TabDescriptor>();
+  private _defaultTabs = new Set<string>();
+  private _detailsPanelRegistry = new Set<DetailsPanel>();
+  private _instantiatedTabs = new Map<string, TabDescriptor>();
+  private _openTabs: string[] = []; // URIs of the tabs open.
+  private _currentTab: string = 'current_selection';
+  private _tabPanelVisibility: TabPanelVisibility = 'COLLAPSED';
+  private _tabPanelVisibilityChanged = false;
+
+  [Symbol.dispose]() {
+    // Dispose of all tabs that are currently alive
+    for (const tab of this._instantiatedTabs.values()) {
+      this.disposeTab(tab);
+    }
+    this._instantiatedTabs.clear();
+  }
+
+  registerTab(desc: TabDescriptor): Disposable {
+    this._registry.set(desc.uri, desc);
+    return {
+      [Symbol.dispose]: () => this._registry.delete(desc.uri),
+    };
+  }
+
+  addDefaultTab(uri: string): Disposable {
+    this._defaultTabs.add(uri);
+    return {
+      [Symbol.dispose]: () => this._defaultTabs.delete(uri),
+    };
+  }
+
+  registerDetailsPanel(section: DetailsPanel): Disposable {
+    this._detailsPanelRegistry.add(section);
+    return {
+      [Symbol.dispose]: () => this._detailsPanelRegistry.delete(section),
+    };
+  }
+
+  resolveTab(uri: string): TabDescriptor | undefined {
+    return this._registry.get(uri);
+  }
+
+  showCurrentSelectionTab(): void {
+    this.showTab('current_selection');
+  }
+
+  showTab(uri: string): void {
+    // Add tab, unless we're talking about the special current_selection tab
+    if (uri !== 'current_selection') {
+      // Add tab to tab list if not already
+      if (!this._openTabs.some((x) => x === uri)) {
+        this._openTabs.push(uri);
+      }
+    }
+    this._currentTab = uri;
+
+    // The first time that we show a tab, auto-expand the tab bottom panel.
+    // However, if the user has later collapsed the panel (hence if
+    // _tabPanelVisibilityChanged == true), don't insist and leave things as
+    // they are.
+    if (
+      !this._tabPanelVisibilityChanged &&
+      this._tabPanelVisibility === 'COLLAPSED'
+    ) {
+      this.setTabPanelVisibility('VISIBLE');
+    }
+
+    raf.scheduleFullRedraw();
+  }
+
+  // Hide a tab in the tab bar pick a new tab to show.
+  // Note: Attempting to hide the "current_selection" tab doesn't work. This tab
+  // is special and cannot be removed.
+  hideTab(uri: string): void {
+    // If the removed tab is the "current" tab, we must find a new tab to focus
+    if (uri === this._currentTab) {
+      // Remember the index of the current tab
+      const currentTabIdx = this._openTabs.findIndex((x) => x === uri);
+
+      // Remove the tab
+      this._openTabs = this._openTabs.filter((x) => x !== uri);
+
+      if (currentTabIdx !== -1) {
+        if (this._openTabs.length === 0) {
+          // No more tabs, use current selection
+          this._currentTab = 'current_selection';
+        } else if (currentTabIdx < this._openTabs.length - 1) {
+          // Pick the tab to the right
+          this._currentTab = this._openTabs[currentTabIdx];
+        } else {
+          // Pick the last tab
+          const lastTab = this._openTabs[this._openTabs.length - 1];
+          this._currentTab = lastTab;
+        }
+      }
+    } else {
+      // Otherwise just remove the tab
+      this._openTabs = this._openTabs.filter((x) => x !== uri);
+    }
+    raf.scheduleFullRedraw();
+  }
+
+  toggleTab(uri: string): void {
+    return this.isOpen(uri) ? this.hideTab(uri) : this.showTab(uri);
+  }
+
+  isOpen(uri: string): boolean {
+    return this._openTabs.find((x) => x == uri) !== undefined;
+  }
+
+  get currentTabUri(): string {
+    return this._currentTab;
+  }
+
+  get openTabsUri(): string[] {
+    return this._openTabs;
+  }
+
+  get tabs(): TabDescriptor[] {
+    return Array.from(this._registry.values());
+  }
+
+  get defaultTabs(): string[] {
+    return Array.from(this._defaultTabs);
+  }
+
+  get detailsPanels(): DetailsPanel[] {
+    return Array.from(this._detailsPanelRegistry);
+  }
+
+  /**
+   * Resolves a list of URIs to tabs and manages tab lifecycles.
+   * @param tabUris List of tabs.
+   * @returns List of resolved tabs.
+   */
+  resolveTabs(tabUris: string[]): ResolvedTab[] {
+    // Refresh the list of old tabs
+    const newTabs = new Map<string, TabDescriptor>();
+    const tabs: ResolvedTab[] = [];
+
+    tabUris.forEach((uri) => {
+      const newTab = this._registry.get(uri);
+      tabs.push({uri, tab: newTab});
+
+      if (newTab) {
+        newTabs.set(uri, newTab);
+      }
+    });
+
+    // Call onShow() on any new tabs.
+    for (const [uri, tab] of newTabs) {
+      const oldTab = this._instantiatedTabs.get(uri);
+      if (!oldTab) {
+        this.initTab(tab);
+      }
+    }
+
+    // Call onHide() on any tabs that have been removed.
+    for (const [uri, tab] of this._instantiatedTabs) {
+      const newTab = newTabs.get(uri);
+      if (!newTab) {
+        this.disposeTab(tab);
+      }
+    }
+
+    this._instantiatedTabs = newTabs;
+
+    return tabs;
+  }
+
+  setTabPanelVisibility(visibility: TabPanelVisibility): void {
+    this._tabPanelVisibility = visibility;
+    this._tabPanelVisibilityChanged = true;
+    raf.scheduleFullRedraw();
+  }
+
+  toggleTabPanelVisibility(): void {
+    switch (this._tabPanelVisibility) {
+      case 'COLLAPSED':
+      case 'FULLSCREEN':
+        return this.setTabPanelVisibility('VISIBLE');
+      case 'VISIBLE':
+        this.setTabPanelVisibility('COLLAPSED');
+        break;
+    }
+  }
+
+  get tabPanelVisibility() {
+    return this._tabPanelVisibility;
+  }
+
+  /**
+   * Call onShow() on this tab.
+   * @param tab The tab to initialize.
+   */
+  private initTab(tab: TabDescriptor): void {
+    tab.onShow?.();
+  }
+
+  /**
+   * Call onHide() and maybe remove from registry if tab is ephemeral.
+   * @param tab The tab to dispose.
+   */
+  private disposeTab(tab: TabDescriptor): void {
+    // Attempt to call onHide
+    tab.onHide?.();
+
+    // If ephemeral, also unregister the tab
+    if (tab.isEphemeral) {
+      this._registry.delete(tab.uri);
+    }
+  }
+}
diff --git a/ui/src/core/timeline.ts b/ui/src/core/timeline.ts
new file mode 100644
index 0000000..d91503c
--- /dev/null
+++ b/ui/src/core/timeline.ts
@@ -0,0 +1,206 @@
+// 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 {assertTrue} 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 {Timeline} from '../public/timeline';
+import {timestampFormat, TimestampFormat} from './timestamp_format';
+import {TraceInfo} from '../public/trace_info';
+
+const MIN_DURATION = 10;
+
+/**
+ * State that is shared between several frontend components, but not the
+ * controller. This state is updated at 60fps.
+ */
+export class TimelineImpl implements Timeline {
+  private _visibleWindow: HighPrecisionTimeSpan;
+  private _hoverCursorTimestamp?: time;
+  private _highlightedSliceId?: number;
+  private _hoveredNoteTimestamp?: time;
+
+  // TODO(stevegolton): These are currently only referenced by the cpu slice
+  // tracks and the process summary tracks. We should just make this a local
+  // property of the cpu slice tracks and ignore them in the process tracks.
+  private _hoveredUtid?: number;
+  private _hoveredPid?: number;
+
+  get highlightedSliceId() {
+    return this._highlightedSliceId;
+  }
+
+  set highlightedSliceId(x) {
+    this._highlightedSliceId = x;
+    raf.scheduleFullRedraw();
+  }
+
+  get hoveredNoteTimestamp() {
+    return this._hoveredNoteTimestamp;
+  }
+
+  set hoveredNoteTimestamp(x) {
+    this._hoveredNoteTimestamp = x;
+    raf.scheduleFullRedraw();
+  }
+
+  get hoveredUtid() {
+    return this._hoveredUtid;
+  }
+
+  set hoveredUtid(x) {
+    this._hoveredUtid = x;
+    raf.scheduleFullRedraw();
+  }
+
+  get hoveredPid() {
+    return this._hoveredPid;
+  }
+
+  set hoveredPid(x) {
+    this._hoveredPid = x;
+    raf.scheduleFullRedraw();
+  }
+
+  // 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,
+      traceInfo.end,
+    );
+  }
+
+  // TODO: there is some redundancy in the fact that both |visibleWindowTime|
+  // and a |timeScale| have a notion of time range. That should live in one
+  // place only.
+
+  zoomVisibleWindow(ratio: number, centerPoint: number) {
+    this._visibleWindow = this._visibleWindow
+      .scale(ratio, centerPoint, MIN_DURATION)
+      .fitWithin(this.traceInfo.start, this.traceInfo.end);
+
+    raf.scheduleRedraw();
+  }
+
+  panVisibleWindow(delta: number) {
+    this._visibleWindow = this._visibleWindow
+      .translate(delta)
+      .fitWithin(this.traceInfo.start, this.traceInfo.end);
+
+    raf.scheduleRedraw();
+  }
+
+  // Given a timestamp, if |ts| is not currently in view move the view to
+  // center |ts|, keeping the same zoom level.
+  panToTimestamp(ts: time) {
+    if (this._visibleWindow.contains(ts)) return;
+    // TODO(hjd): This is an ugly jump, we should do a smooth pan instead.
+    const halfDuration = this.visibleWindow.duration / 2;
+    const newStart = new HighPrecisionTime(ts).subNumber(halfDuration);
+    const newWindow = new HighPrecisionTimeSpan(
+      newStart,
+      this._visibleWindow.duration,
+    );
+    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.scheduleRedraw();
+  }
+
+  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));
+  }
+
+  // TODO(primiano): we ended up with two entry-points for the same function,
+  // unify them.
+  setViewportTime(start: time, end: time): void {
+    this.updateVisibleTime(new TimeSpan(start, end));
+  }
+
+  // Set visible window using a high precision time span
+  updateVisibleTimeHP(ts: HighPrecisionTimeSpan) {
+    this._visibleWindow = ts
+      .clampDuration(MIN_DURATION)
+      .fitWithin(this.traceInfo.start, this.traceInfo.end);
+
+    raf.scheduleRedraw();
+  }
+
+  // Get the bounds of the visible window as a high-precision time span
+  get visibleWindow(): HighPrecisionTimeSpan {
+    return this._visibleWindow;
+  }
+
+  get hoverCursorTimestamp(): time | undefined {
+    return this._hoverCursorTimestamp;
+  }
+
+  set hoverCursorTimestamp(t: time | undefined) {
+    this._hoverCursorTimestamp = t;
+    raf.scheduleRedraw();
+  }
+
+  // Offset between t=0 and the configured time domain.
+  timestampOffset(): time {
+    const fmt = timestampFormat();
+    switch (fmt) {
+      case TimestampFormat.Timecode:
+      case TimestampFormat.Seconds:
+      case TimestampFormat.Milliseoncds:
+      case TimestampFormat.Microseconds:
+        return this.traceInfo.start;
+      case TimestampFormat.TraceNs:
+      case TimestampFormat.TraceNsLocale:
+        return Time.ZERO;
+      case TimestampFormat.UTC:
+        return this.traceInfo.utcOffset;
+      case TimestampFormat.TraceTz:
+        return this.traceInfo.traceTzOffset;
+      default:
+        const x: never = fmt;
+        throw new Error(`Unsupported format ${x}`);
+    }
+  }
+
+  // Convert absolute time to domain time.
+  toDomainTime(ts: time): time {
+    return Time.sub(ts, this.timestampOffset());
+  }
+}
diff --git a/ui/src/core/timeline_cache_unittest.ts b/ui/src/core/timeline_cache_unittest.ts
index c41fb64..b8d8dc3 100644
--- a/ui/src/core/timeline_cache_unittest.ts
+++ b/ui/src/core/timeline_cache_unittest.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import {Time} from '../base/time';
-
 import {CacheKey} from './timeline_cache';
 
 test('cacheKeys', () => {
diff --git a/ui/src/core/timestamp_format.ts b/ui/src/core/timestamp_format.ts
index 4bcfa06..f4d7d80 100644
--- a/ui/src/core/timestamp_format.ts
+++ b/ui/src/core/timestamp_format.ts
@@ -16,9 +16,11 @@
 
 export enum TimestampFormat {
   Timecode = 'timecode',
-  Raw = 'raw',
-  RawLocale = 'rawLocale',
+  TraceNs = 'traceNs',
+  TraceNsLocale = 'traceNsLocale',
   Seconds = 'seconds',
+  Milliseoncds = 'milliseconds',
+  Microseconds = 'microseconds',
   UTC = 'utc',
   TraceTz = 'traceTz',
 }
diff --git a/ui/src/core/trace_config_utils.ts b/ui/src/core/trace_config_utils.ts
deleted file mode 100644
index 43d6c10..0000000
--- a/ui/src/core/trace_config_utils.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {EnableTracingRequest, TraceConfig} from '../protos';
-import {Engine} from '../trace_processor/engine';
-import {NUM} from '../trace_processor/query_result';
-
-// In this file are contained a few functions to simplify the proto parsing.
-
-export function extractTraceConfig(
-  enableTracingRequest: Uint8Array,
-): Uint8Array | undefined {
-  try {
-    const enableTracingObject =
-      EnableTracingRequest.decode(enableTracingRequest);
-    if (!enableTracingObject.traceConfig) return undefined;
-    return TraceConfig.encode(enableTracingObject.traceConfig).finish();
-  } catch (e) {
-    // This catch is for possible proto encoding/decoding issues.
-    console.error('Error extracting the config: ', e.message);
-    return undefined;
-  }
-}
-
-export function extractDurationFromTraceConfig(traceConfigProto: Uint8Array) {
-  try {
-    return TraceConfig.decode(traceConfigProto).durationMs;
-  } catch (e) {
-    // This catch is for possible proto encoding/decoding issues.
-    return undefined;
-  }
-}
-
-export function browserSupportsPerfettoConfig(): boolean {
-  const minimumChromeVersion = '91.0.4448.0';
-  const runningVersion = String(
-    (/Chrome\/(([0-9]+\.?){4})/.exec(navigator.userAgent) || [, 0])[1],
-  );
-
-  if (!runningVersion) return false;
-
-  const minVerArray = minimumChromeVersion.split('.').map(Number);
-  const runVerArray = runningVersion.split('.').map(Number);
-
-  for (let index = 0; index < minVerArray.length; index++) {
-    if (runVerArray[index] === minVerArray[index]) continue;
-    return runVerArray[index] > minVerArray[index];
-  }
-  return true; // Exact version match.
-}
-
-export function hasSystemDataSourceConfig(config: TraceConfig): boolean {
-  for (const ds of config.dataSources) {
-    if (!(ds.config?.name ?? '').startsWith('org.chromium.')) {
-      return true;
-    }
-  }
-  return false;
-}
-
-export async function hasWattsonSupport(engine: Engine): Promise<boolean> {
-  // These tables are hard requirements and are the bare minimum needed for
-  // Wattson to run, so check that these tables are populated
-  const queryChecks: string[] = [
-    `
-    INCLUDE PERFETTO MODULE wattson.device_infos;
-    SELECT COUNT(*) as numRows FROM _wattson_device
-    `,
-    `
-    INCLUDE PERFETTO MODULE linux.cpu.frequency;
-    SELECT COUNT(*) as numRows FROM cpu_frequency_counters
-    `,
-    `
-    INCLUDE PERFETTO MODULE linux.cpu.idle;
-    SELECT COUNT(*) as numRows FROM cpu_idle_counters
-    `,
-  ];
-  for (const queryCheck of queryChecks) {
-    const checkValue = await engine.query(queryCheck);
-    if (checkValue.firstRow({numRows: NUM}).numRows === 0) return false;
-  }
-
-  return true;
-}
diff --git a/ui/src/core/trace_impl.ts b/ui/src/core/trace_impl.ts
new file mode 100644
index 0000000..2ae2d16
--- /dev/null
+++ b/ui/src/core/trace_impl.ts
@@ -0,0 +1,480 @@
+// 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 {DisposableStack} from '../base/disposable_stack';
+import {createStore, Migrate, Store} from '../base/store';
+import {TimelineImpl} from './timeline';
+import {Command} from '../public/command';
+import {EventListeners, Trace} from '../public/trace';
+import {ScrollToArgs, setScrollToFunction} from '../public/scroll_helper';
+import {TrackDescriptor} from '../public/track';
+import {EngineBase, EngineProxy} from '../trace_processor/engine';
+import {CommandManagerImpl} from './command_manager';
+import {NoteManagerImpl} from './note_manager';
+import {OmniboxManagerImpl} from './omnibox_manager';
+import {SearchManagerImpl} from './search_manager';
+import {SelectionManagerImpl} from './selection_manager';
+import {SidebarManagerImpl} from './sidebar_manager';
+import {TabManagerImpl} from './tab_manager';
+import {TrackManagerImpl} from './track_manager';
+import {WorkspaceManagerImpl} from './workspace_manager';
+import {SidebarMenuItem} from '../public/sidebar';
+import {ScrollHelper} from './scroll_helper';
+import {Selection, SelectionOpts} from '../public/selection';
+import {SearchResult} from '../public/search';
+import {PivotTableManager} from './pivot_table_manager';
+import {FlowManager} from './flow_manager';
+import {AppContext, AppImpl} from './app_impl';
+import {PluginManagerImpl} from './plugin_manager';
+import {RouteArgs} from '../public/route_schema';
+import {CORE_PLUGIN_ID} from './plugin_manager';
+import {Analytics} from '../public/analytics';
+import {getOrCreate} from '../base/utils';
+import {fetchWithProgress} from '../base/http_utils';
+import {TraceInfoImpl} from './trace_info_impl';
+import {PageHandler, PageManager} from '../public/page';
+import {createProxy} from '../base/utils';
+import {PageManagerImpl} from './page_manager';
+import {FeatureFlagManager, FlagSettings} from '../public/feature_flag';
+import {featureFlags} from './feature_flags';
+import {SerializedAppState} from './state_serialization_schema';
+import {PostedTrace} from './trace_source';
+
+/**
+ * Handles the per-trace state of the UI
+ * There is an instance of this class per each trace loaded, and typically
+ * between 0 and 1 instances in total (% brief moments while we swap traces).
+ * 90% of the app state live here, including the Engine.
+ * This is the underlying storage for AppImpl, which instead has one instance
+ * per trace per plugin.
+ */
+export class TraceContext implements Disposable {
+  private readonly pluginInstances = new Map<string, TraceImpl>();
+  readonly appCtx: AppContext;
+  readonly engine: EngineBase;
+  readonly omniboxMgr = new OmniboxManagerImpl();
+  readonly searchMgr: SearchManagerImpl;
+  readonly selectionMgr: SelectionManagerImpl;
+  readonly tabMgr = new TabManagerImpl();
+  readonly timeline: TimelineImpl;
+  readonly traceInfo: TraceInfoImpl;
+  readonly trackMgr = new TrackManagerImpl();
+  readonly workspaceMgr = new WorkspaceManagerImpl();
+  readonly noteMgr = new NoteManagerImpl();
+  readonly flowMgr: FlowManager;
+  readonly pluginSerializableState = createStore<{[key: string]: {}}>({});
+  readonly scrollHelper: ScrollHelper;
+  readonly pivotTableMgr;
+  readonly trash = new DisposableStack();
+  readonly eventListeners = new Map<keyof EventListeners, Array<unknown>>();
+
+  // List of errors that were encountered while loading the trace by the TS
+  // code. These are on top of traceInfo.importErrors, which is a summary of
+  // what TraceProcessor reports on the stats table at import time.
+  readonly loadingErrors: string[] = [];
+
+  constructor(gctx: AppContext, engine: EngineBase, traceInfo: TraceInfoImpl) {
+    this.appCtx = gctx;
+    this.engine = engine;
+    this.trash.use(engine);
+    this.traceInfo = traceInfo;
+    this.timeline = new TimelineImpl(traceInfo);
+
+    this.scrollHelper = new ScrollHelper(
+      this.traceInfo,
+      this.timeline,
+      this.workspaceMgr.currentWorkspace,
+      this.trackMgr,
+    );
+
+    this.selectionMgr = new SelectionManagerImpl(
+      this.engine,
+      this.trackMgr,
+      this.noteMgr,
+      this.scrollHelper,
+      this.onSelectionChange.bind(this),
+    );
+
+    this.noteMgr.onNoteDeleted = (noteId) => {
+      if (
+        this.selectionMgr.selection.kind === 'note' &&
+        this.selectionMgr.selection.id === noteId
+      ) {
+        this.selectionMgr.clear();
+      }
+    };
+
+    this.pivotTableMgr = new PivotTableManager(
+      engine.getProxy('PivotTableManager'),
+    );
+
+    this.flowMgr = new FlowManager(
+      engine.getProxy('FlowManager'),
+      this.trackMgr,
+      this.selectionMgr,
+    );
+
+    this.searchMgr = new SearchManagerImpl({
+      timeline: this.timeline,
+      trackManager: this.trackMgr,
+      engine: this.engine,
+      workspace: this.workspaceMgr.currentWorkspace,
+      onResultStep: this.onResultStep.bind(this),
+    });
+  }
+
+  // This method wires up changes to selection to side effects on search and
+  // tabs. This is to avoid entangling too many dependencies between managers.
+  private onSelectionChange(selection: Selection, opts: SelectionOpts) {
+    const {clearSearch = true, switchToCurrentSelectionTab = true} = opts;
+    if (clearSearch) {
+      this.searchMgr.reset();
+    }
+    if (switchToCurrentSelectionTab && selection.kind !== 'empty') {
+      this.tabMgr.showCurrentSelectionTab();
+    }
+
+    if (selection.kind === 'area') {
+      this.pivotTableMgr.setSelectionArea(selection);
+    }
+
+    this.flowMgr.updateFlows(selection);
+  }
+
+  private onResultStep(searchResult: SearchResult) {
+    this.selectionMgr.selectSearchResult(searchResult);
+  }
+
+  // Gets or creates an instance of TraceImpl backed by the current TraceContext
+  // for the given plugin.
+  forPlugin(pluginId: string) {
+    return getOrCreate(this.pluginInstances, pluginId, () => {
+      const appForPlugin = this.appCtx.forPlugin(pluginId);
+      return new TraceImpl(appForPlugin, this);
+    });
+  }
+
+  // Called by AppContext.closeCurrentTrace().
+  [Symbol.dispose]() {
+    this.trash.dispose();
+  }
+}
+
+/**
+ * This implementation provides the plugin access to trace related resources,
+ * such as the engine and the store. This exists for the whole duration a plugin
+ * is active AND a trace is loaded.
+ * There are N+1 instances of this for each trace, one for each plugin plus one
+ * for the core.
+ */
+export class TraceImpl implements Trace {
+  private readonly appImpl: AppImpl;
+  private readonly traceCtx: TraceContext;
+
+  // This is not the original Engine base, rather an EngineProxy based on the
+  // same engineBase.
+  private readonly engineProxy: EngineProxy;
+  private readonly trackMgrProxy: TrackManagerImpl;
+  private readonly commandMgrProxy: CommandManagerImpl;
+  private readonly sidebarProxy: SidebarManagerImpl;
+  private readonly pageMgrProxy: PageManagerImpl;
+
+  // This is called by TraceController when loading a new trace, soon after the
+  // engine has been set up. It obtains a new TraceImpl for the core. From that
+  // we can fork sibling instances (i.e. bound to the same TraceContext) for
+  // the various plugins.
+  static createInstanceForCore(
+    appImpl: AppImpl,
+    engine: EngineBase,
+    traceInfo: TraceInfoImpl,
+  ): TraceImpl {
+    const traceCtx = new TraceContext(
+      appImpl.__appCtxForTrace,
+      engine,
+      traceInfo,
+    );
+    return traceCtx.forPlugin(CORE_PLUGIN_ID);
+  }
+
+  // Only called by TraceContext.forPlugin().
+  constructor(appImpl: AppImpl, ctx: TraceContext) {
+    const pluginId = appImpl.pluginId;
+    this.appImpl = appImpl;
+    this.traceCtx = ctx;
+    const traceUnloadTrash = ctx.trash;
+
+    // Invalidate all the engine proxies when the TraceContext is destroyed.
+    this.engineProxy = ctx.engine.getProxy(pluginId);
+    traceUnloadTrash.use(this.engineProxy);
+
+    // Intercept the registerTrack() method to inject the pluginId into tracks.
+    this.trackMgrProxy = createProxy(ctx.trackMgr, {
+      registerTrack(trackDesc: TrackDescriptor): Disposable {
+        return ctx.trackMgr.registerTrack({...trackDesc, pluginId});
+      },
+    });
+
+    // CommandManager is global. Here we intercept the registerCommand() because
+    // we want any commands registered via the Trace interface to be
+    // unregistered when the trace unloads (before a new trace is loaded) to
+    // avoid ending up with duplicate commands.
+    this.commandMgrProxy = createProxy(ctx.appCtx.commandMgr, {
+      registerCommand(cmd: Command): Disposable {
+        const disposable = appImpl.commands.registerCommand(cmd);
+        traceUnloadTrash.use(disposable);
+        return disposable;
+      },
+    });
+
+    // Likewise, remove all trace-scoped sidebar entries when the trace unloads.
+    this.sidebarProxy = createProxy(ctx.appCtx.sidebarMgr, {
+      addMenuItem(menuItem: SidebarMenuItem): Disposable {
+        const disposable = appImpl.sidebar.addMenuItem(menuItem);
+        traceUnloadTrash.use(disposable);
+        return disposable;
+      },
+    });
+
+    this.pageMgrProxy = createProxy(ctx.appCtx.pageMgr, {
+      registerPage(pageHandler: PageHandler): Disposable {
+        const disposable = appImpl.pages.registerPage({
+          ...pageHandler,
+          pluginId: appImpl.pluginId,
+        });
+        traceUnloadTrash.use(disposable);
+        return disposable;
+      },
+    });
+
+    // TODO(primiano): remove this injection once we plumb Trace everywhere.
+    setScrollToFunction((x: ScrollToArgs) => ctx.scrollHelper.scrollTo(x));
+  }
+
+  scrollTo(where: ScrollToArgs): void {
+    this.traceCtx.scrollHelper.scrollTo(where);
+  }
+
+  // Creates an instance of TraceImpl backed by the same TraceContext for
+  // another plugin. This is effectively a way to "fork" the core instance and
+  // create the N instances for plugins.
+  forkForPlugin(pluginId: string) {
+    return this.traceCtx.forPlugin(pluginId);
+  }
+
+  mountStore<T>(migrate: Migrate<T>): Store<T> {
+    return this.traceCtx.pluginSerializableState.createSubStore(
+      [this.pluginId],
+      migrate,
+    );
+  }
+
+  getPluginStoreForSerialization() {
+    return this.traceCtx.pluginSerializableState;
+  }
+
+  async getTraceFile(): Promise<Blob> {
+    const src = this.traceInfo.source;
+    if (this.traceInfo.downloadable) {
+      if (src.type === 'ARRAY_BUFFER') {
+        return new Blob([src.buffer]);
+      } else if (src.type === 'FILE') {
+        return src.file;
+      } else if (src.type === 'URL') {
+        return await fetchWithProgress(src.url, (progressPercent: number) =>
+          this.omnibox.showStatusMessage(
+            `Downloading trace ${progressPercent}%`,
+          ),
+        );
+      }
+    }
+    // Not available in HTTP+RPC mode. Rather than propagating an undefined,
+    // show a graceful error (the ERR:trace_src will be intercepted by
+    // error_dialog.ts). We expect all users of this feature to not be able to
+    // do anything useful if we returned undefined (other than showing the same
+    // dialog).
+    // The caller was supposed to check that traceInfo.downloadable === true
+    // before calling this. Throwing while downloadable is true is a bug.
+    throw new Error(`Cannot getTraceFile(${src.type})`);
+  }
+
+  get openerPluginArgs(): {[key: string]: unknown} | undefined {
+    const traceSource = this.traceCtx.traceInfo.source;
+    if (traceSource.type !== 'ARRAY_BUFFER') {
+      return undefined;
+    }
+    const pluginArgs = traceSource.pluginArgs;
+    return (pluginArgs ?? {})[this.pluginId];
+  }
+
+  get trace() {
+    return this;
+  }
+
+  get engine() {
+    return this.engineProxy;
+  }
+
+  get timeline() {
+    return this.traceCtx.timeline;
+  }
+
+  get tracks() {
+    return this.trackMgrProxy;
+  }
+
+  get tabs() {
+    return this.traceCtx.tabMgr;
+  }
+
+  get workspace() {
+    return this.traceCtx.workspaceMgr.currentWorkspace;
+  }
+
+  get workspaces() {
+    return this.traceCtx.workspaceMgr;
+  }
+
+  get search() {
+    return this.traceCtx.searchMgr;
+  }
+
+  get selection() {
+    return this.traceCtx.selectionMgr;
+  }
+
+  get traceInfo(): TraceInfoImpl {
+    return this.traceCtx.traceInfo;
+  }
+
+  get notes() {
+    return this.traceCtx.noteMgr;
+  }
+
+  get pivotTable() {
+    return this.traceCtx.pivotTableMgr;
+  }
+
+  get flows() {
+    return this.traceCtx.flowMgr;
+  }
+
+  get loadingErrors(): ReadonlyArray<string> {
+    return this.traceCtx.loadingErrors;
+  }
+
+  addLoadingError(err: string) {
+    this.traceCtx.loadingErrors.push(err);
+  }
+
+  // App interface implementation.
+
+  get pluginId(): string {
+    return this.appImpl.pluginId;
+  }
+
+  get commands(): CommandManagerImpl {
+    return this.commandMgrProxy;
+  }
+
+  get sidebar(): SidebarManagerImpl {
+    return this.sidebarProxy;
+  }
+
+  get pages(): PageManager {
+    return this.pageMgrProxy;
+  }
+
+  get omnibox(): OmniboxManagerImpl {
+    return this.appImpl.omnibox;
+  }
+
+  get plugins(): PluginManagerImpl {
+    return this.appImpl.plugins;
+  }
+
+  get analytics(): Analytics {
+    return this.appImpl.analytics;
+  }
+
+  get initialRouteArgs(): RouteArgs {
+    return this.appImpl.initialRouteArgs;
+  }
+
+  get featureFlags(): FeatureFlagManager {
+    return {
+      register: (settings: FlagSettings) => featureFlags.register(settings),
+    };
+  }
+
+  scheduleFullRedraw(): void {
+    this.appImpl.scheduleFullRedraw();
+  }
+
+  navigate(newHash: string): void {
+    this.appImpl.navigate(newHash);
+  }
+
+  openTraceFromFile(file: File): void {
+    this.appImpl.openTraceFromFile(file);
+  }
+
+  openTraceFromUrl(url: string, serializedAppState?: SerializedAppState) {
+    this.appImpl.openTraceFromUrl(url, serializedAppState);
+  }
+
+  openTraceFromBuffer(args: PostedTrace): void {
+    this.appImpl.openTraceFromBuffer(args);
+  }
+
+  addEventListener<T extends keyof EventListeners>(
+    event: T,
+    callback: EventListeners[T],
+  ): void {
+    const listeners = getOrCreate(
+      this.traceCtx.eventListeners,
+      event,
+      () => [],
+    );
+    listeners.push(callback);
+  }
+
+  getEventListeners<T extends keyof EventListeners>(
+    event: T,
+  ): ReadonlyArray<EventListeners[T]> {
+    const listeners = this.traceCtx.eventListeners.get(event);
+    if (listeners) {
+      return listeners as ReadonlyArray<EventListeners[T]>;
+    } else {
+      return [];
+    }
+  }
+
+  get trash(): DisposableStack {
+    return this.traceCtx.trash;
+  }
+
+  // Nothing other than AppImpl should ever refer to this, hence the __ name.
+  get __traceCtxForApp() {
+    return this.traceCtx;
+  }
+}
+
+// A convenience interface to inject the App in Mithril components.
+export interface TraceImplAttrs {
+  trace: TraceImpl;
+}
+
+export interface OptionalTraceImplAttrs {
+  trace?: TraceImpl;
+}
diff --git a/ui/src/core/trace_info_impl.ts b/ui/src/core/trace_info_impl.ts
new file mode 100644
index 0000000..1584143
--- /dev/null
+++ b/ui/src/core/trace_info_impl.ts
@@ -0,0 +1,20 @@
+// 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 {TraceSource} from '../core/trace_source';
+import {TraceInfo} from '../public/trace_info';
+
+export interface TraceInfoImpl extends TraceInfo {
+  readonly source: TraceSource;
+}
diff --git a/ui/src/core/trace_source.ts b/ui/src/core/trace_source.ts
new file mode 100644
index 0000000..cfc702d
--- /dev/null
+++ b/ui/src/core/trace_source.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 {SerializedAppState} from './state_serialization_schema';
+
+export type TraceSource =
+  | TraceFileSource
+  | TraceArrayBufferSource
+  | TraceUrlSource
+  | TraceHttpRpcSource;
+
+export interface TraceFileSource {
+  type: 'FILE';
+  file: File;
+}
+
+export interface TraceUrlSource {
+  type: 'URL';
+  url: string;
+
+  // When loading from a permalink, the permalink might supply also the app
+  // state alongside the URL of the trace.
+  serializedAppState?: SerializedAppState;
+}
+
+export interface TraceHttpRpcSource {
+  type: 'HTTP_RPC';
+}
+
+export interface TraceArrayBufferSource extends PostedTrace {
+  type: 'ARRAY_BUFFER';
+  // See PostedTrace (which this interface extends).
+}
+
+export interface PostedTrace {
+  buffer: ArrayBuffer;
+  title: string;
+  fileName?: string;
+  url?: string;
+
+  // |uuid| is set only when loading via ?local_cache_key=1234. When set,
+  // this matches global.state.traceUuid, with the exception of the following
+  // time window: When a trace T1 is loaded and the user loads another trace T2,
+  // this |uuid| will be == T2, but the globals.state.traceUuid will be
+  // temporarily == T1 until T2 has been loaded (consistently to what happens
+  // with all other state fields).
+  uuid?: string;
+
+  // if |localOnly| is true then the trace should not be shared or downloaded.
+  localOnly?: boolean;
+  keepApiOpen?: boolean;
+
+  // Allows to pass extra arguments to plugins. This can be read by plugins
+  // onTraceLoad() and can be used to trigger plugin-specific-behaviours (e.g.
+  // allow dashboards like APC to pass extra data to materialize onto tracks).
+  // The format is the following:
+  // pluginArgs: {
+  //   'dev.perfetto.PluginFoo': { 'key1': 'value1', 'key2': 1234 }
+  //   'dev.perfetto.PluginBar': { 'key3': '...', 'key4': ... }
+  // }
+  pluginArgs?: {[pluginId: string]: {[key: string]: unknown}};
+}
diff --git a/ui/src/core/track_kinds.ts b/ui/src/core/track_kinds.ts
deleted file mode 100644
index 93dec00..0000000
--- a/ui/src/core/track_kinds.ts
+++ /dev/null
@@ -1,37 +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.
-
-// This file contains a list of well known (to the core) track kinds.
-// This file exists purely to keep legacy systems in place without introducing a
-// ton of circular imports.
-export const CPU_SLICE_TRACK_KIND = 'CpuSliceTrack';
-export const CPU_FREQ_TRACK_KIND = 'CpuFreqTrack';
-export const THREAD_STATE_TRACK_KIND = 'ThreadStateTrack';
-export const THREAD_SLICE_TRACK_KIND = 'ThreadSliceTrack';
-export const EXPECTED_FRAMES_SLICE_TRACK_KIND = 'ExpectedFramesSliceTrack';
-export const ACTUAL_FRAMES_SLICE_TRACK_KIND = 'ActualFramesSliceTrack';
-export const ASYNC_SLICE_TRACK_KIND = 'AsyncSliceTrack';
-export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
-export const COUNTER_TRACK_KIND = 'CounterTrack';
-export const CPUSS_ESTIMATE_TRACK_KIND = 'CpuSubsystemEstimateTrack';
-export const CPU_PROFILE_TRACK_KIND = 'CpuProfileTrack';
-export const HEAP_PROFILE_TRACK_KIND = 'HeapProfileTrack';
-export const CHROME_TOPLEVEL_SCROLLS_KIND =
-  'org.chromium.TopLevelScrolls.scrolls';
-export const CHROME_EVENT_LATENCY_TRACK_KIND =
-  'org.chromium.ScrollJank.event_latencies';
-export const SCROLL_JANK_V3_TRACK_KIND =
-  'org.chromium.ScrollJank.scroll_jank_v3_track';
-export const CHROME_SCROLL_JANK_TRACK_KIND =
-  'org.chromium.ScrollJank.BrowserUIThreadLongTasks';
diff --git a/ui/src/core/track_manager.ts b/ui/src/core/track_manager.ts
new file mode 100644
index 0000000..8270564
--- /dev/null
+++ b/ui/src/core/track_manager.ts
@@ -0,0 +1,186 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Registry} from '../base/registry';
+import {Track, TrackDescriptor, TrackManager} from '../public/track';
+import {AsyncLimiter} from '../base/async_limiter';
+import {TrackRenderContext} from '../public/track';
+
+export interface TrackRenderer {
+  readonly track: Track;
+  desc: TrackDescriptor;
+  render(ctx: TrackRenderContext): void;
+  getError(): Error | undefined;
+}
+
+/**
+ * TrackManager is responsible for managing the registry of tracks and their
+ * lifecycle of tracks over render cycles.
+ *
+ * Example usage:
+ * function render() {
+ *   const trackCache = new TrackCache();
+ *   const foo = trackCache.getTrackRenderer('foo', 'exampleURI', {});
+ *   const bar = trackCache.getTrackRenderer('bar', 'exampleURI', {});
+ *   trackCache.flushOldTracks(); // <-- Destroys any unused cached tracks
+ * }
+ *
+ * Example of how flushing works:
+ * First cycle
+ *   getTrackRenderer('foo', ...) <-- new track 'foo' created
+ *   getTrackRenderer('bar', ...) <-- new track 'bar' created
+ *   flushTracks()
+ * Second cycle
+ *   getTrackRenderer('foo', ...) <-- returns cached 'foo' track
+ *   flushTracks() <-- 'bar' is destroyed, as it was not resolved this cycle
+ * Third cycle
+ *   flushTracks() <-- 'foo' is destroyed.
+ */
+export class TrackManagerImpl implements TrackManager {
+  private tracks = new Registry<TrackFSM>((x) => x.desc.uri);
+
+  // This property is written by scroll_helper.ts and read&cleared by the
+  // track_panel.ts. This exist for the following use case: the user wants to
+  // scroll to track X, but X is not visible because it's in a collapsed group.
+  // So we want to stash this information in a place that track_panel.ts can
+  // access when creating dom elements.
+  //
+  // Note: this is the node id of the track node to scroll to, not the track
+  // uri, as this allows us to scroll to tracks that have no uri.
+  scrollToTrackNodeId?: string;
+
+  registerTrack(trackDesc: TrackDescriptor): Disposable {
+    return this.tracks.register(new TrackFSM(trackDesc));
+  }
+
+  findTrack(
+    predicate: (desc: TrackDescriptor) => boolean | undefined,
+  ): TrackDescriptor | undefined {
+    for (const t of this.tracks.values()) {
+      if (predicate(t.desc)) return t.desc;
+    }
+    return undefined;
+  }
+
+  getAllTracks(): TrackDescriptor[] {
+    return Array.from(this.tracks.valuesAsArray().map((t) => t.desc));
+  }
+
+  // Look up track into for a given track's URI.
+  // Returns |undefined| if no track can be found.
+  getTrack(uri: string): TrackDescriptor | undefined {
+    return this.tracks.tryGet(uri)?.desc;
+  }
+
+  // This is only called by the viewer_page.ts.
+  getTrackRenderer(uri: string): TrackRenderer | undefined {
+    // Search for a cached version of this track,
+    const trackFsm = this.tracks.tryGet(uri);
+    trackFsm?.markUsed();
+    return trackFsm;
+  }
+
+  // Destroys all tracks that didn't recently get a getTrackRenderer() call.
+  flushOldTracks() {
+    for (const trackFsm of this.tracks.values()) {
+      trackFsm.tick();
+    }
+  }
+}
+
+const DESTROY_IF_NOT_SEEN_FOR_TICK_COUNT = 1;
+
+/**
+ * Owns all runtime information about a track and manages its lifecycle,
+ * ensuring lifecycle hooks are called synchronously and in the correct order.
+ *
+ * There are quite some subtle properties that this class guarantees:
+ * - It make sure that lifecycle methods don't overlap with each other.
+ * - It prevents a chain of onCreate > onDestroy > onCreate if the first
+ *   onCreate() is still oustanding. This is by virtue of using AsyncLimiter
+ *   which under the hoods holds only the most recent task and skips the
+ *   intermediate ones.
+ * - Ensures that a track never sees two consecutive onCreate, or onDestroy or
+ *   an onDestroy without an onCreate.
+ * - Ensures that onUpdate never overlaps or follows with onDestroy. This is
+ *   particularly important because tracks often drop tables/views onDestroy
+ *   and they shouldn't try to fetch more data onUpdate past that point.
+ */
+class TrackFSM implements TrackRenderer {
+  public readonly desc: TrackDescriptor;
+
+  private readonly limiter = new AsyncLimiter();
+  private error?: Error;
+  private tickSinceLastUsed = 0;
+  private created = false;
+
+  constructor(desc: TrackDescriptor) {
+    this.desc = desc;
+  }
+
+  markUsed(): void {
+    this.tickSinceLastUsed = 0;
+  }
+
+  // Increment the lastUsed counter, and maybe call onDestroy().
+  tick(): void {
+    if (this.tickSinceLastUsed++ === DESTROY_IF_NOT_SEEN_FOR_TICK_COUNT) {
+      // Schedule an onDestroy
+      this.limiter.schedule(async () => {
+        // Don't enter the track again once an error is has occurred
+        if (this.error !== undefined) {
+          return;
+        }
+
+        try {
+          if (this.created) {
+            await Promise.resolve(this.track.onDestroy?.());
+            this.created = false;
+          }
+        } catch (e) {
+          this.error = e;
+        }
+      });
+    }
+  }
+
+  render(ctx: TrackRenderContext): void {
+    this.limiter.schedule(async () => {
+      // Don't enter the track again once an error has occurred
+      if (this.error !== undefined) {
+        return;
+      }
+
+      try {
+        // Call onCreate() if this is our first call
+        if (!this.created) {
+          await this.track.onCreate?.(ctx);
+          this.created = true;
+        }
+        await Promise.resolve(this.track.onUpdate?.(ctx));
+      } catch (e) {
+        this.error = e;
+      }
+    });
+    this.track.render(ctx);
+  }
+
+  getError(): Error | undefined {
+    return this.error;
+  }
+
+  get track(): Track {
+    return this.desc.track;
+  }
+}
diff --git a/ui/src/core/track_manager_unittest.ts b/ui/src/core/track_manager_unittest.ts
new file mode 100644
index 0000000..08b23f2
--- /dev/null
+++ b/ui/src/core/track_manager_unittest.ts
@@ -0,0 +1,174 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {assertExists} from '../base/logging';
+import {Duration} from '../base/time';
+import {TimeScale} from '../base/time_scale';
+import {TrackDescriptor, TrackRenderContext} from '../public/track';
+import {HighPrecisionTime} from '../base/high_precision_time';
+import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
+import {TrackManagerImpl} from '../core/track_manager';
+
+function makeMockTrack() {
+  return {
+    onCreate: jest.fn(),
+    onUpdate: jest.fn(),
+    onDestroy: jest.fn(),
+
+    render: jest.fn(),
+    onFullRedraw: jest.fn(),
+    getSliceVerticalBounds: jest.fn(),
+    getHeight: jest.fn(),
+    getTrackShellButtons: jest.fn(),
+    onMouseMove: jest.fn(),
+    onMouseClick: jest.fn(),
+    onMouseOut: jest.fn(),
+  };
+}
+
+async function settle() {
+  await new Promise((r) => setTimeout(r, 0));
+}
+
+let mockTrack: ReturnType<typeof makeMockTrack>;
+let td: TrackDescriptor;
+let trackManager: TrackManagerImpl;
+const visibleWindow = new HighPrecisionTimeSpan(HighPrecisionTime.ZERO, 0);
+const dummyCtx: TrackRenderContext = {
+  trackUri: 'foo',
+  ctx: new CanvasRenderingContext2D(),
+  size: {width: 123, height: 123},
+  visibleWindow,
+  resolution: Duration.ZERO,
+  timescale: new TimeScale(visibleWindow, {left: 0, right: 0}),
+};
+
+beforeEach(() => {
+  mockTrack = makeMockTrack();
+  td = {
+    uri: 'test',
+    title: 'foo',
+    track: mockTrack,
+  };
+  trackManager = new TrackManagerImpl();
+  trackManager.registerTrack(td);
+});
+
+describe('TrackManager', () => {
+  it('calls track lifecycle hooks', async () => {
+    const entry = assertExists(trackManager.getTrackRenderer(td.uri));
+
+    entry.render(dummyCtx);
+    await settle();
+    expect(mockTrack.onCreate).toHaveBeenCalledTimes(1);
+    expect(mockTrack.onUpdate).toHaveBeenCalledTimes(1);
+
+    // Double flush should destroy all tracks
+    trackManager.flushOldTracks();
+    trackManager.flushOldTracks();
+    await settle();
+    expect(mockTrack.onDestroy).toHaveBeenCalledTimes(1);
+  });
+
+  it('calls onCrate lazily', async () => {
+    // Check we wait until the first call to render before calling onCreate
+    const entry = assertExists(trackManager.getTrackRenderer(td.uri));
+    await settle();
+    expect(mockTrack.onCreate).not.toHaveBeenCalled();
+
+    entry.render(dummyCtx);
+    await settle();
+    expect(mockTrack.onCreate).toHaveBeenCalledTimes(1);
+  });
+
+  it('reuses tracks', async () => {
+    const first = assertExists(trackManager.getTrackRenderer(td.uri));
+    trackManager.flushOldTracks();
+    first.render(dummyCtx);
+    await settle();
+
+    const second = assertExists(trackManager.getTrackRenderer(td.uri));
+    trackManager.flushOldTracks();
+    second.render(dummyCtx);
+    await settle();
+
+    expect(first).toBe(second);
+    // Ensure onCreate called only once
+    expect(mockTrack.onCreate).toHaveBeenCalledTimes(1);
+  });
+
+  it('destroys tracks when they are not resolved for one cycle', async () => {
+    const entry = assertExists(trackManager.getTrackRenderer(td.uri));
+    entry.render(dummyCtx);
+
+    // Double flush should destroy all tracks
+    trackManager.flushOldTracks();
+    trackManager.flushOldTracks();
+
+    await settle();
+
+    expect(mockTrack.onDestroy).toHaveBeenCalledTimes(1);
+  });
+
+  it('contains crash inside onCreate()', async () => {
+    const entry = assertExists(trackManager.getTrackRenderer(td.uri));
+    const e = new Error();
+
+    // Mock crash inside onCreate
+    mockTrack.onCreate.mockImplementationOnce(() => {
+      throw e;
+    });
+
+    entry.render(dummyCtx);
+    await settle();
+
+    expect(mockTrack.onCreate).toHaveBeenCalledTimes(1);
+    expect(mockTrack.onUpdate).not.toHaveBeenCalled();
+    expect(entry.getError()).toBe(e);
+  });
+
+  it('contains crash inside onUpdate()', async () => {
+    const entry = assertExists(trackManager.getTrackRenderer(td.uri));
+    const e = new Error();
+
+    // Mock crash inside onUpdate
+    mockTrack.onUpdate.mockImplementationOnce(() => {
+      throw e;
+    });
+
+    entry.render(dummyCtx);
+    await settle();
+
+    expect(mockTrack.onCreate).toHaveBeenCalledTimes(1);
+    expect(mockTrack.onUpdate).toHaveBeenCalledTimes(1);
+    expect(entry.getError()).toBe(e);
+  });
+
+  it('handles dispose after crash', async () => {
+    const entry = assertExists(trackManager.getTrackRenderer(td.uri));
+    const e = new Error();
+
+    // Mock crash inside onUpdate
+    mockTrack.onUpdate.mockImplementationOnce(() => {
+      throw e;
+    });
+
+    entry.render(dummyCtx);
+    await settle();
+
+    // Ensure we don't crash during the next render cycle
+    entry.render(dummyCtx);
+    await settle();
+  });
+});
diff --git a/ui/src/core/workspace_manager.ts b/ui/src/core/workspace_manager.ts
new file mode 100644
index 0000000..77803b7
--- /dev/null
+++ b/ui/src/core/workspace_manager.ts
@@ -0,0 +1,52 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+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 {
+  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);
+  }
+
+  createEmptyWorkspace(title: string): Workspace {
+    const workspace = new Workspace();
+    workspace.title = title;
+    workspace.onchange = () => raf.scheduleFullRedraw();
+    this._workspaces.push(workspace);
+    return workspace;
+  }
+
+  switchWorkspace(workspace: Workspace): void {
+    // If this fails the workspace doesn't come from createEmptyWorkspace().
+    assertTrue(this._workspaces.includes(workspace));
+    this._currentWorkspace = workspace;
+  }
+
+  get all(): ReadonlyArray<Workspace> {
+    return this._workspaces;
+  }
+
+  get currentWorkspace() {
+    return this._currentWorkspace;
+  }
+}
diff --git a/ui/src/core_plugins/android_log/index.ts b/ui/src/core_plugins/android_log/index.ts
deleted file mode 100644
index 2634670..0000000
--- a/ui/src/core_plugins/android_log/index.ts
+++ /dev/null
@@ -1,99 +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 m from 'mithril';
-
-import {LogFilteringCriteria, LogPanel} from './logs_panel';
-import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {NUM} from '../../trace_processor/query_result';
-import {AndroidLogTrack} from './logs_track';
-import {exists} from '../../base/utils';
-
-export const ANDROID_LOGS_TRACK_KIND = 'AndroidLogTrack';
-
-const VERSION = 1;
-
-const DEFAULT_STATE: AndroidLogPluginState = {
-  version: VERSION,
-  filter: {
-    // The first two log priorities are ignored.
-    minimumLevel: 2,
-    tags: [],
-    textEntry: '',
-    hideNonMatching: true,
-  },
-};
-
-interface AndroidLogPluginState {
-  version: number;
-  filter: LogFilteringCriteria;
-}
-
-class AndroidLog implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const store = ctx.mountStore<AndroidLogPluginState>((init) => {
-      return exists(init) && (init as {version: unknown}).version === VERSION
-        ? (init as AndroidLogPluginState)
-        : DEFAULT_STATE;
-    });
-
-    const result = await ctx.engine.query(
-      `select count(1) as cnt from android_logs`,
-    );
-    const logCount = result.firstRow({cnt: NUM}).cnt;
-    if (logCount > 0) {
-      ctx.registerStaticTrack({
-        uri: 'perfetto.AndroidLog',
-        title: 'Android logs',
-        tags: {kind: ANDROID_LOGS_TRACK_KIND},
-        trackFactory: () => new AndroidLogTrack(ctx.engine),
-      });
-    }
-
-    const androidLogsTabUri = 'perfetto.AndroidLog#tab';
-
-    // Eternal tabs should always be available even if there is nothing to show
-    const filterStore = store.createSubStore(
-      ['filter'],
-      (x) => x as LogFilteringCriteria,
-    );
-
-    ctx.registerTab({
-      isEphemeral: false,
-      uri: androidLogsTabUri,
-      content: {
-        render: () =>
-          m(LogPanel, {filterStore: filterStore, engine: ctx.engine}),
-        getTitle: () => 'Android Logs',
-      },
-    });
-
-    if (logCount > 0) {
-      ctx.addDefaultTab(androidLogsTabUri);
-    }
-
-    ctx.registerCommand({
-      id: 'perfetto.AndroidLog#ShowLogsTab',
-      name: 'Show android logs tab',
-      callback: () => {
-        ctx.tabs.showTab(androidLogsTabUri);
-      },
-    });
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.AndroidLog',
-  plugin: AndroidLog,
-};
diff --git a/ui/src/core_plugins/android_log/logs_panel.ts b/ui/src/core_plugins/android_log/logs_panel.ts
deleted file mode 100644
index e4b23a4..0000000
--- a/ui/src/core_plugins/android_log/logs_panel.ts
+++ /dev/null
@@ -1,437 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {time, Time, TimeSpan} from '../../base/time';
-import {Actions} from '../../common/actions';
-import {raf} from '../../core/raf_scheduler';
-import {DetailsShell} from '../../widgets/details_shell';
-
-import {globals} from '../../frontend/globals';
-import {Timestamp} from '../../frontend/widgets/timestamp';
-import {Engine, LONG, NUM, NUM_NULL, Store, STR} from '../../public';
-import {Monitor} from '../../base/monitor';
-import {AsyncLimiter} from '../../base/async_limiter';
-import {escapeGlob, escapeQuery} from '../../trace_processor/query_utils';
-import {Select} from '../../widgets/select';
-import {Button} from '../../widgets/button';
-import {TextInput} from '../../widgets/text_input';
-import {VirtualTable, VirtualTableRow} from '../../widgets/virtual_table';
-import {classNames} from '../../base/classnames';
-import {TagInput} from '../../widgets/tag_input';
-
-const ROW_H = 20;
-
-export interface LogFilteringCriteria {
-  minimumLevel: number;
-  tags: string[];
-  textEntry: string;
-  hideNonMatching: boolean;
-}
-
-export interface LogPanelAttrs {
-  filterStore: Store<LogFilteringCriteria>;
-  engine: Engine;
-}
-
-interface Pagination {
-  offset: number;
-  count: number;
-}
-
-interface LogEntries {
-  offset: number;
-  timestamps: time[];
-  priorities: number[];
-  tags: string[];
-  messages: string[];
-  isHighlighted: boolean[];
-  processName: string[];
-  totalEvents: number; // Count of the total number of events within this window
-}
-
-export class LogPanel implements m.ClassComponent<LogPanelAttrs> {
-  private entries?: LogEntries;
-
-  private pagination: Pagination = {
-    offset: 0,
-    count: 0,
-  };
-  private readonly rowsMonitor: Monitor;
-  private readonly filterMonitor: Monitor;
-  private readonly queryLimiter = new AsyncLimiter();
-
-  constructor({attrs}: m.CVnode<LogPanelAttrs>) {
-    this.rowsMonitor = new Monitor([
-      () => attrs.filterStore.state,
-      () => globals.timeline.visibleWindow.toTimeSpan().start,
-      () => globals.timeline.visibleWindow.toTimeSpan().end,
-    ]);
-
-    this.filterMonitor = new Monitor([() => attrs.filterStore.state]);
-  }
-
-  view({attrs}: m.CVnode<LogPanelAttrs>) {
-    if (this.rowsMonitor.ifStateChanged()) {
-      this.reloadData(attrs);
-    }
-
-    const hasProcessNames =
-      this.entries &&
-      this.entries.processName.filter((name) => name).length > 0;
-    const totalEvents = this.entries?.totalEvents ?? 0;
-
-    return m(
-      DetailsShell,
-      {
-        title: 'Android Logs',
-        description: `Total messages: ${totalEvents}`,
-        buttons: m(LogsFilters, {store: attrs.filterStore}),
-      },
-      m(VirtualTable, {
-        className: 'pf-android-logs-table',
-        columns: [
-          {header: 'Timestamp', width: '13em'},
-          {header: 'Level', width: '4em'},
-          {header: 'Tag', width: '13em'},
-          ...(hasProcessNames ? [{header: 'Process', width: '18em'}] : []),
-          // '' means column width can vary depending on the content.
-          // This works as this is the last column, but using this for other
-          // columns will pull the columns to the right out of line.
-          {header: 'Message', width: ''},
-        ],
-        rows: this.renderRows(hasProcessNames),
-        firstRowOffset: this.entries?.offset ?? 0,
-        numRows: this.entries?.totalEvents ?? 0,
-        rowHeight: ROW_H,
-        onReload: (offset, count) => {
-          this.pagination = {offset, count};
-          this.reloadData(attrs);
-        },
-        onRowHover: (id) => {
-          const timestamp = this.entries?.timestamps[id];
-          if (timestamp !== undefined) {
-            globals.dispatch(Actions.setHoverCursorTimestamp({ts: timestamp}));
-          }
-        },
-        onRowOut: () => {
-          globals.dispatch(Actions.setHoverCursorTimestamp({ts: Time.INVALID}));
-        },
-      }),
-    );
-  }
-
-  private reloadData(attrs: LogPanelAttrs) {
-    this.queryLimiter.schedule(async () => {
-      const visibleSpan = globals.timeline.visibleWindow.toTimeSpan();
-
-      if (this.filterMonitor.ifStateChanged()) {
-        await updateLogView(attrs.engine, attrs.filterStore.state);
-      }
-
-      this.entries = await updateLogEntries(
-        attrs.engine,
-        visibleSpan,
-        this.pagination,
-      );
-
-      raf.scheduleFullRedraw();
-    });
-  }
-
-  private renderRows(hasProcessNames: boolean | undefined): VirtualTableRow[] {
-    if (!this.entries) {
-      return [];
-    }
-
-    const timestamps = this.entries.timestamps;
-    const priorities = this.entries.priorities;
-    const tags = this.entries.tags;
-    const messages = this.entries.messages;
-    const processNames = this.entries.processName;
-
-    const rows: VirtualTableRow[] = [];
-    for (let i = 0; i < this.entries.timestamps.length; i++) {
-      const priorityLetter = LOG_PRIORITIES[priorities[i]][0];
-      const ts = timestamps[i];
-      const prioClass = priorityLetter ?? '';
-
-      rows.push({
-        id: i,
-        className: classNames(
-          prioClass,
-          this.entries.isHighlighted[i] && 'pf-highlighted',
-        ),
-        cells: [
-          m(Timestamp, {ts}),
-          priorityLetter || '?',
-          tags[i],
-          ...(hasProcessNames ? [processNames[i]] : []),
-          messages[i],
-        ],
-      });
-    }
-
-    return rows;
-  }
-}
-
-export const LOG_PRIORITIES = [
-  '-',
-  '-',
-  'Verbose',
-  'Debug',
-  'Info',
-  'Warn',
-  'Error',
-  'Fatal',
-];
-const IGNORED_STATES = 2;
-
-interface LogPriorityWidgetAttrs {
-  options: string[];
-  selectedIndex: number;
-  onSelect: (id: number) => void;
-}
-
-class LogPriorityWidget implements m.ClassComponent<LogPriorityWidgetAttrs> {
-  view(vnode: m.Vnode<LogPriorityWidgetAttrs>) {
-    const attrs = vnode.attrs;
-    const optionComponents = [];
-    for (let i = IGNORED_STATES; i < attrs.options.length; i++) {
-      const selected = i === attrs.selectedIndex;
-      optionComponents.push(
-        m('option', {value: i, selected}, attrs.options[i]),
-      );
-    }
-    return m(
-      Select,
-      {
-        onchange: (e: Event) => {
-          const selectionValue = (e.target as HTMLSelectElement).value;
-          attrs.onSelect(Number(selectionValue));
-        },
-      },
-      optionComponents,
-    );
-  }
-}
-
-interface LogTextWidgetAttrs {
-  onChange: (value: string) => void;
-}
-
-class LogTextWidget implements m.ClassComponent<LogTextWidgetAttrs> {
-  view({attrs}: m.CVnode<LogTextWidgetAttrs>) {
-    return m(TextInput, {
-      placeholder: 'Search logs...',
-      onkeyup: (e: KeyboardEvent) => {
-        // We want to use the value of the input field after it has been
-        // updated with the latest key (onkeyup).
-        const htmlElement = e.target as HTMLInputElement;
-        attrs.onChange(htmlElement.value);
-      },
-    });
-  }
-}
-
-interface FilterByTextWidgetAttrs {
-  hideNonMatching: boolean;
-  disabled: boolean;
-  onClick: () => void;
-}
-
-class FilterByTextWidget implements m.ClassComponent<FilterByTextWidgetAttrs> {
-  view({attrs}: m.Vnode<FilterByTextWidgetAttrs>) {
-    const icon = attrs.hideNonMatching ? 'unfold_less' : 'unfold_more';
-    const tooltip = attrs.hideNonMatching
-      ? 'Expand all and view highlighted'
-      : 'Collapse all';
-    return m(Button, {
-      icon,
-      title: tooltip,
-      disabled: attrs.disabled,
-      onclick: attrs.onClick,
-    });
-  }
-}
-
-interface LogsFiltersAttrs {
-  store: Store<LogFilteringCriteria>;
-}
-
-export class LogsFilters implements m.ClassComponent<LogsFiltersAttrs> {
-  view({attrs}: m.CVnode<LogsFiltersAttrs>) {
-    return [
-      m('.log-label', 'Log Level'),
-      m(LogPriorityWidget, {
-        options: LOG_PRIORITIES,
-        selectedIndex: attrs.store.state.minimumLevel,
-        onSelect: (minimumLevel) => {
-          attrs.store.edit((draft) => {
-            draft.minimumLevel = minimumLevel;
-          });
-        },
-      }),
-      m(TagInput, {
-        placeholder: 'Filter by tag...',
-        tags: attrs.store.state.tags,
-        onTagAdd: (tag) => {
-          attrs.store.edit((draft) => {
-            draft.tags.push(tag);
-          });
-        },
-        onTagRemove: (index) => {
-          attrs.store.edit((draft) => {
-            draft.tags.splice(index, 1);
-          });
-        },
-      }),
-      m(LogTextWidget, {
-        onChange: (text) => {
-          attrs.store.edit((draft) => {
-            draft.textEntry = text;
-          });
-        },
-      }),
-      m(FilterByTextWidget, {
-        hideNonMatching: attrs.store.state.hideNonMatching,
-        onClick: () => {
-          attrs.store.edit((draft) => {
-            draft.hideNonMatching = !draft.hideNonMatching;
-          });
-        },
-        disabled: attrs.store.state.textEntry === '',
-      }),
-    ];
-  }
-}
-
-async function updateLogEntries(
-  engine: Engine,
-  span: TimeSpan,
-  pagination: Pagination,
-): Promise<LogEntries> {
-  const rowsResult = await engine.query(`
-        select
-          ts,
-          prio,
-          ifnull(tag, '[NULL]') as tag,
-          ifnull(msg, '[NULL]') as msg,
-          is_msg_highlighted as isMsgHighlighted,
-          is_process_highlighted as isProcessHighlighted,
-          ifnull(process_name, '') as processName
-        from filtered_logs
-        where ts >= ${span.start} and ts <= ${span.end}
-        order by ts
-        limit ${pagination.offset}, ${pagination.count}
-    `);
-
-  const timestamps: time[] = [];
-  const priorities = [];
-  const tags = [];
-  const messages = [];
-  const isHighlighted = [];
-  const processName = [];
-
-  const it = rowsResult.iter({
-    ts: LONG,
-    prio: NUM,
-    tag: STR,
-    msg: STR,
-    isMsgHighlighted: NUM_NULL,
-    isProcessHighlighted: NUM,
-    processName: STR,
-  });
-  for (; it.valid(); it.next()) {
-    timestamps.push(Time.fromRaw(it.ts));
-    priorities.push(it.prio);
-    tags.push(it.tag);
-    messages.push(it.msg);
-    isHighlighted.push(
-      it.isMsgHighlighted === 1 || it.isProcessHighlighted === 1,
-    );
-    processName.push(it.processName);
-  }
-
-  const queryRes = await engine.query(`
-    select
-      count(*) as totalEvents
-    from filtered_logs
-    where ts >= ${span.start} and ts <= ${span.end}
-  `);
-  const {totalEvents} = queryRes.firstRow({totalEvents: NUM});
-
-  return {
-    offset: pagination.offset,
-    timestamps,
-    priorities,
-    tags,
-    messages,
-    isHighlighted,
-    processName,
-    totalEvents,
-  };
-}
-
-async function updateLogView(engine: Engine, filter: LogFilteringCriteria) {
-  await engine.query('drop view if exists filtered_logs');
-
-  const globMatch = composeGlobMatch(filter.hideNonMatching, filter.textEntry);
-  let selectedRows = `select prio, ts, tag, msg,
-      process.name as process_name, ${globMatch}
-      from android_logs
-      left join thread using(utid)
-      left join process using(upid)
-      where prio >= ${filter.minimumLevel}`;
-  if (filter.tags.length) {
-    selectedRows += ` and tag in (${serializeTags(filter.tags)})`;
-  }
-
-  // We extract only the rows which will be visible.
-  await engine.query(`create view filtered_logs as select *
-    from (${selectedRows})
-    where is_msg_chosen is 1 or is_process_chosen is 1`);
-}
-
-function serializeTags(tags: string[]) {
-  return tags.map((tag) => escapeQuery(tag)).join();
-}
-
-function composeGlobMatch(isCollaped: boolean, textEntry: string) {
-  if (isCollaped) {
-    // If the entries are collapsed, we won't highlight any lines.
-    return `msg glob ${escapeGlob(textEntry)} as is_msg_chosen,
-      (process.name is not null and process.name glob ${escapeGlob(
-        textEntry,
-      )}) as is_process_chosen,
-      0 as is_msg_highlighted,
-      0 as is_process_highlighted`;
-  } else if (!textEntry) {
-    // If there is no text entry, we will show all lines, but won't highlight.
-    // any.
-    return `1 as is_msg_chosen,
-      1 as is_process_chosen,
-      0 as is_msg_highlighted,
-      0 as is_process_highlighted`;
-  } else {
-    return `1 as is_msg_chosen,
-      1 as is_process_chosen,
-      msg glob ${escapeGlob(textEntry)} as is_msg_highlighted,
-      (process.name is not null and process.name glob ${escapeGlob(
-        textEntry,
-      )}) as is_process_highlighted`;
-  }
-}
diff --git a/ui/src/core_plugins/android_log/logs_track.ts b/ui/src/core_plugins/android_log/logs_track.ts
deleted file mode 100644
index 7eed69f..0000000
--- a/ui/src/core_plugins/android_log/logs_track.ts
+++ /dev/null
@@ -1,143 +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 {Time, duration, time} from '../../base/time';
-import {LIMIT, TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {Engine, LONG, NUM, Track} from '../../public';
-import {TrackRenderContext} from '../../public/tracks';
-
-export interface Data extends TrackData {
-  // Total number of log events within [start, end], before any quantization.
-  numEvents: number;
-
-  // Below: data quantized by resolution and aggregated by event priority.
-  timestamps: BigInt64Array;
-
-  // Each Uint8 value has the i-th bit is set if there is at least one log
-  // event at the i-th priority level at the corresponding time in |timestamps|.
-  priorities: Uint8Array;
-}
-
-const LEVELS: LevelCfg[] = [
-  {color: 'hsl(122, 39%, 49%)', prios: [0, 1, 2, 3]}, // Up to DEBUG: Green.
-  {color: 'hsl(0, 0%, 70%)', prios: [4]}, // 4 (INFO) -> Gray.
-  {color: 'hsl(45, 100%, 51%)', prios: [5]}, // 5 (WARN) -> Amber.
-  {color: 'hsl(4, 90%, 58%)', prios: [6]}, // 6 (ERROR) -> Red.
-  {color: 'hsl(291, 64%, 42%)', prios: [7]}, // 7 (FATAL) -> Purple
-];
-
-const MARGIN_TOP = 2;
-const RECT_HEIGHT = 35;
-const EVT_PX = 2; // Width of an event tick in pixels.
-
-interface LevelCfg {
-  color: string;
-  prios: number[];
-}
-
-export class AndroidLogTrack implements Track {
-  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-
-  constructor(private engine: Engine) {}
-
-  async onUpdate({
-    visibleWindow,
-    resolution,
-  }: TrackRenderContext): Promise<void> {
-    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
-  }
-
-  async onDestroy(): Promise<void> {
-    this.fetcher[Symbol.dispose]();
-  }
-
-  getHeight(): number {
-    return 40;
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    const queryRes = await this.engine.query(`
-      select
-        cast(ts / ${resolution} as integer) * ${resolution} as tsQuant,
-        prio,
-        count(prio) as numEvents
-      from android_logs
-      where ts >= ${start} and ts <= ${end}
-      group by tsQuant, prio
-      order by tsQuant, prio limit ${LIMIT};`);
-
-    const rowCount = queryRes.numRows();
-    const result = {
-      start,
-      end,
-      resolution,
-      length: rowCount,
-      numEvents: 0,
-      timestamps: new BigInt64Array(rowCount),
-      priorities: new Uint8Array(rowCount),
-    };
-
-    const it = queryRes.iter({tsQuant: LONG, prio: NUM, numEvents: NUM});
-    for (let row = 0; it.valid(); it.next(), row++) {
-      result.timestamps[row] = it.tsQuant;
-      const prio = Math.min(it.prio, 7);
-      result.priorities[row] |= 1 << prio;
-      result.numEvents += it.numEvents;
-    }
-    return result;
-  }
-
-  render({ctx, size, timescale}: TrackRenderContext): void {
-    const data = this.fetcher.data;
-
-    if (data === undefined) return; // Can't possibly draw anything.
-
-    const dataStartPx = timescale.timeToPx(data.start);
-    const dataEndPx = timescale.timeToPx(data.end);
-
-    checkerboardExcept(
-      ctx,
-      this.getHeight(),
-      0,
-      size.width,
-      dataStartPx,
-      dataEndPx,
-    );
-
-    const quantWidth = Math.max(
-      EVT_PX,
-      timescale.durationToPx(data.resolution),
-    );
-    const blockH = RECT_HEIGHT / LEVELS.length;
-    for (let i = 0; i < data.timestamps.length; i++) {
-      for (let lev = 0; lev < LEVELS.length; lev++) {
-        let hasEventsForCurColor = false;
-        for (const prio of LEVELS[lev].prios) {
-          if (data.priorities[i] & (1 << prio)) hasEventsForCurColor = true;
-        }
-        if (!hasEventsForCurColor) continue;
-        ctx.fillStyle = LEVELS[lev].color;
-        const timestamp = Time.fromRaw(data.timestamps[i]);
-        const px = Math.floor(timescale.timeToPx(timestamp));
-        ctx.fillRect(px, MARGIN_TOP + blockH * lev, quantWidth, blockH);
-      } // for(lev)
-    } // for (timestamps)
-  }
-}
diff --git a/ui/src/core_plugins/annotation/index.ts b/ui/src/core_plugins/annotation/index.ts
deleted file mode 100644
index cbc495e..0000000
--- a/ui/src/core_plugins/annotation/index.ts
+++ /dev/null
@@ -1,127 +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 {
-  COUNTER_TRACK_KIND,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
-import {ThreadSliceTrack} from '../../frontend/thread_slice_track';
-import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
-import {TraceProcessorCounterTrack} from '../counter/trace_processor_counter_track';
-import {THREAD_SLICE_TRACK_KIND} from '../../public';
-
-class AnnotationPlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    await this.addAnnotationTracks(ctx);
-    await this.addAnnotationCounterTracks(ctx);
-  }
-
-  private async addAnnotationTracks(ctx: PluginContextTrace) {
-    const {engine} = ctx;
-
-    const result = await engine.query(`
-      select
-        id,
-        name,
-        upid,
-        group_name as groupName
-      from annotation_slice_track
-      order by name
-    `);
-
-    const it = result.iter({
-      id: NUM,
-      name: STR,
-      upid: NUM,
-      groupName: STR_NULL,
-    });
-
-    for (; it.valid(); it.next()) {
-      const {id, name, upid, groupName} = it;
-
-      ctx.registerTrack({
-        uri: `/annotation_${id}`,
-        title: name,
-        tags: {
-          kind: THREAD_SLICE_TRACK_KIND,
-          scope: 'annotation',
-          upid,
-          ...(groupName && {groupName}),
-        },
-        chips: ['metric'],
-        trackFactory: ({trackKey}) => {
-          return new ThreadSliceTrack(
-            {
-              engine: ctx.engine,
-              trackKey,
-            },
-            id,
-            0,
-            'annotation_slice',
-          );
-        },
-      });
-    }
-  }
-
-  private async addAnnotationCounterTracks(ctx: PluginContextTrace) {
-    const {engine} = ctx;
-    const counterResult = await engine.query(`
-      SELECT
-        id,
-        name,
-        min_value as minValue,
-        max_value as maxValue,
-        upid
-      FROM annotation_counter_track`);
-
-    const counterIt = counterResult.iter({
-      id: NUM,
-      name: STR,
-      minValue: NUM_NULL,
-      maxValue: NUM_NULL,
-      upid: NUM,
-    });
-
-    for (; counterIt.valid(); counterIt.next()) {
-      const {id: trackId, name, upid} = counterIt;
-
-      ctx.registerTrack({
-        uri: `/annotation_counter_${trackId}`,
-        title: name,
-        tags: {
-          kind: COUNTER_TRACK_KIND,
-          scope: 'annotation',
-          upid,
-        },
-        chips: ['metric'],
-        trackFactory: (trackCtx) => {
-          return new TraceProcessorCounterTrack({
-            engine: ctx.engine,
-            trackKey: trackCtx.trackKey,
-            trackId,
-            rootTable: 'annotation_counter',
-          });
-        },
-      });
-    }
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.Annotation',
-  plugin: AnnotationPlugin,
-};
diff --git a/ui/src/core_plugins/async_slices/async_slice_track.ts b/ui/src/core_plugins/async_slices/async_slice_track.ts
deleted file mode 100644
index 018376d..0000000
--- a/ui/src/core_plugins/async_slices/async_slice_track.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {
-  NAMED_ROW,
-  NamedRow,
-  NamedSliceTrack,
-} from '../../frontend/named_slice_track';
-import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../frontend/slice_layout';
-import {NewTrackArgs} from '../../frontend/track';
-import {Slice} from '../../public';
-
-export class AsyncSliceTrack extends NamedSliceTrack {
-  constructor(
-    args: NewTrackArgs,
-    maxDepth: number,
-    private trackIds: number[],
-  ) {
-    super(args);
-    this.sliceLayout = {
-      ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
-      depthGuess: maxDepth,
-    };
-  }
-
-  getRowSpec(): NamedRow {
-    return NAMED_ROW;
-  }
-
-  rowToSlice(row: NamedRow): Slice {
-    return this.rowToSliceBase(row);
-  }
-
-  getSqlSource(): string {
-    return `
-      select
-        ts,
-        dur,
-        layout_depth as depth,
-        ifnull(name, '[null]') as name,
-        id,
-        thread_dur as threadDur
-      from experimental_slice_layout
-      where filter_track_ids = '${this.trackIds.join(',')}'
-    `;
-  }
-
-  onUpdatedSlices(slices: Slice[]) {
-    for (const slice of slices) {
-      slice.isHighlighted = slice === this.hoveredSlice;
-    }
-  }
-}
diff --git a/ui/src/core_plugins/async_slices/index.ts b/ui/src/core_plugins/async_slices/index.ts
deleted file mode 100644
index d0cf31d..0000000
--- a/ui/src/core_plugins/async_slices/index.ts
+++ /dev/null
@@ -1,212 +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 {ASYNC_SLICE_TRACK_KIND} from '../../public';
-import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {getTrackName} from '../../public/utils';
-import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
-
-import {AsyncSliceTrack} from './async_slice_track';
-
-class AsyncSlicePlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    await this.addGlobalAsyncTracks(ctx);
-    await this.addProcessAsyncSliceTracks(ctx);
-    await this.addUserAsyncSliceTracks(ctx);
-  }
-
-  async addGlobalAsyncTracks(ctx: PluginContextTrace): Promise<void> {
-    const {engine} = ctx;
-    const rawGlobalAsyncTracks = await engine.query(`
-      with global_tracks_grouped as (
-        select
-          parent_id,
-          name,
-          group_concat(id) as trackIds,
-          count() as trackCount
-        from track t
-        join _slice_track_summary using (id)
-        where t.type in ('track', 'gpu_track', 'cpu_track')
-        group by parent_id, name
-      )
-      select
-        t.name as name,
-        t.parent_id as parentId,
-        t.trackIds as trackIds,
-        __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
-      from global_tracks_grouped t
-    `);
-    const it = rawGlobalAsyncTracks.iter({
-      name: STR_NULL,
-      parentId: NUM_NULL,
-      trackIds: STR,
-      maxDepth: NUM,
-    });
-
-    for (; it.valid(); it.next()) {
-      const rawName = it.name === null ? undefined : it.name;
-      const displayName = getTrackName({
-        name: rawName,
-        kind: ASYNC_SLICE_TRACK_KIND,
-      });
-      const rawTrackIds = it.trackIds;
-      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
-      const maxDepth = it.maxDepth;
-
-      ctx.registerTrack({
-        uri: `/async_slices_${rawName}_${it.parentId}`,
-        title: displayName,
-        tags: {
-          trackIds,
-          kind: ASYNC_SLICE_TRACK_KIND,
-          scope: 'global',
-        },
-        trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrack({engine, trackKey}, maxDepth, trackIds);
-        },
-      });
-    }
-  }
-
-  async addProcessAsyncSliceTracks(ctx: PluginContextTrace): 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,
-        __max_layout_depth(t.track_count, t.track_ids) as maxDepth
-      from _process_track_summary_by_upid_and_name t
-      join process using(upid)
-      where t.name is null or t.name not glob "* Timeline"
-    `);
-
-    const it = result.iter({
-      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 kind = ASYNC_SLICE_TRACK_KIND;
-      const displayName = getTrackName({
-        name: trackName,
-        upid,
-        pid,
-        processName,
-        kind,
-      });
-
-      ctx.registerTrack({
-        uri: `/process_${upid}/async_slices_${rawTrackIds}`,
-        title: displayName,
-        tags: {
-          trackIds,
-          kind: ASYNC_SLICE_TRACK_KIND,
-          scope: 'process',
-          upid,
-        },
-        trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrack(
-            {engine: ctx.engine, trackKey},
-            maxDepth,
-            trackIds,
-          );
-        },
-      });
-    }
-  }
-
-  async addUserAsyncSliceTracks(ctx: PluginContextTrace): Promise<void> {
-    const {engine} = ctx;
-    const result = await engine.query(`
-      with grouped_packages as materialized (
-        select
-          uid,
-          group_concat(package_name, ',') as package_name,
-          count() as cnt
-        from package_list
-        group by uid
-      )
-      select
-        t.name as name,
-        t.uid as uid,
-        t.track_ids as trackIds,
-        __max_layout_depth(t.track_count, t.track_ids) as maxDepth,
-        iif(g.cnt = 1, g.package_name, 'UID ' || g.uid) as packageName
-      from _uid_track_track_summary_by_uid_and_name t
-      left join grouped_packages g using (uid)
-    `);
-
-    const it = result.iter({
-      name: STR_NULL,
-      uid: NUM_NULL,
-      packageName: STR_NULL,
-      trackIds: STR,
-      maxDepth: NUM_NULL,
-    });
-
-    for (; it.valid(); it.next()) {
-      const kind = ASYNC_SLICE_TRACK_KIND;
-      const rawName = it.name === null ? undefined : it.name;
-      const uid = it.uid === null ? undefined : it.uid;
-      const userName = it.packageName === null ? `UID ${uid}` : it.packageName;
-      const rawTrackIds = it.trackIds;
-      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
-      const maxDepth = it.maxDepth;
-
-      // If there are no slices in this track, skip it.
-      if (maxDepth === null) {
-        continue;
-      }
-
-      const displayName = getTrackName({
-        name: rawName,
-        uid,
-        userName,
-        kind,
-        uidTrack: true,
-      });
-
-      ctx.registerTrack({
-        uri: `/async_slices_${rawName}_${uid}`,
-        title: displayName,
-        tags: {
-          trackIds,
-          kind: ASYNC_SLICE_TRACK_KIND,
-          scope: 'user',
-        },
-        trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrack({engine, trackKey}, maxDepth, trackIds);
-        },
-      });
-    }
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.AsyncSlices',
-  plugin: AsyncSlicePlugin,
-};
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
deleted file mode 100644
index d074b46..0000000
--- a/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
+++ /dev/null
@@ -1,167 +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 {Actions} from '../../common/actions';
-import {OnSliceClickArgs} from '../../frontend/base_slice_track';
-import {GenericSliceDetailsTab} from '../../frontend/generic_slice_details_tab';
-import {globals} from '../../frontend/globals';
-import {NAMED_ROW} from '../../frontend/named_slice_track';
-import {NUM, Slice, STR} from '../../public';
-import {
-  CustomSqlDetailsPanelConfig,
-  CustomSqlImportConfig,
-  CustomSqlTableDefConfig,
-  CustomSqlTableSliceTrack,
-} from '../../frontend/tracks/custom_sql_table_slice_track';
-
-import {PageLoadDetailsPanel} from './page_load_details_panel';
-import {StartupDetailsPanel} from './startup_details_panel';
-import {WebContentInteractionPanel} from './web_content_interaction_details_panel';
-
-export const CRITICAL_USER_INTERACTIONS_KIND =
-  'org.chromium.CriticalUserInteraction.track';
-
-export const CRITICAL_USER_INTERACTIONS_ROW = {
-  ...NAMED_ROW,
-  scopedId: NUM,
-  type: STR,
-};
-export type CriticalUserInteractionRow = typeof CRITICAL_USER_INTERACTIONS_ROW;
-
-export interface CriticalUserInteractionSlice extends Slice {
-  scopedId: number;
-  type: string;
-}
-
-enum CriticalUserInteractionType {
-  UNKNOWN = 'Unknown',
-  PAGE_LOAD = 'chrome_page_loads',
-  STARTUP = 'chrome_startups',
-  WEB_CONTENT_INTERACTION = 'chrome_web_content_interactions',
-}
-
-function convertToCriticalUserInteractionType(
-  cujType: string,
-): CriticalUserInteractionType {
-  switch (cujType) {
-    case CriticalUserInteractionType.PAGE_LOAD:
-      return CriticalUserInteractionType.PAGE_LOAD;
-    case CriticalUserInteractionType.STARTUP:
-      return CriticalUserInteractionType.STARTUP;
-    case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
-      return CriticalUserInteractionType.WEB_CONTENT_INTERACTION;
-    default:
-      return CriticalUserInteractionType.UNKNOWN;
-  }
-}
-
-export class CriticalUserInteractionTrack extends CustomSqlTableSliceTrack {
-  static readonly kind = `/critical_user_interactions`;
-
-  getSqlDataSource(): CustomSqlTableDefConfig {
-    return {
-      columns: [
-        // The scoped_id is not a unique identifier within the table; generate
-        // a unique id from type and scoped_id on the fly to use for slice
-        // selection.
-        'hash(type, scoped_id) AS id',
-        'scoped_id AS scopedId',
-        'name',
-        'ts',
-        'dur',
-        'type',
-      ],
-      sqlTableName: 'chrome_interactions',
-    };
-  }
-
-  getDetailsPanel(
-    args: OnSliceClickArgs<CriticalUserInteractionSlice>,
-  ): CustomSqlDetailsPanelConfig {
-    let detailsPanel = {
-      kind: GenericSliceDetailsTab.kind,
-      config: {
-        sqlTableName: this.tableName,
-        title: 'Chrome Interaction',
-      },
-    };
-
-    switch (convertToCriticalUserInteractionType(args.slice.type)) {
-      case CriticalUserInteractionType.PAGE_LOAD:
-        detailsPanel = {
-          kind: PageLoadDetailsPanel.kind,
-          config: {
-            sqlTableName: this.tableName,
-            title: 'Chrome Page Load',
-          },
-        };
-        break;
-      case CriticalUserInteractionType.STARTUP:
-        detailsPanel = {
-          kind: StartupDetailsPanel.kind,
-          config: {
-            sqlTableName: this.tableName,
-            title: 'Chrome Startup',
-          },
-        };
-        break;
-      case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
-        detailsPanel = {
-          kind: WebContentInteractionPanel.kind,
-          config: {
-            sqlTableName: this.tableName,
-            title: 'Chrome Web Content Interaction',
-          },
-        };
-        break;
-      default:
-        break;
-    }
-    return detailsPanel;
-  }
-
-  onSliceClick(args: OnSliceClickArgs<CriticalUserInteractionSlice>) {
-    const detailsPanelConfig = this.getDetailsPanel(args);
-    globals.makeSelection(
-      Actions.selectGenericSlice({
-        id: args.slice.scopedId,
-        sqlTableName: this.tableName,
-        start: args.slice.ts,
-        duration: args.slice.dur,
-        trackKey: this.trackKey,
-        detailsPanelConfig: {
-          kind: detailsPanelConfig.kind,
-          config: detailsPanelConfig.config,
-        },
-      }),
-    );
-  }
-
-  getSqlImports(): CustomSqlImportConfig {
-    return {
-      modules: ['chrome.interactions'],
-    };
-  }
-
-  getRowSpec(): CriticalUserInteractionRow {
-    return CRITICAL_USER_INTERACTIONS_ROW;
-  }
-
-  rowToSlice(row: CriticalUserInteractionRow): CriticalUserInteractionSlice {
-    const baseSlice = super.rowToSlice(row);
-    const scopedId = row.scopedId;
-    const type = row.type;
-    return {...baseSlice, scopedId, type};
-  }
-}
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
deleted file mode 100644
index 6887dfe..0000000
--- a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {v4 as uuidv4} from 'uuid';
-
-import {Actions} from '../../common/actions';
-import {SCROLLING_TRACK_GROUP} from '../../common/state';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {globals} from '../../frontend/globals';
-import {
-  BottomTabToSCSAdapter,
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-  PrimaryTrackSortKey,
-} from '../../public';
-
-import {PageLoadDetailsPanel} from './page_load_details_panel';
-import {StartupDetailsPanel} from './startup_details_panel';
-import {WebContentInteractionPanel} from './web_content_interaction_details_panel';
-import {CriticalUserInteractionTrack} from './critical_user_interaction_track';
-
-function addCriticalUserInteractionTrack() {
-  const trackKey = uuidv4();
-  globals.dispatchMultiple([
-    Actions.addTrack({
-      key: trackKey,
-      uri: CriticalUserInteractionTrack.kind,
-      name: `Chrome Interactions`,
-      trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
-      trackGroup: SCROLLING_TRACK_GROUP,
-    }),
-    Actions.toggleTrackPinned({trackKey}),
-  ]);
-}
-
-class CriticalUserInteractionPlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerTrack({
-      uri: CriticalUserInteractionTrack.kind,
-      tags: {
-        kind: CriticalUserInteractionTrack.kind,
-      },
-      title: 'Chrome Interactions',
-      trackFactory: (trackCtx) =>
-        new CriticalUserInteractionTrack({
-          engine: ctx.engine,
-          trackKey: trackCtx.trackKey,
-        }),
-    });
-
-    ctx.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind === PageLoadDetailsPanel.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new PageLoadDetailsPanel({
-              config: config as GenericSliceDetailsTabConfig,
-              engine: ctx.engine,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
-
-    ctx.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind === StartupDetailsPanel.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new StartupDetailsPanel({
-              config: config as GenericSliceDetailsTabConfig,
-              engine: ctx.engine,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
-
-    ctx.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind ===
-              WebContentInteractionPanel.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new WebContentInteractionPanel({
-              config: config as GenericSliceDetailsTabConfig,
-              engine: ctx.engine,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
-  }
-
-  onActivate(ctx: PluginContext): void {
-    ctx.registerCommand({
-      id: 'perfetto.CriticalUserInteraction.AddInteractionTrack',
-      name: 'Add track: Chrome interactions',
-      callback: () => addCriticalUserInteractionTrack(),
-    });
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.CriticalUserInteraction',
-  plugin: CriticalUserInteractionPlugin,
-};
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts b/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts
deleted file mode 100644
index 37f80f0..0000000
--- a/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {DetailsShell} from '../../widgets/details_shell';
-import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
-import {
-  Details,
-  DetailsSchema,
-} from '../../frontend/widgets/sql/details/details';
-import {wellKnownTypes} from '../../frontend/widgets/sql/details/well_known_types';
-
-import d = DetailsSchema;
-
-export class PageLoadDetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'org.perfetto.PageLoadDetailsPanel';
-  private data: Details;
-
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): PageLoadDetailsPanel {
-    return new PageLoadDetailsPanel(args);
-  }
-
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-    this.data = new Details(
-      this.engine,
-      'chrome_page_loads',
-      this.config.id,
-      {
-        'Navigation start': d.Timestamp('navigation_start_ts'),
-        'FCP event': d.Timestamp('fcp_ts'),
-        'FCP': d.Interval('navigation_start_ts', 'fcp'),
-        'LCP event': d.Timestamp('lcp_ts', {skipIfNull: true}),
-        'LCP': d.Interval('navigation_start_ts', 'lcp', {skipIfNull: true}),
-        'DOMContentLoaded': d.Timestamp('dom_content_loaded_event_ts', {
-          skipIfNull: true,
-        }),
-        'onload timestamp': d.Timestamp('load_event_ts', {skipIfNull: true}),
-        'performance.mark timings': d.Dict({
-          data: {
-            'Fully loaded': d.Timestamp('mark_fully_loaded_ts', {
-              skipIfNull: true,
-            }),
-            'Fully visible': d.Timestamp('mark_fully_visible_ts', {
-              skipIfNull: true,
-            }),
-            'Interactive': d.Timestamp('mark_interactive_ts', {
-              skipIfNull: true,
-            }),
-          },
-          skipIfEmpty: true,
-        }),
-        'Navigation ID': 'navigation_id',
-        'Browser process': d.SqlIdRef('process', 'browser_upid'),
-        'URL': d.URLValue('url'),
-      },
-      wellKnownTypes,
-    );
-  }
-
-  viewTab() {
-    return m(
-      DetailsShell,
-      {
-        title: this.getTitle(),
-      },
-      m(GridLayout, m(GridLayoutColumn, this.data.render())),
-    );
-  }
-
-  getTitle(): string {
-    return this.config.title;
-  }
-
-  isLoading() {
-    return this.data.isLoading();
-  }
-}
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/startup_details_panel.ts b/ui/src/core_plugins/chrome_critical_user_interactions/startup_details_panel.ts
deleted file mode 100644
index 19ae82d..0000000
--- a/ui/src/core_plugins/chrome_critical_user_interactions/startup_details_panel.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {duration, Time, time} from '../../base/time';
-import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {DurationWidget} from '../../frontend/widgets/duration';
-import {Timestamp} from '../../frontend/widgets/timestamp';
-import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result';
-import {DetailsShell} from '../../widgets/details_shell';
-import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
-import {Section} from '../../widgets/section';
-import {SqlRef} from '../../widgets/sql_ref';
-import {dictToTreeNodes, Tree} from '../../widgets/tree';
-import {asUpid, Upid} from '../../trace_processor/sql_utils/core_types';
-
-interface Data {
-  startupId: number;
-  eventName: string;
-  startupBeginTs: time;
-  durToFirstVisibleContent: duration;
-  launchCause?: string;
-  upid: Upid;
-}
-
-export class StartupDetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'org.perfetto.StartupDetailsPanel';
-  private loaded = false;
-  private data: Data | undefined;
-
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): StartupDetailsPanel {
-    return new StartupDetailsPanel(args);
-  }
-
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-    this.loadData();
-  }
-
-  private async loadData() {
-    const queryResult = await this.engine.query(`
-      SELECT
-        activity_id AS startupId,
-        name,
-        startup_begin_ts AS startupBeginTs,
-        CASE
-          WHEN first_visible_content_ts IS NULL THEN 0
-          ELSE first_visible_content_ts - startup_begin_ts
-        END AS durTofirstVisibleContent,
-        launch_cause AS launchCause,
-        browser_upid AS upid
-      FROM chrome_startups
-      WHERE id = ${this.config.id};
-    `);
-
-    const iter = queryResult.firstRow({
-      startupId: NUM,
-      name: STR,
-      startupBeginTs: LONG,
-      durTofirstVisibleContent: LONG,
-      launchCause: STR_NULL,
-      upid: NUM,
-    });
-
-    this.data = {
-      startupId: iter.startupId,
-      eventName: iter.name,
-      startupBeginTs: Time.fromRaw(iter.startupBeginTs),
-      durToFirstVisibleContent: iter.durTofirstVisibleContent,
-      upid: asUpid(iter.upid),
-    };
-
-    if (iter.launchCause) {
-      this.data.launchCause = iter.launchCause;
-    }
-
-    this.loaded = true;
-  }
-
-  private getDetailsDictionary() {
-    const details: {[key: string]: m.Child} = {};
-    if (this.data === undefined) return details;
-    details['Activity ID'] = this.data.startupId;
-    details['Browser Upid'] = this.data.upid;
-    details['Startup Event'] = this.data.eventName;
-    details['Startup Timestamp'] = m(Timestamp, {ts: this.data.startupBeginTs});
-    details['Duration to First Visible Content'] = m(DurationWidget, {
-      dur: this.data.durToFirstVisibleContent,
-    });
-    if (this.data.launchCause) {
-      details['Launch Cause'] = this.data.launchCause;
-    }
-    details['SQL ID'] = m(SqlRef, {
-      table: 'chrome_startups',
-      id: this.config.id,
-    });
-    return details;
-  }
-
-  viewTab() {
-    if (this.isLoading()) {
-      return m('h2', 'Loading');
-    }
-
-    return m(
-      DetailsShell,
-      {
-        title: this.getTitle(),
-      },
-      m(
-        GridLayout,
-        m(
-          GridLayoutColumn,
-          m(
-            Section,
-            {title: 'Details'},
-            m(Tree, dictToTreeNodes(this.getDetailsDictionary())),
-          ),
-        ),
-      ),
-    );
-  }
-
-  getTitle(): string {
-    return this.config.title;
-  }
-
-  isLoading() {
-    return !this.loaded;
-  }
-}
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts b/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts
deleted file mode 100644
index 77e1b9e..0000000
--- a/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts
+++ /dev/null
@@ -1,147 +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.
-
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {duration, Time, time} from '../../base/time';
-import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {asUpid, Upid} from '../../trace_processor/sql_utils/core_types';
-import {DurationWidget} from '../../frontend/widgets/duration';
-import {Timestamp} from '../../frontend/widgets/timestamp';
-import {LONG, NUM, STR} from '../../trace_processor/query_result';
-import {DetailsShell} from '../../widgets/details_shell';
-import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
-import {Section} from '../../widgets/section';
-import {SqlRef} from '../../widgets/sql_ref';
-import {dictToTreeNodes, Tree} from '../../widgets/tree';
-
-interface Data {
-  ts: time;
-  dur: duration;
-  interactionType: string;
-  totalDurationMs: duration;
-  upid: Upid;
-}
-
-export class WebContentInteractionPanel extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'org.perfetto.WebContentInteractionPanel';
-  private loaded = false;
-  private data: Data | undefined;
-
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): WebContentInteractionPanel {
-    return new WebContentInteractionPanel(args);
-  }
-
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-    this.loadData();
-  }
-
-  private async loadData() {
-    const queryResult = await this.engine.query(`
-      SELECT
-        ts,
-        dur,
-        interaction_type AS interactionType,
-        total_duration_ms AS totalDurationMs,
-        renderer_upid AS upid
-      FROM chrome_web_content_interactions
-      WHERE id = ${this.config.id};
-    `);
-
-    const iter = queryResult.firstRow({
-      ts: LONG,
-      dur: LONG,
-      interactionType: STR,
-      totalDurationMs: LONG,
-      upid: NUM,
-    });
-
-    this.data = {
-      ts: Time.fromRaw(iter.ts),
-      dur: iter.ts,
-      interactionType: iter.interactionType,
-      totalDurationMs: iter.totalDurationMs,
-      upid: asUpid(iter.upid),
-    };
-
-    this.loaded = true;
-  }
-
-  private getDetailsDictionary() {
-    const details: {[key: string]: m.Child} = {};
-    if (this.data === undefined) return details;
-    details['Interaction'] = this.data.interactionType;
-    details['Timestamp'] = m(Timestamp, {ts: this.data.ts});
-    details['Duration'] = m(DurationWidget, {dur: this.data.dur});
-    details['Renderer Upid'] = this.data.upid;
-    details['Total duration of all events'] = m(DurationWidget, {
-      dur: this.data.totalDurationMs,
-    });
-    details['SQL ID'] = m(SqlRef, {
-      table: 'chrome_web_content_interactions',
-      id: this.config.id,
-    });
-    return details;
-  }
-
-  viewTab() {
-    if (this.isLoading()) {
-      return m('h2', 'Loading');
-    }
-
-    return m(
-      DetailsShell,
-      {
-        title: this.getTitle(),
-      },
-      m(
-        GridLayout,
-        m(
-          GridLayoutColumn,
-          m(
-            Section,
-            {title: 'Details'},
-            m(Tree, dictToTreeNodes(this.getDetailsDictionary())),
-          ),
-        ),
-      ),
-    );
-  }
-
-  getTitle(): string {
-    return this.config.title;
-  }
-
-  isLoading() {
-    return !this.loaded;
-  }
-}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts b/ui/src/core_plugins/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts
deleted file mode 100644
index 3eb0f98..0000000
--- a/ui/src/core_plugins/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts
+++ /dev/null
@@ -1,48 +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 {
-  NAMED_ROW,
-  NamedRow,
-  NamedSliceTrack,
-} from '../../frontend/named_slice_track';
-import {NewTrackArgs} from '../../frontend/track';
-import {Slice} from '../../public';
-
-export class ChromeTasksScrollJankTrack extends NamedSliceTrack {
-  constructor(args: NewTrackArgs) {
-    super(args);
-  }
-
-  getRowSpec(): NamedRow {
-    return NAMED_ROW;
-  }
-
-  rowToSlice(row: NamedRow): Slice {
-    return this.rowToSliceBase(row);
-  }
-
-  getSqlSource(): string {
-    return `
-      select
-        s2.ts as ts,
-        s2.dur as dur,
-        s2.id as id,
-        0 as depth,
-        s1.full_name as name
-      from chrome_tasks_delaying_input_processing s1
-      join slice s2 on s2.id=s1.slice_id
-    `;
-  }
-}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/common.ts b/ui/src/core_plugins/chrome_scroll_jank/common.ts
deleted file mode 100644
index d3eeb15..0000000
--- a/ui/src/core_plugins/chrome_scroll_jank/common.ts
+++ /dev/null
@@ -1,74 +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 {AddTrackArgs} from '../../common/actions';
-import {ObjectByKey} from '../../common/state';
-import {featureFlags} from '../../core/feature_flags';
-import {CustomSqlDetailsPanelConfig} from '../../frontend/tracks/custom_sql_table_slice_track';
-
-export const ENABLE_CHROME_SCROLL_JANK_PLUGIN = featureFlags.register({
-  id: 'enableChromeScrollJankPlugin',
-  name: 'Enable Chrome Scroll Jank plugin',
-  description: 'Adds new tracks for scroll jank in Chrome',
-  defaultValue: false,
-});
-
-export type DecideTracksResult = {
-  tracksToAdd: AddTrackArgs[];
-};
-
-export interface ScrollJankTrackSpec {
-  key: string;
-  sqlTableName: string;
-  detailsPanelConfig: CustomSqlDetailsPanelConfig;
-}
-
-// Global state for the scroll jank plugin.
-export class ScrollJankPluginState {
-  private static instance?: ScrollJankPluginState;
-  private tracks: ObjectByKey<ScrollJankTrackSpec>;
-
-  private constructor() {
-    this.tracks = {};
-  }
-
-  public static getInstance(): ScrollJankPluginState {
-    if (!ScrollJankPluginState.instance) {
-      ScrollJankPluginState.instance = new ScrollJankPluginState();
-    }
-
-    return ScrollJankPluginState.instance;
-  }
-
-  public registerTrack(args: {
-    kind: string;
-    trackKey: string;
-    tableName: string;
-    detailsPanelConfig: CustomSqlDetailsPanelConfig;
-  }): void {
-    this.tracks[args.kind] = {
-      key: args.trackKey,
-      sqlTableName: args.tableName,
-      detailsPanelConfig: args.detailsPanelConfig,
-    };
-  }
-
-  public unregisterTrack(kind: string): void {
-    delete this.tracks[kind];
-  }
-
-  public getTrack(kind: string): ScrollJankTrackSpec | undefined {
-    return this.tracks[kind];
-  }
-}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts b/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts
index a170a22..cfcdf87 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts
@@ -13,17 +13,12 @@
 // limitations under the License.
 
 import m from 'mithril';
-
-import {Duration, duration, time} from '../../base/time';
-import {raf} from '../../core/raf_scheduler';
-import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
+import {Duration, duration, Time, time} from '../../base/time';
 import {hasArgs, renderArguments} from '../../frontend/slice_args';
 import {renderDetails} from '../../frontend/slice_details';
 import {
   getDescendantSliceTree,
   getSlice,
-  getSliceFromConstraints,
   SliceDetails,
   SliceTreeNode,
 } from '../../trace_processor/sql_utils/slice';
@@ -36,15 +31,14 @@
   Table,
   TableData,
   widgetColumn,
-} from '../../frontend/tables/table';
+} from '../../widgets/table';
 import {TreeTable, TreeTableAttrs} from '../../frontend/widgets/treetable';
-import {NUM, STR} from '../../trace_processor/query_result';
+import {LONG, NUM, STR} from '../../trace_processor/query_result';
 import {DetailsShell} from '../../widgets/details_shell';
 import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
 import {Section} from '../../widgets/section';
 import {MultiParagraphText, TextParagraph} from '../../widgets/text_paragraph';
 import {Tree, TreeNode} from '../../widgets/tree';
-
 import {
   EventLatencyCauseThreadTracks,
   EventLatencyStage,
@@ -53,13 +47,10 @@
   getScrollJankCauseStage,
 } from './scroll_jank_cause_link_utils';
 import {ScrollJankCauseMap} from './scroll_jank_cause_map';
-import {
-  getScrollJankSlices,
-  getSliceForTrack,
-  ScrollJankSlice,
-} from './scroll_jank_slice';
 import {sliceRef} from '../../frontend/widgets/slice';
-import {SCROLL_JANK_V3_TRACK_KIND} from '../../public';
+import {JANKS_TRACK_URI, renderSliceRef} from './selection_utils';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
 
 // Given a node in the slice tree, return a path from root to it.
 function getPath(slice: SliceTreeNode): string[] {
@@ -106,15 +97,17 @@
   return `${delta > 0 ? '+' : ''}${Duration.humanise(delta)}`;
 }
 
-export class EventLatencySliceDetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'dev.perfetto.EventLatencySliceDetailsPanel';
-
-  private loaded = false;
+export class EventLatencySliceDetailsPanel implements TrackEventDetailsPanel {
   private name = '';
   private topEventLatencyId: SliceSqlId | undefined = undefined;
 
   private sliceDetails?: SliceDetails;
-  private jankySlice?: ScrollJankSlice;
+  private jankySlice?: {
+    ts: time;
+    dur: duration;
+    id: number;
+    causeOfJank: string;
+  };
 
   // Whether this stage has caused jank. This is also true for top level
   // EventLatency slices where a descendant is a cause of jank.
@@ -132,24 +125,26 @@
   // Stages tree for the prev EventLatency.
   private prevEventLatencyBreakdown?: SliceTreeNode;
 
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): EventLatencySliceDetailsPanel {
-    return new EventLatencySliceDetailsPanel(args);
+  private tracksByTrackId: Map<number, string>;
+
+  constructor(
+    private readonly trace: Trace,
+    private readonly id: number,
+  ) {
+    this.tracksByTrackId = new Map<number, string>();
+    this.trace.tracks.getAllTracks().forEach((td) => {
+      td.tags?.trackIds?.forEach((trackId) => {
+        this.tracksByTrackId.set(trackId, td.uri);
+      });
+    });
   }
 
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-
-    this.loadData();
-  }
-
-  async loadData() {
-    const queryResult = await this.engine.query(`
+  async load() {
+    const queryResult = await this.trace.engine.query(`
       SELECT
         name
-      FROM ${this.config.sqlTableName}
-      WHERE id = ${this.config.id}
+      FROM slice
+      WHERE id = ${this.id}
       `);
 
     const iter = queryResult.firstRow({
@@ -162,16 +157,14 @@
     await this.loadJankSlice();
     await this.loadRelevantThreads();
     await this.loadEventLatencyBreakdown();
-
-    this.loaded = true;
   }
 
   async loadSlice() {
     this.sliceDetails = await getSlice(
-      this.engine,
-      asSliceSqlId(this.config.id),
+      this.trace.engine,
+      asSliceSqlId(this.id),
     );
-    raf.scheduleRedraw();
+    this.trace.scheduleFullRedraw();
   }
 
   async loadJankSlice() {
@@ -187,14 +180,25 @@
       );
     }
 
-    const possibleSlices = await getScrollJankSlices(
-      this.engine,
-      this.topEventLatencyId,
-    );
-    // We may not get any slices if the EventLatency doesn't indicate any
-    // jank occurred.
-    if (possibleSlices.length > 0) {
-      this.jankySlice = possibleSlices[0];
+    const it = (
+      await this.trace.engine.query(`
+      SELECT ts, dur, id, cause_of_jank as causeOfJank
+      FROM chrome_janky_frame_presentation_intervals
+      WHERE event_latency_id = ${this.topEventLatencyId}`)
+    ).iter({
+      id: NUM,
+      ts: LONG,
+      dur: LONG,
+      causeOfJank: STR,
+    });
+
+    if (it.valid()) {
+      this.jankySlice = {
+        id: it.id,
+        ts: Time.fromRaw(it.ts),
+        dur: Duration.fromRaw(it.dur),
+        causeOfJank: it.causeOfJank,
+      };
     }
   }
 
@@ -207,7 +211,7 @@
     if (this.sliceDetails.name === 'EventLatency' && !this.jankySlice) return;
 
     const possibleScrollJankStage = await getScrollJankCauseStage(
-      this.engine,
+      this.trace.engine,
       this.topEventLatencyId,
     );
     if (this.sliceDetails.name === 'EventLatency') {
@@ -230,7 +234,7 @@
 
     if (this.relevantThreadStage) {
       this.relevantThreadTracks = await getEventLatencyCauseTracks(
-        this.engine,
+        this.trace.engine,
         this.relevantThreadStage,
       );
     }
@@ -241,51 +245,55 @@
       return;
     }
     this.eventLatencyBreakdown = await getDescendantSliceTree(
-      this.engine,
+      this.trace.engine,
       this.topEventLatencyId,
     );
 
-    // TODO(altimin): this should be based on an stdlib table and consider only
-    // EventLatencies within the same scroll.
-    // This is a copy of the statement in event_latency_track. It should move to
-    // stdlib instead of living in the UI code.
-    const whereClause = `
-    EXTRACT_ARG(arg_set_id, 'event_latency.event_type') IN (
-      'FIRST_GESTURE_SCROLL_UPDATE',
-      'GESTURE_SCROLL_UPDATE',
-      'INERTIAL_GESTURE_SCROLL_UPDATE')
-    AND HAS_DESCENDANT_SLICE_WITH_NAME(
-      id,
-      'SubmitCompositorFrameToPresentationCompositorFrame')`;
-    const prevEventLatency = await getSliceFromConstraints(this.engine, {
-      filters: [
-        `name = 'EventLatency'`,
-        `id < ${this.topEventLatencyId}`,
-        whereClause,
-      ],
-      orderBy: [{fieldName: 'id', direction: 'DESC'}],
-      limit: 1,
-    });
-    if (prevEventLatency.length > 0) {
+    // TODO(altimin): this should only consider EventLatencies within the same scroll.
+    const prevEventLatency = (
+      await this.trace.engine.query(`
+      INCLUDE PERFETTO MODULE chrome.event_latency;
+      SELECT
+        id
+      FROM chrome_event_latencies
+      WHERE event_type IN (
+        'FIRST_GESTURE_SCROLL_UPDATE',
+        'GESTURE_SCROLL_UPDATE',
+        'INERTIAL_GESTURE_SCROLL_UPDATE')
+      AND is_presented
+      AND id < ${this.topEventLatencyId}
+      ORDER BY id DESC
+      LIMIT 1
+      ;
+    `)
+    ).maybeFirstRow({id: NUM});
+    if (prevEventLatency !== undefined) {
       this.prevEventLatencyBreakdown = await getDescendantSliceTree(
-        this.engine,
-        prevEventLatency[0].id,
+        this.trace.engine,
+        asSliceSqlId(prevEventLatency.id),
       );
     }
 
-    const nextEventLatency = await getSliceFromConstraints(this.engine, {
-      filters: [
-        `name = 'EventLatency'`,
-        `id > ${this.topEventLatencyId}`,
-        whereClause,
-      ],
-      orderBy: ['id'],
-      limit: 1,
-    });
-    if (nextEventLatency.length > 0) {
+    const nextEventLatency = (
+      await this.trace.engine.query(`
+      INCLUDE PERFETTO MODULE chrome.event_latency;
+      SELECT
+        id
+      FROM chrome_event_latencies
+      WHERE event_type IN (
+        'FIRST_GESTURE_SCROLL_UPDATE',
+        'GESTURE_SCROLL_UPDATE',
+        'INERTIAL_GESTURE_SCROLL_UPDATE')
+      AND is_presented
+      AND id > ${this.topEventLatencyId}
+      ORDER BY id DESC
+      LIMIT 1;
+    `)
+    ).maybeFirstRow({id: NUM});
+    if (nextEventLatency !== undefined) {
       this.nextEventLatencyBreakdown = await getDescendantSliceTree(
-        this.engine,
-        nextEventLatency[0].id,
+        this.trace.engine,
+        asSliceSqlId(nextEventLatency.id),
       );
     }
   }
@@ -326,7 +334,7 @@
 
     const columns: ColumnDescriptor<RelevantThreadRow>[] = [
       widgetColumn<RelevantThreadRow>('Relevant Thread', (x) =>
-        getCauseLink(x.tracks, x.ts, x.dur),
+        getCauseLink(this.trace, x.tracks, this.tracksByTrackId, x.ts, x.dur),
       ),
       widgetColumn<RelevantThreadRow>('Description', (x) => {
         if (x.description === '') {
@@ -374,7 +382,7 @@
   private async getOldestAncestorSliceId(): Promise<number> {
     let eventLatencyId = -1;
     if (!this.sliceDetails) return eventLatencyId;
-    const queryResult = await this.engine.query(`
+    const queryResult = await this.trace.engine.query(`
       SELECT
         id
       FROM ancestor_slice(${this.sliceDetails.id})
@@ -408,16 +416,15 @@
             : 'EventLatency in context of other Input events',
           right: this.sliceDetails ? '' : 'N/A',
         }),
-        m(TreeNode, {
-          left: this.jankySlice
-            ? getSliceForTrack(
-                this.jankySlice,
-                SCROLL_JANK_V3_TRACK_KIND,
-                'Jank Interval',
-              )
-            : 'Jank Interval',
-          right: this.jankySlice ? '' : 'N/A',
-        }),
+        this.jankySlice &&
+          m(TreeNode, {
+            left: renderSliceRef({
+              trace: this.trace,
+              id: this.jankySlice.id,
+              trackUri: JANKS_TRACK_URI,
+              title: this.jankySlice.causeOfJank,
+            }),
+          }),
       ),
     );
   }
@@ -491,7 +498,7 @@
     );
   }
 
-  viewTab() {
+  render() {
     if (this.sliceDetails) {
       const slice = this.sliceDetails;
 
@@ -522,12 +529,12 @@
           GridLayout,
           m(
             GridLayoutColumn,
-            renderDetails(slice),
+            renderDetails(this.trace, slice),
             hasArgs(slice.args) &&
               m(
                 Section,
                 {title: 'Arguments'},
-                m(Tree, renderArguments(this.engine, slice.args)),
+                m(Tree, renderArguments(this.trace, slice.args)),
               ),
           ),
           m(GridLayoutColumn, rightSideWidgets),
@@ -537,12 +544,4 @@
       return m(DetailsShell, {title: 'Slice', description: 'Loading...'});
     }
   }
-
-  isLoading() {
-    return !this.loaded;
-  }
-
-  getTitle(): string {
-    return `Current Selection`;
-  }
 }
diff --git a/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts b/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
index 4660e51..33b42b4 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
@@ -12,20 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {globals} from '../../frontend/globals';
 import {NamedRow} from '../../frontend/named_slice_track';
 import {NewTrackArgs} from '../../frontend/track';
-import {CHROME_EVENT_LATENCY_TRACK_KIND, Slice} from '../../public';
+import {Slice} from '../../public/track';
 import {
-  CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../../frontend/tracks/custom_sql_table_slice_track';
-
-import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
 import {JANK_COLOR} from './jank_colors';
-import {getLegacySelection} from '../../common/state';
-import {ScrollJankPluginState} from './common';
+import {TrackEventSelection} from '../../public/selection';
+import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
 
 export const JANKY_LATENCY_NAME = 'Janky EventLatency';
 
@@ -35,32 +31,12 @@
     private baseTable: string,
   ) {
     super(args);
-    ScrollJankPluginState.getInstance().registerTrack({
-      kind: CHROME_EVENT_LATENCY_TRACK_KIND,
-      trackKey: this.trackKey,
-      tableName: this.tableName,
-      detailsPanelConfig: this.getDetailsPanel(),
-    });
-  }
-
-  async onDestroy(): Promise<void> {
-    await super.onDestroy();
-    ScrollJankPluginState.getInstance().unregisterTrack(
-      CHROME_EVENT_LATENCY_TRACK_KIND,
-    );
   }
 
   getSqlSource(): string {
     return `SELECT * FROM ${this.baseTable}`;
   }
 
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    return {
-      kind: EventLatencySliceDetailsPanel.kind,
-      config: {title: '', sqlTableName: this.tableName},
-    };
-  }
-
   getSqlDataSource(): CustomSqlTableDefConfig {
     return {
       sqlTableName: this.baseTable,
@@ -76,22 +52,7 @@
     }
   }
 
-  onUpdatedSlices(slices: Slice[]) {
-    for (const slice of slices) {
-      const currentSelection = getLegacySelection(globals.state);
-      const isSelected =
-        currentSelection &&
-        currentSelection.kind === 'GENERIC_SLICE' &&
-        currentSelection.id !== undefined &&
-        currentSelection.id === slice.id;
-
-      const highlighted = globals.state.highlightedSliceId === slice.id;
-      const hasFocus = highlighted || isSelected;
-      slice.isHighlighted = !!hasFocus;
-    }
-    super.onUpdatedSlices(slices);
+  override detailsPanel(sel: TrackEventSelection) {
+    return new EventLatencySliceDetailsPanel(this.trace, sel.eventId);
   }
-
-  // At the moment we will just display the slice details. However, on select,
-  // this behavior should be customized to show jank-related data.
 }
diff --git a/ui/src/core_plugins/chrome_scroll_jank/index.ts b/ui/src/core_plugins/chrome_scroll_jank/index.ts
index fabd49e..19a0c70 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/index.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/index.ts
@@ -12,186 +12,114 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {v4 as uuidv4} from 'uuid';
-
 import {uuidv4Sql} from '../../base/uuid';
-import {DeferredAction} from '../../common/actions';
-import {generateSqlWithInternalLayout} from '../../common/internal_layout_utils';
-import {featureFlags} from '../../core/feature_flags';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {
-  BottomTabToSCSAdapter,
-  CHROME_EVENT_LATENCY_TRACK_KIND,
-  CHROME_TOPLEVEL_SCROLLS_KIND,
-  NUM,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  CHROME_SCROLL_JANK_TRACK_KIND,
-  SCROLL_JANK_V3_TRACK_KIND,
-} from '../../public';
-import {Engine} from '../../trace_processor/engine';
-
-import {ChromeTasksScrollJankTrack} from './chrome_tasks_scroll_jank_track';
-import {DecideTracksResult, ENABLE_CHROME_SCROLL_JANK_PLUGIN} from './common';
-import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
+import {generateSqlWithInternalLayout} from '../../trace_processor/sql_utils/layout';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
 import {EventLatencyTrack, JANKY_LATENCY_NAME} from './event_latency_track';
-import {ScrollDetailsPanel} from './scroll_details_panel';
-import {ScrollJankV3DetailsPanel} from './scroll_jank_v3_details_panel';
 import {ScrollJankV3Track} from './scroll_jank_v3_track';
 import {TopLevelScrollTrack} from './scroll_track';
 import {ScrollJankCauseMap} from './scroll_jank_cause_map';
+import {TrackNode} from '../../public/workspace';
+import {featureFlags} from '../../core/feature_flags';
+import {OverrideState} from '../../public/feature_flag';
 
-const ENABLE_SCROLL_JANK_PLUGIN_V2 = featureFlags.register({
-  id: 'enableScrollJankPluginV2',
-  name: 'Enable Chrome Scroll Jank plugin V2',
-  description: 'Adds new tracks and visualizations for scroll jank.',
-  defaultValue: false,
-});
+// Before plugins were a thing, this plugin was enabled using a feature flag.
+// However, nowadays, plugins themselves can be selectively enabled and
+// disabled. This function inspects local storage to see whether the old feature
+// flag is enabled, and patches the flags settings to enable the chrome scroll
+// jank plugin, before deleting the old flag. This provides a seamless
+// experience for anyone who currently uses the chrome scroll jank plugin.
+//
+// TODO(stevegolton): Remove this code after 2025-01-01. This should give it
+// enough time on stable for most relevant users to have run it at least once.
+function patchChromeScrollJankFlag() {
+  try {
+    const flagsKey = 'perfettoFeatureFlags';
+    const enableScrollJankPluginV2FlagKey = 'enableScrollJankPluginV2';
+    const chromeScrollJankPuginFlagKey = 'plugin_perfetto.ChromeScrollJank';
 
-export type ScrollJankTrackGroup = {
-  tracks: DecideTracksResult;
-  addTrackGroup: DeferredAction;
-};
+    const flagsRaw = localStorage.getItem(flagsKey);
+    if (flagsRaw) {
+      const flags = JSON.parse(flagsRaw);
+      if (flags[enableScrollJankPluginV2FlagKey] === 'OVERRIDE_TRUE') {
+        featureFlags.patchOverride(
+          chromeScrollJankPuginFlagKey,
+          OverrideState.TRUE,
+        );
+        console.log(
+          `Cleared deprecated 'enableScrollJankPluginV2' flag & enabled 'ChromeScrollJank' plugin.`,
+        );
+      }
 
-class ChromeScrollJankPlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    if (!ENABLE_CHROME_SCROLL_JANK_PLUGIN.get()) {
-      return;
+      // Just remove the original flag
+      delete flags[enableScrollJankPluginV2FlagKey];
+      localStorage.setItem(flagsKey, JSON.stringify(flags));
     }
+  } catch {
+    // Ignore - this was very much best-effort.
+  }
+}
 
-    await this.addChromeScrollJankTrack(ctx);
+patchChromeScrollJankFlag();
 
-    if (ENABLE_SCROLL_JANK_PLUGIN_V2.get()) {
-      await this.addTopLevelScrollTrack(ctx);
-      await this.addEventLatencyTrack(ctx);
-      await this.addScrollJankV3ScrollTrack(ctx);
-      await ScrollJankCauseMap.initialize(ctx.engine);
-    }
-
-    if (!(await isChromeTrace(ctx.engine))) {
-      return;
-    }
-
-    // Initialise the chrome_tasks_delaying_input_processing table. It will be
-    // used in the tracks above.
-    await ctx.engine.query(`
-      INCLUDE PERFETTO MODULE deprecated.v42.common.slices;
-      SELECT RUN_METRIC(
-        'chrome/chrome_tasks_delaying_input_processing.sql',
-        'duration_causing_jank_ms',
-        /* duration_causing_jank_ms = */ '8');`);
-
-    const query = `
-       select
-         s1.full_name,
-         s1.duration_ms,
-         s1.slice_id,
-         s1.thread_dur_ms,
-         s2.id,
-         s2.ts,
-         s2.dur,
-         s2.track_id
-       from chrome_tasks_delaying_input_processing s1
-       join slice s2 on s1.slice_id=s2.id
-       `;
-    ctx.tabs.openQuery(query, 'Scroll Jank: long tasks');
+export default class implements PerfettoPlugin {
+  static readonly id = 'perfetto.ChromeScrollJank';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const group = new TrackNode({
+      title: 'Chrome Scroll Jank',
+      sortOrder: -30,
+      isSummary: true,
+    });
+    await this.addTopLevelScrollTrack(ctx, group);
+    await this.addEventLatencyTrack(ctx, group);
+    await this.addScrollJankV3ScrollTrack(ctx, group);
+    await ScrollJankCauseMap.initialize(ctx.engine);
+    ctx.workspace.addChildInOrder(group);
+    group.expand();
   }
 
-  private async addChromeScrollJankTrack(
-    ctx: PluginContextTrace,
+  private async addTopLevelScrollTrack(
+    ctx: Trace,
+    group: TrackNode,
   ): Promise<void> {
-    const queryResult = await ctx.engine.query(`
-      select
-        utid,
-        upid
-      from thread
-      where name='CrBrowserMain'
-    `);
-
-    if (queryResult.numRows() === 0) {
-      return;
-    }
-
-    const it = queryResult.firstRow({
-      utid: NUM,
-      upid: NUM,
-    });
-
-    const {upid, utid} = it;
-    ctx.registerTrack({
-      uri: 'perfetto.ChromeScrollJank',
-      title: 'Scroll Jank causes - long tasks',
-      tags: {
-        kind: CHROME_SCROLL_JANK_TRACK_KIND,
-        upid,
-        utid,
-      },
-      trackFactory: ({trackKey}) => {
-        return new ChromeTasksScrollJankTrack({
-          engine: ctx.engine,
-          trackKey,
-        });
-      },
-    });
-  }
-
-  private async addTopLevelScrollTrack(ctx: PluginContextTrace): Promise<void> {
     await ctx.engine.query(`
       INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
       INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_offsets;
+      INCLUDE PERFETTO MODULE chrome.event_latency;
     `);
 
-    ctx.registerTrack({
-      uri: 'perfetto.ChromeScrollJank#toplevelScrolls',
-      title: 'Chrome Scrolls',
-      tags: {
-        kind: CHROME_TOPLEVEL_SCROLLS_KIND,
-      },
-      trackFactory: ({trackKey}) => {
-        return new TopLevelScrollTrack({
-          engine: ctx.engine,
-          trackKey,
-        });
-      },
+    const uri = 'perfetto.ChromeScrollJank#toplevelScrolls';
+    const title = 'Chrome Scrolls';
+
+    ctx.tracks.registerTrack({
+      uri,
+      title,
+      track: new TopLevelScrollTrack({
+        trace: ctx,
+        uri,
+      }),
     });
 
-    ctx.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind === ScrollDetailsPanel.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new ScrollDetailsPanel({
-              config: config as GenericSliceDetailsTabConfig,
-              engine: ctx.engine,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
+    const track = new TrackNode({uri, title});
+    group.addChildInOrder(track);
   }
 
-  private async addEventLatencyTrack(ctx: PluginContextTrace): Promise<void> {
+  private async addEventLatencyTrack(
+    ctx: Trace,
+    group: TrackNode,
+  ): Promise<void> {
     const subTableSql = generateSqlWithInternalLayout({
       columns: ['id', 'ts', 'dur', 'track_id', 'name'],
-      sourceTable: 'slice',
+      sourceTable: 'chrome_event_latencies',
       ts: 'ts',
       dur: 'dur',
       whereClause: `
-        EXTRACT_ARG(arg_set_id, 'event_latency.event_type') IN (
+        event_type IN (
           'FIRST_GESTURE_SCROLL_UPDATE',
           'GESTURE_SCROLL_UPDATE',
           'INERTIAL_GESTURE_SCROLL_UPDATE')
-        AND has_descendant_slice_with_name(
-          id,
-          'SubmitCompositorFrameToPresentationCompositorFrame')
-        AND name = 'EventLatency'
-        AND depth = 0`,
+        AND is_presented`,
     });
 
     // Table name must be unique - it cannot include '-' characters or begin
@@ -273,96 +201,40 @@
     );
     await ctx.engine.query(tableDefSql);
 
-    ctx.registerTrack({
-      uri: 'perfetto.ChromeScrollJank#eventLatency',
-      title: 'Chrome Scroll Input Latencies',
-      tags: {
-        kind: CHROME_EVENT_LATENCY_TRACK_KIND,
-      },
-      trackFactory: ({trackKey}) => {
-        return new EventLatencyTrack({engine: ctx.engine, trackKey}, baseTable);
-      },
+    const uri = 'perfetto.ChromeScrollJank#eventLatency';
+    const title = 'Chrome Scroll Input Latencies';
+
+    ctx.tracks.registerTrack({
+      uri,
+      title,
+      track: new EventLatencyTrack({trace: ctx, uri}, baseTable),
     });
 
-    ctx.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind ===
-              EventLatencySliceDetailsPanel.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new EventLatencySliceDetailsPanel({
-              config: config as GenericSliceDetailsTabConfig,
-              engine: ctx.engine,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
+    const track = new TrackNode({uri, title});
+    group.addChildInOrder(track);
   }
 
   private async addScrollJankV3ScrollTrack(
-    ctx: PluginContextTrace,
+    ctx: Trace,
+    group: TrackNode,
   ): Promise<void> {
     await ctx.engine.query(
       `INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_intervals`,
     );
 
-    ctx.registerTrack({
-      uri: 'perfetto.ChromeScrollJank#scrollJankV3',
-      title: 'Chrome Scroll Janks',
-      tags: {
-        kind: SCROLL_JANK_V3_TRACK_KIND,
-      },
-      trackFactory: ({trackKey}) => {
-        return new ScrollJankV3Track({
-          engine: ctx.engine,
-          trackKey,
-        });
-      },
+    const uri = 'perfetto.ChromeScrollJank#scrollJankV3';
+    const title = 'Chrome Scroll Janks';
+
+    ctx.tracks.registerTrack({
+      uri,
+      title,
+      track: new ScrollJankV3Track({
+        trace: ctx,
+        uri,
+      }),
     });
 
-    ctx.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind === ScrollJankV3DetailsPanel.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new ScrollJankV3DetailsPanel({
-              config: config as GenericSliceDetailsTabConfig,
-              engine: ctx.engine,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
+    const track = new TrackNode({uri, title});
+    group.addChildInOrder(track);
   }
 }
-
-async function isChromeTrace(engine: Engine) {
-  const queryResult = await engine.query(`
-      select utid, upid
-      from thread
-      where name='CrBrowserMain'
-      `);
-
-  const it = queryResult.iter({
-    utid: NUM,
-    upid: NUM,
-  });
-
-  return it.valid();
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.ChromeScrollJank',
-  plugin: ChromeScrollJankPlugin,
-};
diff --git a/ui/src/core_plugins/chrome_scroll_jank/jank_colors.ts b/ui/src/core_plugins/chrome_scroll_jank/jank_colors.ts
index a18ea03..f764ebf 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/jank_colors.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/jank_colors.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {HSLColor} from '../../core/color';
-import {makeColorScheme} from '../../core/colorizer';
+import {HSLColor} from '../../public/color';
+import {makeColorScheme} from '../../public/lib/colorizer';
 
 export const JANK_COLOR = makeColorScheme(new HSLColor([343, 100, 43]));
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_delta_graph.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_delta_graph.ts
index 71be317..b637020 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_delta_graph.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_delta_graph.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {duration, Time, time} from '../../base/time';
 import {Engine} from '../../trace_processor/engine';
 import {LONG, NUM} from '../../trace_processor/query_result';
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts
index b0f4394..eed111f 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts
@@ -13,29 +13,29 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {duration, Time, time} from '../../base/time';
 import {exists} from '../../base/utils';
-import {raf} from '../../core/raf_scheduler';
-import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {
   ColumnDescriptor,
-  numberColumn,
   Table,
   TableData,
   widgetColumn,
-} from '../../frontend/tables/table';
+} from '../../widgets/table';
 import {DurationWidget} from '../../frontend/widgets/duration';
 import {Timestamp} from '../../frontend/widgets/timestamp';
-import {LONG, NUM, STR} from '../../trace_processor/query_result';
+import {
+  LONG,
+  LONG_NULL,
+  NUM,
+  NUM_NULL,
+  STR,
+} from '../../trace_processor/query_result';
 import {DetailsShell} from '../../widgets/details_shell';
 import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
 import {Section} from '../../widgets/section';
 import {SqlRef} from '../../widgets/sql_ref';
 import {MultiParagraphText, TextParagraph} from '../../widgets/text_paragraph';
 import {dictToTreeNodes, Tree} from '../../widgets/tree';
-
 import {
   buildScrollOffsetsGraph,
   getInputScrollDeltas,
@@ -43,12 +43,9 @@
   getPredictorJankDeltas,
   getPresentedScrollDeltas,
 } from './scroll_delta_graph';
-import {
-  getScrollJankSlices,
-  getSliceForTrack,
-  ScrollJankSlice,
-} from './scroll_jank_slice';
-import {SCROLL_JANK_V3_TRACK_KIND} from '../../public';
+import {JANKS_TRACK_URI, renderSliceRef} from './selection_utils';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
 
 interface Data {
   // Scroll ID.
@@ -73,43 +70,38 @@
 
 interface JankSliceDetails {
   cause: string;
-  jankSlice: ScrollJankSlice;
-  delayDur: duration;
-  delayVsync: number;
+  id: number;
+  ts: time;
+  dur?: duration;
+  delayVsync?: number;
 }
 
-export class ScrollDetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'org.perfetto.ScrollDetailsPanel';
-  loaded = false;
-  data: Data | undefined;
-  metrics: Metrics = {};
-  orderedJankSlices: JankSliceDetails[] = [];
-  scrollDeltas: m.Child;
+export class ScrollDetailsPanel implements TrackEventDetailsPanel {
+  private data?: Data;
+  private metrics: Metrics = {};
+  private orderedJankSlices: JankSliceDetails[] = [];
 
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): ScrollDetailsPanel {
-    return new ScrollDetailsPanel(args);
-  }
+  // TODO(altimin): Don't store Mithril vnodes between render cycles.
+  private scrollDeltas: m.Child;
 
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-    this.loadData();
-  }
+  constructor(
+    private readonly trace: Trace,
+    private readonly id: number,
+  ) {}
 
-  private async loadData() {
-    const queryResult = await this.engine.query(`
+  async load() {
+    const queryResult = await this.trace.engine.query(`
       WITH scrolls AS (
         SELECT
           id,
           IFNULL(gesture_scroll_begin_ts, ts) AS start_ts,
           CASE
             WHEN gesture_scroll_end_ts IS NOT NULL THEN gesture_scroll_end_ts
-            WHEN gesture_scroll_begin_ts IS NOT NULL 
+            WHEN gesture_scroll_begin_ts IS NOT NULL
               THEN gesture_scroll_begin_ts + dur
             ELSE ts + dur
           END AS end_ts
-        FROM chrome_scrolls WHERE id = ${this.config.id})
+        FROM chrome_scrolls WHERE id = ${this.id})
       SELECT
         id,
         start_ts AS ts,
@@ -128,8 +120,6 @@
     };
 
     await this.loadMetrics();
-    this.loaded = true;
-    raf.scheduleFullRedraw();
   }
 
   private async loadMetrics() {
@@ -141,7 +131,7 @@
 
   private async loadInputEventCount() {
     if (exists(this.data)) {
-      const queryResult = await this.engine.query(`
+      const queryResult = await this.trace.engine.query(`
         SELECT
           COUNT(*) AS inputEventCount
         FROM slice s
@@ -161,7 +151,7 @@
 
   private async loadFrameStats() {
     if (exists(this.data)) {
-      const queryResult = await this.engine.query(`
+      const queryResult = await this.trace.engine.query(`
         SELECT
           IFNULL(frame_count, 0) AS frameCount,
           IFNULL(missed_vsyncs, 0) AS missedVsyncs,
@@ -192,39 +182,36 @@
 
   private async loadDelayData() {
     if (exists(this.data)) {
-      const queryResult = await this.engine.query(`
+      const queryResult = await this.trace.engine.query(`
         SELECT
+          id,
+          ts,
+          dur,
           IFNULL(sub_cause_of_jank, IFNULL(cause_of_jank, 'Unknown')) AS cause,
-          IFNULL(event_latency_id, 0) AS eventLatencyId,
-          IFNULL(dur, 0) AS delayDur,
-          IFNULL(delayed_frame_count, 0) AS delayVsync
+          event_latency_id AS eventLatencyId,
+          delayed_frame_count AS delayVsync
         FROM chrome_janky_frame_presentation_intervals s
         WHERE s.ts >= ${this.data.ts}
           AND s.ts + s.dur <= ${this.data.ts + this.data.dur}
-        ORDER by delayDur DESC;
+        ORDER by dur DESC;
       `);
 
-      const iter = queryResult.iter({
+      const it = queryResult.iter({
+        id: NUM,
+        ts: LONG,
+        dur: LONG_NULL,
         cause: STR,
-        eventLatencyId: NUM,
-        delayDur: LONG,
-        delayVsync: NUM,
+        eventLatencyId: NUM_NULL,
+        delayVsync: NUM_NULL,
       });
 
-      for (; iter.valid(); iter.next()) {
-        if (iter.delayDur <= 0) {
-          break;
-        }
-        const jankSlices = await getScrollJankSlices(
-          this.engine,
-          iter.eventLatencyId,
-        );
-
+      for (; it.valid(); it.next()) {
         this.orderedJankSlices.push({
-          cause: iter.cause,
-          jankSlice: jankSlices[0],
-          delayDur: iter.delayDur,
-          delayVsync: iter.delayVsync,
+          id: it.id,
+          ts: Time.fromRaw(it.ts),
+          dur: it.dur ?? undefined,
+          cause: it.cause,
+          delayVsync: it.delayVsync ?? undefined,
         });
       }
     }
@@ -232,17 +219,20 @@
 
   private async loadScrollOffsets() {
     if (exists(this.data)) {
-      const inputDeltas = await getInputScrollDeltas(this.engine, this.data.id);
+      const inputDeltas = await getInputScrollDeltas(
+        this.trace.engine,
+        this.data.id,
+      );
       const presentedDeltas = await getPresentedScrollDeltas(
-        this.engine,
+        this.trace.engine,
         this.data.id,
       );
       const predictorDeltas = await getPredictorJankDeltas(
-        this.engine,
+        this.trace.engine,
         this.data.id,
       );
       const jankIntervals = await getJankIntervals(
-        this.engine,
+        this.trace.engine,
         this.data.ts,
         this.data.dur,
       );
@@ -312,31 +302,27 @@
 
   private getDelayTable(): m.Child {
     if (this.orderedJankSlices.length > 0) {
-      interface DelayData {
-        jankLink: m.Child;
-        dur: m.Child;
-        delayedVSyncs: number;
-      }
-
-      const columns: ColumnDescriptor<DelayData>[] = [
-        widgetColumn<DelayData>('Cause', (x) => x.jankLink),
-        widgetColumn<DelayData>('Duration', (x) => x.dur),
-        numberColumn<DelayData>('Delayed Vsyncs', (x) => x.delayedVSyncs),
+      const columns: ColumnDescriptor<JankSliceDetails>[] = [
+        widgetColumn<JankSliceDetails>('Cause', (jankSlice) =>
+          renderSliceRef({
+            trace: this.trace,
+            id: jankSlice.id,
+            trackUri: JANKS_TRACK_URI,
+            title: jankSlice.cause,
+          }),
+        ),
+        widgetColumn<JankSliceDetails>('Duration', (jankSlice) =>
+          jankSlice.dur !== undefined
+            ? m(DurationWidget, {dur: jankSlice.dur})
+            : 'NULL',
+        ),
+        widgetColumn<JankSliceDetails>(
+          'Delayed Vsyncs',
+          (jankSlice) => jankSlice.delayVsync,
+        ),
       ];
-      const data: DelayData[] = [];
-      for (const jankSlice of this.orderedJankSlices) {
-        data.push({
-          jankLink: getSliceForTrack(
-            jankSlice.jankSlice,
-            SCROLL_JANK_V3_TRACK_KIND,
-            jankSlice.cause,
-          ),
-          dur: m(DurationWidget, {dur: jankSlice.delayDur}),
-          delayedVSyncs: jankSlice.delayVsync,
-        });
-      }
 
-      const tableData = new TableData(data);
+      const tableData = new TableData(this.orderedJankSlices);
 
       return m(Table, {
         data: tableData,
@@ -360,7 +346,7 @@
                  and not moving and no active scrolling is occurring.`,
       }),
       m(TextParagraph, {
-        text: `Note: Sometimes if a user touches the screen quickly after 
+        text: `Note: Sometimes if a user touches the screen quickly after
                  letting go or Chrome was hung and got into a bad state. A new
                  scroll will start which will result in a slightly overlapping
                  scroll. This can occur due to the last scroll still outputting
@@ -393,8 +379,8 @@
     );
   }
 
-  viewTab() {
-    if (this.isLoading() || this.data == undefined) {
+  render() {
+    if (this.data == undefined) {
       return m('h2', 'Loading');
     }
 
@@ -402,13 +388,13 @@
       'Scroll ID': this.data.id,
       'Start time': m(Timestamp, {ts: this.data.ts}),
       'Duration': m(DurationWidget, {dur: this.data.dur}),
-      'SQL ID': m(SqlRef, {table: 'chrome_scrolls', id: this.config.id}),
+      'SQL ID': m(SqlRef, {table: 'chrome_scrolls', id: this.id}),
     });
 
     return m(
       DetailsShell,
       {
-        title: this.getTitle(),
+        title: 'Scroll',
       },
       m(
         GridLayout,
@@ -439,12 +425,4 @@
       ),
     );
   }
-
-  getTitle(): string {
-    return this.config.title;
-  }
-
-  isLoading() {
-    return !this.loaded;
-  }
 }
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_link_utils.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
index 6b57e46..a804ca9 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
@@ -13,26 +13,20 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {Icons} from '../../base/semantic_icons';
 import {duration, Time, time} from '../../base/time';
 import {exists} from '../../base/utils';
-import {Actions} from '../../common/actions';
-import {globals} from '../../frontend/globals';
-import {
-  focusHorizontalRange,
-  verticalScrollToTrack,
-} from '../../frontend/scroll_helper';
 import {SliceSqlId} from '../../trace_processor/sql_utils/core_types';
 import {Engine} from '../../trace_processor/engine';
 import {LONG, NUM, STR} from '../../trace_processor/query_result';
 import {Anchor} from '../../widgets/anchor';
-
 import {
   CauseProcess,
   CauseThread,
   ScrollJankCauseMap,
 } from './scroll_jank_cause_map';
+import {scrollTo} from '../../public/scroll_helper';
+import {Trace} from '../../public/trace';
 
 const UNKNOWN_NAME = 'Unknown';
 
@@ -178,20 +172,22 @@
 }
 
 export function getCauseLink(
+  trace: Trace,
   threadTracks: EventLatencyCauseThreadTracks,
+  tracksByTrackId: Map<number, string>,
   ts: time | undefined,
   dur: duration | undefined,
 ): m.Child {
-  const trackKeys: string[] = [];
+  const trackUris: string[] = [];
   for (const trackId of threadTracks.trackIds) {
-    const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
-    if (trackKey === undefined) {
+    const track = tracksByTrackId.get(trackId);
+    if (track === undefined) {
       return `Could not locate track ${trackId} for thread ${threadTracks.thread} in the global state`;
     }
-    trackKeys.push(trackKey);
+    trackUris.push(track);
   }
 
-  if (trackKeys.length == 0) {
+  if (trackUris.length == 0) {
     return `No valid tracks for thread ${threadTracks.thread}.`;
   }
 
@@ -204,18 +200,22 @@
       {
         icon: Icons.UpdateSelection,
         onclick: () => {
-          verticalScrollToTrack(trackKeys[0], true);
+          scrollTo({
+            track: {uri: trackUris[0], expandGroup: true},
+          });
           if (exists(ts) && exists(dur)) {
-            focusHorizontalRange(ts, Time.fromRaw(ts + dur), 0.3);
-            globals.timeline.selectArea(ts, Time.fromRaw(ts + dur), trackKeys);
-
-            globals.dispatch(
-              Actions.selectArea({
+            scrollTo({
+              time: {
                 start: ts,
                 end: Time.fromRaw(ts + dur),
-                tracks: trackKeys,
-              }),
-            );
+                viewPercentage: 0.3,
+              },
+            });
+            trace.selection.selectArea({
+              start: ts,
+              end: Time.fromRaw(ts + dur),
+              trackUris,
+            });
           }
         },
       },
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts
deleted file mode 100644
index 34dc713..0000000
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts
+++ /dev/null
@@ -1,215 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {Icons} from '../../base/semantic_icons';
-import {duration, time, Time} from '../../base/time';
-import {Actions} from '../../common/actions';
-import {globals} from '../../frontend/globals';
-import {scrollToTrackAndTs} from '../../frontend/scroll_helper';
-import {SliceSqlId} from '../../trace_processor/sql_utils/core_types';
-import {Engine} from '../../trace_processor/engine';
-import {LONG, NUM} from '../../trace_processor/query_result';
-import {
-  constraintsToQuerySuffix,
-  SQLConstraints,
-} from '../../trace_processor/sql_utils';
-import {Anchor} from '../../widgets/anchor';
-
-import {ScrollJankPluginState, ScrollJankTrackSpec} from './common';
-import {
-  CHROME_EVENT_LATENCY_TRACK_KIND,
-  SCROLL_JANK_V3_TRACK_KIND,
-} from '../../public';
-
-interface BasicSlice {
-  // ID of slice.
-  sliceId: number;
-  // Timestamp of the beginning of this slice in nanoseconds.
-  ts: time;
-  // Duration of this slice in nanoseconds.
-  dur: duration;
-}
-
-async function getSlicesFromTrack(
-  engine: Engine,
-  track: ScrollJankTrackSpec,
-  constraints: SQLConstraints,
-): Promise<BasicSlice[]> {
-  const query = await engine.query(`
-    SELECT
-      id AS sliceId,
-      ts,
-      dur AS dur
-    FROM ${track.sqlTableName}
-    ${constraintsToQuerySuffix(constraints)}`);
-  const it = query.iter({
-    sliceId: NUM,
-    ts: LONG,
-    dur: LONG,
-  });
-
-  const result: BasicSlice[] = [];
-  for (; it.valid(); it.next()) {
-    result.push({
-      sliceId: it.sliceId as number,
-      ts: Time.fromRaw(it.ts),
-      dur: it.dur,
-    });
-  }
-  return result;
-}
-
-export type ScrollJankSlice = BasicSlice;
-export async function getScrollJankSlices(
-  engine: Engine,
-  id: number,
-): Promise<ScrollJankSlice[]> {
-  const track = ScrollJankPluginState.getInstance().getTrack(
-    SCROLL_JANK_V3_TRACK_KIND,
-  );
-  if (track == undefined) {
-    throw new Error(`${SCROLL_JANK_V3_TRACK_KIND} track is not registered.`);
-  }
-
-  const slices = await getSlicesFromTrack(engine, track, {
-    filters: [`event_latency_id=${id}`],
-  });
-  return slices;
-}
-
-export type EventLatencySlice = BasicSlice;
-export async function getEventLatencySlice(
-  engine: Engine,
-  id: number,
-): Promise<EventLatencySlice | undefined> {
-  const track = ScrollJankPluginState.getInstance().getTrack(
-    CHROME_EVENT_LATENCY_TRACK_KIND,
-  );
-  if (track == undefined) {
-    throw new Error(
-      `${CHROME_EVENT_LATENCY_TRACK_KIND} track is not registered.`,
-    );
-  }
-
-  const slices = await getSlicesFromTrack(engine, track, {
-    filters: [`id=${id}`],
-  });
-  return slices[0];
-}
-
-export async function getEventLatencyDescendantSlice(
-  engine: Engine,
-  id: number,
-  descendant: string | undefined,
-): Promise<EventLatencySlice | undefined> {
-  const query = await engine.query(`
-    SELECT
-      id as sliceId,
-      ts,
-      dur as dur
-    FROM descendant_slice(${id})
-    WHERE name='${descendant}'`);
-  const it = query.iter({
-    sliceId: NUM,
-    ts: LONG,
-    dur: LONG,
-  });
-
-  const result: EventLatencySlice[] = [];
-
-  for (; it.valid(); it.next()) {
-    result.push({
-      sliceId: it.sliceId as SliceSqlId,
-      ts: Time.fromRaw(it.ts),
-      dur: it.dur,
-    });
-  }
-
-  const eventLatencyTrack = ScrollJankPluginState.getInstance().getTrack(
-    CHROME_EVENT_LATENCY_TRACK_KIND,
-  );
-  if (eventLatencyTrack == undefined) {
-    throw new Error(
-      `${CHROME_EVENT_LATENCY_TRACK_KIND} track is not registered.`,
-    );
-  }
-
-  if (result.length > 1) {
-    throw new Error(`
-        Slice table and track view ${eventLatencyTrack.sqlTableName} has more than one descendant of slice id ${id} with name ${descendant}`);
-  }
-  if (result.length === 0) {
-    return undefined;
-  }
-  return result[0];
-}
-
-interface BasicScrollJankSliceRefAttrs {
-  id: number;
-  ts: time;
-  dur: duration;
-  name: string;
-  kind: string;
-}
-
-export class ScrollJankSliceRef
-  implements m.ClassComponent<BasicScrollJankSliceRefAttrs>
-{
-  view(vnode: m.Vnode<BasicScrollJankSliceRefAttrs>) {
-    return m(
-      Anchor,
-      {
-        icon: Icons.UpdateSelection,
-        onclick: () => {
-          const track = ScrollJankPluginState.getInstance().getTrack(
-            vnode.attrs.kind,
-          );
-          if (track == undefined) {
-            throw new Error(`${vnode.attrs.kind} track is not registered.`);
-          }
-
-          globals.makeSelection(
-            Actions.selectGenericSlice({
-              id: vnode.attrs.id,
-              sqlTableName: track.sqlTableName,
-              start: vnode.attrs.ts,
-              duration: vnode.attrs.dur,
-              trackKey: track.key,
-              detailsPanelConfig: track.detailsPanelConfig,
-            }),
-          );
-
-          scrollToTrackAndTs(track.key, vnode.attrs.ts, true);
-        },
-      },
-      vnode.attrs.name,
-    );
-  }
-}
-
-export function getSliceForTrack(
-  state: BasicSlice,
-  trackKind: string,
-  name: string,
-): m.Child {
-  return m(ScrollJankSliceRef, {
-    id: state.sliceId,
-    ts: state.ts,
-    dur: state.dur,
-    name: name,
-    kind: trackKind,
-  });
-}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
index d222155..fc51eb4 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
@@ -13,12 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {duration, Time, time} from '../../base/time';
 import {exists} from '../../base/utils';
-import {raf} from '../../core/raf_scheduler';
-import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {getSlice, SliceDetails} from '../../trace_processor/sql_utils/slice';
 import {asSliceSqlId} from '../../trace_processor/sql_utils/core_types';
 import {DurationWidget} from '../../frontend/widgets/duration';
@@ -31,14 +27,9 @@
 import {SqlRef} from '../../widgets/sql_ref';
 import {MultiParagraphText, TextParagraph} from '../../widgets/text_paragraph';
 import {dictToTreeNodes, Tree, TreeNode} from '../../widgets/tree';
-
-import {
-  EventLatencySlice,
-  getEventLatencyDescendantSlice,
-  getEventLatencySlice,
-  getSliceForTrack,
-} from './scroll_jank_slice';
-import {CHROME_EVENT_LATENCY_TRACK_KIND} from '../../public';
+import {EVENT_LATENCY_TRACK_URI, renderSliceRef} from './selection_utils';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
 
 interface Data {
   name: string;
@@ -66,10 +57,8 @@
   return getSlice(engine, asSliceSqlId(id));
 }
 
-export class ScrollJankV3DetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'org.perfetto.ScrollJankV3DetailsPanel';
-  data: Data | undefined;
-  loaded = false;
+export class ScrollJankV3DetailsPanel implements TrackEventDetailsPanel {
+  private data?: Data;
 
   //
   // Linking to associated slices
@@ -82,29 +71,34 @@
 
   // Link to the Event Latency in the EventLatencyTrack (subset of event
   // latencies associated with input events).
-  private eventLatencySliceDetails?: EventLatencySlice;
+  private eventLatencySliceDetails?: {
+    ts: time;
+    dur: duration;
+  };
 
   // Link to the scroll jank cause stage of the associated EventLatencyTrack
   // slice. May be unknown.
-  private causeSliceDetails?: EventLatencySlice;
+  private causeSliceDetails?: {
+    id: number;
+    ts: time;
+    dur: duration;
+  };
 
   // Link to the scroll jank sub-cause stage of the associated EventLatencyTrack
   // slice. Does not apply to all causes.
-  private subcauseSliceDetails?: EventLatencySlice;
+  private subcauseSliceDetails?: {
+    id: number;
+    ts: time;
+    dur: duration;
+  };
 
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): ScrollJankV3DetailsPanel {
-    return new ScrollJankV3DetailsPanel(args);
-  }
+  constructor(
+    private readonly trace: Trace,
+    private readonly id: number,
+  ) {}
 
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-    this.loadData();
-  }
-
-  private async loadData() {
-    const queryResult = await this.engine.query(`
+  async load() {
+    const queryResult = await this.trace.engine.query(`
       SELECT
         IIF(
           cause_of_jank IS NOT NULL,
@@ -119,7 +113,7 @@
         IFNULL(cause_of_jank, "UNKNOWN") AS causeOfJank,
         IFNULL(sub_cause_of_jank, "UNKNOWN") AS subcauseOfJank
       FROM chrome_janky_frame_presentation_intervals
-      WHERE id = ${this.config.id}`);
+      WHERE id = ${this.id}`);
 
     const iter = queryResult.firstRow({
       name: STR,
@@ -145,8 +139,7 @@
     await this.loadJankyFrames();
 
     await this.loadSlices();
-    this.loaded = true;
-    raf.scheduleFullRedraw();
+    this.trace.scheduleFullRedraw();
   }
 
   private hasCause(): boolean {
@@ -166,35 +159,62 @@
   private async loadSlices() {
     if (exists(this.data)) {
       this.sliceDetails = await getSliceDetails(
-        this.engine,
+        this.trace.engine,
         this.data.eventLatencyId,
       );
-      this.eventLatencySliceDetails = await getEventLatencySlice(
-        this.engine,
-        this.data.eventLatencyId,
-      );
+      const it = (
+        await this.trace.engine.query(`
+        SELECT ts, dur
+        FROM slice
+        WHERE id = ${this.data.eventLatencyId}
+      `)
+      ).iter({ts: LONG, dur: LONG});
+      this.eventLatencySliceDetails = {
+        ts: Time.fromRaw(it.ts),
+        dur: it.dur,
+      };
 
       if (this.hasCause()) {
-        this.causeSliceDetails = await getEventLatencyDescendantSlice(
-          this.engine,
-          this.data.eventLatencyId,
-          this.data.jankCause,
-        );
+        const it = (
+          await this.trace.engine.query(`
+          SELECT id, ts, dur
+          FROM descendant_slice(${this.data.eventLatencyId})
+          WHERE name = "${this.data.jankCause}"
+        `)
+        ).iter({id: NUM, ts: LONG, dur: LONG});
+
+        if (it.valid()) {
+          this.causeSliceDetails = {
+            id: it.id,
+            ts: Time.fromRaw(it.ts),
+            dur: it.dur,
+          };
+        }
       }
 
       if (this.hasSubcause()) {
-        this.subcauseSliceDetails = await getEventLatencyDescendantSlice(
-          this.engine,
-          this.data.eventLatencyId,
-          this.data.jankSubcause,
-        );
+        const it = (
+          await this.trace.engine.query(`
+          SELECT id, ts, dur
+          FROM descendant_slice(${this.data.eventLatencyId})
+          WHERE name = "${this.data.jankSubcause}"
+        `)
+        ).iter({id: NUM, ts: LONG, dur: LONG});
+
+        if (it.valid()) {
+          this.subcauseSliceDetails = {
+            id: it.id,
+            ts: Time.fromRaw(it.ts),
+            dur: it.dur,
+          };
+        }
       }
     }
   }
 
   private async loadJankyFrames() {
     if (exists(this.data)) {
-      const queryResult = await this.engine.query(`
+      const queryResult = await this.trace.engine.query(`
         SELECT
           COUNT(*) AS jankyFrames
         FROM chrome_frame_info_with_delay
@@ -265,33 +285,36 @@
     const result: {[key: string]: m.Child} = {};
 
     if (exists(this.sliceDetails) && exists(this.data)) {
-      result['Janked Event Latency stage'] = exists(this.causeSliceDetails)
-        ? getSliceForTrack(
-            this.causeSliceDetails,
-            CHROME_EVENT_LATENCY_TRACK_KIND,
-            this.data.jankCause,
-          )
-        : this.data.jankCause;
+      result['Janked Event Latency stage'] =
+        exists(this.causeSliceDetails) &&
+        renderSliceRef({
+          trace: this.trace,
+          id: this.causeSliceDetails.id,
+          trackUri: EVENT_LATENCY_TRACK_URI,
+          title: this.data.jankCause,
+        });
 
       if (this.hasSubcause()) {
-        result['Sub-cause of Jank'] = exists(this.subcauseSliceDetails)
-          ? getSliceForTrack(
-              this.subcauseSliceDetails,
-              CHROME_EVENT_LATENCY_TRACK_KIND,
-              this.data.jankSubcause,
-            )
-          : this.data.jankSubcause;
+        result['Sub-cause of Jank'] =
+          exists(this.subcauseSliceDetails) &&
+          renderSliceRef({
+            trace: this.trace,
+            id: this.subcauseSliceDetails.id,
+            trackUri: EVENT_LATENCY_TRACK_URI,
+            title: this.data.jankCause,
+          });
       }
 
       const children = dictToTreeNodes(result);
       if (exists(this.eventLatencySliceDetails)) {
         children.unshift(
           m(TreeNode, {
-            left: getSliceForTrack(
-              this.eventLatencySliceDetails,
-              CHROME_EVENT_LATENCY_TRACK_KIND,
-              'Input EventLatency in context of ScrollUpdates',
-            ),
+            left: renderSliceRef({
+              trace: this.trace,
+              id: this.data.eventLatencyId,
+              trackUri: EVENT_LATENCY_TRACK_URI,
+              title: this.data.jankCause,
+            }),
             right: '',
           }),
         );
@@ -305,7 +328,7 @@
     return dictToTreeNodes(result);
   }
 
-  viewTab() {
+  render() {
     if (this.data === undefined) {
       return m('h2', 'Loading');
     }
@@ -315,7 +338,7 @@
     return m(
       DetailsShell,
       {
-        title: this.getTitle(),
+        title: 'EventLatency',
       },
       m(
         GridLayout,
@@ -328,12 +351,4 @@
       ),
     );
   }
-
-  getTitle(): string {
-    return this.config.title;
-  }
-
-  isLoading() {
-    return !this.loaded;
-  }
 }
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
index 65c5ade..4683dfa 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
@@ -12,36 +12,21 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {globals} from '../../frontend/globals';
 import {NamedRow} from '../../frontend/named_slice_track';
-import {NewTrackArgs} from '../../frontend/track';
-import {SCROLL_JANK_V3_TRACK_KIND, Slice} from '../../public';
+import {Slice} from '../../public/track';
 import {
-  CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../../frontend/tracks/custom_sql_table_slice_track';
-
 import {JANK_COLOR} from './jank_colors';
+import {getColorForSlice} from '../../public/lib/colorizer';
+import {TrackEventSelection} from '../../public/selection';
 import {ScrollJankV3DetailsPanel} from './scroll_jank_v3_details_panel';
-import {getColorForSlice} from '../../core/colorizer';
-import {getLegacySelection} from '../../common/state';
-import {ScrollJankPluginState} from './common';
 
 const UNKNOWN_SLICE_NAME = 'Unknown';
 const JANK_SLICE_NAME = ' Jank';
 
 export class ScrollJankV3Track extends CustomSqlTableSliceTrack {
-  constructor(args: NewTrackArgs) {
-    super(args);
-    ScrollJankPluginState.getInstance().registerTrack({
-      kind: SCROLL_JANK_V3_TRACK_KIND,
-      trackKey: this.trackKey,
-      tableName: this.tableName,
-      detailsPanelConfig: this.getDetailsPanel(),
-    });
-  }
-
   getSqlDataSource(): CustomSqlTableDefConfig {
     return {
       columns: [
@@ -59,23 +44,6 @@
     };
   }
 
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    return {
-      kind: ScrollJankV3DetailsPanel.kind,
-      config: {
-        sqlTableName: 'chrome_janky_frame_presentation_intervals',
-        title: 'Chrome Scroll Janks',
-      },
-    };
-  }
-
-  async onDestroy(): Promise<void> {
-    await super.onDestroy();
-    ScrollJankPluginState.getInstance().unregisterTrack(
-      SCROLL_JANK_V3_TRACK_KIND,
-    );
-  }
-
   rowToSlice(row: NamedRow): Slice {
     const slice = super.rowToSlice(row);
 
@@ -94,19 +62,7 @@
     }
   }
 
-  onUpdatedSlices(slices: Slice[]) {
-    for (const slice of slices) {
-      const currentSelection = getLegacySelection(globals.state);
-      const isSelected =
-        currentSelection &&
-        currentSelection.kind === 'GENERIC_SLICE' &&
-        currentSelection.id !== undefined &&
-        currentSelection.id === slice.id;
-
-      const highlighted = globals.state.highlightedSliceId === slice.id;
-      const hasFocus = highlighted || isSelected;
-      slice.isHighlighted = !!hasFocus;
-    }
-    super.onUpdatedSlices(slices);
+  override detailsPanel(sel: TrackEventSelection) {
+    return new ScrollJankV3DetailsPanel(this.trace, sel.eventId);
   }
 }
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
index 5819968..3721123 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
@@ -12,20 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {NewTrackArgs} from '../../frontend/track';
-import {CHROME_TOPLEVEL_SCROLLS_KIND} from '../../public';
 import {
-  CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../../frontend/tracks/custom_sql_table_slice_track';
-import {ScrollJankPluginState} from './common';
-
+import {TrackEventSelection} from '../../public/selection';
 import {ScrollDetailsPanel} from './scroll_details_panel';
 
 export class TopLevelScrollTrack extends CustomSqlTableSliceTrack {
-  public static kind = CHROME_TOPLEVEL_SCROLLS_KIND;
-
   getSqlDataSource(): CustomSqlTableDefConfig {
     return {
       columns: [`printf("Scroll %s", CAST(id AS STRING)) AS name`, '*'],
@@ -33,31 +27,7 @@
     };
   }
 
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    return {
-      kind: ScrollDetailsPanel.kind,
-      config: {
-        sqlTableName: this.tableName,
-        title: 'Chrome Top Level Scrolls',
-      },
-    };
-  }
-
-  constructor(args: NewTrackArgs) {
-    super(args);
-
-    ScrollJankPluginState.getInstance().registerTrack({
-      kind: TopLevelScrollTrack.kind,
-      trackKey: this.trackKey,
-      tableName: this.tableName,
-      detailsPanelConfig: this.getDetailsPanel(),
-    });
-  }
-
-  async onDestroy(): Promise<void> {
-    await super.onDestroy();
-    ScrollJankPluginState.getInstance().unregisterTrack(
-      TopLevelScrollTrack.kind,
-    );
+  override detailsPanel(sel: TrackEventSelection) {
+    return new ScrollDetailsPanel(this.trace, sel.eventId);
   }
 }
diff --git a/ui/src/core_plugins/chrome_scroll_jank/selection_utils.ts b/ui/src/core_plugins/chrome_scroll_jank/selection_utils.ts
new file mode 100644
index 0000000..4b79e05
--- /dev/null
+++ b/ui/src/core_plugins/chrome_scroll_jank/selection_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.
+
+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/core_plugins/chrome_tasks/details.ts b/ui/src/core_plugins/chrome_tasks/details.ts
deleted file mode 100644
index a7e2a95..0000000
--- a/ui/src/core_plugins/chrome_tasks/details.ts
+++ /dev/null
@@ -1,70 +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 {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {DetailsShell} from '../../widgets/details_shell';
-import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
-import {
-  Details,
-  DetailsSchema,
-} from '../../frontend/widgets/sql/details/details';
-import {wellKnownTypes} from '../../frontend/widgets/sql/details/well_known_types';
-
-import d = DetailsSchema;
-
-export class ChromeTasksDetailsTab extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'org.chromium.ChromeTasks.TaskDetailsTab';
-
-  private data: Details;
-
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-
-    this.data = new Details(
-      this.engine,
-      'chrome_tasks',
-      this.config.id,
-      {
-        'Task name': 'name',
-        'Start time': d.Timestamp('ts'),
-        'Duration': d.Interval('ts', 'dur'),
-        'Process': d.SqlIdRef('process', 'upid'),
-        'Thread': d.SqlIdRef('thread', 'utid'),
-        'Slice': d.SqlIdRef('slice', 'id'),
-      },
-      wellKnownTypes,
-    );
-  }
-
-  viewTab() {
-    return m(
-      DetailsShell,
-      {
-        title: this.getTitle(),
-      },
-      m(GridLayout, m(GridLayoutColumn, this.data.render())),
-    );
-  }
-
-  getTitle(): string {
-    return this.config.title;
-  }
-
-  isLoading() {
-    return this.data.isLoading();
-  }
-}
diff --git a/ui/src/core_plugins/chrome_tasks/index.ts b/ui/src/core_plugins/chrome_tasks/index.ts
deleted file mode 100644
index 4024c98..0000000
--- a/ui/src/core_plugins/chrome_tasks/index.ts
+++ /dev/null
@@ -1,138 +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 {uuidv4} from '../../base/uuid';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {addSqlTableTab} from '../../frontend/sql_table_tab';
-import {asUtid} from '../../trace_processor/sql_utils/core_types';
-import {
-  BottomTabToSCSAdapter,
-  NUM,
-  NUM_NULL,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  STR_NULL,
-} from '../../public';
-
-import {ChromeTasksDetailsTab} from './details';
-import {chromeTasksTable} from './table';
-import {ChromeTasksThreadTrack} from './track';
-
-class ChromeTasksPlugin implements Plugin {
-  onActivate() {}
-
-  async onTraceLoad(ctx: PluginContextTrace) {
-    await this.createTracks(ctx);
-
-    ctx.registerCommand({
-      id: 'org.chromium.ChromeTasks.ShowChromeTasksTable',
-      name: 'Show chrome_tasks table',
-      callback: () =>
-        addSqlTableTab({
-          table: chromeTasksTable,
-        }),
-    });
-  }
-
-  async createTracks(ctx: PluginContextTrace) {
-    const it = (
-      await ctx.engine.query(`
-      INCLUDE PERFETTO MODULE chrome.tasks;
-
-      with relevant_threads as (
-        select distinct utid from chrome_tasks
-      )
-      select
-        (CASE process.name
-          WHEN 'Browser' THEN 1
-          WHEN 'Gpu' THEN 2
-          WHEN 'Renderer' THEN 4
-          ELSE 3
-        END) as processRank,
-        process.name as processName,
-        process.pid,
-        process.upid,
-        (CASE thread.name
-          WHEN 'CrBrowserMain' THEN 1
-          WHEN 'CrRendererMain' THEN 1
-          WHEN 'CrGpuMain' THEN 1
-          WHEN 'Chrome_IOThread' THEN 2
-          WHEN 'Chrome_ChildIOThread' THEN 2
-          WHEN 'VizCompositorThread' THEN 3
-          WHEN 'NetworkService' THEN 3
-          WHEN 'Compositor' THEN 3
-          WHEN 'CompositorGpuThread' THEN 4
-          WHEN 'CompositorTileWorker&' THEN 5
-          WHEN 'ThreadPoolService' THEN 6
-          WHEN 'ThreadPoolSingleThreadForegroundBlocking&' THEN 6
-          WHEN 'ThreadPoolForegroundWorker' THEN 6
-          ELSE 7
-         END) as threadRank,
-         thread.name as threadName,
-         thread.tid,
-         thread.utid
-      from relevant_threads
-      join thread using (utid)
-      join process using (upid)
-      order by processRank, upid, threadRank, utid
-    `)
-    ).iter({
-      processRank: NUM,
-      processName: STR_NULL,
-      pid: NUM_NULL,
-      upid: NUM,
-      threadRank: NUM,
-      threadName: STR_NULL,
-      tid: NUM_NULL,
-      utid: NUM,
-    });
-
-    for (; it.valid(); it.next()) {
-      const utid = it.utid;
-      const uri = `org.chromium.ChromeTasks#thread.${utid}`;
-      ctx.registerStaticTrack({
-        uri,
-        trackFactory: ({trackKey}) =>
-          new ChromeTasksThreadTrack(ctx.engine, trackKey, asUtid(utid)),
-        groupName: `Chrome Tasks`,
-        title: `${it.threadName} ${it.tid}`,
-      });
-    }
-
-    ctx.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind === ChromeTasksDetailsTab.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new ChromeTasksDetailsTab({
-              config: config as GenericSliceDetailsTabConfig,
-              engine: ctx.engine,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'org.chromium.ChromeTasks',
-  plugin: ChromeTasksPlugin,
-};
diff --git a/ui/src/core_plugins/chrome_tasks/table.ts b/ui/src/core_plugins/chrome_tasks/table.ts
deleted file mode 100644
index 9547d09..0000000
--- a/ui/src/core_plugins/chrome_tasks/table.ts
+++ /dev/null
@@ -1,92 +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 {SqlTableDescription} from '../../frontend/widgets/sql/table/table_description';
-
-export const chromeTasksTable: SqlTableDescription = {
-  imports: ['chrome.tasks'],
-  name: 'chrome_tasks',
-  columns: [
-    {
-      name: 'id',
-      title: 'ID',
-      display: {
-        type: 'slice_id',
-        ts: 'ts',
-        dur: 'dur',
-        trackId: 'track_id',
-      },
-    },
-    {
-      name: 'ts',
-      title: 'Timestamp',
-      display: {
-        type: 'timestamp',
-      },
-    },
-    {
-      name: 'dur',
-      title: 'Duration',
-      display: {
-        type: 'duration',
-      },
-    },
-    {
-      name: 'thread_dur',
-      title: 'Thread duration',
-      display: {
-        type: 'thread_duration',
-      },
-    },
-    {
-      name: 'category',
-      title: 'Category',
-    },
-    {
-      name: 'name',
-      title: 'Name',
-    },
-    {
-      name: 'track_id',
-      title: 'Track ID',
-      startsHidden: true,
-    },
-    {
-      name: 'track_name',
-      title: 'Track name',
-      startsHidden: true,
-    },
-    {
-      name: 'thread_name',
-      title: 'Thread name',
-    },
-    {
-      name: 'utid',
-      startsHidden: true,
-    },
-    {
-      name: 'process_name',
-      title: 'Process name',
-    },
-    {
-      name: 'upid',
-      startsHidden: true,
-    },
-    {
-      name: 'arg_set_id',
-      title: 'Arg',
-      type: 'arg_set_id',
-    },
-  ],
-};
diff --git a/ui/src/core_plugins/chrome_tasks/track.ts b/ui/src/core_plugins/chrome_tasks/track.ts
deleted file mode 100644
index 6e25b03..0000000
--- a/ui/src/core_plugins/chrome_tasks/track.ts
+++ /dev/null
@@ -1,51 +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 {Utid} from '../../trace_processor/sql_utils/core_types';
-import {
-  CustomSqlDetailsPanelConfig,
-  CustomSqlTableDefConfig,
-  CustomSqlTableSliceTrack,
-} from '../../frontend/tracks/custom_sql_table_slice_track';
-import {Engine} from '../../public';
-
-import {ChromeTasksDetailsTab} from './details';
-
-export class ChromeTasksThreadTrack extends CustomSqlTableSliceTrack {
-  constructor(
-    engine: Engine,
-    trackKey: string,
-    private utid: Utid,
-  ) {
-    super({engine, trackKey});
-  }
-
-  getSqlDataSource(): CustomSqlTableDefConfig {
-    return {
-      columns: ['name', 'id', 'ts', 'dur'],
-      sqlTableName: 'chrome_tasks',
-      whereClause: `utid = ${this.utid}`,
-    };
-  }
-
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    return {
-      kind: ChromeTasksDetailsTab.kind,
-      config: {
-        sqlTableName: 'chrome_tasks',
-        title: 'Chrome Tasks',
-      },
-    };
-  }
-}
diff --git a/ui/src/core_plugins/commands/index.ts b/ui/src/core_plugins/commands/index.ts
index c87284f..b937daa 100644
--- a/ui/src/core_plugins/commands/index.ts
+++ b/ui/src/core_plugins/commands/index.ts
@@ -14,20 +14,16 @@
 
 import {Time, time} from '../../base/time';
 import {exists} from '../../base/utils';
-import {Actions} from '../../common/actions';
-import {globals} from '../../frontend/globals';
 import {openInOldUIWithSizeCheck} from '../../frontend/legacy_trace_viewer';
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {Trace} from '../../public/trace';
+import {App} from '../../public/app';
+import {PerfettoPlugin} from '../../public/plugin';
 import {
   isLegacyTrace,
   openFileWithLegacyTraceViewer,
 } from '../../frontend/legacy_trace_viewer';
-import {DisposableStack} from '../../base/disposable_stack';
+import {AppImpl} from '../../core/app_impl';
+import {addQueryResultsTab} from '../../public/lib/query_table/query_result_tab';
 
 const SQL_STATS = `
 with first as (select started as ts from sqlstats limit 1)
@@ -95,30 +91,19 @@
 order by total_self_size desc
 limit 100;`;
 
-class CoreCommandsPlugin implements Plugin {
-  private readonly disposable = new DisposableStack();
-
-  onActivate(ctx: PluginContext) {
-    ctx.registerCommand({
-      id: 'perfetto.CoreCommands#ToggleLeftSidebar',
-      name: 'Toggle left sidebar',
-      callback: () => {
-        if (globals.state.sidebarVisible) {
-          globals.dispatch(
-            Actions.setSidebar({
-              visible: false,
-            }),
-          );
-        } else {
-          globals.dispatch(
-            Actions.setSidebar({
-              visible: true,
-            }),
-          );
-        }
-      },
-      defaultHotkey: '!Mod+B',
-    });
+export default class implements PerfettoPlugin {
+  static readonly id = 'perfetto.CoreCommands';
+  static onActivate(ctx: App) {
+    if (ctx.sidebar.enabled) {
+      ctx.commands.registerCommand({
+        id: 'perfetto.CoreCommands#ToggleLeftSidebar',
+        name: 'Toggle left sidebar',
+        callback: () => {
+          ctx.sidebar.toggleVisibility();
+        },
+        defaultHotkey: '!Mod+B',
+      });
+    }
 
     const input = document.createElement('input');
     input.classList.add('trace_file');
@@ -126,12 +111,9 @@
     input.style.display = 'none';
     input.addEventListener('change', onInputElementFileSelectionChanged);
     document.body.appendChild(input);
-    this.disposable.defer(() => {
-      document.body.removeChild(input);
-    });
 
     const OPEN_TRACE_COMMAND_ID = 'perfetto.CoreCommands#openTrace';
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: OPEN_TRACE_COMMAND_ID,
       name: 'Open trace file',
       callback: () => {
@@ -140,118 +122,121 @@
       },
       defaultHotkey: '!Mod+O',
     });
-    ctx.addSidebarMenuItem({
+    ctx.sidebar.addMenuItem({
       commandId: OPEN_TRACE_COMMAND_ID,
-      group: 'navigation',
+      section: 'navigation',
       icon: 'folder_open',
     });
 
-    const OPEN_LEGACY_TRACE_COMMAND_ID =
-      'perfetto.CoreCommands#openTraceInLegacyUi';
-    ctx.registerCommand({
-      id: OPEN_LEGACY_TRACE_COMMAND_ID,
+    const OPEN_LEGACY_COMMAND_ID = 'perfetto.CoreCommands#openTraceInLegacyUi';
+    ctx.commands.registerCommand({
+      id: OPEN_LEGACY_COMMAND_ID,
       name: 'Open with legacy UI',
       callback: () => {
         input.dataset['useCatapultLegacyUi'] = '1';
         input.click();
       },
     });
-    ctx.addSidebarMenuItem({
-      commandId: OPEN_LEGACY_TRACE_COMMAND_ID,
-      group: 'navigation',
+    ctx.sidebar.addMenuItem({
+      commandId: OPEN_LEGACY_COMMAND_ID,
+      section: 'navigation',
       icon: 'filter_none',
     });
   }
 
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerCommand({
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#RunQueryAllProcesses',
       name: 'Run query: All processes',
       callback: () => {
-        ctx.tabs.openQuery(ALL_PROCESSES_QUERY, 'All Processes');
+        addQueryResultsTab(ctx, {
+          query: ALL_PROCESSES_QUERY,
+          title: 'All Processes',
+        });
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#RunQueryCpuTimeByProcess',
       name: 'Run query: CPU time by process',
       callback: () => {
-        ctx.tabs.openQuery(CPU_TIME_FOR_PROCESSES, 'CPU time by process');
+        addQueryResultsTab(ctx, {
+          query: CPU_TIME_FOR_PROCESSES,
+          title: 'CPU time by process',
+        });
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#RunQueryCyclesByStateByCpu',
       name: 'Run query: cycles by p-state by CPU',
       callback: () => {
-        ctx.tabs.openQuery(
-          CYCLES_PER_P_STATE_PER_CPU,
-          'Cycles by p-state by CPU',
-        );
+        addQueryResultsTab(ctx, {
+          query: CYCLES_PER_P_STATE_PER_CPU,
+          title: 'Cycles by p-state by CPU',
+        });
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#RunQueryCyclesByCpuByProcess',
       name: 'Run query: CPU Time by CPU by process',
       callback: () => {
-        ctx.tabs.openQuery(
-          CPU_TIME_BY_CPU_BY_PROCESS,
-          'CPU time by CPU by process',
-        );
+        addQueryResultsTab(ctx, {
+          query: CPU_TIME_BY_CPU_BY_PROCESS,
+          title: 'CPU time by CPU by process',
+        });
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#RunQueryHeapGraphBytesPerType',
       name: 'Run query: heap graph bytes per type',
       callback: () => {
-        ctx.tabs.openQuery(
-          HEAP_GRAPH_BYTES_PER_TYPE,
-          'Heap graph bytes per type',
-        );
+        addQueryResultsTab(ctx, {
+          query: HEAP_GRAPH_BYTES_PER_TYPE,
+          title: 'Heap graph bytes per type',
+        });
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#DebugSqlPerformance',
       name: 'Debug SQL performance',
       callback: () => {
-        ctx.tabs.openQuery(SQL_STATS, 'Recent SQL queries');
+        addQueryResultsTab(ctx, {
+          query: SQL_STATS,
+          title: 'Recent SQL queries',
+        });
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#UnpinAllTracks',
       name: 'Unpin all pinned tracks',
       callback: () => {
-        ctx.timeline.unpinTracksByPredicate((_) => {
-          return true;
-        });
+        const workspace = ctx.workspace;
+        workspace.pinnedTracks.forEach((t) => workspace.unpinTrack(t));
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#ExpandAllGroups',
       name: 'Expand all track groups',
       callback: () => {
-        ctx.timeline.expandGroupsByPredicate((_) => {
-          return true;
-        });
+        ctx.workspace.flatTracks.forEach((track) => track.expand());
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#CollapseAllGroups',
       name: 'Collapse all track groups',
       callback: () => {
-        ctx.timeline.collapseGroupsByPredicate((_) => {
-          return true;
-        });
+        ctx.workspace.flatTracks.forEach((track) => track.collapse());
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#PanToTimestamp',
       name: 'Pan to timestamp',
       callback: (tsRaw: unknown) => {
@@ -270,17 +255,46 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#ShowCurrentSelectionTab',
       name: 'Show current selection tab',
       callback: () => {
         ctx.tabs.showTab('current_selection');
       },
     });
-  }
 
-  onDeactivate(_: PluginContext): void {
-    this.disposable[Symbol.dispose]();
+    ctx.commands.registerCommand({
+      id: 'createNewEmptyWorkspace',
+      name: 'Create new empty workspace',
+      callback: async () => {
+        const workspaces = AppImpl.instance.trace?.workspaces;
+        if (workspaces === undefined) return; // No trace loaded.
+        const name = await ctx.omnibox.prompt('Give it a name...');
+        if (name === undefined || name === '') return;
+        workspaces.switchWorkspace(workspaces.createEmptyWorkspace(name));
+      },
+    });
+
+    ctx.commands.registerCommand({
+      id: 'switchWorkspace',
+      name: 'Switch workspace',
+      callback: async () => {
+        const workspaces = AppImpl.instance.trace?.workspaces;
+        if (workspaces === undefined) return; // No trace loaded.
+        const options = workspaces.all.map((ws) => {
+          return {key: ws.id, displayName: ws.title};
+        });
+        const workspaceId = await ctx.omnibox.prompt(
+          'Choose a workspace...',
+          options,
+        );
+        if (workspaceId === undefined) return;
+        const workspace = workspaces.all.find((ws) => ws.id === workspaceId);
+        if (workspace) {
+          workspaces.switchWorkspace(workspace);
+        }
+      },
+    });
   }
 }
 
@@ -310,21 +324,18 @@
     return;
   }
 
-  globals.logging.logEvent('Trace Actions', 'Open trace from file');
-  globals.dispatch(Actions.openTraceFromFile({file}));
+  AppImpl.instance.analytics.logEvent('Trace Actions', 'Open trace from file');
+  AppImpl.instance.openTraceFromFile(file);
 }
 
 async function openWithLegacyUi(file: File) {
   // Switch back to the old catapult UI.
-  globals.logging.logEvent('Trace Actions', 'Open trace in Legacy UI');
+  AppImpl.instance.analytics.logEvent(
+    'Trace Actions',
+    'Open trace in Legacy UI',
+  );
   if (await isLegacyTrace(file)) {
-    openFileWithLegacyTraceViewer(file);
-    return;
+    return await openFileWithLegacyTraceViewer(file);
   }
-  openInOldUIWithSizeCheck(file);
+  return await openInOldUIWithSizeCheck(file);
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.CoreCommands',
-  plugin: CoreCommandsPlugin,
-};
diff --git a/ui/src/core_plugins/counter/counter_details_panel.ts b/ui/src/core_plugins/counter/counter_details_panel.ts
deleted file mode 100644
index 0ee28bd..0000000
--- a/ui/src/core_plugins/counter/counter_details_panel.ts
+++ /dev/null
@@ -1,165 +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 {AsyncLimiter} from '../../base/async_limiter';
-import {Time, duration, time} from '../../base/time';
-import {raf} from '../../core/raf_scheduler';
-import {
-  Engine,
-  LONG,
-  LONG_NULL,
-  NUM,
-  NUM_NULL,
-  TrackSelectionDetailsPanel,
-} from '../../public';
-import m from 'mithril';
-import {DetailsShell} from '../../widgets/details_shell';
-import {GridLayout} from '../../widgets/grid_layout';
-import {Section} from '../../widgets/section';
-import {Tree, TreeNode} from '../../widgets/tree';
-import {Timestamp} from '../../frontend/widgets/timestamp';
-import {DurationWidget} from '../../frontend/widgets/duration';
-
-interface CounterDetails {
-  // The "left" timestamp of the counter sample T(N)
-  ts: time;
-
-  // The delta between this sample and the next one's timestamps T(N+1) - T(N)
-  duration: duration;
-
-  // The value of the counter sample F(N)
-  value: number;
-
-  // The delta between this sample's value and the previous one F(N) - F(N-1)
-  delta: number;
-}
-
-export class CounterDetailsPanel implements TrackSelectionDetailsPanel {
-  private readonly queryLimiter = new AsyncLimiter();
-  private readonly engine: Engine;
-  private readonly trackId: number;
-  private readonly rootTable: string;
-  private readonly trackName: string;
-  private id?: number;
-  private counterDetails?: CounterDetails;
-
-  constructor(
-    engine: Engine,
-    trackId: number,
-    trackName: string,
-    rootTable = 'counter',
-  ) {
-    this.engine = engine;
-    this.trackId = trackId;
-    this.trackName = trackName;
-    this.rootTable = rootTable;
-  }
-
-  render(id: number): m.Children {
-    if (id !== this.id) {
-      this.id = id;
-      this.queryLimiter.schedule(async () => {
-        this.counterDetails = await loadCounterDetails(
-          this.engine,
-          this.trackId,
-          id,
-          this.rootTable,
-        );
-        raf.scheduleFullRedraw();
-      });
-    }
-
-    return this.renderView();
-  }
-
-  private renderView() {
-    const counterInfo = this.counterDetails;
-    if (counterInfo) {
-      return m(
-        DetailsShell,
-        {title: 'Counter', description: `${this.trackName}`},
-        m(
-          GridLayout,
-          m(
-            Section,
-            {title: 'Properties'},
-            m(
-              Tree,
-              m(TreeNode, {left: 'Name', right: `${this.trackName}`}),
-              m(TreeNode, {
-                left: 'Start time',
-                right: m(Timestamp, {ts: counterInfo.ts}),
-              }),
-              m(TreeNode, {
-                left: 'Value',
-                right: `${counterInfo.value.toLocaleString()}`,
-              }),
-              m(TreeNode, {
-                left: 'Delta',
-                right: `${counterInfo.delta.toLocaleString()}`,
-              }),
-              m(TreeNode, {
-                left: 'Duration',
-                right: m(DurationWidget, {dur: counterInfo.duration}),
-              }),
-            ),
-          ),
-        ),
-      );
-    } else {
-      return m(DetailsShell, {title: 'Counter', description: 'Loading...'});
-    }
-  }
-
-  isLoading(): boolean {
-    return this.counterDetails === undefined;
-  }
-}
-
-async function loadCounterDetails(
-  engine: Engine,
-  trackId: number,
-  id: number,
-  rootTable: string,
-): Promise<CounterDetails> {
-  const query = `
-    WITH CTE AS (
-      SELECT
-        id,
-        ts as leftTs,
-        value,
-        LAG(value) OVER (ORDER BY ts) AS prevValue,
-        LEAD(ts) OVER (ORDER BY ts) AS rightTs
-      FROM ${rootTable}
-      WHERE track_id = ${trackId}
-    )
-    SELECT * FROM CTE WHERE id = ${id}
-  `;
-
-  const counter = await engine.query(query);
-  const row = counter.iter({
-    value: NUM,
-    prevValue: NUM_NULL,
-    leftTs: LONG,
-    rightTs: LONG_NULL,
-  });
-  const value = row.value;
-  const leftTs = Time.fromRaw(row.leftTs);
-  const rightTs = row.rightTs !== null ? Time.fromRaw(row.rightTs) : leftTs;
-  const prevValue = row.prevValue !== null ? row.prevValue : value;
-
-  const delta = value - prevValue;
-  const duration = rightTs - leftTs;
-  return {ts: leftTs, value, delta, duration};
-}
diff --git a/ui/src/core_plugins/counter/index.ts b/ui/src/core_plugins/counter/index.ts
deleted file mode 100644
index 8164043..0000000
--- a/ui/src/core_plugins/counter/index.ts
+++ /dev/null
@@ -1,448 +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,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  PrimaryTrackSortKey,
-  STR,
-  LONG,
-  Engine,
-  COUNTER_TRACK_KIND,
-} from '../../public';
-import {getThreadUriPrefix, getTrackName} from '../../public/utils';
-import {CounterOptions} from '../../frontend/base_counter_track';
-import {TraceProcessorCounterTrack} from './trace_processor_counter_track';
-import {CounterDetailsPanel} from './counter_details_panel';
-import {Time, duration, time} from '../../base/time';
-import {exists, Optional} from '../../base/utils';
-
-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';
-  // }
-
-  if (name.startsWith('battery_stats.')) {
-    options.yRangeSharingKey = 'battery_stats';
-  }
-
-  // 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;
-}
-
-async function getCounterEventBounds(
-  engine: Engine,
-  trackId: number,
-  id: number,
-): Promise<Optional<{ts: time; dur: duration}>> {
-  const query = `
-    WITH CTE AS (
-      SELECT
-        id,
-        ts as leftTs,
-        LEAD(ts) OVER (ORDER BY ts) AS rightTs
-      FROM counter
-      WHERE track_id = ${trackId}
-    )
-    SELECT * FROM CTE WHERE id = ${id}
-  `;
-
-  const counter = await engine.query(query);
-  const row = counter.iter({
-    leftTs: LONG,
-    rightTs: LONG_NULL,
-  });
-  const leftTs = Time.fromRaw(row.leftTs);
-  const rightTs = row.rightTs !== null ? Time.fromRaw(row.rightTs) : leftTs;
-  const duration = rightTs - leftTs;
-  return {ts: leftTs, dur: duration};
-}
-
-class CounterPlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    await this.addCounterTracks(ctx);
-    await this.addGpuFrequencyTracks(ctx);
-    await this.addCpuFreqLimitCounterTracks(ctx);
-    await this.addCpuPerfCounterTracks(ctx);
-    await this.addThreadCounterTracks(ctx);
-    await this.addProcessCounterTracks(ctx);
-  }
-
-  private async addCounterTracks(ctx: PluginContextTrace) {
-    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 type = 'counter_track'
-        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 displayName = it.name;
-      const unit = it.unit ?? undefined;
-      ctx.registerStaticTrack({
-        uri: `/counter_${trackId}`,
-        title: displayName,
-        tags: {
-          kind: COUNTER_TRACK_KIND,
-          trackIds: [trackId],
-        },
-        trackFactory: (trackCtx) => {
-          return new TraceProcessorCounterTrack({
-            engine: ctx.engine,
-            trackKey: trackCtx.trackKey,
-            trackId,
-            options: {
-              ...getDefaultCounterOptions(displayName),
-              unit,
-            },
-          });
-        },
-        sortKey: PrimaryTrackSortKey.COUNTER_TRACK,
-        detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, displayName),
-        getEventBounds: async (id) => {
-          return await getCounterEventBounds(ctx.engine, trackId, id);
-        },
-      });
-    }
-  }
-
-  async addCpuFreqLimitCounterTracks(ctx: PluginContextTrace): 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 addCpuPerfCounterTracks(ctx: PluginContextTrace): 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: PluginContextTrace,
-    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;
-      ctx.registerTrack({
-        uri: `counter.cpu.${trackId}`,
-        title: name,
-        tags: {
-          kind: COUNTER_TRACK_KIND,
-          trackIds: [trackId],
-          scope,
-        },
-        trackFactory: (trackCtx) => {
-          return new TraceProcessorCounterTrack({
-            engine: ctx.engine,
-            trackKey: trackCtx.trackKey,
-            trackId: trackId,
-            options: getDefaultCounterOptions(name),
-          });
-        },
-        detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name),
-        getEventBounds: async (id) => {
-          return await getCounterEventBounds(ctx.engine, trackId, id);
-        },
-      });
-    }
-  }
-
-  async addThreadCounterTracks(ctx: PluginContextTrace): 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,
-      });
-      ctx.registerTrack({
-        uri: `${getThreadUriPrefix(upid, utid)}_counter_${trackId}`,
-        title: name,
-        tags: {
-          kind,
-          trackIds: [trackId],
-          utid,
-          upid: upid ?? undefined,
-          scope: 'thread',
-        },
-        trackFactory: (trackCtx) => {
-          return new TraceProcessorCounterTrack({
-            engine: ctx.engine,
-            trackKey: trackCtx.trackKey,
-            trackId: trackId,
-            options: getDefaultCounterOptions(name),
-          });
-        },
-        detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name),
-        getEventBounds: async (id) => {
-          return await getCounterEventBounds(ctx.engine, trackId, id);
-        },
-      });
-    }
-  }
-
-  async addProcessCounterTracks(ctx: PluginContextTrace): 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);
-  `);
-    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}),
-      });
-      ctx.registerTrack({
-        uri: `/process_${upid}/counter_${trackId}`,
-        title: name,
-        tags: {
-          kind,
-          trackIds: [trackId],
-          upid,
-          scope: 'process',
-        },
-        trackFactory: (trackCtx) => {
-          return new TraceProcessorCounterTrack({
-            engine: ctx.engine,
-            trackKey: trackCtx.trackKey,
-            trackId: trackId,
-            options: getDefaultCounterOptions(name),
-          });
-        },
-        detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name),
-        getEventBounds: async (id) => {
-          return await getCounterEventBounds(ctx.engine, trackId, id);
-        },
-      });
-    }
-  }
-
-  private async addGpuFrequencyTracks(ctx: PluginContextTrace) {
-    const engine = ctx.engine;
-    const numGpus = ctx.trace.gpuCount;
-
-    for (let gpu = 0; gpu < numGpus; gpu++) {
-      // Only add a gpu freq track if we have
-      // gpu freq data.
-      const freqExistsResult = await engine.query(`
-        select id
-        from gpu_counter_track
-        join _counter_track_summary using (id)
-        where name = 'gpufreq' and gpu_id = ${gpu}
-        limit 1;
-      `);
-      if (freqExistsResult.numRows() > 0) {
-        const trackId = freqExistsResult.firstRow({id: NUM}).id;
-        const uri = `/gpu_frequency_${gpu}`;
-        const name = `Gpu ${gpu} Frequency`;
-        ctx.registerTrack({
-          uri,
-          title: name,
-          tags: {
-            kind: COUNTER_TRACK_KIND,
-            trackIds: [trackId],
-            scope: 'gpuFreq',
-          },
-          trackFactory: (trackCtx) => {
-            return new TraceProcessorCounterTrack({
-              engine: ctx.engine,
-              trackKey: trackCtx.trackKey,
-              trackId: trackId,
-              options: getDefaultCounterOptions(name),
-            });
-          },
-          detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name),
-          getEventBounds: async (id) => {
-            return await getCounterEventBounds(ctx.engine, trackId, id);
-          },
-        });
-      }
-    }
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.Counter',
-  plugin: CounterPlugin,
-};
diff --git a/ui/src/core_plugins/counter/trace_processor_counter_track.ts b/ui/src/core_plugins/counter/trace_processor_counter_track.ts
deleted file mode 100644
index b02278b..0000000
--- a/ui/src/core_plugins/counter/trace_processor_counter_track.ts
+++ /dev/null
@@ -1,87 +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 {globals} from '../../frontend/globals';
-import {LONG, LONG_NULL, NUM} from '../../public';
-import {
-  BaseCounterTrack,
-  BaseCounterTrackArgs,
-} from '../../frontend/base_counter_track';
-import {TrackMouseEvent} from '../../public/tracks';
-
-interface TraceProcessorCounterTrackArgs extends BaseCounterTrackArgs {
-  trackId: number;
-  rootTable?: string;
-}
-
-export class TraceProcessorCounterTrack extends BaseCounterTrack {
-  private trackId: number;
-  private rootTable: string;
-
-  constructor(args: TraceProcessorCounterTrackArgs) {
-    super(args);
-    this.trackId = args.trackId;
-    this.rootTable = args.rootTable ?? 'counter';
-  }
-
-  getSqlSource() {
-    return `
-      select
-        ts,
-        value
-      from ${this.rootTable}
-      where track_id = ${this.trackId}
-    `;
-  }
-
-  onMouseClick({x, timescale}: TrackMouseEvent): boolean {
-    const time = timescale.pxToHpTime(x).toTime('floor');
-
-    const query = `
-      select
-        id,
-        ts as leftTs,
-        (
-          select ts
-          from ${this.rootTable}
-          where
-            track_id = ${this.trackId}
-            and ts >= ${time}
-          order by ts
-          limit 1
-        ) as rightTs
-      from ${this.rootTable}
-      where
-        track_id = ${this.trackId}
-        and ts < ${time}
-      order by ts DESC
-      limit 1
-    `;
-
-    this.engine.query(query).then((result) => {
-      const it = result.iter({
-        id: NUM,
-        leftTs: LONG,
-        rightTs: LONG_NULL,
-      });
-      if (!it.valid()) {
-        return;
-      }
-      const id = it.id;
-      globals.selectSingleEvent(this.trackKey, id);
-    });
-
-    return true;
-  }
-}
diff --git a/ui/src/core_plugins/cpu_freq/index.ts b/ui/src/core_plugins/cpu_freq/index.ts
deleted file mode 100644
index de40c5c..0000000
--- a/ui/src/core_plugins/cpu_freq/index.ts
+++ /dev/null
@@ -1,471 +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 {BigintMath as BIMath} from '../../base/bigint_math';
-import {searchSegment} from '../../base/binary_search';
-import {assertTrue} from '../../base/logging';
-import {duration, time, Time} from '../../base/time';
-import {drawTrackHoverTooltip} from '../../common/canvas_utils';
-import {colorForCpu} from '../../core/colorizer';
-import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {globals} from '../../frontend/globals';
-import {
-  CPU_FREQ_TRACK_KIND,
-  Engine,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  Track,
-} from '../../public';
-import {LONG, NUM, NUM_NULL} from '../../trace_processor/query_result';
-import {uuidv4Sql} from '../../base/uuid';
-import {TrackMouseEvent, TrackRenderContext} from '../../public/tracks';
-import {Vector} from '../../base/geom';
-
-export interface Data extends TrackData {
-  timestamps: BigInt64Array;
-  minFreqKHz: Uint32Array;
-  maxFreqKHz: Uint32Array;
-  lastFreqKHz: Uint32Array;
-  lastIdleValues: Int8Array;
-}
-
-interface Config {
-  cpu: number;
-  freqTrackId: number;
-  idleTrackId?: number;
-  maximumValue: number;
-}
-
-// 0.5 Makes the horizontal lines sharp.
-const MARGIN_TOP = 4.5;
-const RECT_HEIGHT = 20;
-
-class CpuFreqTrack implements Track {
-  private mousePos: Vector = {x: 0, y: 0};
-  private hoveredValue: number | undefined = undefined;
-  private hoveredTs: time | undefined = undefined;
-  private hoveredTsEnd: time | undefined = undefined;
-  private hoveredIdle: number | undefined = undefined;
-  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-
-  private engine: Engine;
-  private config: Config;
-  private trackUuid = uuidv4Sql();
-
-  constructor(config: Config, engine: Engine) {
-    this.config = config;
-    this.engine = engine;
-  }
-
-  async onCreate() {
-    if (this.config.idleTrackId === undefined) {
-      await this.engine.query(`
-        create view raw_freq_idle_${this.trackUuid} as
-        select ts, dur, value as freqValue, -1 as idleValue
-        from experimental_counter_dur c
-        where track_id = ${this.config.freqTrackId}
-      `);
-    } else {
-      await this.engine.query(`
-        create view raw_freq_${this.trackUuid} as
-        select ts, dur, value as freqValue
-        from experimental_counter_dur c
-        where track_id = ${this.config.freqTrackId};
-
-        create view raw_idle_${this.trackUuid} as
-        select
-          ts,
-          dur,
-          iif(value = 4294967295, -1, cast(value as int)) as idleValue
-        from experimental_counter_dur c
-        where track_id = ${this.config.idleTrackId};
-
-        create virtual table raw_freq_idle_${this.trackUuid}
-        using span_join(raw_freq_${this.trackUuid}, raw_idle_${this.trackUuid});
-      `);
-    }
-
-    await this.engine.query(`
-      create virtual table cpu_freq_${this.trackUuid}
-      using __intrinsic_counter_mipmap((
-        select ts, freqValue as value
-        from raw_freq_idle_${this.trackUuid}
-      ));
-
-      create virtual table cpu_idle_${this.trackUuid}
-      using __intrinsic_counter_mipmap((
-        select ts, idleValue as value
-        from raw_freq_idle_${this.trackUuid}
-      ));
-    `);
-  }
-
-  async onUpdate({
-    visibleWindow,
-    resolution,
-  }: TrackRenderContext): Promise<void> {
-    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
-  }
-
-  async onDestroy(): Promise<void> {
-    await this.engine.tryQuery(`drop table cpu_freq_${this.trackUuid}`);
-    await this.engine.tryQuery(`drop table cpu_idle_${this.trackUuid}`);
-    await this.engine.tryQuery(`drop table raw_freq_idle_${this.trackUuid}`);
-    await this.engine.tryQuery(
-      `drop view if exists raw_freq_${this.trackUuid}`,
-    );
-    await this.engine.tryQuery(
-      `drop view if exists raw_idle_${this.trackUuid}`,
-    );
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    // The resolution should always be a power of two for the logic of this
-    // function to make sense.
-    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
-
-    const freqResult = await this.engine.query(`
-      SELECT
-        min_value as minFreq,
-        max_value as maxFreq,
-        last_ts as ts,
-        last_value as lastFreq
-      FROM cpu_freq_${this.trackUuid}(
-        ${start},
-        ${end},
-        ${resolution}
-      );
-    `);
-    const idleResult = await this.engine.query(`
-      SELECT last_value as lastIdle
-      FROM cpu_idle_${this.trackUuid}(
-        ${start},
-        ${end},
-        ${resolution}
-      );
-    `);
-
-    const freqRows = freqResult.numRows();
-    const idleRows = idleResult.numRows();
-    assertTrue(freqRows == idleRows);
-
-    const data: Data = {
-      start,
-      end,
-      resolution,
-      length: freqRows,
-      timestamps: new BigInt64Array(freqRows),
-      minFreqKHz: new Uint32Array(freqRows),
-      maxFreqKHz: new Uint32Array(freqRows),
-      lastFreqKHz: new Uint32Array(freqRows),
-      lastIdleValues: new Int8Array(freqRows),
-    };
-
-    const freqIt = freqResult.iter({
-      ts: LONG,
-      minFreq: NUM,
-      maxFreq: NUM,
-      lastFreq: NUM,
-    });
-    const idleIt = idleResult.iter({
-      lastIdle: NUM,
-    });
-    for (let i = 0; freqIt.valid(); ++i, freqIt.next(), idleIt.next()) {
-      data.timestamps[i] = freqIt.ts;
-      data.minFreqKHz[i] = freqIt.minFreq;
-      data.maxFreqKHz[i] = freqIt.maxFreq;
-      data.lastFreqKHz[i] = freqIt.lastFreq;
-      data.lastIdleValues[i] = idleIt.lastIdle;
-    }
-    return data;
-  }
-
-  getHeight() {
-    return MARGIN_TOP + RECT_HEIGHT;
-  }
-
-  render({ctx, size, timescale, visibleWindow}: TrackRenderContext): void {
-    // TODO: fonts and colors should come from the CSS and not hardcoded here.
-    const data = this.fetcher.data;
-
-    if (data === undefined || data.timestamps.length === 0) {
-      // Can't possibly draw anything.
-      return;
-    }
-
-    assertTrue(data.timestamps.length === data.lastFreqKHz.length);
-    assertTrue(data.timestamps.length === data.minFreqKHz.length);
-    assertTrue(data.timestamps.length === data.maxFreqKHz.length);
-    assertTrue(data.timestamps.length === data.lastIdleValues.length);
-
-    const endPx = size.width;
-    const zeroY = MARGIN_TOP + RECT_HEIGHT;
-
-    // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
-    let yMax = this.config.maximumValue;
-    const kUnits = ['', 'K', 'M', 'G', 'T', 'E'];
-    const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
-    const pow10 = Math.pow(10, exp);
-    yMax = Math.ceil(yMax / (pow10 / 4)) * (pow10 / 4);
-    const unitGroup = Math.floor(exp / 3);
-    const num = yMax / Math.pow(10, unitGroup * 3);
-    // The values we have for cpufreq are in kHz so +1 to unitGroup.
-    const yLabel = `${num} ${kUnits[unitGroup + 1]}Hz`;
-
-    const color = colorForCpu(this.config.cpu);
-    let saturation = 45;
-    if (globals.state.hoveredUtid !== -1) {
-      saturation = 0;
-    }
-
-    ctx.fillStyle = color.setHSL({s: saturation, l: 70}).cssString;
-    ctx.strokeStyle = color.setHSL({s: saturation, l: 55}).cssString;
-
-    const calculateX = (timestamp: time) => {
-      return Math.floor(timescale.timeToPx(timestamp));
-    };
-    const calculateY = (value: number) => {
-      return zeroY - Math.round((value / yMax) * RECT_HEIGHT);
-    };
-
-    const timespan = visibleWindow.toTimeSpan();
-    const start = timespan.start;
-    const end = timespan.end;
-
-    const [rawStartIdx] = searchSegment(data.timestamps, start);
-    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
-
-    const [, rawEndIdx] = searchSegment(data.timestamps, end);
-    const endIdx = rawEndIdx === -1 ? data.timestamps.length : rawEndIdx;
-
-    // Draw the CPU frequency graph.
-    {
-      ctx.beginPath();
-      const timestamp = Time.fromRaw(data.timestamps[startIdx]);
-      ctx.moveTo(Math.max(calculateX(timestamp), 0), zeroY);
-
-      let lastDrawnY = zeroY;
-      for (let i = startIdx; i < endIdx; i++) {
-        const timestamp = Time.fromRaw(data.timestamps[i]);
-        const x = Math.max(0, calculateX(timestamp));
-        const minY = calculateY(data.minFreqKHz[i]);
-        const maxY = calculateY(data.maxFreqKHz[i]);
-        const lastY = calculateY(data.lastFreqKHz[i]);
-
-        ctx.lineTo(x, lastDrawnY);
-        if (minY === maxY) {
-          assertTrue(lastY === minY);
-          ctx.lineTo(x, lastY);
-        } else {
-          ctx.lineTo(x, minY);
-          ctx.lineTo(x, maxY);
-          ctx.lineTo(x, lastY);
-        }
-        lastDrawnY = lastY;
-      }
-      ctx.lineTo(endPx, lastDrawnY);
-      ctx.lineTo(endPx, zeroY);
-      ctx.closePath();
-      ctx.fill();
-      ctx.stroke();
-    }
-
-    // Draw CPU idle rectangles that overlay the CPU freq graph.
-    ctx.fillStyle = `rgba(240, 240, 240, 1)`;
-    {
-      for (let i = startIdx; i < endIdx; i++) {
-        if (data.lastIdleValues[i] < 0) {
-          continue;
-        }
-
-        // We intentionally don't use the floor function here when computing x
-        // coordinates. Instead we use floating point which prevents flickering as
-        // we pan and zoom; this relies on the browser anti-aliasing pixels
-        // correctly.
-        const timestamp = Time.fromRaw(data.timestamps[i]);
-        const x = timescale.timeToPx(timestamp);
-        const xEnd =
-          i === data.lastIdleValues.length - 1
-            ? endPx
-            : timescale.timeToPx(Time.fromRaw(data.timestamps[i + 1]));
-
-        const width = xEnd - x;
-        const height = calculateY(data.lastFreqKHz[i]) - zeroY;
-
-        ctx.fillRect(x, zeroY, width, height);
-      }
-    }
-
-    ctx.font = '10px Roboto Condensed';
-
-    if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) {
-      let text = `${this.hoveredValue.toLocaleString()}kHz`;
-
-      ctx.fillStyle = color.setHSL({s: 45, l: 75}).cssString;
-      ctx.strokeStyle = color.setHSL({s: 45, l: 45}).cssString;
-
-      const xStart = Math.floor(timescale.timeToPx(this.hoveredTs));
-      const xEnd =
-        this.hoveredTsEnd === undefined
-          ? endPx
-          : Math.floor(timescale.timeToPx(this.hoveredTsEnd));
-      const y = zeroY - Math.round((this.hoveredValue / yMax) * RECT_HEIGHT);
-
-      // Highlight line.
-      ctx.beginPath();
-      ctx.moveTo(xStart, y);
-      ctx.lineTo(xEnd, y);
-      ctx.lineWidth = 3;
-      ctx.stroke();
-      ctx.lineWidth = 1;
-
-      // Draw change marker.
-      ctx.beginPath();
-      ctx.arc(
-        xStart,
-        y,
-        3 /* r*/,
-        0 /* start angle*/,
-        2 * Math.PI /* end angle*/,
-      );
-      ctx.fill();
-      ctx.stroke();
-
-      // Display idle value if current hover is idle.
-      if (this.hoveredIdle !== undefined && this.hoveredIdle !== -1) {
-        // Display the idle value +1 to be consistent with catapult.
-        text += ` (Idle: ${(this.hoveredIdle + 1).toLocaleString()})`;
-      }
-
-      // Draw the tooltip.
-      drawTrackHoverTooltip(ctx, this.mousePos, size, text);
-    }
-
-    // Write the Y scale on the top left corner.
-    ctx.textBaseline = 'alphabetic';
-    ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
-    ctx.fillRect(0, 0, 42, 18);
-    ctx.fillStyle = '#666';
-    ctx.textAlign = 'left';
-    ctx.fillText(`${yLabel}`, 4, 14);
-
-    // If the cached trace slices don't fully cover the visible time range,
-    // show a gray rectangle with a "Loading..." label.
-    checkerboardExcept(
-      ctx,
-      this.getHeight(),
-      0,
-      size.width,
-      timescale.timeToPx(data.start),
-      timescale.timeToPx(data.end),
-    );
-  }
-
-  onMouseMove({x, y, timescale}: TrackMouseEvent) {
-    const data = this.fetcher.data;
-    if (data === undefined) return;
-    this.mousePos = {x, y};
-    const time = timescale.pxToHpTime(x);
-
-    const [left, right] = searchSegment(data.timestamps, time.toTime());
-
-    this.hoveredTs =
-      left === -1 ? undefined : Time.fromRaw(data.timestamps[left]);
-    this.hoveredTsEnd =
-      right === -1 ? undefined : Time.fromRaw(data.timestamps[right]);
-    this.hoveredValue = left === -1 ? undefined : data.lastFreqKHz[left];
-    this.hoveredIdle = left === -1 ? undefined : data.lastIdleValues[left];
-  }
-
-  onMouseOut() {
-    this.hoveredValue = undefined;
-    this.hoveredTs = undefined;
-    this.hoveredTsEnd = undefined;
-    this.hoveredIdle = undefined;
-  }
-}
-
-class CpuFreq implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const {engine} = ctx;
-
-    const cpus = ctx.trace.cpus;
-
-    const maxCpuFreqResult = await engine.query(`
-      select ifnull(max(value), 0) as freq
-      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';
-    `);
-    const maxCpuFreq = maxCpuFreqResult.firstRow({freq: NUM}).freq;
-
-    for (const cpu of cpus) {
-      // Only add a cpu freq track if we have cpu freq data.
-      const cpuFreqIdleResult = await engine.query(`
-        select
-          id as cpuFreqId,
-          (
-            select id
-            from cpu_counter_track
-            where name = 'cpuidle'
-            and cpu = ${cpu}
-            limit 1
-          ) as cpuIdleId
-        from cpu_counter_track
-        join _counter_track_summary using (id)
-        where name = 'cpufreq' and cpu = ${cpu}
-        limit 1;
-      `);
-
-      if (cpuFreqIdleResult.numRows() > 0) {
-        const row = cpuFreqIdleResult.firstRow({
-          cpuFreqId: NUM,
-          cpuIdleId: NUM_NULL,
-        });
-        const freqTrackId = row.cpuFreqId;
-        const idleTrackId = row.cpuIdleId === null ? undefined : row.cpuIdleId;
-
-        const config = {
-          cpu,
-          maximumValue: maxCpuFreq,
-          freqTrackId,
-          idleTrackId,
-        };
-
-        ctx.registerTrack({
-          uri: `/cpu_freq_cpu${cpu}`,
-          title: `Cpu ${cpu} Frequency`,
-          tags: {
-            kind: CPU_FREQ_TRACK_KIND,
-            cpu,
-          },
-          trackFactory: () => new CpuFreqTrack(config, ctx.engine),
-        });
-      }
-    }
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.CpuFreq',
-  plugin: CpuFreq,
-};
diff --git a/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts b/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts
deleted file mode 100644
index 5c283db..0000000
--- a/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts
+++ /dev/null
@@ -1,248 +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 {searchSegment} from '../../base/binary_search';
-import {duration, Time, time} from '../../base/time';
-import {getLegacySelection} from '../../common/state';
-import {Actions} from '../../common/actions';
-import {colorForSample} from '../../core/colorizer';
-import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {globals} from '../../frontend/globals';
-import {TimeScale} from '../../frontend/time_scale';
-import {Engine, Track} from '../../public';
-import {LONG, NUM} from '../../trace_processor/query_result';
-import {TrackMouseEvent, TrackRenderContext} from '../../public/tracks';
-
-const BAR_HEIGHT = 3;
-const MARGIN_TOP = 4.5;
-const RECT_HEIGHT = 30.5;
-
-interface Data extends TrackData {
-  ids: Float64Array;
-  tsStarts: BigInt64Array;
-  callsiteId: Uint32Array;
-}
-
-export class CpuProfileTrack implements Track {
-  private centerY = this.getHeight() / 2 + BAR_HEIGHT;
-  private markerWidth = (this.getHeight() - MARGIN_TOP - BAR_HEIGHT) / 2;
-  private hoveredTs: time | undefined = undefined;
-  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-  private engine: Engine;
-  private utid: number;
-
-  constructor(engine: Engine, utid: number) {
-    this.engine = engine;
-    this.utid = utid;
-  }
-
-  async onUpdate({
-    visibleWindow,
-    resolution,
-  }: TrackRenderContext): Promise<void> {
-    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    const query = `select
-        id,
-        ts,
-        callsite_id as callsiteId
-      from cpu_profile_stack_sample
-      where utid = ${this.utid}
-      order by ts`;
-
-    const result = await this.engine.query(query);
-    const numRows = result.numRows();
-    const data: Data = {
-      start,
-      end,
-      resolution,
-      length: numRows,
-      ids: new Float64Array(numRows),
-      tsStarts: new BigInt64Array(numRows),
-      callsiteId: new Uint32Array(numRows),
-    };
-
-    const it = result.iter({id: NUM, ts: LONG, callsiteId: NUM});
-    for (let row = 0; it.valid(); it.next(), ++row) {
-      data.ids[row] = it.id;
-      data.tsStarts[row] = it.ts;
-      data.callsiteId[row] = it.callsiteId;
-    }
-
-    return data;
-  }
-
-  async onDestroy(): Promise<void> {
-    this.fetcher[Symbol.dispose]();
-  }
-
-  getHeight() {
-    return MARGIN_TOP + RECT_HEIGHT - 1;
-  }
-
-  render({ctx, timescale: timeScale}: TrackRenderContext): void {
-    const data = this.fetcher.data;
-
-    if (data === undefined) return;
-
-    for (let i = 0; i < data.tsStarts.length; i++) {
-      const centerX = Time.fromRaw(data.tsStarts[i]);
-      const selection = getLegacySelection(globals.state);
-      const isHovered = this.hoveredTs === centerX;
-      const isSelected =
-        selection !== null &&
-        selection.kind === 'CPU_PROFILE_SAMPLE' &&
-        selection.ts === centerX;
-      const strokeWidth = isSelected ? 3 : 0;
-      this.drawMarker(
-        ctx,
-        timeScale.timeToPx(centerX),
-        this.centerY,
-        isHovered,
-        strokeWidth,
-        data.callsiteId[i],
-      );
-    }
-
-    // Group together identical identical CPU profile samples by connecting them
-    // with an horizontal bar.
-    let clusterStartIndex = 0;
-    while (clusterStartIndex < data.tsStarts.length) {
-      const callsiteId = data.callsiteId[clusterStartIndex];
-
-      // Find the end of the cluster by searching for the next different CPU
-      // sample. The resulting range [clusterStartIndex, clusterEndIndex] is
-      // inclusive and within array bounds.
-      let clusterEndIndex = clusterStartIndex;
-      while (
-        clusterEndIndex + 1 < data.tsStarts.length &&
-        data.callsiteId[clusterEndIndex + 1] === callsiteId
-      ) {
-        clusterEndIndex++;
-      }
-
-      // If there are multiple CPU samples in the cluster, draw a line.
-      if (clusterStartIndex !== clusterEndIndex) {
-        const startX = Time.fromRaw(data.tsStarts[clusterStartIndex]);
-        const endX = Time.fromRaw(data.tsStarts[clusterEndIndex]);
-        const leftPx = timeScale.timeToPx(startX) - this.markerWidth;
-        const rightPx = timeScale.timeToPx(endX) + this.markerWidth;
-        const width = rightPx - leftPx;
-        ctx.fillStyle = colorForSample(callsiteId, false);
-        ctx.fillRect(leftPx, MARGIN_TOP, width, BAR_HEIGHT);
-      }
-
-      // Move to the next cluster.
-      clusterStartIndex = clusterEndIndex + 1;
-    }
-  }
-
-  drawMarker(
-    ctx: CanvasRenderingContext2D,
-    x: number,
-    y: number,
-    isHovered: boolean,
-    strokeWidth: number,
-    callsiteId: number,
-  ): void {
-    ctx.beginPath();
-    ctx.moveTo(x - this.markerWidth, y - this.markerWidth);
-    ctx.lineTo(x, y + this.markerWidth);
-    ctx.lineTo(x + this.markerWidth, y - this.markerWidth);
-    ctx.lineTo(x - this.markerWidth, y - this.markerWidth);
-    ctx.closePath();
-    ctx.fillStyle = colorForSample(callsiteId, isHovered);
-    ctx.fill();
-    if (strokeWidth > 0) {
-      ctx.strokeStyle = colorForSample(callsiteId, false);
-      ctx.lineWidth = strokeWidth;
-      ctx.stroke();
-    }
-  }
-
-  onMouseMove({x, y, timescale}: TrackMouseEvent) {
-    const data = this.fetcher.data;
-    if (data === undefined) return;
-    const time = timescale.pxToHpTime(x);
-    const [left, right] = searchSegment(data.tsStarts, time.toTime());
-    const index = this.findTimestampIndex(left, timescale, data, x, y, right);
-    this.hoveredTs =
-      index === -1 ? undefined : Time.fromRaw(data.tsStarts[index]);
-  }
-
-  onMouseOut() {
-    this.hoveredTs = undefined;
-  }
-
-  onMouseClick({x, y, timescale}: TrackMouseEvent) {
-    const data = this.fetcher.data;
-    if (data === undefined) return false;
-
-    const time = timescale.pxToHpTime(x);
-    const [left, right] = searchSegment(data.tsStarts, time.toTime());
-
-    const index = this.findTimestampIndex(left, timescale, data, x, y, right);
-
-    if (index !== -1) {
-      const id = data.ids[index];
-      const ts = Time.fromRaw(data.tsStarts[index]);
-
-      globals.makeSelection(
-        Actions.selectCpuProfileSample({id, utid: this.utid, ts}),
-      );
-      return true;
-    }
-    return false;
-  }
-
-  // If the markers overlap the rightmost one will be selected.
-  findTimestampIndex(
-    left: number,
-    timeScale: TimeScale,
-    data: Data,
-    x: number,
-    y: number,
-    right: number,
-  ): number {
-    let index = -1;
-    if (left !== -1) {
-      const start = Time.fromRaw(data.tsStarts[left]);
-      const centerX = timeScale.timeToPx(start);
-      if (this.isInMarker(x, y, centerX)) {
-        index = left;
-      }
-    }
-    if (right !== -1) {
-      const start = Time.fromRaw(data.tsStarts[right]);
-      const centerX = timeScale.timeToPx(start);
-      if (this.isInMarker(x, y, centerX)) {
-        index = right;
-      }
-    }
-    return index;
-  }
-
-  isInMarker(x: number, y: number, centerX: number) {
-    return (
-      Math.abs(x - centerX) + Math.abs(y - this.centerY) <= this.markerWidth
-    );
-  }
-}
diff --git a/ui/src/core_plugins/cpu_profile/index.ts b/ui/src/core_plugins/cpu_profile/index.ts
deleted file mode 100644
index bc60d66..0000000
--- a/ui/src/core_plugins/cpu_profile/index.ts
+++ /dev/null
@@ -1,82 +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 m from 'mithril';
-
-import {CpuProfileDetailsPanel} from '../../frontend/cpu_profile_panel';
-import {
-  CPU_PROFILE_TRACK_KIND,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
-import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
-import {CpuProfileTrack} from './cpu_profile_track';
-import {getThreadUriPrefix} from '../../public/utils';
-import {exists} from '../../base/utils';
-
-class CpuProfile implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const result = await ctx.engine.query(`
-      with thread_cpu_sample as (
-        select distinct utid
-        from cpu_profile_stack_sample
-        where utid != 0
-      )
-      select
-        utid,
-        tid,
-        upid,
-        thread.name as threadName
-      from thread_cpu_sample
-      join thread using(utid)`);
-
-    const it = result.iter({
-      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 threadName = it.threadName;
-      ctx.registerTrack({
-        uri: `${getThreadUriPrefix(upid, utid)}_cpu_samples`,
-        title: `${threadName} (CPU Stack Samples)`,
-        tags: {
-          kind: CPU_PROFILE_TRACK_KIND,
-          utid,
-          ...(exists(upid) && {upid}),
-        },
-        trackFactory: () => new CpuProfileTrack(ctx.engine, utid),
-      });
-    }
-
-    ctx.registerDetailsPanel({
-      render: (sel) => {
-        if (sel.kind === 'CPU_PROFILE_SAMPLE') {
-          return m(CpuProfileDetailsPanel);
-        } else {
-          return undefined;
-        }
-      },
-    });
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.CpuProfile',
-  plugin: CpuProfile,
-};
diff --git a/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts b/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts
deleted file mode 100644
index 7f1b619..0000000
--- a/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts
+++ /dev/null
@@ -1,475 +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 {BigintMath as BIMath} from '../../base/bigint_math';
-import {search, searchEq, searchSegment} from '../../base/binary_search';
-import {assertExists, assertTrue} from '../../base/logging';
-import {Duration, duration, Time, time} from '../../base/time';
-import {Actions} from '../../common/actions';
-import {getLegacySelection} from '../../common/state';
-import {
-  drawDoubleHeadedArrow,
-  drawIncompleteSlice,
-  drawTrackHoverTooltip,
-} from '../../common/canvas_utils';
-import {cropText} from '../../base/string_utils';
-import {Color} from '../../core/color';
-import {colorForThread} from '../../core/colorizer';
-import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {globals} from '../../frontend/globals';
-import {Vector} from '../../base/geom';
-import {Engine, Track} from '../../public';
-import {LONG, NUM} from '../../trace_processor/query_result';
-import {uuidv4Sql} from '../../base/uuid';
-import {TrackMouseEvent, TrackRenderContext} from '../../public/tracks';
-
-export interface Data extends TrackData {
-  // Slices are stored in a columnar fashion. All fields have the same length.
-  ids: Float64Array;
-  startQs: BigInt64Array;
-  endQs: BigInt64Array;
-  utids: Uint32Array;
-  flags: Uint8Array;
-  lastRowId: number;
-}
-
-const MARGIN_TOP = 3;
-const RECT_HEIGHT = 24;
-const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
-
-const CPU_SLICE_FLAGS_INCOMPLETE = 1;
-const CPU_SLICE_FLAGS_REALTIME = 2;
-
-export class CpuSliceTrack implements Track {
-  private mousePos?: Vector;
-  private utidHoveredInThisTrack = -1;
-  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-
-  private lastRowId = -1;
-  private engine: Engine;
-  private cpu: number;
-  private trackKey: string;
-  private trackUuid = uuidv4Sql();
-
-  constructor(engine: Engine, trackKey: string, cpu: number) {
-    this.engine = engine;
-    this.trackKey = trackKey;
-    this.cpu = cpu;
-  }
-
-  async onCreate() {
-    await this.engine.query(`
-      create virtual table cpu_slice_${this.trackUuid}
-      using __intrinsic_slice_mipmap((
-        select
-          id,
-          ts,
-          iif(dur = -1, lead(ts, 1, trace_end()) over (order by ts) - ts, dur),
-          0 as depth
-        from sched
-        where cpu = ${this.cpu} and utid != 0
-      ));
-    `);
-    const it = await this.engine.query(`
-      select coalesce(max(id), -1) as lastRowId
-      from sched
-      where cpu = ${this.cpu} and utid != 0
-    `);
-    this.lastRowId = it.firstRow({lastRowId: NUM}).lastRowId;
-  }
-
-  async onUpdate({
-    visibleWindow,
-    resolution,
-  }: TrackRenderContext): Promise<void> {
-    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
-
-    const queryRes = await this.engine.query(`
-      select
-        (z.ts / ${resolution}) * ${resolution} as tsQ,
-        (((z.ts + z.dur) / ${resolution}) + 1) * ${resolution} as tsEndQ,
-        s.utid,
-        s.id,
-        s.dur = -1 as isIncomplete,
-        ifnull(s.priority < 100, 0) as isRealtime
-      from cpu_slice_${this.trackUuid}(${start}, ${end}, ${resolution}) z
-      cross join sched s using (id)
-    `);
-
-    const numRows = queryRes.numRows();
-    const slices: Data = {
-      start,
-      end,
-      resolution,
-      length: numRows,
-      lastRowId: this.lastRowId,
-      ids: new Float64Array(numRows),
-      startQs: new BigInt64Array(numRows),
-      endQs: new BigInt64Array(numRows),
-      utids: new Uint32Array(numRows),
-      flags: new Uint8Array(numRows),
-    };
-
-    const it = queryRes.iter({
-      tsQ: LONG,
-      tsEndQ: LONG,
-      utid: NUM,
-      id: NUM,
-      isIncomplete: NUM,
-      isRealtime: NUM,
-    });
-    for (let row = 0; it.valid(); it.next(), row++) {
-      slices.startQs[row] = it.tsQ;
-      slices.endQs[row] = it.tsEndQ;
-      slices.utids[row] = it.utid;
-      slices.ids[row] = it.id;
-
-      slices.flags[row] = 0;
-      if (it.isIncomplete) {
-        slices.flags[row] |= CPU_SLICE_FLAGS_INCOMPLETE;
-      }
-      if (it.isRealtime) {
-        slices.flags[row] |= CPU_SLICE_FLAGS_REALTIME;
-      }
-    }
-    return slices;
-  }
-
-  async onDestroy() {
-    await this.engine.tryQuery(
-      `drop table if exists cpu_slice_${this.trackUuid}`,
-    );
-    this.fetcher[Symbol.dispose]();
-  }
-
-  getHeight(): number {
-    return TRACK_HEIGHT;
-  }
-
-  render(trackCtx: TrackRenderContext): void {
-    const {ctx, size, timescale} = trackCtx;
-
-    // TODO: fonts and colors should come from the CSS and not hardcoded here.
-    const data = this.fetcher.data;
-
-    if (data === undefined) return; // Can't possibly draw anything.
-
-    // If the cached trace slices don't fully cover the visible time range,
-    // show a gray rectangle with a "Loading..." label.
-    checkerboardExcept(
-      ctx,
-      this.getHeight(),
-      0,
-      size.width,
-      timescale.timeToPx(data.start),
-      timescale.timeToPx(data.end),
-    );
-
-    this.renderSlices(trackCtx, data);
-  }
-
-  renderSlices(
-    {ctx, timescale, size, visibleWindow}: TrackRenderContext,
-    data: Data,
-  ): void {
-    assertTrue(data.startQs.length === data.endQs.length);
-    assertTrue(data.startQs.length === data.utids.length);
-
-    const visWindowEndPx = size.width;
-
-    ctx.textAlign = 'center';
-    ctx.font = '12px Roboto Condensed';
-    const charWidth = ctx.measureText('dbpqaouk').width / 8;
-
-    const timespan = visibleWindow.toTimeSpan();
-
-    const startTime = timespan.start;
-    const endTime = timespan.end;
-
-    const rawStartIdx = data.endQs.findIndex((end) => end >= startTime);
-    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
-
-    const [, rawEndIdx] = searchSegment(data.startQs, endTime);
-    const endIdx = rawEndIdx === -1 ? data.startQs.length : rawEndIdx;
-
-    for (let i = startIdx; i < endIdx; i++) {
-      const tStart = Time.fromRaw(data.startQs[i]);
-      let tEnd = Time.fromRaw(data.endQs[i]);
-      const utid = data.utids[i];
-
-      // If the last slice is incomplete, it should end with the end of the
-      // window, else it might spill over the window and the end would not be
-      // visible as a zigzag line.
-      if (
-        data.ids[i] === data.lastRowId &&
-        data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE
-      ) {
-        tEnd = endTime;
-      }
-      const rectStart = timescale.timeToPx(tStart);
-      const rectEnd = timescale.timeToPx(tEnd);
-      const rectWidth = Math.max(1, rectEnd - rectStart);
-
-      const threadInfo = globals.threads.get(utid);
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      const pid = threadInfo && threadInfo.pid ? threadInfo.pid : -1;
-
-      const isHovering = globals.state.hoveredUtid !== -1;
-      const isThreadHovered = globals.state.hoveredUtid === utid;
-      const isProcessHovered = globals.state.hoveredPid === pid;
-      const colorScheme = colorForThread(threadInfo);
-      let color: Color;
-      let textColor: Color;
-      if (isHovering && !isThreadHovered) {
-        if (!isProcessHovered) {
-          color = colorScheme.disabled;
-          textColor = colorScheme.textDisabled;
-        } else {
-          color = colorScheme.variant;
-          textColor = colorScheme.textVariant;
-        }
-      } else {
-        color = colorScheme.base;
-        textColor = colorScheme.textBase;
-      }
-      ctx.fillStyle = color.cssString;
-
-      if (data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE) {
-        drawIncompleteSlice(ctx, rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
-      } else {
-        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
-      }
-
-      // Don't render text when we have less than 5px to play with.
-      if (rectWidth < 5) continue;
-
-      // Stylize real-time threads. We don't do it when zoomed out as the
-      // fillRect is expensive.
-      if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
-        ctx.fillStyle = getHatchedPattern(ctx);
-        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
-      }
-
-      // TODO: consider de-duplicating this code with the copied one from
-      // chrome_slices/frontend.ts.
-      let title = `[utid:${utid}]`;
-      let subTitle = '';
-      if (threadInfo) {
-        if (threadInfo.pid !== undefined && threadInfo.pid !== 0) {
-          let procName = threadInfo.procName ?? '';
-          if (procName.startsWith('/')) {
-            // Remove folder paths from name
-            procName = procName.substring(procName.lastIndexOf('/') + 1);
-          }
-          title = `${procName} [${threadInfo.pid}]`;
-          subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`;
-        } else {
-          title = `${threadInfo.threadName} [${threadInfo.tid}]`;
-        }
-      }
-
-      if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
-        subTitle = subTitle + ' (RT)';
-      }
-
-      const right = Math.min(visWindowEndPx, rectEnd);
-      const left = Math.max(rectStart, 0);
-      const visibleWidth = Math.max(right - left, 1);
-      title = cropText(title, charWidth, visibleWidth);
-      subTitle = cropText(subTitle, charWidth, visibleWidth);
-      const rectXCenter = left + visibleWidth / 2;
-      ctx.fillStyle = textColor.cssString;
-      ctx.font = '12px Roboto Condensed';
-      ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 1);
-      ctx.fillStyle = textColor.setAlpha(0.6).cssString;
-      ctx.font = '10px Roboto Condensed';
-      ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 9);
-    }
-
-    const selection = getLegacySelection(globals.state);
-    const details = globals.sliceDetails;
-    if (selection !== null && selection.kind === 'SCHED_SLICE') {
-      const [startIndex, endIndex] = searchEq(data.ids, selection.id);
-      if (startIndex !== endIndex) {
-        const tStart = Time.fromRaw(data.startQs[startIndex]);
-        const tEnd = Time.fromRaw(data.endQs[startIndex]);
-        const utid = data.utids[startIndex];
-        const color = colorForThread(globals.threads.get(utid));
-        const rectStart = timescale.timeToPx(tStart);
-        const rectEnd = timescale.timeToPx(tEnd);
-        const rectWidth = Math.max(1, rectEnd - rectStart);
-
-        // Draw a rectangle around the slice that is currently selected.
-        ctx.strokeStyle = color.base.setHSL({l: 30}).cssString;
-        ctx.beginPath();
-        ctx.lineWidth = 3;
-        ctx.strokeRect(rectStart, MARGIN_TOP - 1.5, rectWidth, RECT_HEIGHT + 3);
-        ctx.closePath();
-        // Draw arrow from wakeup time of current slice.
-        if (details.wakeupTs) {
-          const wakeupPos = timescale.timeToPx(details.wakeupTs);
-          const latencyWidth = rectStart - wakeupPos;
-          drawDoubleHeadedArrow(
-            ctx,
-            wakeupPos,
-            MARGIN_TOP + RECT_HEIGHT,
-            latencyWidth,
-            latencyWidth >= 20,
-          );
-          // Latency time with a white semi-transparent background.
-          const latency = tStart - details.wakeupTs;
-          const displayText = Duration.humanise(latency);
-          const measured = ctx.measureText(displayText);
-          if (latencyWidth >= measured.width + 2) {
-            ctx.fillStyle = 'rgba(255,255,255,0.7)';
-            ctx.fillRect(
-              wakeupPos + latencyWidth / 2 - measured.width / 2 - 1,
-              MARGIN_TOP + RECT_HEIGHT - 12,
-              measured.width + 2,
-              11,
-            );
-            ctx.textBaseline = 'bottom';
-            ctx.fillStyle = 'black';
-            ctx.fillText(
-              displayText,
-              wakeupPos + latencyWidth / 2,
-              MARGIN_TOP + RECT_HEIGHT - 1,
-            );
-          }
-        }
-      }
-
-      // Draw diamond if the track being drawn is the cpu of the waker.
-      if (this.cpu === details.wakerCpu && details.wakeupTs) {
-        const wakeupPos = Math.floor(timescale.timeToPx(details.wakeupTs));
-        ctx.beginPath();
-        ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8);
-        ctx.fillStyle = 'black';
-        ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2);
-        ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8);
-        ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2);
-        ctx.fill();
-        ctx.closePath();
-      }
-    }
-
-    const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
-    if (hoveredThread !== undefined && this.mousePos !== undefined) {
-      const tidText = `T: ${hoveredThread.threadName}
-      [${hoveredThread.tid}]`;
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      if (hoveredThread.pid) {
-        const pidText = `P: ${hoveredThread.procName}
-        [${hoveredThread.pid}]`;
-        drawTrackHoverTooltip(ctx, this.mousePos, size, pidText, tidText);
-      } else {
-        drawTrackHoverTooltip(ctx, this.mousePos, size, tidText);
-      }
-    }
-  }
-
-  onMouseMove({x, y, timescale}: TrackMouseEvent) {
-    const data = this.fetcher.data;
-    this.mousePos = {x, y};
-    if (data === undefined) return;
-    if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) {
-      this.utidHoveredInThisTrack = -1;
-      globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
-      return;
-    }
-    const t = timescale.pxToHpTime(x);
-    let hoveredUtid = -1;
-
-    for (let i = 0; i < data.startQs.length; i++) {
-      const tStart = Time.fromRaw(data.startQs[i]);
-      const tEnd = Time.fromRaw(data.endQs[i]);
-      const utid = data.utids[i];
-      if (t.gte(tStart) && t.lt(tEnd)) {
-        hoveredUtid = utid;
-        break;
-      }
-    }
-    this.utidHoveredInThisTrack = hoveredUtid;
-    const threadInfo = globals.threads.get(hoveredUtid);
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    const hoveredPid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
-    globals.dispatch(
-      Actions.setHoveredUtidAndPid({utid: hoveredUtid, pid: hoveredPid}),
-    );
-  }
-
-  onMouseOut() {
-    this.utidHoveredInThisTrack = -1;
-    globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
-    this.mousePos = undefined;
-  }
-
-  onMouseClick({x, timescale}: TrackMouseEvent) {
-    const data = this.fetcher.data;
-    if (data === undefined) return false;
-    const time = timescale.pxToHpTime(x);
-    const index = search(data.startQs, time.toTime());
-    const id = index === -1 ? undefined : data.ids[index];
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    if (!id || this.utidHoveredInThisTrack === -1) return false;
-
-    globals.setLegacySelection(
-      {
-        kind: 'SCHED_SLICE',
-        id,
-        trackKey: this.trackKey,
-      },
-      {
-        clearSearch: true,
-        pendingScrollId: undefined,
-        switchToCurrentSelectionTab: true,
-      },
-    );
-
-    return true;
-  }
-}
-
-// Creates a diagonal hatched pattern to be used for distinguishing slices with
-// real-time priorities. The pattern is created once as an offscreen canvas and
-// is kept cached inside the Context2D of the main canvas, without making
-// assumptions on the lifetime of the main canvas.
-function getHatchedPattern(mainCtx: CanvasRenderingContext2D): CanvasPattern {
-  const mctx = mainCtx as CanvasRenderingContext2D & {
-    sliceHatchedPattern?: CanvasPattern;
-  };
-  if (mctx.sliceHatchedPattern !== undefined) return mctx.sliceHatchedPattern;
-  const canvas = document.createElement('canvas');
-  const SIZE = 8;
-  canvas.width = canvas.height = SIZE;
-  const ctx = assertExists(canvas.getContext('2d'));
-  ctx.strokeStyle = 'rgba(255,255,255,0.3)';
-  ctx.beginPath();
-  ctx.lineWidth = 1;
-  ctx.moveTo(0, SIZE);
-  ctx.lineTo(SIZE, 0);
-  ctx.stroke();
-  mctx.sliceHatchedPattern = assertExists(mctx.createPattern(canvas, 'repeat'));
-  return mctx.sliceHatchedPattern;
-}
diff --git a/ui/src/core_plugins/cpu_slices/index.ts b/ui/src/core_plugins/cpu_slices/index.ts
deleted file mode 100644
index 5895145..0000000
--- a/ui/src/core_plugins/cpu_slices/index.ts
+++ /dev/null
@@ -1,92 +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 m from 'mithril';
-
-import {CPU_SLICE_TRACK_KIND} from '../../public';
-import {SliceDetailsPanel} from '../../frontend/slice_details_panel';
-import {
-  Engine,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
-import {NUM, STR_NULL} from '../../trace_processor/query_result';
-import {CpuSliceTrack} from './cpu_slice_track';
-
-class CpuSlices implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const cpus = ctx.trace.cpus;
-    const cpuToClusterType = await this.getAndroidCpuClusterTypes(ctx.engine);
-
-    for (const cpu of cpus) {
-      const size = cpuToClusterType.get(cpu);
-      const uri = `/sched_cpu${cpu}`;
-
-      const name = size === undefined ? `Cpu ${cpu}` : `Cpu ${cpu} (${size})`;
-      ctx.registerTrack({
-        uri,
-        title: name,
-        tags: {
-          kind: CPU_SLICE_TRACK_KIND,
-          cpu,
-        },
-        trackFactory: ({trackKey}) => {
-          return new CpuSliceTrack(ctx.engine, trackKey, cpu);
-        },
-      });
-    }
-
-    ctx.registerDetailsPanel({
-      render: (sel) => {
-        if (sel.kind === 'SCHED_SLICE') {
-          return m(SliceDetailsPanel);
-        }
-        return undefined;
-      },
-    });
-  }
-
-  async getAndroidCpuClusterTypes(
-    engine: Engine,
-  ): Promise<Map<number, string>> {
-    const cpuToClusterType = new Map<number, string>();
-    await engine.query(`
-      include perfetto module android.cpu.cluster_type;
-    `);
-    const result = await engine.query(`
-      select cpu, cluster_type as clusterType
-      from android_cpu_cluster_mapping
-    `);
-
-    const it = result.iter({
-      cpu: NUM,
-      clusterType: STR_NULL,
-    });
-
-    for (; it.valid(); it.next()) {
-      const clusterType = it.clusterType;
-      if (clusterType !== null) {
-        cpuToClusterType.set(it.cpu, clusterType);
-      }
-    }
-
-    return cpuToClusterType;
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.CpuSlices',
-  plugin: CpuSlices,
-};
diff --git a/ui/src/core_plugins/debug/index.ts b/ui/src/core_plugins/debug/index.ts
deleted file mode 100644
index 0c9b1e6..0000000
--- a/ui/src/core_plugins/debug/index.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {uuidv4} from '../../base/uuid';
-import {
-  addDebugCounterTrack,
-  addDebugSliceTrack,
-} from '../../frontend/debug_tracks/debug_tracks';
-import {
-  BottomTabToSCSAdapter,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
-
-import {DebugSliceDetailsTab} from '../../frontend/debug_tracks/details_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {Optional, exists} from '../../base/utils';
-
-class DebugTracksPlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerCommand({
-      id: 'perfetto.DebugTracks#addDebugSliceTrack',
-      name: 'Add debug slice track',
-      callback: async (arg: unknown) => {
-        // This command takes a query and creates a debug track out of it The
-        // query can be passed in using the first arg, or if this is not defined
-        // or is the wrong type, we prompt the user for it.
-        const query = await getStringFromArgOrPrompt(ctx, arg);
-        if (exists(query)) {
-          await addDebugSliceTrack(
-            ctx,
-            {
-              sqlSource: query,
-            },
-            'Debug slice track',
-            {ts: 'ts', dur: 'dur', name: 'name'},
-            [],
-          );
-        }
-      },
-    });
-
-    ctx.registerCommand({
-      id: 'perfetto.DebugTracks#addDebugCounterTrack',
-      name: 'Add debug counter track',
-      callback: async (arg: unknown) => {
-        const query = await getStringFromArgOrPrompt(ctx, arg);
-        if (exists(query)) {
-          await addDebugCounterTrack(
-            ctx,
-            {
-              sqlSource: query,
-            },
-            'Debug slice track',
-            {ts: 'ts', value: 'value'},
-          );
-        }
-      },
-    });
-
-    // TODO(stevegolton): While debug tracks are in their current state, we rely
-    // on this plugin to provide the details panel for them. In the future, this
-    // details panel will become part of the debug track's definition.
-    ctx.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind === DebugSliceDetailsTab.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new DebugSliceDetailsTab({
-              config: config as GenericSliceDetailsTabConfig,
-              engine: ctx.engine,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
-  }
-}
-
-// If arg is a string, return it, otherwise prompt the user for a string. An
-// exception is thrown if the prompt is cancelled, so this function handles this
-// and returns undefined in this case.
-async function getStringFromArgOrPrompt(
-  ctx: PluginContextTrace,
-  arg: unknown,
-): Promise<Optional<string>> {
-  if (typeof arg === 'string') {
-    return arg;
-  } else {
-    try {
-      return await ctx.prompt('Enter a query...');
-    } catch {
-      // Prompt was ignored
-      return undefined;
-    }
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.DebugTracks',
-  plugin: DebugTracksPlugin,
-};
diff --git a/ui/src/core_plugins/example_traces/index.ts b/ui/src/core_plugins/example_traces/index.ts
index d1fc43d..4b69ec9 100644
--- a/ui/src/core_plugins/example_traces/index.ts
+++ b/ui/src/core_plugins/example_traces/index.ts
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Actions} from '../../common/actions';
-import {globals} from '../../frontend/globals';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
+import {AppImpl} from '../../core/app_impl';
+import {App} from '../../public/app';
+import {PerfettoPlugin} from '../../public/plugin';
 
 const EXAMPLE_ANDROID_TRACE_URL =
   'https://storage.googleapis.com/perfetto-misc/example_android_trace_15s';
@@ -22,46 +22,42 @@
 const EXAMPLE_CHROME_TRACE_URL =
   'https://storage.googleapis.com/perfetto-misc/chrome_example_wikipedia.perfetto_trace.gz';
 
-function openTraceUrl(url: string): void {
-  globals.logging.logEvent('Trace Actions', 'Open example trace');
-  globals.dispatch(Actions.openTraceFromUrl({url}));
+function openTraceUrl(app: App, url: string): void {
+  app.analytics.logEvent('Trace Actions', 'Open example trace');
+  AppImpl.instance.openTraceFromUrl(url);
 }
 
-class ExampleTracesPlugin implements Plugin {
-  onActivate(ctx: PluginContext) {
+export default class implements PerfettoPlugin {
+  static readonly id = 'perfetto.ExampleTraces';
+  static onActivate(ctx: App) {
     const OPEN_EXAMPLE_ANDROID_TRACE_COMMAND_ID =
       'perfetto.CoreCommands#openExampleAndroidTrace';
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: OPEN_EXAMPLE_ANDROID_TRACE_COMMAND_ID,
       name: 'Open Android example',
       callback: () => {
-        openTraceUrl(EXAMPLE_ANDROID_TRACE_URL);
+        openTraceUrl(ctx, EXAMPLE_ANDROID_TRACE_URL);
       },
     });
-    ctx.addSidebarMenuItem({
+    ctx.sidebar.addMenuItem({
+      section: 'example_traces',
       commandId: OPEN_EXAMPLE_ANDROID_TRACE_COMMAND_ID,
-      group: 'example_traces',
       icon: 'description',
     });
 
     const OPEN_EXAMPLE_CHROME_TRACE_COMMAND_ID =
       'perfetto.CoreCommands#openExampleChromeTrace';
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: OPEN_EXAMPLE_CHROME_TRACE_COMMAND_ID,
       name: 'Open Chrome example',
       callback: () => {
-        openTraceUrl(EXAMPLE_CHROME_TRACE_URL);
+        openTraceUrl(ctx, EXAMPLE_CHROME_TRACE_URL);
       },
     });
-    ctx.addSidebarMenuItem({
+    ctx.sidebar.addMenuItem({
+      section: 'example_traces',
       commandId: OPEN_EXAMPLE_CHROME_TRACE_COMMAND_ID,
-      group: 'example_traces',
       icon: 'description',
     });
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.ExampleTraces',
-  plugin: ExampleTracesPlugin,
-};
diff --git a/ui/src/core_plugins/flags_page/flags_page.ts b/ui/src/core_plugins/flags_page/flags_page.ts
new file mode 100644
index 0000000..d8cfdf6
--- /dev/null
+++ b/ui/src/core_plugins/flags_page/flags_page.ts
@@ -0,0 +1,162 @@
+// 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.
+
+import m from 'mithril';
+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';
+
+const RELEASE_PROCESS_URL =
+  'https://perfetto.dev/docs/visualization/perfetto-ui-release-process';
+
+interface FlagOption {
+  id: string;
+  name: string;
+}
+
+interface SelectWidgetAttrs {
+  id: string;
+  label: string;
+  description: m.Children;
+  options: FlagOption[];
+  selected: string;
+  onSelect: (id: string) => void;
+}
+
+class SelectWidget implements m.ClassComponent<SelectWidgetAttrs> {
+  view(vnode: m.Vnode<SelectWidgetAttrs>) {
+    const route = Router.parseUrl(window.location.href);
+    const attrs = vnode.attrs;
+    const cssClass = route.subpage === `/${attrs.id}` ? '.focused' : '';
+    return m(
+      '.flag-widget' + cssClass,
+      {id: attrs.id},
+      m('label', attrs.label),
+      m(
+        'select',
+        {
+          onchange: (e: InputEvent) => {
+            const value = (e.target as HTMLSelectElement).value;
+            attrs.onSelect(value);
+            raf.scheduleFullRedraw();
+          },
+        },
+        attrs.options.map((o) => {
+          const selected = o.id === attrs.selected;
+          return m('option', {value: o.id, selected}, o.name);
+        }),
+      ),
+      m('.description', attrs.description),
+    );
+  }
+}
+
+interface FlagWidgetAttrs {
+  flag: Flag;
+}
+
+class FlagWidget implements m.ClassComponent<FlagWidgetAttrs> {
+  view(vnode: m.Vnode<FlagWidgetAttrs>) {
+    const flag = vnode.attrs.flag;
+    const defaultState = flag.defaultValue ? 'Enabled' : 'Disabled';
+    return m(SelectWidget, {
+      label: flag.name,
+      id: flag.id,
+      description: flag.description,
+      options: [
+        {id: OverrideState.DEFAULT, name: `Default (${defaultState})`},
+        {id: OverrideState.TRUE, name: 'Enabled'},
+        {id: OverrideState.FALSE, name: 'Disabled'},
+      ],
+      selected: flag.overriddenState(),
+      onSelect: (value: string) => {
+        switch (value) {
+          case OverrideState.TRUE:
+            flag.set(true);
+            break;
+          case OverrideState.FALSE:
+            flag.set(false);
+            break;
+          default:
+          case OverrideState.DEFAULT:
+            flag.reset();
+            break;
+        }
+      },
+    });
+  }
+}
+
+export class FlagsPage implements m.ClassComponent<PageAttrs> {
+  view() {
+    const needsReload = channelChanged();
+    return m(
+      '.flags-page',
+      m(
+        '.flags-content',
+        m('h1', 'Feature flags'),
+        needsReload && [
+          m('h2', 'Please reload for your changes to take effect'),
+        ],
+        m(SelectWidget, {
+          label: 'Release channel',
+          id: 'releaseChannel',
+          description: [
+            'Which release channel of the UI to use. See ',
+            m(
+              'a',
+              {
+                href: RELEASE_PROCESS_URL,
+              },
+              'Release Process',
+            ),
+            ' for more information.',
+          ],
+          options: [
+            {id: 'stable', name: 'Stable (default)'},
+            {id: 'canary', name: 'Canary'},
+            {id: 'autopush', name: 'Autopush'},
+          ],
+          selected: getNextChannel(),
+          onSelect: (id) => setChannel(id),
+        }),
+        m(
+          'button',
+          {
+            onclick: () => {
+              featureFlags.resetAll();
+              raf.scheduleFullRedraw();
+            },
+          },
+          'Reset all below',
+        ),
+
+        featureFlags.allFlags().map((flag) => m(FlagWidget, {flag})),
+      ),
+    );
+  }
+
+  oncreate(vnode: m.VnodeDOM<PageAttrs>) {
+    const flagId = /[/](\w+)/.exec(vnode.attrs.subpage ?? '')?.slice(1, 2)[0];
+    if (flagId) {
+      const flag = vnode.dom.querySelector(`#${flagId}`);
+      if (flag) {
+        flag.scrollIntoView({block: 'center'});
+      }
+    }
+  }
+}
diff --git a/ui/src/core_plugins/flags_page/index.ts b/ui/src/core_plugins/flags_page/index.ts
new file mode 100644
index 0000000..85039c4
--- /dev/null
+++ b/ui/src/core_plugins/flags_page/index.ts
@@ -0,0 +1,62 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {featureFlags} from '../../core/feature_flags';
+import {App} from '../../public/app';
+import {PerfettoPlugin} from '../../public/plugin';
+import {FlagsPage} from './flags_page';
+import {PluginsPage} from './plugins_page';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.FlagsPage';
+
+  static onActivate(app: App) {
+    // Flags page
+    app.pages.registerPage({
+      route: '/flags',
+      page: FlagsPage,
+      traceless: true,
+    });
+    app.sidebar.addMenuItem({
+      section: 'support',
+      sortOrder: 3,
+      text: 'Flags',
+      href: '#!/flags',
+      icon: 'emoji_flags',
+    });
+
+    // Plugins page.
+    app.pages.registerPage({
+      route: '/plugins',
+      page: PluginsPage,
+      traceless: true,
+    });
+
+    const PLUGINS_PAGE_IN_NAV_FLAG = featureFlags.register({
+      id: 'showPluginsPageInNav',
+      name: 'Show plugins page',
+      description: 'Show a link to the plugins page in the side bar.',
+      defaultValue: false,
+    });
+    if (PLUGINS_PAGE_IN_NAV_FLAG.get()) {
+      app.sidebar.addMenuItem({
+        section: 'support',
+        text: 'Plugins',
+        href: '#!/plugins',
+        icon: 'extension',
+        sortOrder: 9,
+      });
+    }
+  }
+}
diff --git a/ui/src/core_plugins/flags_page/plugins_page.ts b/ui/src/core_plugins/flags_page/plugins_page.ts
new file mode 100644
index 0000000..ed5bd0b
--- /dev/null
+++ b/ui/src/core_plugins/flags_page/plugins_page.ts
@@ -0,0 +1,125 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {Button} from '../../widgets/button';
+import {exists} from '../../base/utils';
+import {defaultPlugins} from '../../core/default_plugins';
+import {Intent} from '../../widgets/common';
+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
+// component, and it'll be reset we restart anyway.
+let needsRestart = false;
+
+export class PluginsPage implements m.ClassComponent<PageAttrs> {
+  view() {
+    const pluginManager = AppImpl.instance.plugins;
+    const registeredPlugins = pluginManager.getAllPlugins();
+    return m(
+      '.pf-plugins-page',
+      m('h1', 'Plugins'),
+      needsRestart &&
+        m(
+          'h3.restart_needed',
+          'Some plugins have been disabled. ' +
+            'Please reload your page to apply the changes.',
+        ),
+      m(
+        '.pf-plugins-topbar',
+        m(Button, {
+          intent: Intent.Primary,
+          label: 'Disable All',
+          onclick: async () => {
+            for (const plugin of registeredPlugins) {
+              plugin.enableFlag.set(false);
+            }
+            needsRestart = true;
+            raf.scheduleFullRedraw();
+          },
+        }),
+        m(Button, {
+          intent: Intent.Primary,
+          label: 'Enable All',
+          onclick: async () => {
+            for (const plugin of registeredPlugins) {
+              plugin.enableFlag.set(true);
+            }
+            needsRestart = true;
+            raf.scheduleFullRedraw();
+          },
+        }),
+        m(Button, {
+          intent: Intent.Primary,
+          label: 'Restore Defaults',
+          onclick: async () => {
+            for (const plugin of registeredPlugins) {
+              plugin.enableFlag.reset();
+            }
+            needsRestart = true;
+            raf.scheduleFullRedraw();
+          },
+        }),
+      ),
+      m(
+        '.pf-plugins-grid',
+        m('span', 'Plugin'),
+        m('span', 'Default?'),
+        m('span', 'Enabled?'),
+        m('span', 'Active?'),
+        m('span', 'Control'),
+        m('span', 'Load Time'),
+        registeredPlugins.map((plugin) => this.renderPluginRow(plugin)),
+      ),
+    );
+  }
+
+  private renderPluginRow(plugin: PluginWrapper): m.Children {
+    const pluginId = plugin.desc.id;
+    const isDefault = defaultPlugins.includes(pluginId);
+    const isActive = plugin.active;
+    const isEnabled = plugin.enableFlag.get();
+    const loadTime = plugin.traceContext?.loadTimeMs;
+    return [
+      m('span', pluginId),
+      m('span', isDefault ? 'Yes' : 'No'),
+      isEnabled
+        ? m('.pf-tag.pf-active', 'Enabled')
+        : m('.pf-tag.pf-inactive', 'Disabled'),
+      isActive
+        ? m('.pf-tag.pf-active', 'Active')
+        : m('.pf-tag.pf-inactive', 'Inactive'),
+      m(Button, {
+        label: isEnabled ? 'Disable' : 'Enable',
+        intent: Intent.Primary,
+        onclick: () => {
+          if (isEnabled) {
+            plugin.enableFlag.set(false);
+          } else {
+            plugin.enableFlag.set(true);
+          }
+          needsRestart = true;
+          raf.scheduleFullRedraw();
+        },
+      }),
+      exists(loadTime)
+        ? m('span', `${loadTime.toFixed(1)} ms`)
+        : m('span', `-`),
+    ];
+  }
+}
diff --git a/ui/src/core_plugins/frames/actual_frames_track.ts b/ui/src/core_plugins/frames/actual_frames_track.ts
deleted file mode 100644
index 1f3fe4c..0000000
--- a/ui/src/core_plugins/frames/actual_frames_track.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {HSLColor} from '../../core/color';
-import {ColorScheme, makeColorScheme} from '../../core/colorizer';
-import {NAMED_ROW, NamedSliceTrack} from '../../frontend/named_slice_track';
-import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../frontend/slice_layout';
-import {Engine, Slice, STR_NULL} from '../../public';
-
-// 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
-// full jank)
-const BLUE_500 = makeColorScheme(new HSLColor('#03A9F4'));
-const BLUE_200 = makeColorScheme(new HSLColor('#90CAF9'));
-const GREEN_500 = makeColorScheme(new HSLColor('#4CAF50'));
-const GREEN_200 = makeColorScheme(new HSLColor('#A5D6A7'));
-const YELLOW_500 = makeColorScheme(new HSLColor('#FFEB3B'));
-const YELLOW_100 = makeColorScheme(new HSLColor('#FFF9C4'));
-const RED_500 = makeColorScheme(new HSLColor('#FF5722'));
-const RED_200 = makeColorScheme(new HSLColor('#EF9A9A'));
-const LIGHT_GREEN_500 = makeColorScheme(new HSLColor('#C0D588'));
-const LIGHT_GREEN_100 = makeColorScheme(new HSLColor('#DCEDC8'));
-const PINK_500 = makeColorScheme(new HSLColor('#F515E0'));
-const PINK_200 = makeColorScheme(new HSLColor('#F48FB1'));
-
-export const ACTUAL_FRAME_ROW = {
-  // Base columns (tsq, ts, dur, id, depth).
-  ...NAMED_ROW,
-
-  // Jank-specific columns.
-  jankTag: STR_NULL,
-  jankSeverityType: STR_NULL,
-};
-export type ActualFrameRow = typeof ACTUAL_FRAME_ROW;
-
-export class ActualFramesTrack extends NamedSliceTrack<Slice, ActualFrameRow> {
-  constructor(
-    engine: Engine,
-    maxDepth: number,
-    trackKey: string,
-    private trackIds: number[],
-  ) {
-    super({engine, trackKey});
-    this.sliceLayout = {
-      ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
-      depthGuess: maxDepth,
-    };
-  }
-
-  // This is used by the base class to call iter().
-  protected getRowSpec() {
-    return ACTUAL_FRAME_ROW;
-  }
-
-  getSqlSource(): string {
-    return `
-      SELECT
-        s.ts as ts,
-        s.dur as dur,
-        s.layout_depth as depth,
-        s.name as name,
-        s.id as id,
-        afs.jank_tag as jankTag,
-        afs.jank_severity_type as jankSeverityType
-      from experimental_slice_layout s
-      join actual_frame_timeline_slice afs using(id)
-      where
-        filter_track_ids = '${this.trackIds.join(',')}'
-    `;
-  }
-
-  rowToSlice(row: ActualFrameRow): Slice {
-    const baseSlice = this.rowToSliceBase(row);
-    return {
-      ...baseSlice,
-      colorScheme: getColorSchemeForJank(row.jankTag, row.jankSeverityType),
-    };
-  }
-}
-
-function getColorSchemeForJank(
-  jankTag: string | null,
-  jankSeverityType: string | null,
-): ColorScheme {
-  if (jankSeverityType === 'Partial') {
-    switch (jankTag) {
-      case 'Self Jank':
-        return RED_200;
-      case 'Other Jank':
-        return YELLOW_100;
-      case 'Dropped Frame':
-        return BLUE_200;
-      case 'Buffer Stuffing':
-      case 'SurfaceFlinger Stuffing':
-        return LIGHT_GREEN_100;
-      case 'No Jank': // should not happen
-        return GREEN_200;
-      default:
-        return PINK_200;
-    }
-  } else {
-    switch (jankTag) {
-      case 'Self Jank':
-        return RED_500;
-      case 'Other Jank':
-        return YELLOW_500;
-      case 'Dropped Frame':
-        return BLUE_500;
-      case 'Buffer Stuffing':
-      case 'SurfaceFlinger Stuffing':
-        return LIGHT_GREEN_500;
-      case 'No Jank':
-        return GREEN_500;
-      default:
-        return PINK_500;
-    }
-  }
-}
diff --git a/ui/src/core_plugins/frames/expected_frames_track.ts b/ui/src/core_plugins/frames/expected_frames_track.ts
deleted file mode 100644
index ee51060..0000000
--- a/ui/src/core_plugins/frames/expected_frames_track.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {HSLColor} from '../../core/color';
-import {makeColorScheme} from '../../core/colorizer';
-import {
-  NAMED_ROW,
-  NamedRow,
-  NamedSliceTrack,
-} from '../../frontend/named_slice_track';
-import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../frontend/slice_layout';
-import {Engine, Slice} from '../../public';
-
-const GREEN = makeColorScheme(new HSLColor('#4CAF50')); // Green 500
-
-export class ExpectedFramesTrack extends NamedSliceTrack {
-  constructor(
-    engine: Engine,
-    maxDepth: number,
-    trackKey: string,
-    private trackIds: number[],
-  ) {
-    super({engine, trackKey});
-    this.sliceLayout = {
-      ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
-      depthGuess: maxDepth,
-    };
-  }
-
-  getSqlSource(): string {
-    return `
-      SELECT
-        ts,
-        dur,
-        layout_depth as depth,
-        name,
-        id
-      from experimental_slice_layout
-      where
-        filter_track_ids = '${this.trackIds.join(',')}'
-    `;
-  }
-
-  rowToSlice(row: NamedRow): Slice {
-    const baseSlice = this.rowToSliceBase(row);
-    return {...baseSlice, colorScheme: GREEN};
-  }
-
-  getRowSpec(): NamedRow {
-    return NAMED_ROW;
-  }
-}
diff --git a/ui/src/core_plugins/frames/index.ts b/ui/src/core_plugins/frames/index.ts
deleted file mode 100644
index b37134f..0000000
--- a/ui/src/core_plugins/frames/index.ts
+++ /dev/null
@@ -1,153 +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 {
-  ACTUAL_FRAMES_SLICE_TRACK_KIND,
-  EXPECTED_FRAMES_SLICE_TRACK_KIND,
-} from '../../public';
-import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {getTrackName} from '../../public/utils';
-import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
-
-import {ActualFramesTrack} from './actual_frames_track';
-import {ExpectedFramesTrack} from './expected_frames_track';
-
-class FramesPlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    this.addExpectedFrames(ctx);
-    this.addActualFrames(ctx);
-  }
-
-  async addExpectedFrames(ctx: PluginContextTrace): Promise<void> {
-    const {engine} = ctx;
-    const result = await engine.query(`
-      select
-        upid,
-        t.name as trackName,
-        t.track_ids as trackIds,
-        process.name as processName,
-        process.pid as pid,
-        __max_layout_depth(t.track_count, t.track_ids) as maxDepth
-      from _process_track_summary_by_upid_and_name t
-      join process using(upid)
-      where t.name = "Expected Timeline"
-    `);
-
-    const it = result.iter({
-      upid: NUM,
-      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 displayName = getTrackName({
-        name: trackName,
-        upid,
-        pid,
-        processName,
-        kind: 'ExpectedFrames',
-      });
-
-      ctx.registerTrack({
-        uri: `/process_${upid}/expected_frames`,
-        title: displayName,
-        trackFactory: ({trackKey}) => {
-          return new ExpectedFramesTrack(engine, maxDepth, trackKey, trackIds);
-        },
-        tags: {
-          trackIds,
-          upid,
-          kind: EXPECTED_FRAMES_SLICE_TRACK_KIND,
-        },
-      });
-    }
-  }
-
-  async addActualFrames(ctx: PluginContextTrace): Promise<void> {
-    const {engine} = ctx;
-    const result = await engine.query(`
-      select
-        upid,
-        t.name as trackName,
-        t.track_ids as trackIds,
-        process.name as processName,
-        process.pid as pid,
-        __max_layout_depth(t.track_count, t.track_ids) as maxDepth
-      from _process_track_summary_by_upid_and_name t
-      join process using(upid)
-      where t.name = "Actual Timeline"
-    `);
-
-    const it = result.iter({
-      upid: NUM,
-      trackName: STR_NULL,
-      trackIds: STR,
-      processName: STR_NULL,
-      pid: NUM_NULL,
-      maxDepth: NUM_NULL,
-    });
-    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 displayName = getTrackName({
-        name: trackName,
-        upid,
-        pid,
-        processName,
-        kind,
-      });
-
-      ctx.registerTrack({
-        uri: `/process_${upid}/actual_frames`,
-        title: displayName,
-        trackFactory: ({trackKey}) => {
-          return new ActualFramesTrack(engine, maxDepth, trackKey, trackIds);
-        },
-        tags: {
-          upid,
-          trackIds,
-          kind: ACTUAL_FRAMES_SLICE_TRACK_KIND,
-        },
-      });
-    }
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.Frames',
-  plugin: FramesPlugin,
-};
diff --git a/ui/src/core_plugins/ftrace/ftrace_explorer.ts b/ui/src/core_plugins/ftrace/ftrace_explorer.ts
deleted file mode 100644
index 5590a29..0000000
--- a/ui/src/core_plugins/ftrace/ftrace_explorer.ts
+++ /dev/null
@@ -1,330 +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 {time, Time} from '../../base/time';
-import {Actions} from '../../common/actions';
-import {colorForFtrace} from '../../core/colorizer';
-import {DetailsShell} from '../../widgets/details_shell';
-import {
-  MultiSelectDiff,
-  Option as MultiSelectOption,
-  PopupMultiSelect,
-} from '../../widgets/multiselect';
-import {PopupPosition} from '../../widgets/popup';
-
-import {globals} from '../../frontend/globals';
-import {Timestamp} from '../../frontend/widgets/timestamp';
-import {FtraceFilter, FtraceStat} from './common';
-import {Engine, LONG, NUM, Store, STR, STR_NULL} from '../../public';
-import {raf} from '../../core/raf_scheduler';
-import {AsyncLimiter} from '../../base/async_limiter';
-import {Monitor} from '../../base/monitor';
-import {Button} from '../../widgets/button';
-import {VirtualTable, VirtualTableRow} from '../../widgets/virtual_table';
-
-const ROW_H = 20;
-
-interface FtraceExplorerAttrs {
-  cache: FtraceExplorerCache;
-  filterStore: Store<FtraceFilter>;
-  engine: Engine;
-}
-
-interface FtraceEvent {
-  id: number;
-  ts: time;
-  name: string;
-  cpu: number;
-  thread: string | null;
-  process: string | null;
-  args: string;
-}
-
-interface FtracePanelData {
-  events: FtraceEvent[];
-  offset: number;
-  numEvents: number; // Number of events in the visible window
-}
-
-interface Pagination {
-  offset: number;
-  count: number;
-}
-
-export interface FtraceExplorerCache {
-  state: 'blank' | 'loading' | 'valid';
-  counters: FtraceStat[];
-}
-
-async function getFtraceCounters(engine: Engine): Promise<FtraceStat[]> {
-  // TODO(stevegolton): this is an extraordinarily slow query on large traces
-  // as it goes through every ftrace event which can be a lot on big traces.
-  // Consider if we can have some different UX which avoids needing these
-  // counts
-  // TODO(mayzner): the +name below is an awful hack to workaround
-  // extraordinarily slow sorting of strings. However, even with this hack,
-  // this is just a slow query. There are various ways we can improve this
-  // (e.g. with using the vtab_distinct APIs of SQLite).
-  const result = await engine.query(`
-    select
-      name,
-      count(1) as cnt
-    from ftrace_event
-    group by name
-    order by cnt desc
-  `);
-  const counters: FtraceStat[] = [];
-  const it = result.iter({name: STR, cnt: NUM});
-  for (let row = 0; it.valid(); it.next(), row++) {
-    counters.push({name: it.name, count: it.cnt});
-  }
-  return counters;
-}
-
-export class FtraceExplorer implements m.ClassComponent<FtraceExplorerAttrs> {
-  private pagination: Pagination = {
-    offset: 0,
-    count: 0,
-  };
-  private readonly monitor: Monitor;
-  private readonly queryLimiter = new AsyncLimiter();
-
-  // A cache of the data we have most recently loaded from our store
-  private data?: FtracePanelData;
-
-  constructor({attrs}: m.CVnode<FtraceExplorerAttrs>) {
-    this.monitor = new Monitor([
-      () => globals.timeline.visibleWindow.toTimeSpan().start,
-      () => globals.timeline.visibleWindow.toTimeSpan().end,
-      () => attrs.filterStore.state,
-    ]);
-
-    if (attrs.cache.state === 'blank') {
-      getFtraceCounters(attrs.engine)
-        .then((counters) => {
-          attrs.cache.counters = counters;
-          attrs.cache.state = 'valid';
-        })
-        .catch(() => {
-          attrs.cache.state = 'blank';
-        });
-      attrs.cache.state = 'loading';
-    }
-  }
-
-  view({attrs}: m.CVnode<FtraceExplorerAttrs>) {
-    this.monitor.ifStateChanged(() => {
-      this.reloadData(attrs);
-    });
-
-    return m(
-      DetailsShell,
-      {
-        title: this.renderTitle(),
-        buttons: this.renderFilterPanel(attrs),
-        fillParent: true,
-      },
-      m(VirtualTable, {
-        className: 'pf-ftrace-explorer',
-        columns: [
-          {header: 'ID', width: '5em'},
-          {header: 'Timestamp', width: '13em'},
-          {header: 'Name', width: '24em'},
-          {header: 'CPU', width: '3em'},
-          {header: 'Process', width: '24em'},
-          {header: 'Args', width: '200em'},
-        ],
-        firstRowOffset: this.data?.offset ?? 0,
-        numRows: this.data?.numEvents ?? 0,
-        rowHeight: ROW_H,
-        rows: this.renderData(),
-        onReload: (offset, count) => {
-          this.pagination = {offset, count};
-          this.reloadData(attrs);
-        },
-        onRowHover: this.onRowOver.bind(this),
-        onRowOut: this.onRowOut.bind(this),
-      }),
-    );
-  }
-
-  private reloadData(attrs: FtraceExplorerAttrs): void {
-    this.queryLimiter.schedule(async () => {
-      this.data = await lookupFtraceEvents(
-        attrs.engine,
-        this.pagination.offset,
-        this.pagination.count,
-        attrs.filterStore.state,
-      );
-      raf.scheduleFullRedraw();
-    });
-  }
-
-  private renderData(): VirtualTableRow[] {
-    if (!this.data) {
-      return [];
-    }
-
-    return this.data.events.map((event) => {
-      const {ts, name, cpu, process, args, id} = event;
-      const timestamp = m(Timestamp, {ts});
-      const color = colorForFtrace(name).base.cssString;
-
-      return {
-        id,
-        cells: [
-          id,
-          timestamp,
-          m(
-            '.pf-ftrace-namebox',
-            m('.pf-ftrace-colorbox', {style: {background: color}}),
-            name,
-          ),
-          cpu,
-          process,
-          args,
-        ],
-      };
-    });
-  }
-
-  private onRowOver(id: number) {
-    const event = this.data?.events.find((event) => event.id === id);
-    if (event) {
-      globals.dispatch(Actions.setHoverCursorTimestamp({ts: event.ts}));
-    }
-  }
-
-  private onRowOut() {
-    globals.dispatch(Actions.setHoverCursorTimestamp({ts: Time.INVALID}));
-  }
-
-  private renderTitle() {
-    if (this.data) {
-      const {numEvents} = this.data;
-      return `Ftrace Events (${numEvents})`;
-    } else {
-      return 'Ftrace Events';
-    }
-  }
-
-  private renderFilterPanel(attrs: FtraceExplorerAttrs) {
-    if (attrs.cache.state !== 'valid') {
-      return m(Button, {
-        label: 'Filter',
-        disabled: true,
-        loading: true,
-      });
-    }
-
-    const excludeList = attrs.filterStore.state.excludeList;
-    const options: MultiSelectOption[] = attrs.cache.counters.map(
-      ({name, count}) => {
-        return {
-          id: name,
-          name: `${name} (${count})`,
-          checked: !excludeList.some((excluded: string) => excluded === name),
-        };
-      },
-    );
-
-    return m(PopupMultiSelect, {
-      label: 'Filter',
-      icon: 'filter_list_alt',
-      popupPosition: PopupPosition.Top,
-      options,
-      onChange: (diffs: MultiSelectDiff[]) => {
-        const newList = new Set<string>(excludeList);
-        diffs.forEach(({checked, id}) => {
-          if (checked) {
-            newList.delete(id);
-          } else {
-            newList.add(id);
-          }
-        });
-        attrs.filterStore.edit((draft) => {
-          draft.excludeList = Array.from(newList);
-        });
-      },
-    });
-  }
-}
-
-async function lookupFtraceEvents(
-  engine: Engine,
-  offset: number,
-  count: number,
-  filter: FtraceFilter,
-): Promise<FtracePanelData> {
-  const {start, end} = globals.timeline.visibleWindow.toTimeSpan();
-
-  const excludeList = filter.excludeList;
-  const excludeListSql = excludeList.map((s) => `'${s}'`).join(',');
-
-  // TODO(stevegolton): This query can be slow when traces are huge.
-  // The number of events is only used for correctly sizing the panel's
-  // scroll container so that the scrollbar works as if the panel were fully
-  // populated.
-  // Perhaps we could work out some UX that doesn't need this.
-  let queryRes = await engine.query(`
-    select count(id) as numEvents
-    from ftrace_event
-    where
-      ftrace_event.name not in (${excludeListSql}) and
-      ts >= ${start} and ts <= ${end}
-    `);
-  const {numEvents} = queryRes.firstRow({numEvents: NUM});
-
-  queryRes = await engine.query(`
-    select
-      ftrace_event.id as id,
-      ftrace_event.ts as ts,
-      ftrace_event.name as name,
-      ftrace_event.cpu as cpu,
-      thread.name as thread,
-      process.name as process,
-      to_ftrace(ftrace_event.id) as args
-    from ftrace_event
-    join thread using (utid)
-    left join process on thread.upid = process.upid
-    where
-      ftrace_event.name not in (${excludeListSql}) and
-      ts >= ${start} and ts <= ${end}
-    order by id
-    limit ${count} offset ${offset};`);
-  const events: FtraceEvent[] = [];
-  const it = queryRes.iter({
-    id: NUM,
-    ts: LONG,
-    name: STR,
-    cpu: NUM,
-    thread: STR_NULL,
-    process: STR_NULL,
-    args: STR,
-  });
-  for (let row = 0; it.valid(); it.next(), row++) {
-    events.push({
-      id: it.id,
-      ts: Time.fromRaw(it.ts),
-      name: it.name,
-      cpu: it.cpu,
-      thread: it.thread,
-      process: it.process,
-      args: it.args,
-    });
-  }
-  return {events, offset, numEvents};
-}
diff --git a/ui/src/core_plugins/ftrace/ftrace_track.ts b/ui/src/core_plugins/ftrace/ftrace_track.ts
deleted file mode 100644
index 4a2aa3d..0000000
--- a/ui/src/core_plugins/ftrace/ftrace_track.ts
+++ /dev/null
@@ -1,145 +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 {duration, Time, time} from '../../base/time';
-import {colorForFtrace} from '../../core/colorizer';
-import {LIMIT} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {TrackData} from '../../common/track_data';
-import {Engine, Track} from '../../public';
-import {LONG, STR} from '../../trace_processor/query_result';
-import {FtraceFilter} from './common';
-import {Store} from '../../public';
-import {Monitor} from '../../base/monitor';
-import {TrackRenderContext} from '../../public/tracks';
-
-const MARGIN = 2;
-const RECT_HEIGHT = 18;
-const TRACK_HEIGHT = RECT_HEIGHT + 2 * MARGIN;
-
-export interface Data extends TrackData {
-  timestamps: BigInt64Array;
-  names: string[];
-}
-
-export interface Config {
-  cpu?: number;
-}
-
-export class FtraceRawTrack implements Track {
-  private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
-  private engine: Engine;
-  private cpu: number;
-  private store: Store<FtraceFilter>;
-  private readonly monitor: Monitor;
-
-  constructor(engine: Engine, cpu: number, store: Store<FtraceFilter>) {
-    this.engine = engine;
-    this.cpu = cpu;
-    this.store = store;
-
-    this.monitor = new Monitor([() => store.state]);
-  }
-
-  async onUpdate({
-    visibleWindow,
-    resolution,
-  }: TrackRenderContext): Promise<void> {
-    this.monitor.ifStateChanged(() => {
-      this.fetcher.invalidate();
-    });
-    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
-  }
-
-  async onDestroy?(): Promise<void> {
-    this.fetcher[Symbol.dispose]();
-  }
-
-  getHeight(): number {
-    return TRACK_HEIGHT;
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    const excludeList = Array.from(this.store.state.excludeList);
-    const excludeListSql = excludeList.map((s) => `'${s}'`).join(',');
-    const cpuFilter = this.cpu === undefined ? '' : `and cpu = ${this.cpu}`;
-
-    const queryRes = await this.engine.query(`
-      select
-        cast(ts / ${resolution} as integer) * ${resolution} as tsQuant,
-        name
-      from ftrace_event
-      where
-        name not in (${excludeListSql}) and
-        ts >= ${start} and ts <= ${end} ${cpuFilter}
-      group by tsQuant
-      order by tsQuant limit ${LIMIT};`);
-
-    const rowCount = queryRes.numRows();
-    const result: Data = {
-      start,
-      end,
-      resolution,
-      length: rowCount,
-      timestamps: new BigInt64Array(rowCount),
-      names: [],
-    };
-
-    const it = queryRes.iter({tsQuant: LONG, name: STR});
-    for (let row = 0; it.valid(); it.next(), row++) {
-      result.timestamps[row] = it.tsQuant;
-      result.names[row] = it.name;
-    }
-    return result;
-  }
-
-  render({ctx, size, timescale}: TrackRenderContext): void {
-    const data = this.fetcher.data;
-
-    if (data === undefined) return; // Can't possibly draw anything.
-
-    const dataStartPx = timescale.timeToPx(data.start);
-    const dataEndPx = timescale.timeToPx(data.end);
-
-    checkerboardExcept(
-      ctx,
-      this.getHeight(),
-      0,
-      size.width,
-      dataStartPx,
-      dataEndPx,
-    );
-
-    const diamondSideLen = RECT_HEIGHT / Math.sqrt(2);
-
-    for (let i = 0; i < data.timestamps.length; i++) {
-      const name = data.names[i];
-      ctx.fillStyle = colorForFtrace(name).base.cssString;
-      const timestamp = Time.fromRaw(data.timestamps[i]);
-      const xPos = Math.floor(timescale.timeToPx(timestamp));
-
-      // Draw a diamond over the event
-      ctx.save();
-      ctx.translate(xPos, MARGIN);
-      ctx.rotate(Math.PI / 4);
-      ctx.fillRect(0, 0, diamondSideLen, diamondSideLen);
-      ctx.restore();
-    }
-  }
-}
diff --git a/ui/src/core_plugins/ftrace/index.ts b/ui/src/core_plugins/ftrace/index.ts
deleted file mode 100644
index 865a98f..0000000
--- a/ui/src/core_plugins/ftrace/index.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {FtraceExplorer, FtraceExplorerCache} from './ftrace_explorer';
-import {
-  Engine,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
-import {NUM} from '../../trace_processor/query_result';
-
-import {FtraceFilter, FtracePluginState} from './common';
-import {FtraceRawTrack} from './ftrace_track';
-import {DisposableStack} from '../../base/disposable_stack';
-
-const VERSION = 1;
-
-const DEFAULT_STATE: FtracePluginState = {
-  version: VERSION,
-  filter: {
-    excludeList: [],
-  },
-};
-
-class FtraceRawPlugin implements Plugin {
-  private trash = new DisposableStack();
-
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const store = ctx.mountStore<FtracePluginState>((init: unknown) => {
-      if (
-        typeof init === 'object' &&
-        init !== null &&
-        'version' in init &&
-        init.version === VERSION
-      ) {
-        return init as {} as FtracePluginState;
-      } else {
-        return DEFAULT_STATE;
-      }
-    });
-    this.trash.use(store);
-
-    const filterStore = store.createSubStore(
-      ['filter'],
-      (x) => x as FtraceFilter,
-    );
-    this.trash.use(filterStore);
-
-    const cpus = await this.lookupCpuCores(ctx.engine);
-    for (const cpuNum of cpus) {
-      const uri = `/ftrace/cpu${cpuNum}`;
-
-      ctx.registerStaticTrack({
-        uri,
-        groupName: 'Ftrace Events',
-        title: `Ftrace Track for CPU ${cpuNum}`,
-        tags: {
-          cpu: cpuNum,
-        },
-        trackFactory: () => {
-          return new FtraceRawTrack(ctx.engine, cpuNum, filterStore);
-        },
-      });
-    }
-
-    const cache: FtraceExplorerCache = {
-      state: 'blank',
-      counters: [],
-    };
-
-    const ftraceTabUri = 'perfetto.FtraceRaw#FtraceEventsTab';
-
-    ctx.registerTab({
-      uri: ftraceTabUri,
-      isEphemeral: false,
-      content: {
-        render: () =>
-          m(FtraceExplorer, {
-            filterStore,
-            cache,
-            engine: ctx.engine,
-          }),
-        getTitle: () => 'Ftrace Events',
-      },
-    });
-
-    ctx.registerCommand({
-      id: 'perfetto.FtraceRaw#ShowFtraceTab',
-      name: 'Show ftrace tab',
-      callback: () => {
-        ctx.tabs.showTab(ftraceTabUri);
-      },
-    });
-  }
-
-  async onTraceUnload(): Promise<void> {
-    this.trash[Symbol.dispose]();
-  }
-
-  private async lookupCpuCores(engine: Engine): Promise<number[]> {
-    const query = 'select distinct cpu from ftrace_event order by cpu';
-
-    const result = await engine.query(query);
-    const it = result.iter({cpu: NUM});
-
-    const cpuCores: number[] = [];
-
-    for (; it.valid(); it.next()) {
-      cpuCores.push(it.cpu);
-    }
-
-    return cpuCores;
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.FtraceRaw',
-  plugin: FtraceRawPlugin,
-};
diff --git a/ui/src/core_plugins/global_groups/index.ts b/ui/src/core_plugins/global_groups/index.ts
new file mode 100644
index 0000000..6c9243d
--- /dev/null
+++ b/ui/src/core_plugins/global_groups/index.ts
@@ -0,0 +1,251 @@
+// 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 {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.addEventListener('traceready', () => {
+      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) => {
+          // Get the index in the order array
+          const indexA = a.hasChildren ? 1 : 0;
+          const indexB = b.hasChildren ? 1 : 0;
+          return indexA - indexB;
+        })
+        .forEach((n) => trace.workspace.addChildLast(n));
+
+      // If there is only one group, expand it
+      const rootLevelChildren = trace.workspace.children;
+      if (rootLevelChildren.length === 1 && rootLevelChildren[0].hasChildren) {
+        rootLevelChildren[0].expand();
+      }
+    });
+  }
+}
+
+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 group = new TrackNode({title: 'Ion Tracks'});
+  group.isSummary = true;
+  trace.workspace.addChildInOrder(group);
+
+  for (const track of ionTracks) {
+    if ([MEM_DMA_COUNTER_NAME, MEM_ION].includes(track.title)) {
+      trace.workspace.removeChild(track);
+      group.uri = track.uri;
+      group.title = track.title;
+    } else {
+      group.addChildInOrder(track);
+    }
+  }
+}
+
+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/core_plugins/heap_profile/heap_profile_track.ts b/ui/src/core_plugins/heap_profile/heap_profile_track.ts
deleted file mode 100644
index 12311dc..0000000
--- a/ui/src/core_plugins/heap_profile/heap_profile_track.ts
+++ /dev/null
@@ -1,130 +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 {Actions} from '../../common/actions';
-import {LegacySelection, ProfileType} from '../../common/state';
-import {
-  BASE_ROW,
-  BaseSliceTrack,
-  OnSliceClickArgs,
-  OnSliceOverArgs,
-} from '../../frontend/base_slice_track';
-import {globals} from '../../frontend/globals';
-import {profileType} from '../../frontend/legacy_flamegraph_panel';
-import {NewTrackArgs} from '../../frontend/track';
-import {Slice} from '../../public';
-import {STR} from '../../trace_processor/query_result';
-import {createPerfettoTable} from '../../trace_processor/sql_utils';
-
-const HEAP_PROFILE_ROW = {
-  ...BASE_ROW,
-  type: STR,
-};
-type HeapProfileRow = typeof HEAP_PROFILE_ROW;
-interface HeapProfileSlice extends Slice {
-  type: ProfileType;
-}
-
-export class HeapProfileTrack extends BaseSliceTrack<
-  HeapProfileSlice,
-  HeapProfileRow
-> {
-  private upid: number;
-
-  constructor(args: NewTrackArgs, upid: number) {
-    super(args);
-    this.upid = upid;
-  }
-
-  async onInit() {
-    return createPerfettoTable(
-      this.engine,
-      `_heap_profile_track_${this.trackUuid}`,
-      `
-      with
-        heaps as (select group_concat(distinct heap_name) h from heap_profile_allocation where upid = ${this.upid}),
-        allocation_tses as (select distinct ts from heap_profile_allocation where upid = ${this.upid}),
-        graph_tses as (select distinct graph_sample_ts from heap_graph_object where upid = ${this.upid})
-      select
-        *,
-        0 AS dur,
-        0 AS depth
-      from (
-        select
-          (
-            select a.id
-            from heap_profile_allocation a
-            where a.ts = t.ts
-            order by a.id
-            limit 1
-          ) as id,
-          ts,
-          'heap_profile:' || (select h from heaps) AS type
-        from allocation_tses t
-        union all
-        select
-          (
-            select o.id
-            from heap_graph_object o
-            where o.graph_sample_ts = g.graph_sample_ts
-            order by o.id
-            limit 1
-          ) as id,
-          graph_sample_ts AS ts,
-          'graph' AS type
-        from graph_tses g
-      )
-    `,
-    );
-  }
-
-  getSqlSource(): string {
-    return `_heap_profile_track_${this.trackUuid}`;
-  }
-
-  getRowSpec(): HeapProfileRow {
-    return HEAP_PROFILE_ROW;
-  }
-
-  rowToSlice(row: HeapProfileRow): HeapProfileSlice {
-    const slice = this.rowToSliceBase(row);
-    let type = row.type;
-    if (type === 'heap_profile:libc.malloc,com.android.art') {
-      type = 'heap_profile:com.android.art,libc.malloc';
-    }
-    return {
-      ...slice,
-      type: profileType(type),
-    };
-  }
-
-  onSliceOver(args: OnSliceOverArgs<HeapProfileSlice>) {
-    args.tooltip = [args.slice.type];
-  }
-
-  onSliceClick(args: OnSliceClickArgs<HeapProfileSlice>) {
-    globals.makeSelection(
-      Actions.selectHeapProfile({
-        id: args.slice.id,
-        upid: this.upid,
-        ts: args.slice.ts,
-        type: args.slice.type,
-      }),
-    );
-  }
-
-  protected isSelectionHandled(selection: LegacySelection): boolean {
-    return selection.kind === 'HEAP_PROFILE';
-  }
-}
diff --git a/ui/src/core_plugins/heap_profile/index.ts b/ui/src/core_plugins/heap_profile/index.ts
deleted file mode 100644
index f0ea4c7..0000000
--- a/ui/src/core_plugins/heap_profile/index.ts
+++ /dev/null
@@ -1,456 +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 m from 'mithril';
-
-import {assertExists, assertFalse} from '../../base/logging';
-import {Monitor} from '../../base/monitor';
-import {LegacyFlamegraphCache} from '../../core/legacy_flamegraph_cache';
-import {
-  HeapProfileSelection,
-  LegacySelection,
-  ProfileType,
-} from '../../core/selection_manager';
-import {
-  LegacyFlamegraphDetailsPanel,
-  profileType,
-} from '../../frontend/legacy_flamegraph_panel';
-import {Timestamp} from '../../frontend/widgets/timestamp';
-import {
-  Engine,
-  HEAP_PROFILE_TRACK_KIND,
-  LegacyDetailsPanel,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
-import {NUM} from '../../trace_processor/query_result';
-import {DetailsShell} from '../../widgets/details_shell';
-
-import {HeapProfileTrack} from './heap_profile_track';
-import {
-  QueryFlamegraph,
-  QueryFlamegraphAttrs,
-  USE_NEW_FLAMEGRAPH_IMPL,
-  metricsFromTableOrSubquery,
-} from '../../core/query_flamegraph';
-import {time} from '../../base/time';
-import {Popup} from '../../widgets/popup';
-import {Icon} from '../../widgets/icon';
-import {Button} from '../../widgets/button';
-import {Intent} from '../../widgets/common';
-import {getCurrentTrace} from '../../frontend/sidebar';
-import {convertTraceToPprofAndDownload} from '../../frontend/trace_converter';
-import {raf} from '../../core/raf_scheduler';
-import {globals} from '../../frontend/globals';
-import {Modal} from '../../widgets/modal';
-import {Router} from '../../frontend/router';
-import {Actions} from '../../common/actions';
-import {SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG} from '../../common/legacy_flamegraph_util';
-
-class HeapProfilePlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const result = await ctx.engine.query(`
-      select distinct upid from heap_profile_allocation
-      union
-      select distinct upid from heap_graph_object
-    `);
-    for (const it = result.iter({upid: NUM}); it.valid(); it.next()) {
-      const upid = it.upid;
-      ctx.registerTrack({
-        uri: `/process_${upid}/heap_profile`,
-        title: 'Heap Profile',
-        tags: {
-          kind: HEAP_PROFILE_TRACK_KIND,
-          upid,
-        },
-        trackFactory: ({trackKey}) => {
-          return new HeapProfileTrack(
-            {
-              engine: ctx.engine,
-              trackKey,
-            },
-            upid,
-          );
-        },
-      });
-    }
-    const it = await ctx.engine.query(`
-      select value from stats
-      where name = 'heap_graph_non_finalized_graph'
-    `);
-    const incomplete = it.firstRow({value: NUM}).value > 0;
-    ctx.registerDetailsPanel(
-      new HeapProfileFlamegraphDetailsPanel(ctx.engine, incomplete),
-    );
-  }
-}
-
-class HeapProfileFlamegraphDetailsPanel implements LegacyDetailsPanel {
-  private sel?: HeapProfileSelection;
-  private selMonitor = new Monitor([
-    () => this.sel?.ts,
-    () => this.sel?.upid,
-    () => this.sel?.type,
-  ]);
-  private flamegraphAttrs?: QueryFlamegraphAttrs;
-  private cache = new LegacyFlamegraphCache('heap_profile');
-
-  constructor(
-    private engine: Engine,
-    private heapGraphIncomplete: boolean,
-  ) {}
-
-  render(sel: LegacySelection) {
-    if (sel.kind !== 'HEAP_PROFILE') {
-      this.sel = undefined;
-      return undefined;
-    }
-    if (!USE_NEW_FLAMEGRAPH_IMPL.get()) {
-      this.sel = undefined;
-      return m(LegacyFlamegraphDetailsPanel, {
-        cache: this.cache,
-        selection: {
-          profileType: profileType(sel.type),
-          start: sel.ts,
-          end: sel.ts,
-          upids: [sel.upid],
-        },
-      });
-    }
-
-    const {ts, upid, type} = sel;
-    this.sel = sel;
-    if (this.selMonitor.ifStateChanged()) {
-      this.flamegraphAttrs = flamegraphAttrs(this.engine, ts, upid, type);
-    }
-    return m(
-      '.flamegraph-profile',
-      maybeShowModal(type, this.heapGraphIncomplete),
-      m(
-        DetailsShell,
-        {
-          fillParent: true,
-          title: m(
-            '.title',
-            getFlamegraphTitle(type),
-            sel.type === ProfileType.MIXED_HEAP_PROFILE &&
-              m(
-                Popup,
-                {
-                  trigger: m(Icon, {icon: 'warning'}),
-                },
-                m(
-                  '',
-                  {style: {width: '300px'}},
-                  'This is a mixed java/native heap profile, free()s are not visualized. To visualize free()s, remove "all_heaps: true" from the config.',
-                ),
-              ),
-          ),
-          description: [],
-          buttons: [
-            m('.time', `Snapshot time: `, m(Timestamp, {ts})),
-            (sel.type === ProfileType.NATIVE_HEAP_PROFILE ||
-              sel.type === ProfileType.JAVA_HEAP_SAMPLES) &&
-              m(Button, {
-                icon: 'file_download',
-                intent: Intent.Primary,
-                onclick: () => {
-                  downloadPprof(this.engine, upid, ts);
-                  raf.scheduleFullRedraw();
-                },
-              }),
-          ],
-        },
-        m(QueryFlamegraph, assertExists(this.flamegraphAttrs)),
-      ),
-    );
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.HeapProfile',
-  plugin: HeapProfilePlugin,
-};
-
-function flamegraphAttrs(
-  engine: Engine,
-  ts: time,
-  upid: number,
-  type: ProfileType,
-): QueryFlamegraphAttrs {
-  switch (type) {
-    case ProfileType.NATIVE_HEAP_PROFILE:
-      return flamegraphAttrsForHeapProfile(engine, ts, upid, [
-        {
-          name: 'Unreleased Malloc Size',
-          unit: 'B',
-          columnName: 'self_size',
-        },
-        {
-          name: 'Unreleased Malloc Count',
-          unit: '',
-          columnName: 'self_count',
-        },
-        {
-          name: 'Total Malloc Size',
-          unit: 'B',
-          columnName: 'self_alloc_size',
-        },
-        {
-          name: 'Total Malloc Count',
-          unit: '',
-          columnName: 'self_alloc_count',
-        },
-      ]);
-    case ProfileType.HEAP_PROFILE:
-      return flamegraphAttrsForHeapProfile(engine, ts, upid, [
-        {
-          name: 'Unreleased Size',
-          unit: 'B',
-          columnName: 'self_size',
-        },
-        {
-          name: 'Unreleased Count',
-          unit: '',
-          columnName: 'self_count',
-        },
-        {
-          name: 'Total Size',
-          unit: 'B',
-          columnName: 'self_alloc_size',
-        },
-        {
-          name: 'Total Count',
-          unit: '',
-          columnName: 'self_alloc_count',
-        },
-      ]);
-    case ProfileType.JAVA_HEAP_SAMPLES:
-      return flamegraphAttrsForHeapProfile(engine, ts, upid, [
-        {
-          name: 'Unreleased Allocation Size',
-          unit: 'B',
-          columnName: 'self_size',
-        },
-        {
-          name: 'Unreleased Allocation Count',
-          unit: '',
-          columnName: 'self_count',
-        },
-      ]);
-    case ProfileType.MIXED_HEAP_PROFILE:
-      return flamegraphAttrsForHeapProfile(engine, ts, upid, [
-        {
-          name: 'Unreleased Allocation Size (malloc + java)',
-          unit: 'B',
-          columnName: 'self_size',
-        },
-        {
-          name: 'Unreleased Allocation Count (malloc + java)',
-          unit: '',
-          columnName: 'self_count',
-        },
-      ]);
-    case ProfileType.JAVA_HEAP_GRAPH:
-      return flamegraphAttrsForHeapGraph(engine, ts, upid);
-    case ProfileType.PERF_SAMPLE:
-      assertFalse(false, 'Perf sample not supported');
-      return {engine, metrics: []};
-  }
-}
-
-function flamegraphAttrsForHeapProfile(
-  engine: Engine,
-  ts: time,
-  upid: number,
-  metrics: {name: string; unit: string; columnName: string}[],
-) {
-  return {
-    engine,
-    metrics: [
-      ...metricsFromTableOrSubquery(
-        `
-          (
-            select
-              id,
-              parent_id as parentId,
-              name,
-              mapping_name,
-              source_file,
-              cast(line_number AS text) as line_number,
-              self_size,
-              self_count,
-              self_alloc_size,
-              self_alloc_count
-            from _android_heap_profile_callstacks_for_allocations!((
-              select
-                callsite_id,
-                size,
-                count,
-                max(size, 0) as alloc_size,
-                max(count, 0) as alloc_count
-              from heap_profile_allocation a
-              where a.ts <= ${ts} and a.upid = ${upid}
-            ))
-          )
-        `,
-        metrics,
-        'include perfetto module android.memory.heap_profile.callstacks',
-        [{name: 'mapping_name', displayName: 'Mapping'}],
-        [
-          {name: 'source_file', displayName: 'Source File'},
-          {name: 'line_number', displayName: 'Line Number'},
-        ],
-      ),
-    ],
-  };
-}
-
-function flamegraphAttrsForHeapGraph(engine: Engine, ts: time, upid: number) {
-  const dominator = SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG.get()
-    ? metricsFromTableOrSubquery(
-        `
-          (
-            select
-              id,
-              parent_id as parentId,
-              name,
-              root_type,
-              self_size,
-              self_count
-            from _heap_graph_dominator_class_tree
-            where graph_sample_ts = ${ts} and upid = ${upid}
-          )
-        `,
-        [
-          {
-            name: 'Dominated Object Size',
-            unit: 'B',
-            columnName: 'self_size',
-          },
-          {
-            name: 'Dominated Object Count',
-            unit: '',
-            columnName: 'self_count',
-          },
-        ],
-        'include perfetto module android.memory.heap_graph.dominator_class_tree;',
-        [{name: 'root_type', displayName: 'Root Type'}],
-      )
-    : [];
-  return {
-    engine,
-    metrics: [
-      ...metricsFromTableOrSubquery(
-        `
-          (
-            select
-              id,
-              parent_id as parentId,
-              name,
-              root_type,
-              self_size,
-              self_count
-            from _heap_graph_class_tree
-            where graph_sample_ts = ${ts} and upid = ${upid}
-          )
-        `,
-        [
-          {
-            name: 'Object Size',
-            unit: 'B',
-            columnName: 'self_size',
-          },
-          {
-            name: 'Object Count',
-            unit: '',
-            columnName: 'self_count',
-          },
-        ],
-        'include perfetto module android.memory.heap_graph.class_tree;',
-        [{name: 'root_type', displayName: 'Root Type'}],
-      ),
-      ...dominator,
-    ],
-  };
-}
-
-function getFlamegraphTitle(type: ProfileType) {
-  switch (type) {
-    case ProfileType.HEAP_PROFILE:
-      return 'Heap profile';
-    case ProfileType.JAVA_HEAP_GRAPH:
-      return 'Java heap graph';
-    case ProfileType.JAVA_HEAP_SAMPLES:
-      return 'Java heap samples';
-    case ProfileType.MIXED_HEAP_PROFILE:
-      return 'Mixed heap profile';
-    case ProfileType.NATIVE_HEAP_PROFILE:
-      return 'Native heap profile';
-    case ProfileType.PERF_SAMPLE:
-      assertFalse(false, 'Perf sample not supported');
-      return 'Impossible';
-  }
-}
-
-async function downloadPprof(
-  engine: Engine | undefined,
-  upid: number,
-  ts: time,
-) {
-  if (engine === undefined) {
-    return;
-  }
-  try {
-    const pid = await engine.query(
-      `select pid from process where upid = ${upid}`,
-    );
-    const trace = await getCurrentTrace();
-    convertTraceToPprofAndDownload(trace, pid.firstRow({pid: NUM}).pid, ts);
-  } catch (error) {
-    throw new Error(`Failed to get current trace ${error}`);
-  }
-}
-
-function maybeShowModal(type: ProfileType, heapGraphIncomplete: boolean) {
-  if (type !== ProfileType.JAVA_HEAP_GRAPH || !heapGraphIncomplete) {
-    return undefined;
-  }
-  if (globals.state.flamegraphModalDismissed) {
-    return undefined;
-  }
-  return m(Modal, {
-    title: 'The flamegraph is incomplete',
-    vAlign: 'TOP',
-    content: m(
-      'div',
-      'The current trace does not have a fully formed flamegraph',
-    ),
-    buttons: [
-      {
-        text: 'Show the errors',
-        primary: true,
-        action: () => Router.navigate('#!/info'),
-      },
-      {
-        text: 'Skip',
-        action: () => {
-          globals.dispatch(Actions.dismissFlamegraphModal({}));
-          raf.scheduleFullRedraw();
-        },
-      },
-    ],
-  });
-}
diff --git a/ui/src/core_plugins/perf_samples_profile/index.ts b/ui/src/core_plugins/perf_samples_profile/index.ts
deleted file mode 100644
index ddac586..0000000
--- a/ui/src/core_plugins/perf_samples_profile/index.ts
+++ /dev/null
@@ -1,257 +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 m from 'mithril';
-
-import {TrackData} from '../../common/track_data';
-import {
-  Engine,
-  LegacyDetailsPanel,
-  PERF_SAMPLES_PROFILE_TRACK_KIND,
-} from '../../public';
-import {LegacyFlamegraphCache} from '../../core/legacy_flamegraph_cache';
-import {
-  LegacyFlamegraphDetailsPanel,
-  profileType,
-} from '../../frontend/legacy_flamegraph_panel';
-import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
-import {
-  LegacySelection,
-  PerfSamplesSelection,
-} from '../../core/selection_manager';
-import {
-  QueryFlamegraph,
-  QueryFlamegraphAttrs,
-  USE_NEW_FLAMEGRAPH_IMPL,
-  metricsFromTableOrSubquery,
-} from '../../core/query_flamegraph';
-import {Monitor} from '../../base/monitor';
-import {DetailsShell} from '../../widgets/details_shell';
-import {assertExists} from '../../base/logging';
-import {Timestamp} from '../../frontend/widgets/timestamp';
-import {
-  ProcessPerfSamplesProfileTrack,
-  ThreadPerfSamplesProfileTrack,
-} from './perf_samples_profile_track';
-import {getThreadUriPrefix} from '../../public/utils';
-
-export interface Data extends TrackData {
-  tsStarts: BigInt64Array;
-}
-
-class PerfSamplesProfilePlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const pResult = await ctx.engine.query(`
-      select distinct upid
-      from perf_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;
-      ctx.registerTrack({
-        uri: `/process_${upid}/perf_samples_profile`,
-        title: `Process Callstacks`,
-        tags: {
-          kind: PERF_SAMPLES_PROFILE_TRACK_KIND,
-          upid,
-        },
-        trackFactory: ({trackKey}) =>
-          new ProcessPerfSamplesProfileTrack(
-            {
-              engine: ctx.engine,
-              trackKey,
-            },
-            upid,
-          ),
-      });
-    }
-    const tResult = await ctx.engine.query(`
-      select distinct
-        utid,
-        tid,
-        thread.name as threadName,
-        upid
-      from perf_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 displayName =
-        threadName === null
-          ? `Thread Callstacks ${tid}`
-          : `${threadName} Callstacks ${tid}`;
-      ctx.registerTrack({
-        uri: `${getThreadUriPrefix(upid, utid)}_perf_samples_profile`,
-        title: displayName,
-        tags: {
-          kind: PERF_SAMPLES_PROFILE_TRACK_KIND,
-          utid,
-          upid: upid ?? undefined,
-        },
-        trackFactory: ({trackKey}) =>
-          new ThreadPerfSamplesProfileTrack(
-            {
-              engine: ctx.engine,
-              trackKey,
-            },
-            utid,
-          ),
-      });
-    }
-    ctx.registerDetailsPanel(new PerfSamplesFlamegraphDetailsPanel(ctx.engine));
-  }
-}
-
-class PerfSamplesFlamegraphDetailsPanel implements LegacyDetailsPanel {
-  private sel?: PerfSamplesSelection;
-  private selMonitor = new Monitor([
-    () => this.sel?.leftTs,
-    () => this.sel?.rightTs,
-    () => this.sel?.upid,
-    () => this.sel?.type,
-  ]);
-  private flamegraphAttrs?: QueryFlamegraphAttrs;
-  private cache = new LegacyFlamegraphCache('perf_samples');
-
-  constructor(private engine: Engine) {}
-
-  render(sel: LegacySelection) {
-    if (sel.kind !== 'PERF_SAMPLES') {
-      this.sel = undefined;
-      return undefined;
-    }
-    if (!USE_NEW_FLAMEGRAPH_IMPL.get() && sel.upid !== undefined) {
-      this.sel = undefined;
-      return m(LegacyFlamegraphDetailsPanel, {
-        cache: this.cache,
-        selection: {
-          profileType: profileType(sel.type),
-          start: sel.leftTs,
-          end: sel.rightTs,
-          upids: [sel.upid],
-        },
-      });
-    }
-
-    const {leftTs, rightTs, upid, utid} = sel;
-    this.sel = sel;
-    if (this.selMonitor.ifStateChanged()) {
-      this.flamegraphAttrs = {
-        engine: this.engine,
-        metrics: [
-          ...metricsFromTableOrSubquery(
-            upid === undefined
-              ? `
-                (
-                  select
-                    id,
-                    parent_id as parentId,
-                    name,
-                    mapping_name,
-                    source_file,
-                    cast(line_number AS text) as line_number,
-                    self_count
-                  from _linux_perf_callstacks_for_samples!((
-                    select p.callsite_id
-                    from perf_sample p
-                    where p.ts >= ${leftTs}
-                      and p.ts <= ${rightTs}
-                      and p.utid = ${utid}
-                  ))
-                )
-              `
-              : `
-                  (
-                    select
-                      id,
-                      parent_id as parentId,
-                      name,
-                      mapping_name,
-                      source_file,
-                      cast(line_number AS text) as line_number,
-                      self_count
-                    from _linux_perf_callstacks_for_samples!((
-                      select p.callsite_id
-                      from perf_sample p
-                      join thread t using (utid)
-                      where p.ts >= ${leftTs}
-                        and p.ts <= ${rightTs}
-                        and t.upid = ${upid}
-                    ))
-                  )
-                `,
-            [
-              {
-                name: 'Perf Samples',
-                unit: '',
-                columnName: 'self_count',
-              },
-            ],
-            'include perfetto module linux.perf.samples',
-            [{name: 'mapping_name', displayName: 'Mapping'}],
-            [
-              {name: 'source_file', displayName: 'Source File'},
-              {name: 'line_number', displayName: 'Line Number'},
-            ],
-          ),
-        ],
-      };
-    }
-    return m(
-      '.flamegraph-profile',
-      m(
-        DetailsShell,
-        {
-          fillParent: true,
-          title: m('.title', 'Perf Samples'),
-          description: [],
-          buttons: [
-            m(
-              'div.time',
-              `First timestamp: `,
-              m(Timestamp, {
-                ts: this.sel.leftTs,
-              }),
-            ),
-            m(
-              'div.time',
-              `Last timestamp: `,
-              m(Timestamp, {
-                ts: this.sel.rightTs,
-              }),
-            ),
-          ],
-        },
-        m(QueryFlamegraph, assertExists(this.flamegraphAttrs)),
-      ),
-    );
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.PerfSamplesProfile',
-  plugin: PerfSamplesProfilePlugin,
-};
diff --git a/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts b/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
deleted file mode 100644
index 0e43da4..0000000
--- a/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
+++ /dev/null
@@ -1,119 +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 {Slice} from '../../public';
-import {
-  BaseSliceTrack,
-  OnSliceClickArgs,
-} from '../../frontend/base_slice_track';
-import {NewTrackArgs} from '../../frontend/track';
-import {NAMED_ROW, NamedRow} from '../../frontend/named_slice_track';
-import {getColorForSlice} from '../../core/colorizer';
-import {Time} from '../../base/time';
-import {globals} from '../../frontend/globals';
-import {Actions} from '../../common/actions';
-import {LegacySelection, ProfileType} from '../../core/selection_manager';
-
-abstract class BasePerfSamplesProfileTrack extends BaseSliceTrack<
-  Slice,
-  NamedRow
-> {
-  constructor(args: NewTrackArgs) {
-    super(args);
-  }
-
-  protected getRowSpec(): NamedRow {
-    return NAMED_ROW;
-  }
-
-  protected rowToSlice(row: NamedRow): Slice {
-    const baseSlice = super.rowToSliceBase(row);
-    const name = row.name ?? '';
-    const colorScheme = getColorForSlice(name);
-    return {...baseSlice, title: name, colorScheme};
-  }
-
-  isSelectionHandled(selection: LegacySelection): boolean {
-    return selection.kind === 'PERF_SAMPLES';
-  }
-
-  onUpdatedSlices(slices: Slice[]) {
-    for (const slice of slices) {
-      slice.isHighlighted = slice === this.hoveredSlice;
-    }
-  }
-}
-
-export class ProcessPerfSamplesProfileTrack extends BasePerfSamplesProfileTrack {
-  constructor(
-    args: NewTrackArgs,
-    private upid: number,
-  ) {
-    super(args);
-  }
-
-  getSqlSource(): string {
-    return `
-      select p.id, ts, 0 as dur, 0 as depth, 'Perf Sample' as name
-      from perf_sample p
-      join thread using (utid)
-      where upid = ${this.upid}
-        and callsite_id is not null
-      order by ts
-    `;
-  }
-
-  onSliceClick({slice}: OnSliceClickArgs<Slice>) {
-    globals.makeSelection(
-      Actions.selectPerfSamples({
-        id: slice.id,
-        upid: this.upid,
-        leftTs: Time.fromRaw(slice.ts),
-        rightTs: Time.fromRaw(slice.ts),
-        type: ProfileType.PERF_SAMPLE,
-      }),
-    );
-  }
-}
-
-export class ThreadPerfSamplesProfileTrack extends BasePerfSamplesProfileTrack {
-  constructor(
-    args: NewTrackArgs,
-    private utid: number,
-  ) {
-    super(args);
-  }
-
-  getSqlSource(): string {
-    return `
-      select p.id, ts, 0 as dur, 0 as depth, 'Perf Sample' as name
-      from perf_sample p
-      where utid = ${this.utid}
-        and callsite_id is not null
-      order by ts
-    `;
-  }
-
-  onSliceClick({slice}: OnSliceClickArgs<Slice>) {
-    globals.makeSelection(
-      Actions.selectPerfSamples({
-        id: slice.id,
-        utid: this.utid,
-        leftTs: Time.fromRaw(slice.ts),
-        rightTs: Time.fromRaw(slice.ts),
-        type: ProfileType.PERF_SAMPLE,
-      }),
-    );
-  }
-}
diff --git a/ui/src/core_plugins/process_summary/index.ts b/ui/src/core_plugins/process_summary/index.ts
deleted file mode 100644
index 1204dce..0000000
--- a/ui/src/core_plugins/process_summary/index.ts
+++ /dev/null
@@ -1,209 +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 {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {getThreadOrProcUri} from '../../public/utils';
-import {NUM, NUM_NULL, STR} from '../../trace_processor/query_result';
-
-import {
-  Config as ProcessSchedulingTrackConfig,
-  PROCESS_SCHEDULING_TRACK_KIND,
-  ProcessSchedulingTrack,
-} from './process_scheduling_track';
-import {
-  Config as ProcessSummaryTrackConfig,
-  PROCESS_SUMMARY_TRACK,
-  ProcessSummaryTrack,
-} from './process_summary_track';
-
-// This plugin now manages both process "scheduling" and "summary" tracks.
-class ProcessSummaryPlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    await this.addProcessTrackGroups(ctx);
-    await this.addKernelThreadSummary(ctx);
-  }
-
-  private async addProcessTrackGroups(ctx: PluginContextTrace): Promise<void> {
-    const cpuCount = Math.max(...ctx.trace.cpus, -1) + 1;
-
-    const result = await ctx.engine.query(`
-      INCLUDE PERFETTO MODULE android.process_metadata;
-
-      select *
-      from (
-        select
-          _process_available_info_summary.upid,
-          null as utid,
-          process.pid,
-          null as tid,
-          process.name as processName,
-          null as threadName,
-          sum_running_dur > 0 as hasSched,
-          android_process_metadata.debuggable as isDebuggable,
-          ifnull((
-            select group_concat(string_value)
-            from args
-            where
-              process.arg_set_id is not null and
-              arg_set_id = process.arg_set_id and
-              flat_key = 'chrome.process_label'
-          ), '') as chromeProcessLabels
-        from _process_available_info_summary
-        join process using(upid)
-        left join android_process_metadata using(upid)
-      )
-      union all
-      select *
-      from (
-        select
-          null,
-          utid,
-          null as pid,
-          tid,
-          null as processName,
-          thread.name threadName,
-          sum_running_dur > 0 as hasSched,
-          0 as isDebuggable,
-          '' as chromeProcessLabels
-        from _thread_available_info_summary
-        join thread using (utid)
-        where upid is null
-      )
-  `);
-
-    const it = result.iter({
-      upid: NUM_NULL,
-      utid: NUM_NULL,
-      pid: NUM_NULL,
-      tid: NUM_NULL,
-      hasSched: NUM_NULL,
-      isDebuggable: NUM_NULL,
-      chromeProcessLabels: STR,
-    });
-    for (; it.valid(); it.next()) {
-      const upid = it.upid;
-      const utid = it.utid;
-      const pid = it.pid;
-      const tid = it.tid;
-      const hasSched = Boolean(it.hasSched);
-      const isDebuggable = Boolean(it.isDebuggable);
-      const subtitle = it.chromeProcessLabels;
-
-      // Group by upid if present else by utid.
-      const pidForColor = pid ?? tid ?? upid ?? utid ?? 0;
-      const uri = getThreadOrProcUri(upid, utid);
-
-      const chips: string[] = [];
-      isDebuggable && chips.push('debuggable');
-
-      if (hasSched) {
-        const config: ProcessSchedulingTrackConfig = {
-          pidForColor,
-          upid,
-          utid,
-        };
-
-        ctx.registerTrack({
-          uri,
-          title: `${upid === null ? tid : pid} schedule`,
-          tags: {
-            kind: PROCESS_SCHEDULING_TRACK_KIND,
-          },
-          chips,
-          trackFactory: () => {
-            return new ProcessSchedulingTrack(ctx.engine, config, cpuCount);
-          },
-          subtitle,
-        });
-      } else {
-        const config: ProcessSummaryTrackConfig = {
-          pidForColor,
-          upid,
-          utid,
-        };
-
-        ctx.registerTrack({
-          uri,
-          title: `${upid === null ? tid : pid} summary`,
-          tags: {
-            kind: PROCESS_SUMMARY_TRACK,
-          },
-          chips,
-          trackFactory: () => new ProcessSummaryTrack(ctx.engine, config),
-          subtitle,
-        });
-      }
-    }
-  }
-
-  private async addKernelThreadSummary(ctx: PluginContextTrace): Promise<void> {
-    const {engine} = ctx;
-
-    // Identify kernel threads if this is a linux system trace, and sufficient
-    // process information is available. Kernel threads are identified by being
-    // children of kthreadd (always pid 2).
-    // The query will return the kthreadd process row first, which must exist
-    // for any other kthreads to be returned by the query.
-    // TODO(rsavitski): figure out how to handle the idle process (swapper),
-    // which has pid 0 but appears as a distinct process (with its own comm) on
-    // each cpu. It'd make sense to exclude its thread state track, but still
-    // put process-scoped tracks in this group.
-    const result = await engine.query(`
-      select
-        t.utid, p.upid, (case p.pid when 2 then 1 else 0 end) isKthreadd
-      from
-        thread t
-        join process p using (upid)
-        left join process parent on (p.parent_upid = parent.upid)
-        join
-          (select true from metadata m
-             where (m.name = 'system_name' and m.str_value = 'Linux')
-           union
-           select 1 from (select true from sched limit 1))
-      where
-        p.pid = 2 or parent.pid = 2
-      order by isKthreadd desc
-    `);
-
-    const it = result.iter({
-      utid: NUM,
-      upid: NUM,
-    });
-
-    // Not applying kernel thread grouping.
-    if (!it.valid()) {
-      return;
-    }
-
-    const config: ProcessSummaryTrackConfig = {
-      pidForColor: 2,
-      upid: it.upid,
-      utid: it.utid,
-    };
-
-    ctx.registerTrack({
-      uri: '/kernel',
-      title: `Kernel thread summary`,
-      tags: {
-        kind: PROCESS_SUMMARY_TRACK,
-      },
-      trackFactory: () => new ProcessSummaryTrack(ctx.engine, config),
-    });
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.ProcessSummary',
-  plugin: ProcessSummaryPlugin,
-};
diff --git a/ui/src/core_plugins/process_summary/process_scheduling_track.ts b/ui/src/core_plugins/process_summary/process_scheduling_track.ts
deleted file mode 100644
index 23e5f43..0000000
--- a/ui/src/core_plugins/process_summary/process_scheduling_track.ts
+++ /dev/null
@@ -1,299 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {BigintMath as BIMath} from '../../base/bigint_math';
-import {searchEq, searchRange} from '../../base/binary_search';
-import {assertExists, assertTrue} from '../../base/logging';
-import {duration, time, Time} from '../../base/time';
-import {Actions} from '../../common/actions';
-import {drawTrackHoverTooltip} from '../../common/canvas_utils';
-import {Color} from '../../core/color';
-import {colorForThread} from '../../core/colorizer';
-import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {globals} from '../../frontend/globals';
-import {Engine, Track} from '../../public';
-import {LONG, NUM, QueryResult} from '../../trace_processor/query_result';
-import {uuidv4Sql} from '../../base/uuid';
-import {TrackMouseEvent, TrackRenderContext} from '../../public/tracks';
-import {Vector} from '../../base/geom';
-
-export const PROCESS_SCHEDULING_TRACK_KIND = 'ProcessSchedulingTrack';
-
-const MARGIN_TOP = 5;
-const RECT_HEIGHT = 30;
-const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
-
-interface Data extends TrackData {
-  kind: 'slice';
-  maxCpu: number;
-
-  // Slices are stored in a columnar fashion. All fields have the same length.
-  starts: BigInt64Array;
-  ends: BigInt64Array;
-  utids: Uint32Array;
-  cpus: Uint32Array;
-}
-
-export interface Config {
-  pidForColor: number;
-  upid: number | null;
-  utid: number | null;
-}
-
-export class ProcessSchedulingTrack implements Track {
-  private mousePos?: Vector;
-  private utidHoveredInThisTrack = -1;
-  private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
-  private cpuCount: number;
-  private engine: Engine;
-  private trackUuid = uuidv4Sql();
-  private config: Config;
-
-  constructor(engine: Engine, config: Config, cpuCount: number) {
-    this.engine = engine;
-    this.config = config;
-    this.cpuCount = cpuCount;
-  }
-
-  async onCreate(): Promise<void> {
-    if (this.config.upid !== null) {
-      await this.engine.query(`
-        create virtual table process_scheduling_${this.trackUuid}
-        using __intrinsic_slice_mipmap((
-          select
-            id,
-            ts,
-            iif(
-              dur = -1,
-              lead(ts, 1, trace_end()) over (partition by cpu order by ts) - ts,
-              dur
-            ) as dur,
-            cpu as depth
-          from experimental_sched_upid
-          where
-            utid != 0 and
-            upid = ${this.config.upid}
-        ));
-      `);
-    } else {
-      assertExists(this.config.utid);
-      await this.engine.query(`
-        create virtual table process_scheduling_${this.trackUuid}
-        using __intrinsic_slice_mipmap((
-          select
-            id,
-            ts,
-            iif(
-              dur = -1,
-              lead(ts, 1, trace_end()) over (partition by cpu order by ts) - ts,
-              dur
-            ) as dur,
-            cpu as depth
-          from sched
-          where utid = ${this.config.utid}
-        ));
-      `);
-    }
-  }
-
-  async onUpdate({
-    visibleWindow,
-    resolution,
-  }: TrackRenderContext): Promise<void> {
-    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
-  }
-
-  async onDestroy(): Promise<void> {
-    this.fetcher[Symbol.dispose]();
-    await this.engine.tryQuery(`
-      drop table process_scheduling_${this.trackUuid}
-    `);
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    // Resolution must always be a power of 2 for this logic to work
-    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
-
-    const queryRes = await this.queryData(start, end, resolution);
-    const numRows = queryRes.numRows();
-    const slices: Data = {
-      kind: 'slice',
-      start,
-      end,
-      resolution,
-      length: numRows,
-      maxCpu: this.cpuCount,
-      starts: new BigInt64Array(numRows),
-      ends: new BigInt64Array(numRows),
-      cpus: new Uint32Array(numRows),
-      utids: new Uint32Array(numRows),
-    };
-
-    const it = queryRes.iter({
-      ts: LONG,
-      dur: LONG,
-      cpu: NUM,
-      utid: NUM,
-    });
-
-    for (let row = 0; it.valid(); it.next(), row++) {
-      const start = Time.fromRaw(it.ts);
-      const dur = it.dur;
-      const end = Time.add(start, dur);
-
-      slices.starts[row] = start;
-      slices.ends[row] = end;
-      slices.cpus[row] = it.cpu;
-      slices.utids[row] = it.utid;
-      slices.end = Time.max(end, slices.end);
-    }
-    return slices;
-  }
-
-  private async queryData(
-    start: time,
-    end: time,
-    bucketSize: duration,
-  ): Promise<QueryResult> {
-    return this.engine.query(`
-      select
-        (z.ts / ${bucketSize}) * ${bucketSize} as ts,
-        iif(s.dur = -1, s.dur, max(z.dur, ${bucketSize})) as dur,
-        s.id,
-        z.depth as cpu,
-        utid
-      from process_scheduling_${this.trackUuid}(
-        ${start}, ${end}, ${bucketSize}
-      ) z
-      cross join sched s using (id)
-    `);
-  }
-
-  getHeight(): number {
-    return TRACK_HEIGHT;
-  }
-
-  render({ctx, size, timescale, visibleWindow}: TrackRenderContext): void {
-    // TODO: fonts and colors should come from the CSS and not hardcoded here.
-    const data = this.fetcher.data;
-
-    if (data === undefined) return; // Can't possibly draw anything.
-
-    // If the cached trace slices don't fully cover the visible time range,
-    // show a gray rectangle with a "Loading..." label.
-    checkerboardExcept(
-      ctx,
-      this.getHeight(),
-      0,
-      size.width,
-      timescale.timeToPx(data.start),
-      timescale.timeToPx(data.end),
-    );
-
-    assertTrue(data.starts.length === data.ends.length);
-    assertTrue(data.starts.length === data.utids.length);
-
-    const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu);
-
-    for (let i = 0; i < data.ends.length; i++) {
-      const tStart = Time.fromRaw(data.starts[i]);
-      const tEnd = Time.fromRaw(data.ends[i]);
-
-      // Cull slices that lie completely outside the visible window
-      if (!visibleWindow.overlaps(tStart, tEnd)) continue;
-
-      const utid = data.utids[i];
-      const cpu = data.cpus[i];
-
-      const rectStart = Math.floor(timescale.timeToPx(tStart));
-      const rectEnd = Math.floor(timescale.timeToPx(tEnd));
-      const rectWidth = Math.max(1, rectEnd - rectStart);
-
-      const threadInfo = globals.threads.get(utid);
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      const pid = (threadInfo ? threadInfo.pid : -1) || -1;
-
-      const isHovering = globals.state.hoveredUtid !== -1;
-      const isThreadHovered = globals.state.hoveredUtid === utid;
-      const isProcessHovered = globals.state.hoveredPid === pid;
-      const colorScheme = colorForThread(threadInfo);
-      let color: Color;
-      if (isHovering && !isThreadHovered) {
-        if (!isProcessHovered) {
-          color = colorScheme.disabled;
-        } else {
-          color = colorScheme.variant;
-        }
-      } else {
-        color = colorScheme.base;
-      }
-      ctx.fillStyle = color.cssString;
-      const y = MARGIN_TOP + cpuTrackHeight * cpu + cpu;
-      ctx.fillRect(rectStart, y, rectWidth, cpuTrackHeight);
-    }
-
-    const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
-    if (hoveredThread !== undefined && this.mousePos !== undefined) {
-      const tidText = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`;
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      if (hoveredThread.pid) {
-        const pidText = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`;
-        drawTrackHoverTooltip(ctx, this.mousePos, size, pidText, tidText);
-      } else {
-        drawTrackHoverTooltip(ctx, this.mousePos, size, tidText);
-      }
-    }
-  }
-
-  onMouseMove({x, y, timescale}: TrackMouseEvent) {
-    const data = this.fetcher.data;
-    this.mousePos = {x, y};
-    if (data === undefined) return;
-    if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) {
-      this.utidHoveredInThisTrack = -1;
-      globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
-      return;
-    }
-
-    const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu);
-    const cpu = Math.floor((y - MARGIN_TOP) / (cpuTrackHeight + 1));
-    const t = timescale.pxToHpTime(x).toTime('floor');
-
-    const [i, j] = searchRange(data.starts, t, searchEq(data.cpus, cpu));
-    if (i === j || i >= data.starts.length || t > data.ends[i]) {
-      this.utidHoveredInThisTrack = -1;
-      globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
-      return;
-    }
-
-    const utid = data.utids[i];
-    this.utidHoveredInThisTrack = utid;
-    const threadInfo = globals.threads.get(utid);
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    const pid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
-    globals.dispatch(Actions.setHoveredUtidAndPid({utid, pid}));
-  }
-
-  onMouseOut() {
-    this.utidHoveredInThisTrack = -1;
-    globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
-    this.mousePos = undefined;
-  }
-}
diff --git a/ui/src/core_plugins/process_summary/process_summary_track.ts b/ui/src/core_plugins/process_summary/process_summary_track.ts
deleted file mode 100644
index 2e28c35..0000000
--- a/ui/src/core_plugins/process_summary/process_summary_track.ts
+++ /dev/null
@@ -1,204 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {BigintMath} from '../../base/bigint_math';
-import {assertExists, assertTrue} from '../../base/logging';
-import {duration, Time, time} from '../../base/time';
-import {colorForTid} from '../../core/colorizer';
-import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {Engine, Track} from '../../public';
-import {LONG, NUM} from '../../trace_processor/query_result';
-import {uuidv4Sql} from '../../base/uuid';
-import {TrackRenderContext} from '../../public/tracks';
-
-export const PROCESS_SUMMARY_TRACK = 'ProcessSummaryTrack';
-
-interface Data extends TrackData {
-  starts: BigInt64Array;
-  utilizations: Float64Array;
-}
-
-export interface Config {
-  pidForColor: number;
-  upid: number | null;
-  utid: number | null;
-}
-
-const MARGIN_TOP = 5;
-const RECT_HEIGHT = 30;
-const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
-const SUMMARY_HEIGHT = TRACK_HEIGHT - MARGIN_TOP;
-
-export class ProcessSummaryTrack implements Track {
-  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-  private engine: Engine;
-  private config: Config;
-  private uuid = uuidv4Sql();
-
-  constructor(engine: Engine, config: Config) {
-    this.engine = engine;
-    this.config = config;
-  }
-
-  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 = `
-        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
-          )
-        select
-          ts,
-          sum(value) over (order by ts) / (select count() from tt) as value
-        from ss
-        order by ts
-      ));
-    `);
-  }
-
-  async onUpdate({
-    visibleWindow,
-    resolution,
-  }: TrackRenderContext): Promise<void> {
-    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    // Resolution must always be a power of 2 for this logic to work
-    assertTrue(
-      BigintMath.popcount(resolution) === 1,
-      `${resolution} not pow of 2`,
-    );
-
-    const queryRes = await this.engine.query(`
-      select last_ts as ts, last_value as utilization
-      from process_summary_${this.uuid}(${start}, ${end}, ${resolution});
-    `);
-    const numRows = queryRes.numRows();
-    const slices: Data = {
-      start,
-      end,
-      resolution,
-      length: numRows,
-      starts: new BigInt64Array(numRows),
-      utilizations: new Float64Array(numRows),
-    };
-    const it = queryRes.iter({
-      ts: LONG,
-      utilization: NUM,
-    });
-    for (let row = 0; it.valid(); it.next(), row++) {
-      slices.starts[row] = it.ts;
-      slices.utilizations[row] = it.utilization;
-    }
-    return slices;
-  }
-
-  async onDestroy(): Promise<void> {
-    await this.engine.tryQuery(
-      `drop table if exists process_summary_${this.uuid};`,
-    );
-    this.fetcher[Symbol.dispose]();
-  }
-
-  getHeight(): number {
-    return TRACK_HEIGHT;
-  }
-
-  render(trackCtx: TrackRenderContext): void {
-    const {ctx, size, timescale} = trackCtx;
-
-    const data = this.fetcher.data;
-    if (data === undefined) {
-      return;
-    }
-
-    // If the cached trace slices don't fully cover the visible time range,
-    // show a gray rectangle with a "Loading..." label.
-    checkerboardExcept(
-      ctx,
-      this.getHeight(),
-      0,
-      size.width,
-      timescale.timeToPx(data.start),
-      timescale.timeToPx(data.end),
-    );
-
-    this.renderSummary(trackCtx, data);
-  }
-
-  private renderSummary(
-    {ctx, timescale}: TrackRenderContext,
-    data: Data,
-  ): void {
-    const startPx = 0;
-    const bottomY = TRACK_HEIGHT;
-
-    let lastX = startPx;
-    let lastY = bottomY;
-
-    const color = colorForTid(this.config.pidForColor);
-    ctx.fillStyle = color.base.cssString;
-    ctx.beginPath();
-    ctx.moveTo(lastX, lastY);
-    for (let i = 0; i < data.utilizations.length; i++) {
-      const startTime = Time.fromRaw(data.starts[i]);
-      const utilization = data.utilizations[i];
-      lastX = Math.floor(timescale.timeToPx(startTime));
-      ctx.lineTo(lastX, lastY);
-      lastY = MARGIN_TOP + Math.round(SUMMARY_HEIGHT * (1 - utilization));
-      ctx.lineTo(lastX, lastY);
-    }
-    ctx.lineTo(lastX, bottomY);
-    ctx.closePath();
-    ctx.fill();
-  }
-}
diff --git a/ui/src/core_plugins/sched/active_cpu_count.ts b/ui/src/core_plugins/sched/active_cpu_count.ts
deleted file mode 100644
index 794cd62..0000000
--- a/ui/src/core_plugins/sched/active_cpu_count.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {sqliteString} from '../../base/string_utils';
-import {
-  BaseCounterTrack,
-  CounterOptions,
-} from '../../frontend/base_counter_track';
-import {CloseTrackButton} from '../../frontend/close_track_button';
-import {Engine, TrackContext} from '../../public';
-
-export enum CPUType {
-  Big = 'big',
-  Mid = 'mid',
-  Little = 'little',
-}
-
-export class ActiveCPUCountTrack extends BaseCounterTrack {
-  private readonly cpuType?: CPUType;
-
-  constructor(ctx: TrackContext, engine: Engine, cpuType?: CPUType) {
-    super({
-      engine,
-      trackKey: ctx.trackKey,
-    });
-    this.cpuType = cpuType;
-  }
-
-  getTrackShellButtons(): m.Children {
-    return m(CloseTrackButton, {
-      trackKey: this.trackKey,
-    });
-  }
-
-  protected getDefaultCounterOptions(): CounterOptions {
-    const options = super.getDefaultCounterOptions();
-    options.yRangeRounding = 'strict';
-    options.yRange = 'viewport';
-    return options;
-  }
-
-  async onInit() {
-    await this.engine.query(`
-      INCLUDE PERFETTO MODULE sched.thread_level_parallelism;
-      INCLUDE PERFETTO MODULE android.cpu.cluster_type;
-    `);
-  }
-
-  getSqlSource() {
-    const sourceTable =
-      this.cpuType === undefined
-        ? 'sched_active_cpu_count'
-        : `_active_cpu_count_for_cluster_type(${sqliteString(this.cpuType)})`;
-    return `
-      select
-        ts,
-        active_cpu_count as value
-      from ${sourceTable}
-    `;
-  }
-}
diff --git a/ui/src/core_plugins/sched/index.ts b/ui/src/core_plugins/sched/index.ts
deleted file mode 100644
index 749269b..0000000
--- a/ui/src/core_plugins/sched/index.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {uuidv4} from '../../base/uuid';
-import {Actions} from '../../common/actions';
-import {SCROLLING_TRACK_GROUP} from '../../common/state';
-import {globals} from '../../frontend/globals';
-import {
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  PrimaryTrackSortKey,
-} from '../../public';
-
-import {ActiveCPUCountTrack, CPUType} from './active_cpu_count';
-import {RunnableThreadCountTrack} from './runnable_thread_count';
-
-class SchedPlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace) {
-    const runnableThreadCountUri = `/runnable_thread_count`;
-    ctx.registerTrack({
-      uri: runnableThreadCountUri,
-      title: 'Runnable thread count',
-      trackFactory: (trackCtx) =>
-        new RunnableThreadCountTrack({
-          engine: ctx.engine,
-          trackKey: trackCtx.trackKey,
-        }),
-    });
-    ctx.registerCommand({
-      id: 'dev.perfetto.Sched.AddRunnableThreadCountTrackCommand',
-      name: 'Add track: runnable thread count',
-      callback: () =>
-        addPinnedTrack(runnableThreadCountUri, 'Runnable thread count'),
-    });
-
-    const uri = uriForActiveCPUCountTrack();
-    const title = 'Active CPU count';
-    ctx.registerTrack({
-      uri,
-      title: title,
-      trackFactory: (trackCtx) => new ActiveCPUCountTrack(trackCtx, ctx.engine),
-    });
-    ctx.registerCommand({
-      id: 'dev.perfetto.Sched.AddActiveCPUCountTrackCommand',
-      name: 'Add track: active CPU count',
-      callback: () => addPinnedTrack(uri, title),
-    });
-
-    for (const cpuType of Object.values(CPUType)) {
-      const uri = uriForActiveCPUCountTrack(cpuType);
-      const title = `Active ${cpuType} CPU count`;
-      ctx.registerTrack({
-        uri,
-        title: title,
-        trackFactory: (trackCtx) =>
-          new ActiveCPUCountTrack(trackCtx, ctx.engine, cpuType),
-      });
-
-      ctx.registerCommand({
-        id: `dev.perfetto.Sched.AddActiveCPUCountTrackCommand.${cpuType}`,
-        name: `Add track: active ${cpuType} CPU count`,
-        callback: () => addPinnedTrack(uri, title),
-      });
-    }
-  }
-}
-
-function uriForActiveCPUCountTrack(cpuType?: CPUType): string {
-  const prefix = `/active_cpus`;
-  if (cpuType !== undefined) {
-    return `${prefix}_${cpuType}`;
-  } else {
-    return prefix;
-  }
-}
-
-function addPinnedTrack(uri: string, title: string) {
-  const key = uuidv4();
-  globals.dispatchMultiple([
-    Actions.addTrack({
-      key,
-      uri,
-      name: title,
-      trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
-      trackGroup: SCROLLING_TRACK_GROUP,
-    }),
-    Actions.toggleTrackPinned({trackKey: key}),
-  ]);
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.Sched',
-  plugin: SchedPlugin,
-};
diff --git a/ui/src/core_plugins/sched/runnable_thread_count.ts b/ui/src/core_plugins/sched/runnable_thread_count.ts
deleted file mode 100644
index 3908572..0000000
--- a/ui/src/core_plugins/sched/runnable_thread_count.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {
-  BaseCounterTrack,
-  CounterOptions,
-} from '../../frontend/base_counter_track';
-import {CloseTrackButton} from '../../frontend/close_track_button';
-import {NewTrackArgs} from '../../frontend/track';
-
-export class RunnableThreadCountTrack extends BaseCounterTrack {
-  constructor(args: NewTrackArgs) {
-    super(args);
-  }
-
-  getTrackShellButtons(): m.Children {
-    return m(CloseTrackButton, {
-      trackKey: this.trackKey,
-    });
-  }
-
-  protected getDefaultCounterOptions(): CounterOptions {
-    const options = super.getDefaultCounterOptions();
-    options.yRangeRounding = 'strict';
-    options.yRange = 'viewport';
-    return options;
-  }
-
-  async onInit() {
-    await this.engine.query(
-      `INCLUDE PERFETTO MODULE sched.thread_level_parallelism`,
-    );
-  }
-
-  getSqlSource() {
-    return `
-      select
-        ts,
-        runnable_thread_count as value
-      from sched_runnable_thread_count
-    `;
-  }
-}
diff --git a/ui/src/core_plugins/screenshots/index.ts b/ui/src/core_plugins/screenshots/index.ts
deleted file mode 100644
index 7739e3e..0000000
--- a/ui/src/core_plugins/screenshots/index.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {uuidv4} from '../../base/uuid';
-import {AddTrackArgs} from '../../common/actions';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {
-  BottomTabToSCSAdapter,
-  NUM,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
-
-import {ScreenshotTab} from './screenshot_panel';
-import {ScreenshotsTrack} from './screenshots_track';
-
-export type DecideTracksResult = {
-  tracksToAdd: AddTrackArgs[];
-};
-
-class ScreenshotsPlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const res = await ctx.engine.query(`
-      INCLUDE PERFETTO MODULE android.screenshots;
-      select
-        count() as count
-      from android_screenshots
-    `);
-    const {count} = res.firstRow({count: NUM});
-
-    if (count > 0) {
-      const displayName = 'Screenshots';
-      const uri = '/screenshots';
-      ctx.registerTrack({
-        uri,
-        title: displayName,
-        trackFactory: ({trackKey}) => {
-          return new ScreenshotsTrack({
-            engine: ctx.engine,
-            trackKey,
-          });
-        },
-        tags: {
-          kind: ScreenshotsTrack.kind,
-        },
-      });
-
-      ctx.registerDetailsPanel(
-        new BottomTabToSCSAdapter({
-          tabFactory: (selection) => {
-            if (
-              selection.kind === 'GENERIC_SLICE' &&
-              selection.detailsPanelConfig.kind === ScreenshotTab.kind
-            ) {
-              const config = selection.detailsPanelConfig.config;
-              return new ScreenshotTab({
-                config: config as GenericSliceDetailsTabConfig,
-                engine: ctx.engine,
-                uuid: uuidv4(),
-              });
-            }
-            return undefined;
-          },
-        }),
-      );
-    }
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.Screenshots',
-  plugin: ScreenshotsPlugin,
-};
diff --git a/ui/src/core_plugins/screenshots/screenshot_panel.ts b/ui/src/core_plugins/screenshots/screenshot_panel.ts
deleted file mode 100644
index 8f8d469..0000000
--- a/ui/src/core_plugins/screenshots/screenshot_panel.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {assertTrue} from '../../base/logging';
-import {exists} from '../../base/utils';
-import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {getSlice, SliceDetails} from '../../trace_processor/sql_utils/slice';
-import {asSliceSqlId} from '../../trace_processor/sql_utils/core_types';
-import {Engine} from '../../trace_processor/engine';
-
-async function getSliceDetails(
-  engine: Engine,
-  id: number,
-): Promise<SliceDetails | undefined> {
-  return getSlice(engine, asSliceSqlId(id));
-}
-
-export class ScreenshotTab extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'dev.perfetto.ScreenshotDetailsPanel';
-
-  private sliceDetails?: SliceDetails;
-
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): ScreenshotTab {
-    return new ScreenshotTab(args);
-  }
-
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-    getSliceDetails(this.engine, this.config.id).then(
-      (sliceDetails) => (this.sliceDetails = sliceDetails),
-    );
-  }
-
-  renderTabCanvas() {}
-
-  getTitle() {
-    return this.config.title;
-  }
-
-  viewTab() {
-    if (
-      !exists(this.sliceDetails) ||
-      !exists(this.sliceDetails.args) ||
-      this.sliceDetails.args.length == 0
-    ) {
-      return m('h2', 'Loading Screenshot');
-    }
-    assertTrue(this.sliceDetails.args[0].key == 'screenshot.jpg_image');
-    return m(
-      '.screenshot-panel',
-      m('img', {
-        src: 'data:image/png;base64, ' + this.sliceDetails.args[0].displayValue,
-      }),
-    );
-  }
-}
diff --git a/ui/src/core_plugins/screenshots/screenshots_track.ts b/ui/src/core_plugins/screenshots/screenshots_track.ts
deleted file mode 100644
index d88010e..0000000
--- a/ui/src/core_plugins/screenshots/screenshots_track.ts
+++ /dev/null
@@ -1,41 +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 {
-  CustomSqlDetailsPanelConfig,
-  CustomSqlTableDefConfig,
-  CustomSqlTableSliceTrack,
-} from '../../frontend/tracks/custom_sql_table_slice_track';
-import {ScreenshotTab} from './screenshot_panel';
-
-export class ScreenshotsTrack extends CustomSqlTableSliceTrack {
-  static readonly kind = 'dev.perfetto.ScreenshotsTrack';
-
-  getSqlDataSource(): CustomSqlTableDefConfig {
-    return {
-      sqlTableName: 'android_screenshots',
-      columns: ['*'],
-    };
-  }
-
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    return {
-      kind: ScreenshotTab.kind,
-      config: {
-        sqlTableName: this.tableName,
-        title: 'Screenshots',
-      },
-    };
-  }
-}
diff --git a/ui/src/core_plugins/thread_slice/index.ts b/ui/src/core_plugins/thread_slice/index.ts
deleted file mode 100644
index 33c3418..0000000
--- a/ui/src/core_plugins/thread_slice/index.ts
+++ /dev/null
@@ -1,135 +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 {uuidv4} from '../../base/uuid';
-import {THREAD_SLICE_TRACK_KIND} from '../../public';
-import {ThreadSliceDetailsTab} from '../../frontend/thread_slice_details_tab';
-import {
-  BottomTabToSCSAdapter,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
-import {getThreadUriPrefix, getTrackName} from '../../public/utils';
-import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
-import {ThreadSliceTrack} from '../../frontend/thread_slice_track';
-import {removeFalsyValues} from '../../base/array_utils';
-
-class ThreadSlicesPlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const {engine} = ctx;
-
-    const result = await engine.query(`
-      include perfetto module viz.summary.slices;
-      include perfetto module viz.summary.threads;
-      include perfetto module viz.threads;
-
-      select
-        thread_track.utid as utid,
-        thread_track.id as trackId,
-        thread_track.name as trackName,
-        EXTRACT_ARG(thread_track.source_arg_set_id,
-                    'is_root_in_scope') as isDefaultTrackForScope,
-        tid,
-        t.name as threadName,
-        max_depth as maxDepth,
-        t.upid as upid,
-        is_main_thread as isMainThread,
-        is_kernel_thread AS isKernelThread
-      from thread_track
-      join _threads_with_kernel_flag t using(utid)
-      join _slice_track_summary sts on sts.id = thread_track.id
-  `);
-
-    const it = result.iter({
-      utid: NUM,
-      trackId: NUM,
-      trackName: STR_NULL,
-      isDefaultTrackForScope: NUM_NULL,
-      tid: NUM_NULL,
-      threadName: STR_NULL,
-      maxDepth: NUM,
-      upid: NUM_NULL,
-      isMainThread: NUM_NULL,
-      isKernelThread: NUM,
-    });
-
-    for (; it.valid(); it.next()) {
-      const {
-        upid,
-        utid,
-        trackId,
-        trackName,
-        tid,
-        threadName,
-        maxDepth,
-        isMainThread,
-        isKernelThread,
-        isDefaultTrackForScope,
-      } = it;
-      const displayName = getTrackName({
-        name: trackName,
-        utid,
-        tid,
-        threadName,
-        kind: 'Slices',
-      });
-
-      ctx.registerTrack({
-        uri: `${getThreadUriPrefix(upid, utid)}_slice_${trackId}`,
-        title: displayName,
-        tags: {
-          trackIds: [trackId],
-          kind: THREAD_SLICE_TRACK_KIND,
-          utid,
-          upid: upid ?? undefined,
-          ...(isDefaultTrackForScope === 1 && {isDefaultTrackForScope: true}),
-        },
-        chips: removeFalsyValues([
-          isKernelThread === 0 && isMainThread === 1 && 'main thread',
-        ]),
-        trackFactory: ({trackKey}) => {
-          const newTrackArgs = {
-            engine: ctx.engine,
-            trackKey,
-          };
-          return new ThreadSliceTrack(newTrackArgs, trackId, maxDepth);
-        },
-      });
-    }
-
-    ctx.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (sel) => {
-          if (sel.kind !== 'SLICE') {
-            return undefined;
-          }
-          return new ThreadSliceDetailsTab({
-            config: {
-              table: sel.table ?? 'slice',
-              id: sel.id,
-            },
-            engine: ctx.engine,
-            uuid: uuidv4(),
-          });
-        },
-      }),
-    );
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.ThreadSlices',
-  plugin: ThreadSlicesPlugin,
-};
diff --git a/ui/src/core_plugins/thread_state/index.ts b/ui/src/core_plugins/thread_state/index.ts
deleted file mode 100644
index 0e3d021..0000000
--- a/ui/src/core_plugins/thread_state/index.ts
+++ /dev/null
@@ -1,111 +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 {uuidv4} from '../../base/uuid';
-import {THREAD_STATE_TRACK_KIND} from '../../public';
-import {asThreadStateSqlId} from '../../trace_processor/sql_utils/core_types';
-import {ThreadStateTab} from '../../frontend/thread_state_tab';
-import {
-  BottomTabToSCSAdapter,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
-import {getThreadUriPrefix, getTrackName} from '../../public/utils';
-import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
-import {ThreadStateTrack} from './thread_state_track';
-import {removeFalsyValues} from '../../base/array_utils';
-
-class ThreadState implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const {engine} = ctx;
-
-    const result = await engine.query(`
-      include perfetto module viz.threads;
-      include perfetto module viz.summary.threads;
-
-      select
-        utid,
-        t.upid,
-        tid,
-        t.name as threadName,
-        is_main_thread as isMainThread,
-        is_kernel_thread as isKernelThread
-      from _threads_with_kernel_flag t
-      join _sched_summary using (utid)
-    `);
-
-    const it = result.iter({
-      utid: NUM,
-      upid: NUM_NULL,
-      tid: NUM_NULL,
-      threadName: STR_NULL,
-      isMainThread: NUM_NULL,
-      isKernelThread: NUM,
-    });
-    for (; it.valid(); it.next()) {
-      const {utid, upid, tid, threadName, isMainThread, isKernelThread} = it;
-      const displayName = getTrackName({
-        utid,
-        tid,
-        threadName,
-        kind: THREAD_STATE_TRACK_KIND,
-      });
-
-      ctx.registerTrack({
-        uri: `${getThreadUriPrefix(upid, utid)}_state`,
-        title: displayName,
-        tags: {
-          kind: THREAD_STATE_TRACK_KIND,
-          utid,
-          upid: upid ?? undefined,
-        },
-        chips: removeFalsyValues([
-          isKernelThread === 0 && isMainThread === 1 && 'main thread',
-        ]),
-        trackFactory: ({trackKey}) => {
-          return new ThreadStateTrack(
-            {
-              engine: ctx.engine,
-              trackKey,
-            },
-            utid,
-          );
-        },
-      });
-    }
-
-    ctx.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (sel) => {
-          if (sel.kind !== 'THREAD_STATE') {
-            return undefined;
-          }
-          return new ThreadStateTab({
-            config: {
-              id: asThreadStateSqlId(sel.id),
-            },
-            engine: ctx.engine,
-            uuid: uuidv4(),
-          });
-        },
-      }),
-    );
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.ThreadState',
-  plugin: ThreadState,
-};
diff --git a/ui/src/core_plugins/thread_state/thread_state_track.ts b/ui/src/core_plugins/thread_state/thread_state_track.ts
deleted file mode 100644
index 660700e..0000000
--- a/ui/src/core_plugins/thread_state/thread_state_track.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {Actions} from '../../common/actions';
-import {colorForState} from '../../core/colorizer';
-import {LegacySelection} from '../../common/state';
-import {translateState} from '../../common/thread_state';
-import {
-  BASE_ROW,
-  BaseSliceTrack,
-  OnSliceClickArgs,
-} from '../../frontend/base_slice_track';
-import {globals} from '../../frontend/globals';
-import {
-  SLICE_LAYOUT_FLAT_DEFAULTS,
-  SliceLayout,
-} from '../../frontend/slice_layout';
-import {NewTrackArgs} from '../../frontend/track';
-import {NUM_NULL, STR} from '../../trace_processor/query_result';
-import {Slice} from '../../public';
-
-export const THREAD_STATE_ROW = {
-  ...BASE_ROW,
-  state: STR,
-  ioWait: NUM_NULL,
-};
-
-export type ThreadStateRow = typeof THREAD_STATE_ROW;
-
-export class ThreadStateTrack extends BaseSliceTrack<Slice, ThreadStateRow> {
-  protected sliceLayout: SliceLayout = {...SLICE_LAYOUT_FLAT_DEFAULTS};
-
-  constructor(
-    args: NewTrackArgs,
-    private utid: number,
-  ) {
-    super(args);
-  }
-
-  // This is used by the base class to call iter().
-  getRowSpec(): ThreadStateRow {
-    return THREAD_STATE_ROW;
-  }
-
-  getSqlSource(): string {
-    // Do not display states: 'S' (sleeping), 'I' (idle kernel thread).
-    return `
-      select
-        id,
-        ts,
-        dur,
-        cpu,
-        state,
-        io_wait as ioWait,
-        0 as depth
-      from thread_state
-      where
-        utid = ${this.utid} and
-        state not in ('S', 'I')
-    `;
-  }
-
-  rowToSlice(row: ThreadStateRow): Slice {
-    const baseSlice = this.rowToSliceBase(row);
-    const ioWait = row.ioWait === null ? undefined : !!row.ioWait;
-    const title = translateState(row.state, ioWait);
-    const color = colorForState(title);
-    return {...baseSlice, title, colorScheme: color};
-  }
-
-  onUpdatedSlices(slices: Slice[]) {
-    for (const slice of slices) {
-      slice.isHighlighted = slice === this.hoveredSlice;
-    }
-  }
-
-  onSliceClick(args: OnSliceClickArgs<Slice>) {
-    globals.makeSelection(
-      Actions.selectThreadState({
-        id: args.slice.id,
-        trackKey: this.trackKey,
-      }),
-    );
-  }
-
-  protected isSelectionHandled(selection: LegacySelection): boolean {
-    return selection.kind === 'THREAD_STATE';
-  }
-}
diff --git a/ui/src/core_plugins/track_utils/index.ts b/ui/src/core_plugins/track_utils/index.ts
index bb1b78e..369d2fe 100644
--- a/ui/src/core_plugins/track_utils/index.ts
+++ b/ui/src/core_plugins/track_utils/index.ts
@@ -12,87 +12,76 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Actions} from '../../common/actions';
-import {
-  getTimeSpanOfSelectionOrVisibleWindow,
-  globals,
-} from '../../frontend/globals';
-import {OmniboxMode} from '../../frontend/omnibox_manager';
-import {verticalScrollToTrack} from '../../frontend/scroll_helper';
-import {
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  PromptOption,
-} from '../../public';
+import {OmniboxMode} from '../../core/omnibox_manager';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {AppImpl} from '../../core/app_impl';
+import {getTimeSpanOfSelectionOrVisibleWindow} from '../../public/utils';
+import {exists} from '../../base/utils';
 
-class TrackUtilsPlugin implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerCommand({
+export default class implements PerfettoPlugin {
+  static readonly id = 'perfetto.TrackUtils';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    ctx.commands.registerCommand({
       id: 'perfetto.RunQueryInSelectedTimeWindow',
       name: `Run query in selected time window`,
       callback: async () => {
-        const window = await getTimeSpanOfSelectionOrVisibleWindow();
-        globals.omnibox.setMode(OmniboxMode.Query);
-        globals.omnibox.setText(
+        const window = await getTimeSpanOfSelectionOrVisibleWindow(ctx);
+        const omnibox = AppImpl.instance.omnibox;
+        omnibox.setMode(OmniboxMode.Query);
+        omnibox.setText(
           `select  where ts >= ${window.start} and ts < ${window.end}`,
         );
-        globals.omnibox.focusOmnibox(7);
+        omnibox.focus(/* cursorPlacement= */ 7);
       },
     });
 
-    ctx.registerCommand({
-      // Selects & reveals the first track on the timeline with a given URI.
-      id: 'perfetto.FindTrack',
+    ctx.commands.registerCommand({
+      id: 'perfetto.FindTrackByName',
+      name: 'Find track by name',
+      callback: async () => {
+        const options = ctx.workspace.flatTracks
+          .map((node) => {
+            return exists(node.uri)
+              ? {key: node.uri, displayName: node.fullPath.join(' \u2023 ')}
+              : undefined;
+          })
+          .filter((pair) => pair !== undefined);
+        const uri = await ctx.omnibox.prompt('Choose a track...', options);
+        uri && ctx.selection.selectTrack(uri, {scrollToSelection: true});
+      },
+    });
+
+    ctx.commands.registerCommand({
+      id: 'perfetto.FindTrackByUri',
       name: 'Find track by URI',
       callback: async () => {
-        const tracks = globals.trackManager.getAllTracks();
-        const options = tracks.map(({uri}): PromptOption => {
-          return {key: uri, displayName: uri};
-        });
+        const options = ctx.workspace.flatTracks
+          .map((track) => track.uri)
+          .filter((uri) => uri !== undefined)
+          .map((uri) => {
+            return {key: uri, displayName: uri};
+          });
 
-        // Sort tracks in a natural sort order
-        const collator = new Intl.Collator('en', {
-          numeric: true,
-          sensitivity: 'base',
-        });
-        const sortedOptions = options.sort((a, b) => {
-          return collator.compare(a.displayName, b.displayName);
-        });
+        const uri = await ctx.omnibox.prompt('Choose a track...', options);
+        uri && ctx.selection.selectTrack(uri, {scrollToSelection: true});
+      },
+    });
 
-        try {
-          const selectedUri = await ctx.prompt(
-            'Choose a track...',
-            sortedOptions,
-          );
-
-          // Find the first track with this URI
-          const firstTrack = Object.values(globals.state.tracks).find(
-            ({uri}) => uri === selectedUri,
-          );
-          if (firstTrack) {
-            console.log(firstTrack);
-            verticalScrollToTrack(firstTrack.key, true);
-            const traceTime = globals.traceContext;
-            globals.makeSelection(
-              Actions.selectArea({
-                start: traceTime.start,
-                end: traceTime.end,
-                tracks: [firstTrack.key],
-              }),
-            );
-          } else {
-            alert(`No tracks with uri ${selectedUri} on the timeline`);
-          }
-        } catch {
-          // Prompt was probably cancelled - do nothing.
-        }
+    ctx.commands.registerCommand({
+      id: 'perfetto.PinTrackByName',
+      name: 'Pin track by name',
+      callback: async () => {
+        const options = ctx.workspace.flatTracks
+          .map((node) => {
+            return exists(node.uri)
+              ? {key: node.id, displayName: node.fullPath.join(' \u2023 ')}
+              : undefined;
+          })
+          .filter((option) => option !== undefined);
+        const id = await ctx.omnibox.prompt('Choose a track...', options);
+        id && ctx.workspace.getTrackById(id)?.pin();
       },
     });
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.TrackUtils',
-  plugin: TrackUtilsPlugin,
-};
diff --git a/ui/src/core_plugins/wattson/index.ts b/ui/src/core_plugins/wattson/index.ts
deleted file mode 100644
index 99b82e6..0000000
--- a/ui/src/core_plugins/wattson/index.ts
+++ /dev/null
@@ -1,107 +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 {globals} from '../../frontend/globals';
-import {
-  BaseCounterTrack,
-  CounterOptions,
-} from '../../frontend/base_counter_track';
-import {
-  Engine,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
-import {CPUSS_ESTIMATE_TRACK_KIND} from '../../core/track_kinds';
-import {hasWattsonSupport} from '../../core/trace_config_utils';
-
-class Wattson implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    // Short circuit if Wattson is not supported for this Perfetto trace
-    if (!(await hasWattsonSupport(ctx.engine))) return;
-
-    ctx.engine.query(`INCLUDE PERFETTO MODULE wattson.curves.ungrouped;`);
-
-    // CPUs estimate as part of CPU subsystem
-    const cpus = globals.traceContext.cpus;
-    for (const cpu of cpus) {
-      const queryKey = `cpu${cpu}_curve`;
-      ctx.registerStaticTrack({
-        uri: `/wattson/cpu_subsystem_estimate_cpu${cpu}`,
-        title: `Cpu${cpu} Estimate`,
-        trackFactory: ({trackKey}) =>
-          new CpuSubsystemEstimateTrack(ctx.engine, trackKey, queryKey),
-        groupName: `Wattson`,
-        tags: {
-          kind: CPUSS_ESTIMATE_TRACK_KIND,
-          wattson: `CPU${cpu}`,
-        },
-      });
-    }
-
-    ctx.registerStaticTrack({
-      uri: `/wattson/cpu_subsystem_estimate_dsu_scu`,
-      title: `DSU/SCU Estimate`,
-      trackFactory: ({trackKey}) =>
-        new CpuSubsystemEstimateTrack(ctx.engine, trackKey, `dsu_scu`),
-      groupName: `Wattson`,
-      tags: {
-        kind: CPUSS_ESTIMATE_TRACK_KIND,
-        wattson: 'Dsu_Scu',
-      },
-    });
-  }
-}
-
-class CpuSubsystemEstimateTrack extends BaseCounterTrack {
-  readonly engine: Engine;
-  readonly queryKey: string;
-
-  constructor(engine: Engine, trackKey: string, queryKey: string) {
-    super({
-      engine: engine,
-      trackKey: trackKey,
-    });
-    this.engine = engine;
-    this.queryKey = queryKey;
-  }
-
-  protected getDefaultCounterOptions(): CounterOptions {
-    const options = super.getDefaultCounterOptions();
-    options.yRangeSharingKey = `CpuSubsystem`;
-    options.unit = `mW`;
-    return options;
-  }
-
-  getSqlSource() {
-    if (this.queryKey.startsWith(`cpu`)) {
-      return `select ts, ${this.queryKey} as value from _system_state_curves`;
-    } else {
-      return `
-        select
-          ts,
-          -- L3 values are scaled by 1000 because it's divided by ns and L3 LUTs
-          -- are scaled by 10^6. This brings to same units as static_curve (mW)
-          ((IFNULL(l3_hit_value, 0) + IFNULL(l3_miss_value, 0)) * 1000 / dur)
-            + static_curve  as value
-        from _system_state_curves
-      `;
-    }
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: `org.kernel.Wattson`,
-  plugin: Wattson,
-};
diff --git a/ui/src/frontend/aggregation_panel.ts b/ui/src/frontend/aggregation_panel.ts
index 092d520..3d441ba 100644
--- a/ui/src/frontend/aggregation_panel.ts
+++ b/ui/src/frontend/aggregation_panel.ts
@@ -13,31 +13,35 @@
 // limitations under the License.
 
 import m from 'mithril';
-
-import {Actions} from '../common/actions';
 import {
   AggregateData,
   Column,
   ThreadStateExtra,
   isEmptyData,
-} from '../common/aggregation_data';
-import {colorForState} from '../core/colorizer';
-import {translateState} from '../common/thread_state';
-
-import {globals} from './globals';
+} from '../public/aggregation';
+import {colorForState} from '../public/lib/colorizer';
 import {DurationWidget} from './widgets/duration';
 import {EmptyState} from '../widgets/empty_state';
 import {Anchor} from '../widgets/anchor';
 import {Icons} from '../base/semantic_icons';
+import {translateState} from '../trace_processor/sql_utils/thread_state';
+import {TraceImpl} from '../core/trace_impl';
 
 export interface AggregationPanelAttrs {
   data?: AggregateData;
-  kind: string;
+  aggregatorId: string;
+  trace: TraceImpl;
 }
 
 export class AggregationPanel
   implements m.ClassComponent<AggregationPanelAttrs>
 {
+  private trace: TraceImpl;
+
+  constructor({attrs}: m.CVnode<AggregationPanelAttrs>) {
+    this.trace = attrs.trace;
+  }
+
   view({attrs}: m.CVnode<AggregationPanelAttrs>) {
     if (!attrs.data || isEmptyData(attrs.data)) {
       return m(
@@ -51,7 +55,7 @@
           {
             icon: Icons.ChangeTab,
             onclick: () => {
-              globals.dispatch(Actions.showTab({uri: 'current_selection'}));
+              this.trace.tabs.showCurrentSelectionTab();
             },
           },
           'Go to current selection tab',
@@ -73,7 +77,7 @@
           m(
             'tr',
             attrs.data.columns.map((col) =>
-              this.formatColumnHeading(col, attrs.kind),
+              this.formatColumnHeading(attrs.trace, col, attrs.aggregatorId),
             ),
           ),
           m(
@@ -89,20 +93,20 @@
     );
   }
 
-  formatColumnHeading(col: Column, id: string) {
-    const pref = globals.state.aggregatePreferences[id];
+  formatColumnHeading(trace: TraceImpl, col: Column, aggregatorId: string) {
+    const pref = trace.selection.aggregation.getSortingPrefs(aggregatorId);
     let sortIcon = '';
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    if (pref && pref.sorting && pref.sorting.column === col.columnId) {
+    if (pref && pref.column === col.columnId) {
       sortIcon =
-        pref.sorting.direction === 'DESC' ? 'arrow_drop_down' : 'arrow_drop_up';
+        pref.direction === 'DESC' ? 'arrow_drop_down' : 'arrow_drop_up';
     }
     return m(
       'th',
       {
         onclick: () => {
-          globals.dispatch(
-            Actions.updateAggregateSorting({id, column: col.columnId}),
+          trace.selection.aggregation.toggleSortingColumn(
+            aggregatorId,
+            col.columnId,
           );
         },
       },
@@ -145,7 +149,7 @@
   }
 
   showTimeRange() {
-    const selection = globals.state.selection;
+    const selection = this.trace.selection.selection;
     if (selection.kind !== 'area') return undefined;
     const duration = selection.end - selection.start;
     return m(
diff --git a/ui/src/frontend/aggregation_tab.ts b/ui/src/frontend/aggregation_tab.ts
index 72846aa..e410a60 100644
--- a/ui/src/frontend/aggregation_tab.ts
+++ b/ui/src/frontend/aggregation_tab.ts
@@ -13,34 +13,30 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {AggregationPanel} from './aggregation_panel';
-import {globals} from './globals';
-import {isEmptyData} from '../common/aggregation_data';
+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';
-import {
-  LegacyFlamegraphDetailsPanel,
-  FlamegraphSelectionParams,
-} from './legacy_flamegraph_panel';
-import {AreaSelection, ProfileType, TrackState} from '../common/state';
-import {assertExists} from '../base/logging';
+import {AreaSelection} from '../public/selection';
 import {Monitor} from '../base/monitor';
 import {
+  CPU_PROFILE_TRACK_KIND,
   PERF_SAMPLES_PROFILE_TRACK_KIND,
-  THREAD_SLICE_TRACK_KIND,
-} from '../core/track_kinds';
+  SLICE_TRACK_KIND,
+} from '../public/track_kinds';
 import {
   QueryFlamegraph,
-  QueryFlamegraphAttrs,
-  USE_NEW_FLAMEGRAPH_IMPL,
   metricsFromTableOrSubquery,
-} from '../core/query_flamegraph';
+} from '../public/lib/query_flamegraph';
 import {DisposableStack} from '../base/disposable_stack';
+import {assertExists} from '../base/logging';
+import {TraceImpl} from '../core/trace_impl';
+import {Trace} from '../public/trace';
+import {Flamegraph} from '../widgets/flamegraph';
 
 interface View {
   key: string;
@@ -48,12 +44,20 @@
   content: m.Children;
 }
 
-class AreaDetailsPanel implements m.ClassComponent {
-  private readonly monitor = new Monitor([() => globals.state.selection]);
+export type AreaDetailsPanelAttrs = {trace: TraceImpl};
+
+class AreaDetailsPanel implements m.ClassComponent<AreaDetailsPanelAttrs> {
+  private trace: TraceImpl;
+  private monitor: Monitor;
   private currentTab: string | undefined = undefined;
-  private perfSampleFlamegraphAttrs?: QueryFlamegraphAttrs;
-  private sliceFlamegraphAttrs?: QueryFlamegraphAttrs;
-  private legacyFlamegraphSelection?: FlamegraphSelectionParams;
+  private cpuProfileFlamegraph?: QueryFlamegraph;
+  private perfSampleFlamegraph?: QueryFlamegraph;
+  private sliceFlamegraph?: QueryFlamegraph;
+
+  constructor({attrs}: m.CVnode<AreaDetailsPanelAttrs>) {
+    this.trace = attrs.trace;
+    this.monitor = new Monitor([() => this.trace.selection.selection]);
+  }
 
   private getCurrentView(): string | undefined {
     const types = this.getViews().map(({key}) => key);
@@ -76,47 +80,54 @@
   private getViews(): View[] {
     const views: View[] = [];
 
-    for (const [key, value] of globals.aggregateDataStore.entries()) {
-      if (!isEmptyData(value)) {
+    for (const aggregator of this.trace.selection.aggregation.aggregators) {
+      const aggregatorId = aggregator.id;
+      const value =
+        this.trace.selection.aggregation.getAggregatedData(aggregatorId);
+      if (value !== undefined && !isEmptyData(value)) {
         views.push({
           key: value.tabName,
           name: value.tabName,
-          content: m(AggregationPanel, {kind: key, key, data: value}),
+          content: m(AggregationPanel, {
+            aggregatorId,
+            data: value,
+            trace: this.trace,
+          }),
         });
       }
     }
 
-    const pivotTableState = globals.state.nonSerializableState.pivotTable;
-    if (pivotTableState.selectionArea !== undefined) {
+    const pivotTableState = this.trace.pivotTable.state;
+    const tree = pivotTableState.queryResult?.tree;
+    if (
+      pivotTableState.selectionArea != undefined &&
+      (tree === undefined || tree.children.size > 0 || tree?.rows.length > 0)
+    ) {
       views.push({
         key: 'pivot_table',
         name: 'Pivot Table',
         content: m(PivotTable, {
+          trace: this.trace,
           selectionArea: pivotTableState.selectionArea,
         }),
       });
     }
 
-    const isChanged = this.monitor.ifStateChanged();
-    if (USE_NEW_FLAMEGRAPH_IMPL.get()) {
-      this.addFlamegraphView(isChanged, views);
-    } else {
-      this.addLegacyFlamegraphView(isChanged, views);
-    }
+    this.addFlamegraphView(this.trace, this.monitor.ifStateChanged(), views);
 
     // Add this after all aggregation panels, to make it appear after 'Slices'
-    if (globals.selectedFlows.length > 0) {
+    if (this.trace.flows.selectedFlows.length > 0) {
       views.push({
         key: 'selected_flows',
         name: 'Flow Events',
-        content: m(FlowEventsAreaSelectedPanel),
+        content: m(FlowEventsAreaSelectedPanel, {trace: this.trace}),
       });
     }
 
     return views;
   }
 
-  view(_: m.Vnode): m.Children {
+  view(): m.Children {
     const views = this.getViews();
     const currentViewKey = this.getCurrentView();
 
@@ -162,115 +173,164 @@
     );
   }
 
-  private addFlamegraphView(isChanged: boolean, views: View[]) {
-    this.perfSampleFlamegraphAttrs =
-      this.computePerfSampleFlamegraphAttrs(isChanged);
-    if (this.perfSampleFlamegraphAttrs !== undefined) {
+  private addFlamegraphView(trace: Trace, isChanged: boolean, views: View[]) {
+    this.cpuProfileFlamegraph = this.computeCpuProfileFlamegraph(
+      trace,
+      isChanged,
+    );
+    if (this.cpuProfileFlamegraph !== undefined) {
+      views.push({
+        key: 'cpu_profile_flamegraph_selection',
+        name: 'CPU Profile Sample Flamegraph',
+        content: this.cpuProfileFlamegraph.render(),
+      });
+    }
+    this.perfSampleFlamegraph = this.computePerfSampleFlamegraph(
+      trace,
+      isChanged,
+    );
+    if (this.perfSampleFlamegraph !== undefined) {
       views.push({
         key: 'perf_sample_flamegraph_selection',
         name: 'Perf Sample Flamegraph',
-        content: m(QueryFlamegraph, this.perfSampleFlamegraphAttrs),
+        content: this.perfSampleFlamegraph.render(),
       });
     }
-    this.sliceFlamegraphAttrs = this.computeSliceFlamegraphAttrs(isChanged);
-    if (this.sliceFlamegraphAttrs !== undefined) {
+    this.sliceFlamegraph = this.computeSliceFlamegraph(trace, isChanged);
+    if (this.sliceFlamegraph !== undefined) {
       views.push({
         key: 'slice_flamegraph_selection',
         name: 'Slice Flamegraph',
-        content: m(QueryFlamegraph, this.sliceFlamegraphAttrs),
+        content: this.sliceFlamegraph.render(),
       });
     }
   }
 
-  private computePerfSampleFlamegraphAttrs(isChanged: boolean) {
-    const currentSelection = globals.state.selection;
+  private computeCpuProfileFlamegraph(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.perfSampleFlamegraphAttrs;
+      return this.cpuProfileFlamegraph;
     }
-    const upids = getUpidsFromPerfSampleAreaSelection(currentSelection);
-    if (upids.length === 0) {
-      const utids = getUtidsFromPerfSampleAreaSelection(currentSelection);
-      if (utids.length === 0) {
-        return undefined;
+    const utids = [];
+    for (const trackInfo of currentSelection.tracks) {
+      if (trackInfo?.tags?.kind === CPU_PROFILE_TRACK_KIND) {
+        utids.push(trackInfo.tags?.utid);
       }
-      return {
-        engine: assertExists(this.getCurrentEngine()),
-        metrics: [
-          ...metricsFromTableOrSubquery(
-            `
-              (
-                select id, parent_id as parentId, name, self_count
-                from _linux_perf_callstacks_for_samples!((
-                  select p.callsite_id
-                  from perf_sample p
-                  where p.ts >= ${currentSelection.start}
-                    and p.ts <= ${currentSelection.end}
-                    and p.utid in (${utids.join(',')})
-                ))
-              )
-            `,
-            [
-              {
-                name: 'Perf Samples',
-                unit: '',
-                columnName: 'self_count',
-              },
-            ],
-            'include perfetto module linux.perf.samples',
-          ),
-        ],
-      };
     }
-    return {
-      engine: assertExists(this.getCurrentEngine()),
-      metrics: [
-        ...metricsFromTableOrSubquery(
-          `
-            (
-              select id, parent_id as parentId, name, self_count
-              from _linux_perf_callstacks_for_samples!((
-                select p.callsite_id
-                from perf_sample p
-                join thread t using (utid)
-                where p.ts >= ${currentSelection.start}
-                  and p.ts <= ${currentSelection.end}
-                  and t.upid in (${upids.join(',')})
-              ))
-            )
-          `,
-          [
-            {
-              name: 'Perf Samples',
-              unit: '',
-              columnName: 'self_count',
-            },
-          ],
-          'include perfetto module linux.perf.samples',
-        ),
+    if (utids.length === 0) {
+      return undefined;
+    }
+    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 cpu_profile_stack_sample p
+            where p.ts >= ${currentSelection.start}
+              and p.ts <= ${currentSelection.end}
+              and p.utid in (${utids.join(',')})
+          ))
+        )
+      `,
+      [
+        {
+          name: 'CPU Profile Samples',
+          unit: '',
+          columnName: 'self_count',
+        },
       ],
-    };
+      'include perfetto module callstacks.stack_profile',
+      [{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',
+        },
+      ],
+    );
+    return new QueryFlamegraph(trace, metrics, {
+      state: Flamegraph.createDefaultState(metrics),
+    });
   }
 
-  private computeSliceFlamegraphAttrs(isChanged: boolean) {
-    const currentSelection = globals.state.selection;
+  private computePerfSampleFlamegraph(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.sliceFlamegraphAttrs;
+      return this.perfSampleFlamegraph;
+    }
+    const upids = getUpidsFromPerfSampleAreaSelection(currentSelection);
+    const utids = getUtidsFromPerfSampleAreaSelection(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 perf_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: 'Perf Samples',
+          unit: '',
+          columnName: 'self_count',
+        },
+      ],
+      'include perfetto module linux.perf.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') {
+      return undefined;
+    }
+    if (!isChanged) {
+      // If the selection has not changed, just return a copy of the last seen
+      // attrs.
+      return this.sliceFlamegraph;
     }
     const trackIds = [];
-    for (const trackId of currentSelection.tracks) {
-      const track: TrackState | undefined = globals.state.tracks[trackId];
-      const trackInfo = globals.trackManager.resolveTrackInfo(track?.uri);
-      if (trackInfo?.tags?.kind !== THREAD_SLICE_TRACK_KIND) {
+    for (const trackInfo of currentSelection.tracks) {
+      if (trackInfo?.tags?.kind !== SLICE_TRACK_KIND) {
         continue;
       }
       if (trackInfo.tags?.trackIds === undefined) {
@@ -281,93 +341,49 @@
     if (trackIds.length === 0) {
       return undefined;
     }
-    return {
-      engine: assertExists(this.getCurrentEngine()),
-      metrics: [
-        ...metricsFromTableOrSubquery(
-          `(
-            select *
-            from _viz_slice_ancestor_agg!((
-              select s.id, s.dur
-              from slice s
-              left join slice t on t.parent_id = s.id
-              where s.ts >= ${currentSelection.start}
-                and s.ts <= ${currentSelection.end}
-                and s.track_id in (${trackIds.join(',')})
-                and t.id is null
-            ))
-          )`,
-          [
-            {
-              name: 'Duration',
-              unit: 'ns',
-              columnName: 'self_dur',
-            },
-            {
-              name: 'Samples',
-              unit: '',
-              columnName: 'self_count',
-            },
-          ],
-          'include perfetto module viz.slices;',
-        ),
+    const metrics = metricsFromTableOrSubquery(
+      `
+        (
+          select *
+          from _viz_slice_ancestor_agg!((
+            select s.id, s.dur
+            from slice s
+            left join slice t on t.parent_id = s.id
+            where s.ts >= ${currentSelection.start}
+              and s.ts <= ${currentSelection.end}
+              and s.track_id in (${trackIds.join(',')})
+              and t.id is null
+          ))
+        )
+      `,
+      [
+        {
+          name: 'Duration',
+          unit: 'ns',
+          columnName: 'self_dur',
+        },
+        {
+          name: 'Samples',
+          unit: '',
+          columnName: 'self_count',
+        },
       ],
-    };
-  }
-
-  private addLegacyFlamegraphView(isChanged: boolean, views: View[]) {
-    this.legacyFlamegraphSelection =
-      this.computeLegacyFlamegraphSelection(isChanged);
-    if (this.legacyFlamegraphSelection === undefined) {
-      return;
-    }
-    views.push({
-      key: 'flamegraph_selection',
-      name: 'Flamegraph Selection',
-      content: m(LegacyFlamegraphDetailsPanel, {
-        cache: globals.areaFlamegraphCache,
-        selection: this.legacyFlamegraphSelection,
-      }),
+      'include perfetto module viz.slices;',
+    );
+    return new QueryFlamegraph(trace, metrics, {
+      state: Flamegraph.createDefaultState(metrics),
     });
   }
-
-  private computeLegacyFlamegraphSelection(isChanged: boolean) {
-    const currentSelection = globals.state.selection;
-    if (currentSelection.kind !== 'area') {
-      return undefined;
-    }
-    if (!isChanged) {
-      // If the selection has not changed, just return a copy of the last seen
-      // selection.
-      return this.legacyFlamegraphSelection;
-    }
-    const upids = getUpidsFromPerfSampleAreaSelection(currentSelection);
-    if (upids.length === 0) {
-      return undefined;
-    }
-    return {
-      profileType: ProfileType.PERF_SAMPLE,
-      start: currentSelection.start,
-      end: currentSelection.end,
-      upids,
-    };
-  }
-
-  private getCurrentEngine() {
-    const engineId = globals.getCurrentEngine()?.id;
-    if (engineId === undefined) return undefined;
-    return globals.engines.get(engineId);
-  }
 }
 
 export class AggregationsTabs implements Disposable {
   private trash = new DisposableStack();
 
-  constructor() {
-    const unregister = globals.tabManager.registerDetailsPanel({
+  constructor(trace: TraceImpl) {
+    const unregister = trace.tabs.registerDetailsPanel({
       render(selection) {
         if (selection.kind === 'area') {
-          return m(AreaDetailsPanel);
+          return m(AreaDetailsPanel, {trace});
         } else {
           return undefined;
         }
@@ -384,32 +400,26 @@
 
 function getUpidsFromPerfSampleAreaSelection(currentSelection: AreaSelection) {
   const upids = [];
-  for (const trackId of currentSelection.tracks) {
-    const track: TrackState | undefined = globals.state.tracks[trackId];
-    const trackInfo = globals.trackManager.resolveTrackInfo(track?.uri);
-    if (trackInfo?.tags?.kind !== PERF_SAMPLES_PROFILE_TRACK_KIND) {
-      continue;
+  for (const trackInfo of currentSelection.tracks) {
+    if (
+      trackInfo?.tags?.kind === PERF_SAMPLES_PROFILE_TRACK_KIND &&
+      trackInfo.tags?.utid === undefined
+    ) {
+      upids.push(assertExists(trackInfo.tags?.upid));
     }
-    if (trackInfo.tags?.upid === undefined) {
-      continue;
-    }
-    upids.push(trackInfo.tags?.upid);
   }
   return upids;
 }
 
 function getUtidsFromPerfSampleAreaSelection(currentSelection: AreaSelection) {
   const utids = [];
-  for (const trackId of currentSelection.tracks) {
-    const track: TrackState | undefined = globals.state.tracks[trackId];
-    const trackInfo = globals.trackManager.resolveTrackInfo(track?.uri);
-    if (trackInfo?.tags?.kind !== PERF_SAMPLES_PROFILE_TRACK_KIND) {
-      continue;
+  for (const trackInfo of currentSelection.tracks) {
+    if (
+      trackInfo?.tags?.kind === PERF_SAMPLES_PROFILE_TRACK_KIND &&
+      trackInfo.tags?.utid !== undefined
+    ) {
+      utids.push(trackInfo.tags?.utid);
     }
-    if (trackInfo.tags?.utid === undefined) {
-      continue;
-    }
-    utids.push(trackInfo.tags?.utid);
   }
   return utids;
 }
diff --git a/ui/src/frontend/analytics.ts b/ui/src/frontend/analytics.ts
deleted file mode 100644
index 97c6bbe..0000000
--- a/ui/src/frontend/analytics.ts
+++ /dev/null
@@ -1,199 +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.
-
-import {ErrorDetails} from '../base/logging';
-import {getCurrentChannel} from '../common/channels';
-import {VERSION} from '../gen/perfetto_version';
-
-import {globals} from './globals';
-import {Router} from './router';
-
-type TraceCategories = 'Trace Actions' | 'Record Trace' | 'User Actions';
-const ANALYTICS_ID = 'G-BD89KT2P3C';
-const PAGE_TITLE = 'no-page-title';
-
-function isValidUrl(s: string) {
-  let url;
-  try {
-    url = new URL(s);
-  } catch (_) {
-    return false;
-  }
-  return url.protocol === 'http:' || url.protocol === 'https:';
-}
-
-function getReferrerOverride(): string | undefined {
-  const route = Router.parseUrl(window.location.href);
-  const referrer = route.args.referrer;
-  if (referrer) {
-    return referrer;
-  } else {
-    return undefined;
-  }
-}
-
-// Get the referrer from either:
-// - If present: the referrer argument if present
-// - document.referrer
-function getReferrer(): string {
-  const referrer = getReferrerOverride();
-  if (referrer) {
-    if (isValidUrl(referrer)) {
-      return referrer;
-    } else {
-      // Unclear if GA discards non-URL referrers. Lets try faking
-      // a URL to test.
-      const name = referrer.replaceAll('_', '-');
-      return `https://${name}.example.com/converted_non_url_referrer`;
-    }
-  } else {
-    return document.referrer.split('?')[0];
-  }
-}
-
-export function initAnalytics() {
-  // Only initialize logging on the official site and on localhost (to catch
-  // analytics bugs when testing locally).
-  // Skip analytics is the fragment has "testing=1", this is used by UI tests.
-  // Skip analytics in embeddedMode since iFrames do not have the same access to
-  // local storage.
-  if (
-    (window.location.origin.startsWith('http://localhost:') ||
-      window.location.origin.endsWith('.perfetto.dev')) &&
-    !globals.testing &&
-    !globals.embeddedMode
-  ) {
-    return new AnalyticsImpl();
-  }
-  return new NullAnalytics();
-}
-
-const gtagGlobals = window as {} as {
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  dataLayer: any[];
-  gtag: (command: string, event: string | Date, args?: {}) => void;
-};
-
-export interface Analytics {
-  initialize(): void;
-  updatePath(_: string): void;
-  logEvent(category: TraceCategories | null, event: string): void;
-  logError(err: ErrorDetails): void;
-  isEnabled(): boolean;
-}
-
-export class NullAnalytics implements Analytics {
-  initialize() {}
-  updatePath(_: string) {}
-  logEvent(_category: TraceCategories | null, _event: string) {}
-  logError(_err: ErrorDetails) {}
-  isEnabled(): boolean {
-    return false;
-  }
-}
-
-class AnalyticsImpl implements Analytics {
-  private initialized_ = false;
-
-  constructor() {
-    // The code below is taken from the official Google Analytics docs [1] and
-    // adapted to TypeScript. We have it here rather than as an inline script
-    // in index.html (as suggested by GA's docs) because inline scripts don't
-    // play nicely with the CSP policy, at least in Firefox (Firefox doesn't
-    // support all CSP 3 features we use).
-    // [1] https://developers.google.com/analytics/devguides/collection/gtagjs .
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    gtagGlobals.dataLayer = gtagGlobals.dataLayer || [];
-
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    function gtagFunction(..._: any[]) {
-      // This needs to be a function and not a lambda. |arguments| behaves
-      // slightly differently in a lambda and breaks GA.
-      gtagGlobals.dataLayer.push(arguments);
-    }
-    gtagGlobals.gtag = gtagFunction;
-    gtagGlobals.gtag('js', new Date());
-  }
-
-  // This is callled only after the script that sets isInternalUser loads.
-  // It is fine to call updatePath() and log*() functions before initialize().
-  // The gtag() function internally enqueues all requests into |dataLayer|.
-  initialize() {
-    if (this.initialized_) return;
-    this.initialized_ = true;
-    const script = document.createElement('script');
-    script.src = 'https://www.googletagmanager.com/gtag/js?id=' + ANALYTICS_ID;
-    script.defer = true;
-    document.head.appendChild(script);
-    const route = window.location.href;
-    console.log(
-      `GA initialized. route=${route}`,
-      `isInternalUser=${globals.isInternalUser}`,
-    );
-    // GA's recommendation for SPAs is to disable automatic page views and
-    // manually send page_view events. See:
-    // https://developers.google.com/analytics/devguides/collection/gtagjs/pages#manual_pageviews
-    gtagGlobals.gtag('config', ANALYTICS_ID, {
-      allow_google_signals: false,
-      anonymize_ip: true,
-      page_location: route,
-      // Referrer as a URL including query string override.
-      page_referrer: getReferrer(),
-      send_page_view: false,
-      page_title: PAGE_TITLE,
-      perfetto_is_internal_user: globals.isInternalUser ? '1' : '0',
-      perfetto_version: VERSION,
-      // Release channel (canary, stable, autopush)
-      perfetto_channel: getCurrentChannel(),
-      // Referrer *if overridden* via the query string else empty string.
-      perfetto_referrer_override: getReferrerOverride() ?? '',
-    });
-    this.updatePath(route);
-  }
-
-  updatePath(path: string) {
-    gtagGlobals.gtag('event', 'page_view', {
-      page_path: path,
-      page_title: PAGE_TITLE,
-    });
-  }
-
-  logEvent(category: TraceCategories | null, event: string) {
-    gtagGlobals.gtag('event', event, {event_category: category});
-  }
-
-  logError(err: ErrorDetails) {
-    let stack = '';
-    for (const entry of err.stack) {
-      const shortLocation = entry.location.replace('frontend_bundle.js', '$');
-      stack += `${entry.name}(${shortLocation}),`;
-    }
-    // Strip trailing ',' (works also for empty strings without extra checks).
-    stack = stack.substring(0, stack.length - 1);
-
-    gtagGlobals.gtag('event', 'exception', {
-      description: err.message,
-      error_type: err.errType,
-
-      // As per GA4 all field are restrictred to 100 chars.
-      // page_title is the only one restricted to 1000 chars and we use that for
-      // the full crash report.
-      page_location: `http://crash?/${encodeURI(stack)}`,
-    });
-  }
-
-  isEnabled(): boolean {
-    return true;
-  }
-}
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
deleted file mode 100644
index f6d3939..0000000
--- a/ui/src/frontend/app.ts
+++ /dev/null
@@ -1,860 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {copyToClipboard} from '../base/clipboard';
-
-import {findRef} from '../base/dom_utils';
-import {FuzzyFinder} from '../base/fuzzy';
-import {assertExists, assertUnreachable} from '../base/logging';
-import {undoCommonChatAppReplacements} from '../base/string_utils';
-import {Actions} from '../common/actions';
-import {
-  DurationPrecision,
-  setDurationPrecision,
-  setTimestampFormat,
-  TimestampFormat,
-} from '../core/timestamp_format';
-import {raf} from '../core/raf_scheduler';
-import {Command, Engine, addDebugSliceTrack} from '../public';
-import {HotkeyConfig, HotkeyContext} from '../widgets/hotkey_context';
-import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
-import {maybeRenderFullscreenModalDialog} from '../widgets/modal';
-
-import {onClickCopy} from './clipboard';
-import {CookieConsent} from './cookie_consent';
-import {getTimeSpanOfSelectionOrVisibleWindow, globals} from './globals';
-import {toggleHelp} from './help_modal';
-import {Notes} from './notes';
-import {Omnibox, OmniboxOption} from './omnibox';
-import {addQueryResultsTab} from './query_result_tab';
-import {executeSearch} from './search_handler';
-import {Sidebar} from './sidebar';
-import {Topbar} from './topbar';
-import {shareTrace} from './trace_attrs';
-import {AggregationsTabs} from './aggregation_tab';
-import {
-  findCurrentSelection,
-  focusOtherFlow,
-  moveByFocusedFlow,
-} from './keyboard_event_handler';
-import {publishPermalinkHash} from './publish';
-import {OmniboxMode, PromptOption} from './omnibox_manager';
-import {Utid} from '../trace_processor/sql_utils/core_types';
-import {THREAD_STATE_TRACK_KIND} from '../core/track_kinds';
-import {DisposableStack} from '../base/disposable_stack';
-import {addSqlTableTab} from './sql_table_tab';
-import {SqlTables} from './widgets/sql/table/well_known_sql_tables';
-import {getThreadInfo} from '../trace_processor/sql_utils/thread';
-
-function renderPermalink(): m.Children {
-  const hash = globals.permalinkHash;
-  if (!hash) return null;
-  const url = `${self.location.origin}/#!/?s=${hash}`;
-  const linkProps = {title: 'Click to copy the URL', onclick: onClickCopy(url)};
-
-  return m('.alert-permalink', [
-    m('div', 'Permalink: ', m(`a[href=${url}]`, linkProps, url)),
-    m(
-      'button',
-      {
-        onclick: () => publishPermalinkHash(undefined),
-      },
-      m('i.material-icons.disallow-selection', 'close'),
-    ),
-  ]);
-}
-
-class Alerts implements m.ClassComponent {
-  view() {
-    return m('.alerts', renderPermalink());
-  }
-}
-
-const criticalPathSliceColumns = {
-  ts: 'ts',
-  dur: 'dur',
-  name: 'name',
-};
-const criticalPathsliceColumnNames = [
-  'id',
-  'utid',
-  'ts',
-  'dur',
-  'name',
-  'table_name',
-];
-
-const criticalPathsliceLiteColumns = {
-  ts: 'ts',
-  dur: 'dur',
-  name: 'thread_name',
-};
-const criticalPathsliceLiteColumnNames = [
-  'id',
-  'utid',
-  'ts',
-  'dur',
-  'thread_name',
-  'process_name',
-  'table_name',
-];
-
-export class App implements m.ClassComponent {
-  private trash = new DisposableStack();
-  static readonly OMNIBOX_INPUT_REF = 'omnibox';
-  private omniboxInputEl?: HTMLInputElement;
-  private recentCommands: string[] = [];
-
-  constructor() {
-    this.trash.use(new Notes());
-    this.trash.use(new AggregationsTabs());
-  }
-
-  private getEngine(): Engine | undefined {
-    const engineId = globals.getCurrentEngine()?.id;
-    if (engineId === undefined) {
-      return undefined;
-    }
-    const engine = globals.engines.get(engineId)?.getProxy('QueryPage');
-    return engine;
-  }
-
-  private getFirstUtidOfSelectionOrVisibleWindow(): number {
-    const selection = globals.state.selection;
-    if (selection.kind === 'area') {
-      const firstThreadStateTrack = selection.tracks.find((trackId) => {
-        return globals.state.tracks[trackId];
-      });
-
-      if (firstThreadStateTrack) {
-        const trackInfo = globals.state.tracks[firstThreadStateTrack];
-        const trackDesc = globals.trackManager.resolveTrackInfo(trackInfo.uri);
-        if (
-          trackDesc?.tags?.kind === THREAD_STATE_TRACK_KIND &&
-          trackDesc?.tags?.utid !== undefined
-        ) {
-          return trackDesc.tags.utid;
-        }
-      }
-    }
-
-    return 0;
-  }
-
-  private cmds: Command[] = [
-    {
-      id: 'perfetto.SetTimestampFormat',
-      name: 'Set timestamp and duration format',
-      callback: async () => {
-        const options: PromptOption[] = [
-          {key: TimestampFormat.Timecode, displayName: 'Timecode'},
-          {key: TimestampFormat.UTC, displayName: 'Realtime (UTC)'},
-          {
-            key: TimestampFormat.TraceTz,
-            displayName: 'Realtime (Trace TZ)',
-          },
-          {key: TimestampFormat.Seconds, displayName: 'Seconds'},
-          {key: TimestampFormat.Raw, displayName: 'Raw'},
-          {
-            key: TimestampFormat.RawLocale,
-            displayName: 'Raw (with locale-specific formatting)',
-          },
-        ];
-        const promptText = 'Select format...';
-
-        try {
-          const result = await globals.omnibox.prompt(promptText, options);
-          setTimestampFormat(result as TimestampFormat);
-          raf.scheduleFullRedraw();
-        } catch {
-          // Prompt was probably cancelled - do nothing.
-        }
-      },
-    },
-    {
-      id: 'perfetto.SetDurationPrecision',
-      name: 'Set duration precision',
-      callback: async () => {
-        const options: PromptOption[] = [
-          {key: DurationPrecision.Full, displayName: 'Full'},
-          {
-            key: DurationPrecision.HumanReadable,
-            displayName: 'Human readable',
-          },
-        ];
-        const promptText = 'Select duration precision mode...';
-
-        try {
-          const result = await globals.omnibox.prompt(promptText, options);
-          setDurationPrecision(result as DurationPrecision);
-          raf.scheduleFullRedraw();
-        } catch {
-          // Prompt was probably cancelled - do nothing.
-        }
-      },
-    },
-    {
-      id: 'perfetto.CriticalPathLite',
-      name: `Critical path lite`,
-      callback: async () => {
-        const trackUtid = this.getFirstUtidOfSelectionOrVisibleWindow();
-        const window = await getTimeSpanOfSelectionOrVisibleWindow();
-        const engine = this.getEngine();
-
-        if (engine !== undefined && trackUtid != 0) {
-          await engine.query(
-            `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
-          );
-          await addDebugSliceTrack(
-            // NOTE(stevegolton): This is a temporary patch, this menu should
-            // become part of a critical path plugin, at which point we can just
-            // use the plugin's context object.
-            {
-              engine,
-              registerTrack: (x) => globals.trackManager.registerTrack(x),
-            },
-            {
-              sqlSource: `
-                   SELECT
-                      cr.id,
-                      cr.utid,
-                      cr.ts,
-                      cr.dur,
-                      thread.name AS thread_name,
-                      process.name AS process_name,
-                      'thread_state' AS table_name
-                    FROM
-                      _thread_executing_span_critical_path(
-                          ${trackUtid},
-                          ${window.start},
-                          ${window.end} - ${window.start}) cr
-                    JOIN thread USING(utid)
-                    JOIN process USING(upid)
-                  `,
-              columns: criticalPathsliceLiteColumnNames,
-            },
-            (await getThreadInfo(engine, trackUtid as Utid)).name ??
-              '<thread name>',
-            criticalPathsliceLiteColumns,
-            criticalPathsliceLiteColumnNames,
-          );
-        }
-      },
-    },
-    {
-      id: 'perfetto.CriticalPath',
-      name: `Critical path`,
-      callback: async () => {
-        const trackUtid = this.getFirstUtidOfSelectionOrVisibleWindow();
-        const window = await getTimeSpanOfSelectionOrVisibleWindow();
-        const engine = this.getEngine();
-
-        if (engine !== undefined && trackUtid != 0) {
-          await engine.query(
-            `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
-          );
-          await addDebugSliceTrack(
-            // NOTE(stevegolton): This is a temporary patch, this menu should
-            // become part of a critical path plugin, at which point we can just
-            // use the plugin's context object.
-            {
-              engine,
-              registerTrack: (x) => globals.trackManager.registerTrack(x),
-            },
-            {
-              sqlSource: `
-                        SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
-                        FROM
-                        _critical_path_stack(
-                          ${trackUtid},
-                          ${window.start},
-                          ${window.end} - ${window.start}, 1, 1, 1, 1) cr WHERE name IS NOT NULL
-                  `,
-              columns: criticalPathsliceColumnNames,
-            },
-            (await getThreadInfo(engine, trackUtid as Utid)).name ??
-              '<thread name>',
-            criticalPathSliceColumns,
-            criticalPathsliceColumnNames,
-          );
-        }
-      },
-    },
-    {
-      id: 'perfetto.CriticalPathPprof',
-      name: `Critical path pprof`,
-      callback: async () => {
-        const trackUtid = this.getFirstUtidOfSelectionOrVisibleWindow();
-        const window = await getTimeSpanOfSelectionOrVisibleWindow();
-        const engine = this.getEngine();
-
-        if (engine !== undefined && trackUtid != 0) {
-          addQueryResultsTab({
-            query: `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;
-                   SELECT *
-                      FROM
-                        _thread_executing_span_critical_path_graph(
-                        "criical_path",
-                         ${trackUtid},
-                         ${window.start},
-                         ${window.end} - ${window.start}) cr`,
-            title: 'Critical path',
-          });
-        }
-      },
-    },
-    {
-      id: 'perfetto.ShowSliceTable',
-      name: 'Open new slice table tab',
-      callback: () => {
-        addSqlTableTab({
-          table: SqlTables.slice,
-          displayName: 'slice',
-        });
-      },
-    },
-    {
-      id: 'perfetto.TogglePerformanceMetrics',
-      name: 'Toggle performance metrics',
-      callback: () => {
-        globals.dispatch(Actions.togglePerfDebug({}));
-      },
-    },
-    {
-      id: 'perfetto.ShareTrace',
-      name: 'Share trace',
-      callback: shareTrace,
-    },
-    {
-      id: 'perfetto.SearchNext',
-      name: 'Go to next search result',
-      callback: () => {
-        executeSearch();
-      },
-      defaultHotkey: 'Enter',
-    },
-    {
-      id: 'perfetto.SearchPrev',
-      name: 'Go to previous search result',
-      callback: () => {
-        executeSearch(true);
-      },
-      defaultHotkey: 'Shift+Enter',
-    },
-    {
-      id: 'perfetto.OpenCommandPalette',
-      name: 'Open command palette',
-      callback: () => globals.omnibox.setMode(OmniboxMode.Command),
-      defaultHotkey: '!Mod+Shift+P',
-    },
-    {
-      id: 'perfetto.RunQuery',
-      name: 'Run query',
-      callback: () => globals.omnibox.setMode(OmniboxMode.Query),
-    },
-    {
-      id: 'perfetto.Search',
-      name: 'Search',
-      callback: () => globals.omnibox.setMode(OmniboxMode.Search),
-      defaultHotkey: '/',
-    },
-    {
-      id: 'perfetto.ShowHelp',
-      name: 'Show help',
-      callback: () => toggleHelp(),
-      defaultHotkey: '?',
-    },
-    {
-      id: 'perfetto.CopyTimeWindow',
-      name: `Copy selected time window to clipboard`,
-      callback: async () => {
-        const window = await getTimeSpanOfSelectionOrVisibleWindow();
-        const query = `ts >= ${window.start} and ts < ${window.end}`;
-        copyToClipboard(query);
-      },
-    },
-    {
-      id: 'perfetto.FocusSelection',
-      name: 'Focus current selection',
-      callback: () => findCurrentSelection(),
-      defaultHotkey: 'F',
-    },
-    {
-      id: 'perfetto.Deselect',
-      name: 'Deselect',
-      callback: () => {
-        globals.timeline.deselectArea();
-        globals.clearSelection();
-        globals.dispatch(Actions.removeNote({id: '0'}));
-      },
-      defaultHotkey: 'Escape',
-    },
-    {
-      id: 'perfetto.SetTemporarySpanNote',
-      name: 'Set the temporary span note based on the current selection',
-      callback: async () => {
-        const range = await globals.findTimeRangeOfSelection();
-        if (range) {
-          globals.dispatch(
-            Actions.addSpanNote({
-              start: range.start,
-              end: range.end,
-              id: '__temp__',
-            }),
-          );
-        }
-      },
-      defaultHotkey: 'M',
-    },
-    {
-      id: 'perfetto.AddSpanNote',
-      name: 'Add a new span note based on the current selection',
-      callback: async () => {
-        const range = await globals.findTimeRangeOfSelection();
-        if (range) {
-          globals.dispatch(
-            Actions.addSpanNote({start: range.start, end: range.end}),
-          );
-        }
-      },
-      defaultHotkey: 'Shift+M',
-    },
-    {
-      id: 'perfetto.RemoveSelectedNote',
-      name: 'Remove selected note',
-      callback: () => {
-        const selection = globals.state.selection;
-        if (selection.kind === 'note') {
-          globals.dispatch(
-            Actions.removeNote({
-              id: selection.id,
-            }),
-          );
-        }
-      },
-      defaultHotkey: 'Delete',
-    },
-    {
-      id: 'perfetto.NextFlow',
-      name: 'Next flow',
-      callback: () => focusOtherFlow('Forward'),
-      defaultHotkey: 'Mod+]',
-    },
-    {
-      id: 'perfetto.PrevFlow',
-      name: 'Prev flow',
-      callback: () => focusOtherFlow('Backward'),
-      defaultHotkey: 'Mod+[',
-    },
-    {
-      id: 'perfetto.MoveNextFlow',
-      name: 'Move next flow',
-      callback: () => moveByFocusedFlow('Forward'),
-      defaultHotkey: ']',
-    },
-    {
-      id: 'perfetto.MovePrevFlow',
-      name: 'Move prev flow',
-      callback: () => moveByFocusedFlow('Backward'),
-      defaultHotkey: '[',
-    },
-    {
-      id: 'perfetto.SelectAll',
-      name: 'Select all',
-      callback: () => {
-        // This is a dual state command:
-        // - If one ore more tracks are already area selected, expand the time
-        //   range to include the entire trace, but keep the selection on just
-        //   these tracks.
-        // - If nothing is selected, or all selected tracks are entirely
-        //   selected, then select the entire trace. This allows double tapping
-        //   Ctrl+A to select the entire track, then select the entire trace.
-        let tracksToSelect: string[] = [];
-        const selection = globals.state.selection;
-        if (selection.kind === 'area') {
-          // Something is already selected, let's see if it covers the entire
-          // span of the trace or not
-          const coversEntireTimeRange =
-            globals.traceContext.start === selection.start &&
-            globals.traceContext.end === selection.end;
-          if (!coversEntireTimeRange) {
-            // If the current selection is an area which does not cover the
-            // entire time range, preserve the list of selected tracks and
-            // expand the time range.
-            tracksToSelect = selection.tracks;
-          } else {
-            // If the entire time range is already covered, update the selection
-            // to cover all tracks.
-            tracksToSelect = Object.keys(globals.state.tracks);
-          }
-        } else {
-          // If the current selection is not an area, select all.
-          tracksToSelect = Object.keys(globals.state.tracks);
-        }
-        const {start, end} = globals.traceContext;
-        globals.dispatch(
-          Actions.selectArea({
-            start,
-            end,
-            tracks: tracksToSelect,
-          }),
-        );
-      },
-      defaultHotkey: 'Mod+A',
-    },
-  ];
-
-  commands() {
-    return this.cmds;
-  }
-
-  private renderOmnibox(): m.Children {
-    const msgTTL = globals.state.status.timestamp + 1 - Date.now() / 1e3;
-    const engineIsBusy =
-      globals.state.engine !== undefined && !globals.state.engine.ready;
-
-    if (msgTTL > 0 || engineIsBusy) {
-      setTimeout(() => raf.scheduleFullRedraw(), msgTTL * 1000);
-      return m(
-        `.omnibox.message-mode`,
-        m(`input[readonly][disabled][ref=omnibox]`, {
-          value: '',
-          placeholder: globals.state.status.msg,
-        }),
-      );
-    }
-
-    const omniboxMode = globals.omnibox.omniboxMode;
-
-    if (omniboxMode === OmniboxMode.Command) {
-      return this.renderCommandOmnibox();
-    } else if (omniboxMode === OmniboxMode.Prompt) {
-      return this.renderPromptOmnibox();
-    } else if (omniboxMode === OmniboxMode.Query) {
-      return this.renderQueryOmnibox();
-    } else if (omniboxMode === OmniboxMode.Search) {
-      return this.renderSearchOmnibox();
-    } else {
-      assertUnreachable(omniboxMode);
-    }
-  }
-
-  renderPromptOmnibox(): m.Children {
-    const prompt = assertExists(globals.omnibox.pendingPrompt);
-
-    let options: OmniboxOption[] | undefined = undefined;
-
-    if (prompt.options) {
-      const fuzzy = new FuzzyFinder(
-        prompt.options,
-        ({displayName}) => displayName,
-      );
-      const result = fuzzy.find(globals.omnibox.text);
-      options = result.map((result) => {
-        return {
-          key: result.item.key,
-          displayName: result.segments,
-        };
-      });
-    }
-
-    return m(Omnibox, {
-      value: globals.omnibox.text,
-      placeholder: prompt.text,
-      inputRef: App.OMNIBOX_INPUT_REF,
-      extraClasses: 'prompt-mode',
-      closeOnOutsideClick: true,
-      options,
-      selectedOptionIndex: globals.omnibox.omniboxSelectionIndex,
-      onSelectedOptionChanged: (index) => {
-        globals.omnibox.setOmniboxSelectionIndex(index);
-        raf.scheduleFullRedraw();
-      },
-      onInput: (value) => {
-        globals.omnibox.setText(value);
-        globals.omnibox.setOmniboxSelectionIndex(0);
-        raf.scheduleFullRedraw();
-      },
-      onSubmit: (value, _alt) => {
-        globals.omnibox.resolvePrompt(value);
-      },
-      onClose: () => {
-        globals.omnibox.rejectPrompt();
-      },
-    });
-  }
-
-  renderCommandOmnibox(): m.Children {
-    const cmdMgr = globals.commandManager;
-
-    // Fuzzy-filter commands by the filter string.
-    const filteredCmds = cmdMgr.fuzzyFilterCommands(globals.omnibox.text);
-
-    // Create an array of commands with attached heuristics from the recent
-    // command register.
-    const commandsWithHeuristics = filteredCmds.map((cmd) => {
-      return {
-        recentsIndex: this.recentCommands.findIndex((id) => id === cmd.id),
-        cmd,
-      };
-    });
-
-    // Sort by recentsIndex then by alphabetical order
-    const sorted = commandsWithHeuristics.sort((a, b) => {
-      if (b.recentsIndex === a.recentsIndex) {
-        return a.cmd.name.localeCompare(b.cmd.name);
-      } else {
-        return b.recentsIndex - a.recentsIndex;
-      }
-    });
-
-    const options = sorted.map(({recentsIndex, cmd}): OmniboxOption => {
-      const {segments, id, defaultHotkey} = cmd;
-      return {
-        key: id,
-        displayName: segments,
-        tag: recentsIndex !== -1 ? 'recently used' : undefined,
-        rightContent: defaultHotkey && m(HotkeyGlyphs, {hotkey: defaultHotkey}),
-      };
-    });
-
-    return m(Omnibox, {
-      value: globals.omnibox.text,
-      placeholder: 'Filter commands...',
-      inputRef: App.OMNIBOX_INPUT_REF,
-      extraClasses: 'command-mode',
-      options,
-      closeOnSubmit: true,
-      closeOnOutsideClick: true,
-      selectedOptionIndex: globals.omnibox.omniboxSelectionIndex,
-      onSelectedOptionChanged: (index) => {
-        globals.omnibox.setOmniboxSelectionIndex(index);
-        raf.scheduleFullRedraw();
-      },
-      onInput: (value) => {
-        globals.omnibox.setText(value);
-        globals.omnibox.setOmniboxSelectionIndex(0);
-        raf.scheduleFullRedraw();
-      },
-      onClose: () => {
-        if (this.omniboxInputEl) {
-          this.omniboxInputEl.blur();
-        }
-        globals.omnibox.reset();
-      },
-      onSubmit: (key: string) => {
-        this.addRecentCommand(key);
-        cmdMgr.runCommand(key);
-      },
-      onGoBack: () => {
-        globals.omnibox.reset();
-      },
-    });
-  }
-
-  private addRecentCommand(id: string): void {
-    this.recentCommands = this.recentCommands.filter((x) => x !== id);
-    this.recentCommands.push(id);
-    while (this.recentCommands.length > 6) {
-      this.recentCommands.shift();
-    }
-  }
-
-  renderQueryOmnibox(): m.Children {
-    const ph = 'e.g. select * from sched left join thread using(utid) limit 10';
-    return m(Omnibox, {
-      value: globals.omnibox.text,
-      placeholder: ph,
-      inputRef: App.OMNIBOX_INPUT_REF,
-      extraClasses: 'query-mode',
-
-      onInput: (value) => {
-        globals.omnibox.setText(value);
-        raf.scheduleFullRedraw();
-      },
-      onSubmit: (query, alt) => {
-        const config = {
-          query: undoCommonChatAppReplacements(query),
-          title: alt ? 'Pinned query' : 'Omnibox query',
-        };
-        const tag = alt ? undefined : 'omnibox_query';
-        addQueryResultsTab(config, tag);
-      },
-      onClose: () => {
-        globals.omnibox.setText('');
-        if (this.omniboxInputEl) {
-          this.omniboxInputEl.blur();
-        }
-        globals.omnibox.reset();
-        raf.scheduleFullRedraw();
-      },
-      onGoBack: () => {
-        globals.omnibox.reset();
-      },
-    });
-  }
-
-  renderSearchOmnibox(): m.Children {
-    const omniboxState = globals.state.omniboxState;
-    const displayStepThrough =
-      omniboxState.omnibox.length >= 4 || omniboxState.force;
-
-    return m(Omnibox, {
-      value: globals.state.omniboxState.omnibox,
-      placeholder: "Search or type '>' for commands or ':' for SQL mode",
-      inputRef: App.OMNIBOX_INPUT_REF,
-      onInput: (value, prev) => {
-        if (prev === '') {
-          if (value === '>') {
-            globals.omnibox.setMode(OmniboxMode.Command);
-            return;
-          } else if (value === ':') {
-            globals.omnibox.setMode(OmniboxMode.Query);
-            return;
-          }
-        }
-        globals.dispatch(Actions.setOmnibox({omnibox: value, mode: 'SEARCH'}));
-      },
-      onClose: () => {
-        if (this.omniboxInputEl) {
-          this.omniboxInputEl.blur();
-        }
-      },
-      onSubmit: (value, _mod, shift) => {
-        executeSearch(shift);
-        globals.dispatch(
-          Actions.setOmnibox({omnibox: value, mode: 'SEARCH', force: true}),
-        );
-        if (this.omniboxInputEl) {
-          this.omniboxInputEl.blur();
-        }
-      },
-      rightContent: displayStepThrough && this.renderStepThrough(),
-    });
-  }
-
-  private renderStepThrough() {
-    return m(
-      '.stepthrough',
-      m(
-        '.current',
-        `${
-          globals.currentSearchResults.totalResults === 0
-            ? '0 / 0'
-            : `${globals.state.searchIndex + 1} / ${
-                globals.currentSearchResults.totalResults
-              }`
-        }`,
-      ),
-      m(
-        'button',
-        {
-          onclick: () => {
-            executeSearch(true /* reverse direction */);
-          },
-        },
-        m('i.material-icons.left', 'keyboard_arrow_left'),
-      ),
-      m(
-        'button',
-        {
-          onclick: () => {
-            executeSearch();
-          },
-        },
-        m('i.material-icons.right', 'keyboard_arrow_right'),
-      ),
-    );
-  }
-
-  view({children}: m.Vnode): m.Children {
-    const hotkeys: HotkeyConfig[] = [];
-    const commands = globals.commandManager.commands;
-    for (const {id, defaultHotkey} of commands) {
-      if (defaultHotkey) {
-        hotkeys.push({
-          callback: () => {
-            globals.commandManager.runCommand(id);
-          },
-          hotkey: defaultHotkey,
-        });
-      }
-    }
-
-    return m(
-      HotkeyContext,
-      {hotkeys},
-      m(
-        'main',
-        m(Sidebar),
-        m(Topbar, {
-          omnibox: this.renderOmnibox(),
-        }),
-        m(Alerts),
-        children,
-        m(CookieConsent),
-        maybeRenderFullscreenModalDialog(),
-        globals.state.perfDebug && m('.perf-stats'),
-      ),
-    );
-  }
-
-  oncreate({dom}: m.VnodeDOM) {
-    this.updateOmniboxInputRef(dom);
-    this.maybeFocusOmnibar();
-
-    // Register each command with the command manager
-    this.cmds.forEach((cmd) => {
-      const dispose = globals.commandManager.registerCommand(cmd);
-      this.trash.use(dispose);
-    });
-  }
-
-  onupdate({dom}: m.VnodeDOM) {
-    this.updateOmniboxInputRef(dom);
-    this.maybeFocusOmnibar();
-  }
-
-  onremove(_: m.VnodeDOM) {
-    this.trash.dispose();
-    this.omniboxInputEl = undefined;
-  }
-
-  private updateOmniboxInputRef(dom: Element): void {
-    const el = findRef(dom, App.OMNIBOX_INPUT_REF);
-    if (el && el instanceof HTMLInputElement) {
-      this.omniboxInputEl = el;
-    }
-  }
-
-  private maybeFocusOmnibar() {
-    if (globals.omnibox.focusOmniboxNextRender) {
-      const omniboxEl = this.omniboxInputEl;
-      if (omniboxEl) {
-        omniboxEl.focus();
-        if (globals.omnibox.pendingCursorPlacement === undefined) {
-          omniboxEl.select();
-        } else {
-          omniboxEl.setSelectionRange(
-            globals.omnibox.pendingCursorPlacement,
-            globals.omnibox.pendingCursorPlacement,
-          );
-        }
-      }
-      globals.omnibox.clearOmniboxFocusFlag();
-    }
-  }
-}
diff --git a/ui/src/frontend/app_context.ts b/ui/src/frontend/app_context.ts
deleted file mode 100644
index 6f13761..0000000
--- a/ui/src/frontend/app_context.ts
+++ /dev/null
@@ -1,31 +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 {CurrentSearchResults} from '../common/search_data';
-import {State} from '../common/state';
-import {Store} from '../public';
-import {Timeline} from './timeline';
-import {TraceContext} from './trace_context';
-
-export interface AppContext {
-  readonly store: Store<State>;
-  readonly state: State;
-  readonly traceContext: TraceContext;
-
-  // TODO(stevegolton): This could probably be moved into TraceContext.
-  readonly timeline: Timeline;
-
-  // TODO(stevegolton): Move this into the search subsystem when it exists.
-  readonly currentSearchResults: CurrentSearchResults;
-}
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index bafeade..c09ccbc 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -13,24 +13,21 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {searchSegment} from '../base/binary_search';
-
 import {assertTrue, assertUnreachable} from '../base/logging';
 import {Time, time} from '../base/time';
 import {uuidv4Sql} from '../base/uuid';
-import {drawTrackHoverTooltip} from '../common/canvas_utils';
+import {drawTrackHoverTooltip} from '../base/canvas_utils';
 import {raf} from '../core/raf_scheduler';
 import {CacheKey} from '../core/timeline_cache';
-import {Track, TrackMouseEvent, TrackRenderContext} from '../public/tracks';
+import {Track, TrackMouseEvent, TrackRenderContext} from '../public/track';
 import {Button} from '../widgets/button';
 import {MenuDivider, MenuItem, PopupMenu2} from '../widgets/menu';
-import {Engine} from '../trace_processor/engine';
 import {LONG, NUM} from '../trace_processor/query_result';
-
 import {checkerboardExcept} from './checkerboard';
 import {NewTrackArgs} from './track';
 import {AsyncDisposableStack} from '../base/disposable_stack';
+import {Trace} from '../public/trace';
 
 function roundAway(n: number): number {
   const exp = Math.ceil(Math.log10(Math.max(Math.abs(n), 1)));
@@ -188,8 +185,8 @@
 };
 
 export abstract class BaseCounterTrack implements Track {
-  protected engine: Engine;
-  protected trackKey: string;
+  protected trace: Trace;
+  protected uri: string;
   protected trackUuid = uuidv4Sql();
 
   // This is the over-skirted cached bounds:
@@ -248,8 +245,8 @@
   }
 
   constructor(args: BaseCounterTrackArgs) {
-    this.engine = args.engine;
-    this.trackKey = args.trackKey;
+    this.trace = args.trace;
+    this.uri = args.uri;
     this.defaultOptions = args.options ?? {};
     this.trash = new AsyncDisposableStack();
   }
@@ -912,4 +909,8 @@
   get unit(): string {
     return this.getCounterOptions().unit ?? '';
   }
+
+  protected get engine() {
+    return this.trace.engine;
+  }
 }
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 8778a50..7ef1cd4 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -14,35 +14,27 @@
 
 import {assertExists} from '../base/logging';
 import {clamp, floatEqual} from '../base/math_utils';
-import {Time, time} from '../base/time';
+import {Duration, Time, time} from '../base/time';
 import {exists} from '../base/utils';
-import {Actions} from '../common/actions';
-import {
-  drawIncompleteSlice,
-  drawTrackHoverTooltip,
-} from '../common/canvas_utils';
+import {drawIncompleteSlice, drawTrackHoverTooltip} from '../base/canvas_utils';
 import {cropText} from '../base/string_utils';
-import {colorCompare} from '../core/color';
-import {UNEXPECTED_PINK} from '../core/colorizer';
-import {
-  LegacySelection,
-  SelectionKind,
-  getLegacySelection,
-} from '../common/state';
+import {colorCompare} from '../public/color';
+import {UNEXPECTED_PINK} from '../public/lib/colorizer';
+import {TrackEventDetails} from '../public/selection';
 import {featureFlags} from '../core/feature_flags';
 import {raf} from '../core/raf_scheduler';
-import {Engine, Slice, SliceRect, Track} from '../public';
+import {Track} from '../public/track';
+import {Slice} from '../public/track';
 import {LONG, NUM} from '../trace_processor/query_result';
-
 import {checkerboardExcept} from './checkerboard';
-import {globals} from './globals';
 import {DEFAULT_SLICE_LAYOUT, SliceLayout} from './slice_layout';
 import {NewTrackArgs} from './track';
 import {BUCKETS_PER_PIXEL, CacheKey} from '../core/timeline_cache';
 import {uuidv4Sql} from '../base/uuid';
 import {AsyncDisposableStack} from '../base/disposable_stack';
-import {TrackMouseEvent, TrackRenderContext} from '../public/tracks';
-import {Vector} from '../base/geom';
+import {TrackMouseEvent, TrackRenderContext} from '../public/track';
+import {Point2D, VerticalBounds} from '../base/geom';
+import {Trace} from '../public/trace';
 
 // The common class that underpins all tracks drawing slices.
 
@@ -170,8 +162,8 @@
 > implements Track
 {
   protected sliceLayout: SliceLayout = {...DEFAULT_SLICE_LAYOUT};
-  protected engine: Engine;
-  protected trackKey: string;
+  protected trace: Trace;
+  protected uri: string;
   protected trackUuid = uuidv4Sql();
 
   // This is the over-skirted cached bounds:
@@ -194,7 +186,7 @@
   private extraSqlColumns: string[];
 
   private charWidth = -1;
-  private hoverPos?: Vector;
+  private hoverPos?: Point2D;
   protected hoveredSlice?: SliceT;
   private hoverTooltip: string[] = [];
   private maxDataDepth = 0;
@@ -238,7 +230,7 @@
   //  - This is NOT guaranteed to be called on every frame. For instance you
   //    cannot use this to do some colour-based animation.
   onUpdatedSlices(slices: Array<SliceT>): void {
-    this.highlightHovererdAndSameTitle(slices);
+    this.highlightHoveredAndSameTitle(slices);
   }
 
   // TODO(hjd): Remove.
@@ -248,8 +240,8 @@
   ): void {}
 
   constructor(args: NewTrackArgs) {
-    this.engine = args.engine;
-    this.trackKey = args.trackKey;
+    this.trace = args.trace;
+    this.uri = args.uri;
     // Work out the extra columns.
     // This is the union of the embedder-defined columns and the base columns
     // we know about (ts, dur, ...).
@@ -283,16 +275,6 @@
     }
   }
 
-  protected isSelectionHandled(selection: LegacySelection): boolean {
-    // TODO(hjd): Remove when updating selection.
-    // We shouldn't know here about THREAD_SLICE. Maybe should be set by
-    // whatever deals with that. Dunno the namespace of selection is weird. For
-    // most cases in non-ambiguous (because most things are a 'slice'). But some
-    // others (e.g. THREAD_SLICE) have their own ID namespace so we need this.
-    const supportedSelectionKinds: SelectionKind[] = ['SCHED_SLICE', 'SLICE'];
-    return supportedSelectionKinds.includes(selection.kind);
-  }
-
   private getTitleFont(): string {
     const size = this.sliceLayout.titleSizePx ?? 12;
     return `${size}px Roboto Condensed`;
@@ -409,11 +391,12 @@
       visibleWindow.end.toTime('ceil'),
     );
 
-    let selection = getLegacySelection(globals.state);
-    if (!selection || !this.isSelectionHandled(selection)) {
-      selection = null;
-    }
-    const selectedId = selection ? (selection as {id: number}).id : undefined;
+    const selection = this.trace.selection.selection;
+    const selectedId =
+      selection.kind === 'track_event' && selection.trackUri === this.uri
+        ? selection.eventId
+        : undefined;
+
     if (selectedId === undefined) {
       this.selectedSlice = undefined;
     }
@@ -669,7 +652,7 @@
       );
     }
 
-    const resolution = rawSlicesKey.bucketSize;
+    const resolution = slicesKey.bucketSize;
     const extraCols = this.extraSqlColumns.join(',');
     const queryRes = await this.engine.query(`
       SELECT
@@ -683,7 +666,7 @@
       FROM ${this.getTableName()}(
         ${slicesKey.start},
         ${slicesKey.end},
-        ${slicesKey.bucketSize}
+        ${resolution}
       ) z
       CROSS JOIN (${this.getSqlSource()}) s using (id)
     `);
@@ -828,15 +811,13 @@
     if (slice === lastHoveredSlice) return;
 
     if (this.hoveredSlice === undefined) {
-      globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1}));
+      this.trace.timeline.highlightedSliceId = undefined;
       this.onSliceOut({slice: assertExists(lastHoveredSlice)});
       this.hoverTooltip = [];
       this.hoverPos = undefined;
     } else {
       const args: OnSliceOverArgs<SliceT> = {slice: this.hoveredSlice};
-      globals.dispatch(
-        Actions.setHighlightedSliceId({sliceId: this.hoveredSlice.id}),
-      );
+      this.trace.timeline.highlightedSliceId = this.hoveredSlice.id;
       this.onSliceOver(args);
       this.hoverTooltip = args.tooltip || [];
     }
@@ -940,10 +921,10 @@
   // onUpdatedSlices() calls this. However, if the XxxSliceTrack impl overrides
   // onUpdatedSlices() this gives them a chance to call the highlighting without
   // having to reimplement it.
-  protected highlightHovererdAndSameTitle(slices: Slice[]) {
+  protected highlightHoveredAndSameTitle(slices: Slice[]) {
     for (const slice of slices) {
       const isHovering =
-        globals.state.highlightedSliceId === slice.id ||
+        this.trace.timeline.highlightedSliceId === slice.id ||
         (this.hoveredSlice && this.hoveredSlice.title === slice.title);
       slice.isHighlighted = !!isHovering;
     }
@@ -954,30 +935,43 @@
     return this.computedTrackHeight;
   }
 
-  getSliceRect(
-    {visibleWindow, timescale, size}: TrackRenderContext,
-    tStart: time,
-    tEnd: time,
-    depth: number,
-  ): SliceRect | undefined {
+  getSliceVerticalBounds(depth: number): VerticalBounds | undefined {
     this.updateSliceAndTrackHeight();
 
-    const pxEnd = size.width;
-    const left = Math.max(timescale.timeToPx(tStart), 0);
-    const right = Math.min(timescale.timeToPx(tEnd), pxEnd);
-
-    const visible = visibleWindow.overlaps(tStart, tEnd);
-
     const totalSliceHeight = this.computedRowSpacing + this.computedSliceHeight;
+    const top = this.sliceLayout.padding + depth * totalSliceHeight;
 
     return {
-      left,
-      width: Math.max(right - left, 1),
-      top: this.sliceLayout.padding + depth * totalSliceHeight,
-      height: this.computedSliceHeight,
-      visible,
+      top,
+      bottom: top + this.computedSliceHeight,
     };
   }
+
+  protected get engine() {
+    return this.trace.engine;
+  }
+
+  async getSelectionDetails(
+    id: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const query = `
+      SELECT
+        ts,
+        dur
+      FROM (${this.getSqlSource()})
+      WHERE id = ${id}
+    `;
+
+    const result = await this.engine.query(query);
+    if (result.numRows() === 0) {
+      return undefined;
+    }
+    const row = result.iter({
+      ts: LONG,
+      dur: LONG,
+    });
+    return {ts: Time.fromRaw(row.ts), dur: Duration.fromRaw(row.dur)};
+  }
 }
 
 // This is the argument passed to onSliceOver(args).
diff --git a/ui/src/frontend/base_slice_track_unittest.ts b/ui/src/frontend/base_slice_track_unittest.ts
index 4e8fd51..9d07e49 100644
--- a/ui/src/frontend/base_slice_track_unittest.ts
+++ b/ui/src/frontend/base_slice_track_unittest.ts
@@ -13,9 +13,8 @@
 // limitations under the License.
 
 import {Time} from '../base/time';
-import {UNEXPECTED_PINK} from '../core/colorizer';
-import {Slice} from '../public';
-
+import {UNEXPECTED_PINK} from '../public/lib/colorizer';
+import {Slice} from '../public/track';
 import {filterVisibleSlicesForTesting as filterVisibleSlices} from './base_slice_track';
 
 function slice(start: number, duration: number, depth: number = 0): Slice {
diff --git a/ui/src/frontend/bottom_tab.ts b/ui/src/frontend/bottom_tab.ts
deleted file mode 100644
index 9868a60..0000000
--- a/ui/src/frontend/bottom_tab.ts
+++ /dev/null
@@ -1,109 +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 {Engine} from '../trace_processor/engine';
-
-export interface NewBottomTabArgs<Config> {
-  engine: Engine;
-  tag?: string;
-  uuid: string;
-  config: Config;
-}
-
-// An interface representing a bottom tab displayed on the panel in the bottom
-// of the ui (e.g. "Current Selection").
-//
-// The implementations of this class are provided by different plugins, which
-// register the implementations with bottomTabRegistry, keyed by a unique name
-// for each type of BottomTab.
-//
-// Lifetime: the instances of this class are owned by BottomTabPanel and exist
-// for as long as a tab header is shown to the user in the bottom tab list (with
-// minor exceptions, like a small grace period between when the tab is related).
-//
-// BottomTab implementations should pass the unique identifier(s) for the
-// content displayed via the |Config| and fetch additional details via Engine
-// instead of relying on getting the data from the global storage. For example,
-// for tabs corresponding to details of the selected objects on a track, a new
-// BottomTab should be created for each new selection.
-export abstract class BottomTabBase<Config = {}> {
-  // Config for this details panel. Should be serializable.
-  protected readonly config: Config;
-  // Engine for running queries and fetching additional data.
-  protected readonly engine: Engine;
-  // Optional tag, which is used to ensure that only one tab
-  // with the same tag can exist - adding a new tab with the same tag
-  // (e.g. 'current_selection') would close the previous one. This
-  // also can be used to close existing tab.
-  readonly tag?: string;
-  // Unique id for this details panel. Can be used to close previously opened
-  // panel.
-  readonly uuid: string;
-
-  constructor(args: NewBottomTabArgs<Config>) {
-    this.config = args.config;
-    this.engine = args.engine;
-    this.tag = args.tag;
-    this.uuid = args.uuid;
-  }
-
-  // Entry point for customisation of the displayed title for this panel.
-  abstract getTitle(): string;
-
-  // Generate a mithril node for this component.
-  abstract renderPanel(): m.Children;
-
-  // API for the tab to notify the TabList that it's still preparing the data.
-  // If true, adding a new tab will be delayed for a short while (~50ms) to
-  // reduce the flickering.
-  //
-  // Note: it's a "poll" rather than "push" API: there is no explicit API
-  // for the tabs to notify the tab list, as the tabs are expected to schedule
-  // global redraw anyway and the tab list will poll the tabs as necessary
-  // during the redraw.
-  isLoading(): boolean {
-    return false;
-  }
-}
-
-// BottomTabBase provides a more generic API allowing users to provide their
-// custom mithril component, which would allow them to listen to mithril
-// lifecycle events. Most cases, however, don't need them and BottomTab
-// provides a simplified API for the common case.
-export abstract class BottomTab<Config = {}> extends BottomTabBase<Config> {
-  constructor(args: NewBottomTabArgs<Config>) {
-    super(args);
-  }
-
-  abstract viewTab(): m.Children;
-
-  renderPanel(): m.Children {
-    return m(BottomTabAdapter, {
-      key: this.uuid,
-      panel: this,
-    } as BottomTabAdapterAttrs);
-  }
-}
-
-interface BottomTabAdapterAttrs {
-  panel: BottomTab;
-}
-
-class BottomTabAdapter implements m.ClassComponent<BottomTabAdapterAttrs> {
-  view(vnode: m.CVnode<BottomTabAdapterAttrs>): void | m.Children {
-    return vnode.attrs.panel.viewTab();
-  }
-}
diff --git a/ui/src/frontend/charts/histogram/state.ts b/ui/src/frontend/charts/histogram/state.ts
deleted file mode 100644
index b07b9a9..0000000
--- a/ui/src/frontend/charts/histogram/state.ts
+++ /dev/null
@@ -1,95 +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 {raf} from '../../../core/raf_scheduler';
-import {Engine} from '../../../public';
-import {Row} from '../../../trace_processor/query_result';
-
-interface ChartConfig {
-  binAxisType?: 'nominal' | 'quantitative';
-  binAxis: 'x' | 'y';
-  countAxis: 'x' | 'y';
-  sort: string;
-  isBinned: boolean;
-  labelLimit?: number;
-}
-
-export class HistogramState {
-  private readonly sqlColumn: string;
-  private readonly engine: Engine;
-  private readonly query: string;
-
-  data?: Row[];
-  chartConfig: ChartConfig;
-
-  get isLoading() {
-    return this.data === undefined;
-  }
-
-  constructor(engine: Engine, query: string, column: string) {
-    this.engine = engine;
-    this.query = query;
-    this.sqlColumn = column;
-
-    this.chartConfig = {
-      binAxis: 'x',
-      binAxisType: 'nominal',
-      countAxis: 'y',
-      sort: 'false',
-      isBinned: true,
-      labelLimit: 500,
-    };
-
-    this.getData();
-  }
-
-  async getData() {
-    const res = await this.engine.query(`
-      SELECT ${this.sqlColumn}
-      FROM (
-        ${this.query}
-      )
-    `);
-
-    const rows: Row[] = [];
-
-    for (const it = res.iter({}); it.valid(); it.next()) {
-      const rowVal = it.get(this.sqlColumn);
-
-      if (
-        this.chartConfig.binAxisType === 'nominal' &&
-        typeof rowVal === 'bigint'
-      ) {
-        this.chartConfig.binAxisType = 'quantitative';
-      }
-
-      rows.push({
-        [this.sqlColumn]: rowVal,
-      });
-    }
-
-    this.data = rows;
-
-    if (this.chartConfig.binAxisType === 'nominal') {
-      this.chartConfig.binAxis = 'y';
-      this.chartConfig.countAxis = 'x';
-      this.chartConfig.sort = `{
-          "op": "count",
-          "order": "descending"
-        }`;
-      this.chartConfig.isBinned = false;
-    }
-
-    raf.scheduleFullRedraw();
-  }
-}
diff --git a/ui/src/frontend/charts/histogram/tab.ts b/ui/src/frontend/charts/histogram/tab.ts
deleted file mode 100644
index 94773a2..0000000
--- a/ui/src/frontend/charts/histogram/tab.ts
+++ /dev/null
@@ -1,151 +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 {DetailsShell} from '../../../widgets/details_shell';
-import {uuidv4} from '../../../base/uuid';
-import {BottomTab, NewBottomTabArgs} from '../../bottom_tab';
-import {VegaView} from '../../../widgets/vega_view';
-import {addEphemeralTab} from '../../../common/addEphemeralTab';
-import {HistogramState} from './state';
-import {stringifyJsonWithBigints} from '../../../base/json_utils';
-import {Engine} from '../../../public';
-import {isString} from '../../../base/object_utils';
-import {Filter} from '../../widgets/sql/table/state';
-
-interface HistogramTabConfig {
-  columnTitle: string; // Human readable column name (ex: Duration)
-  sqlColumn: string; // SQL column name (ex: dur)
-  filters?: Filter[]; // Filters applied to SQL table
-  tableDisplay?: string; // Human readable table name (ex: slices)
-  query: string; // SQL query for the underlying data
-}
-
-export function addHistogramTab(
-  config: HistogramTabConfig,
-  engine: Engine,
-): void {
-  const histogramTab = new HistogramTab({
-    config,
-    engine,
-    uuid: uuidv4(),
-  });
-
-  addEphemeralTab(histogramTab, 'histogramTab');
-}
-
-export class HistogramTab extends BottomTab<HistogramTabConfig> {
-  static readonly kind = 'dev.perfetto.HistogramTab';
-
-  private state: HistogramState;
-
-  constructor(args: NewBottomTabArgs<HistogramTabConfig>) {
-    super(args);
-
-    this.state = new HistogramState(
-      this.engine,
-      this.config.query,
-      this.config.sqlColumn,
-    );
-  }
-
-  static create(args: NewBottomTabArgs<HistogramTabConfig>): HistogramTab {
-    return new HistogramTab(args);
-  }
-
-  viewTab() {
-    return m(
-      DetailsShell,
-      {
-        title: this.getTitle(),
-        description: this.getDescription(),
-      },
-      m(
-        '.histogram',
-        m(VegaView, {
-          spec: `
-            {
-              "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
-              "mark": "bar",
-              "data": {
-                "values": ${
-                  this.state.data
-                    ? stringifyJsonWithBigints(this.state.data)
-                    : []
-                }
-              },
-              "encoding": {
-                "${this.state.chartConfig.binAxis}": {
-                  "bin": ${this.state.chartConfig.isBinned},
-                  "field": "${this.config.sqlColumn}",
-                  "type": "${this.state.chartConfig.binAxisType}",
-                  "title": "${this.config.columnTitle}",
-                  "sort": ${this.state.chartConfig.sort},
-                  "axis": {
-                    "labelLimit": ${this.state.chartConfig.labelLimit}
-                  }
-                },
-                "${this.state.chartConfig.countAxis}": {
-                  "aggregate": "count",
-                  "title": "Count"
-                }
-              }
-            }
-          `,
-          data: {},
-        }),
-      ),
-    );
-  }
-
-  getTitle(): string {
-    return `${this.toTitleCase(this.config.columnTitle)} ${
-      this.state.chartConfig.binAxisType === 'quantitative'
-        ? 'Histogram'
-        : 'Counts'
-    }`;
-  }
-
-  getDescription(): string {
-    let desc = `Count distribution for ${this.config.tableDisplay ?? ''} table`;
-
-    if (this.config.filters) {
-      const filterStrings: string[] = [];
-      desc += ' where ';
-
-      for (const f of this.config.filters) {
-        filterStrings.push(`${isString(f) ? f : `${f.argName} ${f.op}`}`);
-      }
-
-      desc += filterStrings.join(', ');
-    }
-
-    return desc;
-  }
-
-  toTitleCase(s: string): string {
-    const words = s.split(/\s/);
-
-    for (let i = 0; i < words.length; ++i) {
-      words[i] = words[i][0].toUpperCase() + words[i].substring(1);
-    }
-
-    return words.join(' ');
-  }
-
-  isLoading(): boolean {
-    return this.state.isLoading;
-  }
-}
diff --git a/ui/src/frontend/clipboard.ts b/ui/src/frontend/clipboard.ts
deleted file mode 100644
index 431f93d..0000000
--- a/ui/src/frontend/clipboard.ts
+++ /dev/null
@@ -1,48 +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 {copyToClipboard} from '../base/clipboard';
-import {Actions} from '../common/actions';
-import {QueryResponse} from '../common/queries';
-
-import {globals} from './globals';
-
-export function onClickCopy(url: string) {
-  return (e: Event) => {
-    e.preventDefault();
-    copyToClipboard(url);
-    globals.dispatch(
-      Actions.updateStatus({
-        msg: 'Link copied into the clipboard',
-        timestamp: Date.now() / 1000,
-      }),
-    );
-  };
-}
-
-export async function queryResponseToClipboard(
-  resp: QueryResponse,
-): Promise<void> {
-  const lines: string[][] = [];
-  lines.push(resp.columns);
-  for (const row of resp.rows) {
-    const line = [];
-    for (const col of resp.columns) {
-      const value = row[col];
-      line.push(value === null ? 'NULL' : `${value}`);
-    }
-    lines.push(line);
-  }
-  copyToClipboard(lines.map((line) => line.join('\t')).join('\n'));
-}
diff --git a/ui/src/frontend/close_track_button.ts b/ui/src/frontend/close_track_button.ts
deleted file mode 100644
index 1b0fd91..0000000
--- a/ui/src/frontend/close_track_button.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size 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 {Icons} from '../base/semantic_icons';
-import {Actions} from '../common/actions';
-
-import {globals} from './globals';
-import {Button} from '../widgets/button';
-
-export interface CloseTrackButtonAttrs {
-  trackKey: string;
-}
-
-export class CloseTrackButton
-  implements m.ClassComponent<CloseTrackButtonAttrs>
-{
-  view({attrs}: m.CVnode<CloseTrackButtonAttrs>) {
-    return m(Button, {
-      onclick: () => {
-        globals.dispatch(Actions.removeTracks({trackKeys: [attrs.trackKey]}));
-      },
-      icon: Icons.Close,
-      title: 'Close',
-      compact: true,
-    });
-  }
-}
diff --git a/ui/src/frontend/cookie_consent.ts b/ui/src/frontend/cookie_consent.ts
index dd9ba34..d7cfd84 100644
--- a/ui/src/frontend/cookie_consent.ts
+++ b/ui/src/frontend/cookie_consent.ts
@@ -13,10 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {raf} from '../core/raf_scheduler';
-
-import {globals} from './globals';
+import {AppImpl} from '../core/app_impl';
 
 const COOKIE_ACK_KEY = 'cookieAck';
 
@@ -26,7 +24,7 @@
   oninit() {
     this.showCookieConsent = true;
     if (
-      !globals.logging.isEnabled() ||
+      !AppImpl.instance.analytics.isEnabled() ||
       localStorage.getItem(COOKIE_ACK_KEY) === 'true'
     ) {
       this.showCookieConsent = false;
diff --git a/ui/src/frontend/cpu_profile_panel.ts b/ui/src/frontend/cpu_profile_panel.ts
deleted file mode 100644
index 0987d50..0000000
--- a/ui/src/frontend/cpu_profile_panel.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size 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 {globals} from './globals';
-import {CallsiteInfo} from '../common/legacy_flamegraph_util';
-
-interface CpuProfileDetailsPanelAttrs {}
-
-export class CpuProfileDetailsPanel
-  implements m.ClassComponent<CpuProfileDetailsPanelAttrs>
-{
-  view() {
-    const sampleDetails = globals.cpuProfileDetails;
-    const header = m(
-      '.details-panel-heading',
-      m('h2', `CPU Profile Sample Details`),
-    );
-    if (sampleDetails.id === undefined) {
-      return m('.details-panel', header);
-    }
-
-    return m(
-      '.details-panel',
-      header,
-      m('table', this.getStackText(sampleDetails.stack)),
-    );
-  }
-
-  getStackText(stack?: CallsiteInfo[]): m.Vnode[] {
-    if (!stack) return [];
-
-    const result = [];
-    for (let i = stack.length - 1; i >= 0; --i) {
-      result.push(m('tr', m('td', stack[i].name), m('td', stack[i].mapping)));
-    }
-
-    return result;
-  }
-}
diff --git a/ui/src/frontend/debug.ts b/ui/src/frontend/debug.ts
index 26a7c62..9899380 100644
--- a/ui/src/frontend/debug.ts
+++ b/ui/src/frontend/debug.ts
@@ -14,32 +14,25 @@
 
 import {produce} from 'immer';
 import m from 'mithril';
-
-import {Actions} from '../common/actions';
-import {pluginManager} from '../common/plugins';
-import {getSchema} from '../common/schema';
 import {raf} from '../core/raf_scheduler';
-
 import {globals} from './globals';
+import {App} from '../public/app';
+import {AppImpl} from '../core/app_impl';
 
 declare global {
   interface Window {
     m: typeof m;
-    getSchema: typeof getSchema;
+    app: App;
     globals: typeof globals;
-    Actions: typeof Actions;
     produce: typeof produce;
-    pluginManager: typeof pluginManager;
     raf: typeof raf;
   }
 }
 
 export function registerDebugGlobals() {
-  window.getSchema = getSchema;
   window.m = m;
+  window.app = AppImpl.instance;
   window.globals = globals;
-  window.Actions = Actions;
   window.produce = produce;
-  window.pluginManager = pluginManager;
   window.raf = raf;
 }
diff --git a/ui/src/frontend/debug_tracks/add_debug_track_menu.ts b/ui/src/frontend/debug_tracks/add_debug_track_menu.ts
deleted file mode 100644
index e37dad5..0000000
--- a/ui/src/frontend/debug_tracks/add_debug_track_menu.ts
+++ /dev/null
@@ -1,286 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {findRef} from '../../base/dom_utils';
-import {raf} from '../../core/raf_scheduler';
-import {Engine} from '../../trace_processor/engine';
-import {Form, FormLabel} from '../../widgets/form';
-import {Select} from '../../widgets/select';
-import {TextInput} from '../../widgets/text_input';
-
-import {
-  CounterColumns,
-  SliceColumns,
-  SqlDataSource,
-  addDebugCounterTrack,
-  addDebugSliceTrack,
-  addPivotedTracks,
-} from './debug_tracks';
-import {globals} from '../globals';
-
-export function uuidToViewName(uuid: string): string {
-  return `view_${uuid.split('-').join('_')}`;
-}
-
-interface AddDebugTrackMenuAttrs {
-  dataSource: Required<SqlDataSource>;
-  engine: Engine;
-}
-
-const TRACK_NAME_FIELD_REF = 'TRACK_NAME_FIELD';
-
-export class AddDebugTrackMenu
-  implements m.ClassComponent<AddDebugTrackMenuAttrs>
-{
-  readonly columns: string[];
-
-  name: string = '';
-  trackType: 'slice' | 'counter' = 'slice';
-  // Names of columns which will be used as data sources for rendering.
-  // We store the config for all possible columns used for rendering (i.e.
-  // 'value' for slice and 'name' for counter) and then just don't the values
-  // which don't match the currently selected track type (so changing track type
-  // from A to B and back to A is a no-op).
-  renderParams: {
-    ts: string;
-    dur: string;
-    name: string;
-    value: string;
-    pivot: string;
-  };
-
-  constructor(vnode: m.Vnode<AddDebugTrackMenuAttrs>) {
-    this.columns = [...vnode.attrs.dataSource.columns];
-
-    const chooseDefaultOption = (name: string) => {
-      for (const column of this.columns) {
-        if (column === name) return column;
-      }
-      for (const column of this.columns) {
-        if (column.endsWith(`_${name}`)) return column;
-      }
-      // Debug tracks support data without dur, in which case it's treated as
-      // 0.
-      if (name === 'dur') {
-        return '0';
-      }
-      return this.columns[0];
-    };
-
-    this.renderParams = {
-      ts: chooseDefaultOption('ts'),
-      dur: chooseDefaultOption('dur'),
-      name: chooseDefaultOption('name'),
-      value: chooseDefaultOption('value'),
-      pivot: '',
-    };
-  }
-
-  oncreate({dom}: m.VnodeDOM<AddDebugTrackMenuAttrs>) {
-    this.focusTrackNameField(dom);
-  }
-
-  private focusTrackNameField(dom: Element) {
-    const element = findRef(dom, TRACK_NAME_FIELD_REF);
-    if (element) {
-      if (element instanceof HTMLInputElement) {
-        element.focus();
-      }
-    }
-  }
-
-  private renderTrackTypeSelect() {
-    const options = [];
-    for (const type of ['slice', 'counter']) {
-      options.push(
-        m(
-          'option',
-          {
-            value: type,
-            selected: this.trackType === type ? true : undefined,
-          },
-          type,
-        ),
-      );
-    }
-    return m(
-      Select,
-      {
-        id: 'track_type',
-        oninput: (e: Event) => {
-          if (!e.target) return;
-          this.trackType = (e.target as HTMLSelectElement).value as
-            | 'slice'
-            | 'counter';
-          raf.scheduleFullRedraw();
-        },
-      },
-      options,
-    );
-  }
-
-  view(vnode: m.Vnode<AddDebugTrackMenuAttrs>) {
-    const renderSelect = (name: 'ts' | 'dur' | 'name' | 'value' | 'pivot') => {
-      const options = [];
-
-      if (name === 'pivot') {
-        options.push(
-          m(
-            'option',
-            {selected: this.renderParams[name] === '' ? true : undefined},
-            m('i', ''),
-          ),
-        );
-      }
-      for (const column of this.columns) {
-        options.push(
-          m(
-            'option',
-            {selected: this.renderParams[name] === column ? true : undefined},
-            column,
-          ),
-        );
-      }
-      if (name === 'dur') {
-        options.push(
-          m(
-            'option',
-            {selected: this.renderParams[name] === '0' ? true : undefined},
-            m('i', '0'),
-          ),
-        );
-      }
-      return [
-        m(FormLabel, {for: name}, name),
-        m(
-          Select,
-          {
-            id: name,
-            oninput: (e: Event) => {
-              if (!e.target) return;
-              this.renderParams[name] = (e.target as HTMLSelectElement).value;
-            },
-          },
-          options,
-        ),
-      ];
-    };
-
-    return m(
-      Form,
-      {
-        onSubmit: () => {
-          switch (this.trackType) {
-            case 'slice':
-              const sliceColumns: SliceColumns = {
-                ts: this.renderParams.ts,
-                dur: this.renderParams.dur,
-                name: this.renderParams.name,
-              };
-              if (this.renderParams.pivot) {
-                addPivotedTracks(
-                  {
-                    engine: vnode.attrs.engine,
-                    registerTrack: (x) => globals.trackManager.registerTrack(x),
-                  },
-                  vnode.attrs.dataSource,
-                  this.name,
-                  this.renderParams.pivot,
-                  async (ctx, data, trackName) =>
-                    addDebugSliceTrack(
-                      ctx,
-                      data,
-                      trackName,
-                      sliceColumns,
-                      this.columns,
-                    ),
-                );
-              } else {
-                addDebugSliceTrack(
-                  // TODO(stevegolton): This is a temporary patch, this menu
-                  // should become part of the debug tracks plugin, at which
-                  // point we can just use the plugin's context object.
-                  {
-                    engine: vnode.attrs.engine,
-                    registerTrack: (x) => globals.trackManager.registerTrack(x),
-                  },
-                  vnode.attrs.dataSource,
-                  this.name,
-                  sliceColumns,
-                  this.columns,
-                );
-              }
-              break;
-            case 'counter':
-              const counterColumns: CounterColumns = {
-                ts: this.renderParams.ts,
-                value: this.renderParams.value,
-              };
-
-              if (this.renderParams.pivot) {
-                addPivotedTracks(
-                  {
-                    engine: vnode.attrs.engine,
-                    registerTrack: (x) => globals.trackManager.registerTrack(x),
-                  },
-                  vnode.attrs.dataSource,
-                  this.name,
-                  this.renderParams.pivot,
-                  async (ctx, data, trackName) =>
-                    addDebugCounterTrack(ctx, data, trackName, counterColumns),
-                );
-              } else {
-                addDebugCounterTrack(
-                  // TODO(stevegolton): This is a temporary patch, this menu
-                  // should become part of the debug tracks plugin, at which
-                  // point we can just use the plugin's context object.
-                  {
-                    engine: vnode.attrs.engine,
-                    registerTrack: (x) => globals.trackManager.registerTrack(x),
-                  },
-                  vnode.attrs.dataSource,
-                  this.name,
-                  counterColumns,
-                );
-              }
-              break;
-          }
-        },
-        submitLabel: 'Show',
-      },
-      m(FormLabel, {for: 'track_name'}, 'Track name'),
-      m(TextInput, {
-        id: 'track_name',
-        ref: TRACK_NAME_FIELD_REF,
-        onkeydown: (e: KeyboardEvent) => {
-          // Allow Esc to close popup.
-          if (e.key === 'Escape') return;
-        },
-        oninput: (e: KeyboardEvent) => {
-          if (!e.target) return;
-          this.name = (e.target as HTMLInputElement).value;
-        },
-      }),
-      m(FormLabel, {for: 'track_type'}, 'Track type'),
-      this.renderTrackTypeSelect(),
-      renderSelect('ts'),
-      this.trackType === 'slice' && renderSelect('dur'),
-      this.trackType === 'slice' && renderSelect('name'),
-      this.trackType === 'counter' && renderSelect('value'),
-      renderSelect('pivot'),
-    );
-  }
-}
diff --git a/ui/src/frontend/debug_tracks/counter_track.ts b/ui/src/frontend/debug_tracks/counter_track.ts
deleted file mode 100644
index db00e87..0000000
--- a/ui/src/frontend/debug_tracks/counter_track.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {BaseCounterTrack} from '../../frontend/base_counter_track';
-import {TrackContext} from '../../public';
-import {Engine} from '../../trace_processor/engine';
-
-export class DebugCounterTrack extends BaseCounterTrack {
-  private readonly sqlTableName: string;
-
-  constructor(engine: Engine, ctx: TrackContext, tableName: string) {
-    super({
-      engine,
-      trackKey: ctx.trackKey,
-    });
-    this.sqlTableName = tableName;
-  }
-
-  getSqlSource(): string {
-    return `select * from ${this.sqlTableName}`;
-  }
-}
diff --git a/ui/src/frontend/debug_tracks/debug_tracks.ts b/ui/src/frontend/debug_tracks/debug_tracks.ts
deleted file mode 100644
index a2dbe3b..0000000
--- a/ui/src/frontend/debug_tracks/debug_tracks.ts
+++ /dev/null
@@ -1,264 +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 {uuidv4, uuidv4Sql} from '../../base/uuid';
-import {Actions, DeferredAction} from '../../common/actions';
-import {PrimaryTrackSortKey, SCROLLING_TRACK_GROUP} from '../../common/state';
-import {globals} from '../globals';
-import {TrackDescriptor} from '../../public';
-import {DebugSliceTrack} from './slice_track';
-import {
-  createPerfettoTable,
-  matchesSqlValue,
-  sqlValueToReadableString,
-} from '../../trace_processor/sql_utils';
-import {Engine} from '../../trace_processor/engine';
-import {DebugCounterTrack} from './counter_track';
-import {ARG_PREFIX} from './details_tab';
-
-// We need to add debug tracks from the core and from plugins. In order to add a
-// debug track we need to pass a context through with we can add the track. This
-// is different for plugins vs the core. This interface defines the generic
-// shape of this context, which can be supplied from a plugin or built from
-// globals.
-//
-// TODO(stevegolton): In the future, both the core and plugins should
-// have access to some Context object which implements the various things we
-// want to do in a generic way, so that we don't have to do this mangling to get
-// this to work.
-interface Context {
-  engine: Engine;
-  registerTrack(track: TrackDescriptor): unknown;
-}
-
-// Names of the columns of the underlying view to be used as
-// ts / dur / name / pivot.
-export interface SliceColumns {
-  ts: string;
-  dur: string;
-  name: string;
-}
-
-let debugTrackCount = 0;
-
-export interface SqlDataSource {
-  // SQL source selecting the necessary data.
-  sqlSource: string;
-
-  // Optional: Rename columns from the query result.
-  // If omitted, original column names from the query are used instead.
-  // The caller is responsible for ensuring that the number of items in this
-  // list matches the number of columns returned by sqlSource.
-  columns?: string[];
-}
-
-// Creates actions to add a debug track. The actions must be dispatched to
-// have an effect. Use this variant if you want to create many tracks at
-// once or want to tweak the actions once produced. Otherwise, use
-// addDebugSliceTrack().
-function createAddDebugTrackActions(
-  trackName: string,
-  uri: string,
-): DeferredAction<{}>[] {
-  const debugTrackId = ++debugTrackCount;
-  const trackKey = uuidv4();
-
-  const actions: DeferredAction<{}>[] = [
-    Actions.addTrack({
-      key: trackKey,
-      name: trackName.trim() || `Debug Track ${debugTrackId}`,
-      uri,
-      trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
-      trackGroup: SCROLLING_TRACK_GROUP,
-      closeable: true,
-    }),
-    Actions.toggleTrackPinned({trackKey}),
-  ];
-
-  return actions;
-}
-
-export async function addPivotedTracks(
-  ctx: Context,
-  data: SqlDataSource,
-  trackName: string,
-  pivotColumn: string,
-  createTrack: (
-    ctx: Context,
-    data: SqlDataSource,
-    trackName: string,
-  ) => Promise<void>,
-) {
-  const iter = (
-    await ctx.engine.query(`
-    with all_vals as (${data.sqlSource})
-    select DISTINCT ${pivotColumn} from all_vals
-    order by ${pivotColumn}
-  `)
-  ).iter({});
-
-  for (; iter.valid(); iter.next()) {
-    await createTrack(
-      ctx,
-      {
-        sqlSource: `select * from
-        (${data.sqlSource})
-        where ${pivotColumn} ${matchesSqlValue(iter.get(pivotColumn))}`,
-      },
-      `${trackName.trim() || 'Pivot Track'}: ${sqlValueToReadableString(iter.get(pivotColumn))}`,
-    );
-  }
-}
-
-// Adds a debug track immediately. Use createDebugSliceTrackActions() if you
-// want to create many tracks at once.
-export async function addDebugSliceTrack(
-  ctx: Context,
-  data: SqlDataSource,
-  trackName: string,
-  sliceColumns: SliceColumns,
-  argColumns: string[],
-): Promise<void> {
-  // Create a new table from the debug track definition. This will be used as
-  // the backing data source for our track and its details panel.
-  const tableName = `__debug_slice_${uuidv4Sql()}`;
-
-  // TODO(stevegolton): Right now we ignore the AsyncDisposable that this
-  // function returns, and so never clean up this table. The problem is we have
-  // no where sensible to do this cleanup.
-  // - If we did it in the track's onDestroy function, we could drop the table
-  //   while the details panel still needs access to it.
-  // - If we did it in the plugin's onTraceUnload function, we could risk
-  //   dropping it n the middle of a track update cycle as track lifecycles are
-  //   not synchronized with plugin lifecycles.
-  await createPerfettoTable(
-    ctx.engine,
-    tableName,
-    createDebugSliceTrackTableExpr(data, sliceColumns, argColumns),
-  );
-
-  const uri = `debug.slice.${uuidv4()}`;
-  ctx.registerTrack({
-    uri,
-    title: trackName,
-    trackFactory: (trackCtx) => {
-      return new DebugSliceTrack(ctx.engine, trackCtx, tableName);
-    },
-  });
-
-  // Create the actions to add this track to the tracklist
-  const actions = await createAddDebugTrackActions(trackName, uri);
-  globals.dispatchMultiple(actions);
-}
-
-function createDebugSliceTrackTableExpr(
-  data: SqlDataSource,
-  sliceColumns: SliceColumns,
-  argColumns: string[],
-): string {
-  const dataColumns =
-    data.columns !== undefined ? `(${data.columns.join(', ')})` : '';
-  const dur = sliceColumns.dur === '0' ? 0 : sliceColumns.dur;
-  return `
-    with data${dataColumns} as (
-      ${data.sqlSource}
-    ),
-    prepared_data as (
-      select
-        ${sliceColumns.ts} as ts,
-        ifnull(cast(${dur} as int), -1) as dur,
-        printf('%s', ${sliceColumns.name}) as name
-        ${argColumns.length > 0 ? ',' : ''}
-        ${argColumns.map((c) => `${c} as ${ARG_PREFIX}${c}`).join(',\n')}
-      from data
-    )
-    select
-      row_number() over (order by ts) as id,
-      *
-    from prepared_data
-    order by ts
-  `;
-}
-
-// Names of the columns of the underlying view to be used as ts / dur / name.
-export interface CounterColumns {
-  ts: string;
-  value: string;
-}
-
-export interface CounterDebugTrackConfig {
-  data: SqlDataSource;
-  columns: CounterColumns;
-}
-
-export interface CounterDebugTrackCreateConfig {
-  pinned?: boolean; // default true
-  closeable?: boolean; // default true
-}
-
-// Adds a debug track immediately. Use createDebugCounterTrackActions() if you
-// want to create many tracks at once.
-export async function addDebugCounterTrack(
-  ctx: Context,
-  data: SqlDataSource,
-  trackName: string,
-  columns: CounterColumns,
-): Promise<void> {
-  // Create a new table from the debug track definition. This will be used as
-  // the backing data source for our track and its details panel.
-  const tableName = `__debug_counter_${uuidv4Sql()}`;
-
-  // TODO(stevegolton): Right now we ignore the AsyncDisposable that this
-  // function returns, and so never clean up this table. The problem is we have
-  // no where sensible to do this cleanup.
-  // - If we did it in the track's onDestroy function, we could drop the table
-  //   while the details panel still needs access to it.
-  // - If we did it in the plugin's onTraceUnload function, we could risk
-  //   dropping it n the middle of a track update cycle as track lifecycles are
-  //   not synchronized with plugin lifecycles.
-  await createPerfettoTable(
-    ctx.engine,
-    tableName,
-    createDebugCounterTrackTableExpr(data, columns),
-  );
-
-  const uri = `debug.counter.${uuidv4()}`;
-  ctx.registerTrack({
-    uri,
-    title: trackName,
-    trackFactory: (trackCtx) => {
-      return new DebugCounterTrack(ctx.engine, trackCtx, tableName);
-    },
-  });
-
-  // Create the actions to add this track to the tracklist
-  const actions = await createAddDebugTrackActions(trackName, uri);
-  globals.dispatchMultiple(actions);
-}
-
-function createDebugCounterTrackTableExpr(
-  data: SqlDataSource,
-  columns: CounterColumns,
-): string {
-  return `
-    with data as (
-      ${data.sqlSource}
-    )
-    select
-      ${columns.ts} as ts,
-      ${columns.value} as value
-    from data
-    order by ts
-  `;
-}
diff --git a/ui/src/frontend/debug_tracks/details_tab.ts b/ui/src/frontend/debug_tracks/details_tab.ts
deleted file mode 100644
index b8b8ce9..0000000
--- a/ui/src/frontend/debug_tracks/details_tab.ts
+++ /dev/null
@@ -1,277 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {duration, Time, time} from '../../base/time';
-import {raf} from '../../core/raf_scheduler';
-import {BottomTab, NewBottomTabArgs} from '../bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../generic_slice_details_tab';
-import {hasArgs, renderArguments} from '../slice_args';
-import {getSlice, SliceDetails} from '../../trace_processor/sql_utils/slice';
-import {asSliceSqlId, Utid} from '../../trace_processor/sql_utils/core_types';
-import {
-  getThreadState,
-  ThreadState,
-} from '../../trace_processor/sql_utils/thread_state';
-import {DurationWidget} from '../widgets/duration';
-import {Timestamp} from '../widgets/timestamp';
-import {
-  ColumnType,
-  durationFromSql,
-  LONG,
-  STR,
-  timeFromSql,
-} from '../../trace_processor/query_result';
-import {sqlValueToReadableString} from '../../trace_processor/sql_utils';
-import {DetailsShell} from '../../widgets/details_shell';
-import {GridLayout} from '../../widgets/grid_layout';
-import {Section} from '../../widgets/section';
-import {dictToTree, dictToTreeNodes, Tree, TreeNode} from '../../widgets/tree';
-import {threadStateRef} from '../widgets/thread_state';
-import {getThreadName} from '../../trace_processor/sql_utils/thread';
-import {getProcessName} from '../../trace_processor/sql_utils/process';
-import {sliceRef} from '../widgets/slice';
-
-export const ARG_PREFIX = 'arg_';
-
-function sqlValueToNumber(value?: ColumnType): number | undefined {
-  if (typeof value === 'bigint') return Number(value);
-  if (typeof value !== 'number') return undefined;
-  return value;
-}
-
-function sqlValueToUtid(value?: ColumnType): Utid | undefined {
-  if (typeof value === 'bigint') return Number(value) as Utid;
-  if (typeof value !== 'number') return undefined;
-  return value as Utid;
-}
-
-function renderTreeContents(dict: {[key: string]: m.Child}): m.Child[] {
-  const children: m.Child[] = [];
-  for (const key of Object.keys(dict)) {
-    if (dict[key] === null || dict[key] === undefined) continue;
-    children.push(
-      m(TreeNode, {
-        left: key,
-        right: dict[key],
-      }),
-    );
-  }
-  return children;
-}
-
-export class DebugSliceDetailsTab extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'dev.perfetto.DebugSliceDetailsTab';
-
-  data?: {
-    name: string;
-    ts: time;
-    dur: duration;
-    args: {[key: string]: ColumnType};
-  };
-  // We will try to interpret the arguments as references into well-known
-  // tables. These values will be set if the relevant columns exist and
-  // are consistent (e.g. 'ts' and 'dur' for this slice correspond to values
-  // in these well-known tables).
-  threadState?: ThreadState;
-  slice?: SliceDetails;
-
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): DebugSliceDetailsTab {
-    return new DebugSliceDetailsTab(args);
-  }
-
-  private async maybeLoadThreadState(
-    id: number | undefined,
-    ts: time,
-    dur: duration,
-    table: string | undefined,
-    utid?: Utid,
-  ): Promise<ThreadState | undefined> {
-    if (id === undefined) return undefined;
-    if (utid === undefined) return undefined;
-
-    const threadState = await getThreadState(this.engine, id);
-    if (threadState === undefined) return undefined;
-    if (
-      table === 'thread_state' ||
-      (threadState.ts === ts &&
-        threadState.dur === dur &&
-        threadState.thread?.utid === utid)
-    ) {
-      return threadState;
-    } else {
-      return undefined;
-    }
-  }
-
-  private renderThreadStateInfo(): m.Child {
-    if (this.threadState === undefined) return null;
-    return m(
-      TreeNode,
-      {
-        left: threadStateRef(this.threadState),
-        right: '',
-      },
-      renderTreeContents({
-        Thread: getThreadName(this.threadState.thread),
-        Process: getProcessName(this.threadState.thread?.process),
-        State: this.threadState.state,
-      }),
-    );
-  }
-
-  private async maybeLoadSlice(
-    id: number | undefined,
-    ts: time,
-    dur: duration,
-    table: string | undefined,
-    trackId?: number,
-  ): Promise<SliceDetails | undefined> {
-    if (id === undefined) return undefined;
-    if (table !== 'slice' && trackId === undefined) return undefined;
-
-    const slice = await getSlice(this.engine, asSliceSqlId(id));
-    if (slice === undefined) return undefined;
-    if (
-      table === 'slice' ||
-      (slice.ts === ts && slice.dur === dur && slice.trackId === trackId)
-    ) {
-      return slice;
-    } else {
-      return undefined;
-    }
-  }
-
-  private renderSliceInfo(): m.Child {
-    if (this.slice === undefined) return null;
-    return m(
-      TreeNode,
-      {
-        left: sliceRef(this.slice, 'Slice'),
-        right: '',
-      },
-      m(TreeNode, {
-        left: 'Name',
-        right: this.slice.name,
-      }),
-      m(TreeNode, {
-        left: 'Thread',
-        right: getThreadName(this.slice.thread),
-      }),
-      m(TreeNode, {
-        left: 'Process',
-        right: getProcessName(this.slice.process),
-      }),
-      hasArgs(this.slice.args) &&
-        m(
-          TreeNode,
-          {
-            left: 'Args',
-          },
-          renderArguments(this.engine, this.slice.args),
-        ),
-    );
-  }
-
-  private async loadData() {
-    const queryResult = await this.engine.query(
-      `select * from ${this.config.sqlTableName} where id = ${this.config.id}`,
-    );
-    const row = queryResult.firstRow({
-      ts: LONG,
-      dur: LONG,
-      name: STR,
-    });
-    this.data = {
-      name: row.name,
-      ts: Time.fromRaw(row.ts),
-      dur: row.dur,
-      args: {},
-    };
-
-    for (const key of Object.keys(row)) {
-      if (key.startsWith(ARG_PREFIX)) {
-        this.data.args[key.substr(ARG_PREFIX.length)] = (
-          row as {[key: string]: ColumnType}
-        )[key];
-      }
-    }
-
-    this.threadState = await this.maybeLoadThreadState(
-      sqlValueToNumber(this.data.args['id']),
-      this.data.ts,
-      this.data.dur,
-      sqlValueToReadableString(this.data.args['table_name']),
-      sqlValueToUtid(this.data.args['utid']),
-    );
-
-    this.slice = await this.maybeLoadSlice(
-      sqlValueToNumber(this.data.args['id']) ??
-        sqlValueToNumber(this.data.args['slice_id']),
-      this.data.ts,
-      this.data.dur,
-      sqlValueToReadableString(this.data.args['table_name']),
-      sqlValueToNumber(this.data.args['track_id']),
-    );
-
-    raf.scheduleRedraw();
-  }
-
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-    this.loadData();
-  }
-
-  viewTab() {
-    if (this.data === undefined) {
-      return m('h2', 'Loading');
-    }
-    const details = dictToTreeNodes({
-      'Name': this.data['name'] as string,
-      'Start time': m(Timestamp, {ts: timeFromSql(this.data['ts'])}),
-      'Duration': m(DurationWidget, {dur: durationFromSql(this.data['dur'])}),
-      'Debug slice id': `${this.config.sqlTableName}[${this.config.id}]`,
-    });
-    details.push(this.renderThreadStateInfo());
-    details.push(this.renderSliceInfo());
-
-    const args: {[key: string]: m.Child} = {};
-    for (const key of Object.keys(this.data.args)) {
-      args[key] = sqlValueToReadableString(this.data.args[key]);
-    }
-
-    return m(
-      DetailsShell,
-      {
-        title: 'Debug Slice',
-      },
-      m(
-        GridLayout,
-        m(Section, {title: 'Details'}, m(Tree, details)),
-        m(Section, {title: 'Arguments'}, dictToTree(args)),
-      ),
-    );
-  }
-
-  getTitle(): string {
-    return `Current Selection`;
-  }
-
-  isLoading() {
-    return this.data === undefined;
-  }
-}
diff --git a/ui/src/frontend/debug_tracks/slice_track.ts b/ui/src/frontend/debug_tracks/slice_track.ts
deleted file mode 100644
index 51458b2..0000000
--- a/ui/src/frontend/debug_tracks/slice_track.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {
-  CustomSqlDetailsPanelConfig,
-  CustomSqlTableDefConfig,
-  CustomSqlTableSliceTrack,
-} from '../tracks/custom_sql_table_slice_track';
-import {TrackContext} from '../../public';
-import {Engine} from '../../trace_processor/engine';
-import {DebugSliceDetailsTab} from './details_tab';
-
-export class DebugSliceTrack extends CustomSqlTableSliceTrack {
-  private readonly sqlTableName: string;
-
-  constructor(engine: Engine, ctx: TrackContext, tableName: string) {
-    super({
-      engine,
-      trackKey: ctx.trackKey,
-    });
-    this.sqlTableName = tableName;
-  }
-
-  async getSqlDataSource(): Promise<CustomSqlTableDefConfig> {
-    return {
-      sqlTableName: this.sqlTableName,
-    };
-  }
-
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    return {
-      kind: DebugSliceDetailsTab.kind,
-      config: {
-        sqlTableName: this.sqlTableName,
-        title: 'Debug Slice',
-      },
-    };
-  }
-}
diff --git a/ui/src/frontend/drag/border_drag_strategy.ts b/ui/src/frontend/drag/border_drag_strategy.ts
index d7afafb..c98b02d 100644
--- a/ui/src/frontend/drag/border_drag_strategy.ts
+++ b/ui/src/frontend/drag/border_drag_strategy.ts
@@ -11,8 +11,8 @@
 // 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 '../time_scale';
-import {DragStrategy} from './drag_strategy';
+import {TimeScale} from '../../base/time_scale';
+import {DragStrategy, DragStrategyUpdateTimeFn} from './drag_strategy';
 
 export class BorderDragStrategy extends DragStrategy {
   private moveStart = false;
@@ -20,8 +20,9 @@
   constructor(
     map: TimeScale,
     private pixelBounds: [number, number],
+    updateVizTime: DragStrategyUpdateTimeFn,
   ) {
-    super(map);
+    super(map, updateVizTime);
   }
 
   onDrag(x: number) {
diff --git a/ui/src/frontend/drag/drag_strategy.ts b/ui/src/frontend/drag/drag_strategy.ts
index 46c0d53..0a0f3d4 100644
--- a/ui/src/frontend/drag/drag_strategy.ts
+++ b/ui/src/frontend/drag/drag_strategy.ts
@@ -11,14 +11,17 @@
 // 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 '../../common/high_precision_time';
-import {HighPrecisionTimeSpan} from '../../common/high_precision_time_span';
-import {raf} from '../../core/raf_scheduler';
-import {globals} from '../globals';
-import {TimeScale} from '../time_scale';
+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) {}
+  constructor(
+    protected map: TimeScale,
+    private updateVizTime: DragStrategyUpdateTimeFn,
+  ) {}
 
   abstract onDrag(x: number): void;
 
@@ -29,7 +32,6 @@
       tStart,
       tEnd.sub(tStart).toNumber(),
     );
-    globals.timeline.updateVisibleTimeHP(vizTime);
-    raf.scheduleRedraw();
+    this.updateVizTime(vizTime);
   }
 }
diff --git a/ui/src/frontend/drag/inner_drag_strategy.ts b/ui/src/frontend/drag/inner_drag_strategy.ts
index 0216fdc..61ad694 100644
--- a/ui/src/frontend/drag/inner_drag_strategy.ts
+++ b/ui/src/frontend/drag/inner_drag_strategy.ts
@@ -11,8 +11,8 @@
 // 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 '../time_scale';
-import {DragStrategy} from './drag_strategy';
+import {TimeScale} from '../../base/time_scale';
+import {DragStrategy, DragStrategyUpdateTimeFn} from './drag_strategy';
 
 export class InnerDragStrategy extends DragStrategy {
   private dragStartPx = 0;
@@ -20,8 +20,9 @@
   constructor(
     timeScale: TimeScale,
     private pixelBounds: [number, number],
+    updateVizTime: DragStrategyUpdateTimeFn,
   ) {
-    super(timeScale);
+    super(timeScale, updateVizTime);
   }
 
   onDrag(x: number) {
diff --git a/ui/src/frontend/drag_handle.ts b/ui/src/frontend/drag_handle.ts
deleted file mode 100644
index 36094d5..0000000
--- a/ui/src/frontend/drag_handle.ts
+++ /dev/null
@@ -1,274 +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 {raf} from '../core/raf_scheduler';
-import {Button} from '../widgets/button';
-import {MenuItem, PopupMenu2} from '../widgets/menu';
-
-import {DEFAULT_DETAILS_CONTENT_HEIGHT} from './css_constants';
-import {DragGestureHandler} from './drag_gesture_handler';
-import {globals} from './globals';
-import {DisposableStack} from '../base/disposable_stack';
-
-const DRAG_HANDLE_HEIGHT_PX = 28;
-const UP_ICON = 'keyboard_arrow_up';
-const DOWN_ICON = 'keyboard_arrow_down';
-
-export interface Tab {
-  // Unique key for this tab, passed to callbacks.
-  key: string;
-
-  // Tab title to show on the tab handle.
-  title: m.Children;
-
-  // Whether to show a close button on the tab handle or not.
-  // Default = false.
-  hasCloseButton?: boolean;
-}
-
-export interface TabDropdownEntry {
-  // Unique key for this tab dropdown entry.
-  key: string;
-
-  // Title to show on this entry.
-  title: string;
-
-  // Called when tab dropdown entry is clicked.
-  onClick: () => void;
-
-  // Whether this tab is checked or not
-  checked: boolean;
-}
-
-export interface DragHandleAttrs {
-  // The current height of the panel.
-  height: number;
-
-  // Called when the panel is dragged.
-  resize: (height: number) => void;
-
-  // A list of tabs to show in the tab bar.
-  tabs: Tab[];
-
-  // The key of the "current" tab.
-  currentTabKey?: string;
-
-  // A list of entries to show in the tab dropdown.
-  // If undefined, the tab dropdown button will not be displayed.
-  tabDropdownEntries?: TabDropdownEntry[];
-
-  // Called when a tab is clicked.
-  onTabClick: (key: string) => void;
-
-  // Called when a tab is closed using its close button.
-  onTabClose?: (key: string) => void;
-}
-
-export function getDefaultDetailsHeight() {
-  // This needs to be a function instead of a const to ensure the CSS constants
-  // have been initialized by the time we perform this calculation;
-  return DRAG_HANDLE_HEIGHT_PX + DEFAULT_DETAILS_CONTENT_HEIGHT;
-}
-
-function getFullScreenHeight() {
-  const page = document.querySelector('.page') as HTMLElement;
-  if (page === null) {
-    // Fall back to at least partially open.
-    return getDefaultDetailsHeight();
-  } else {
-    return page.clientHeight;
-  }
-}
-
-export class DragHandle implements m.ClassComponent<DragHandleAttrs> {
-  private dragStartHeight = 0;
-  private height = 0;
-  private previousHeight = this.height;
-  private resize: (height: number) => void = () => {};
-  private isClosed = this.height <= 0;
-  private isFullscreen = false;
-  // We can't get real fullscreen height until the pan_and_zoom_handler
-  // exists.
-  private fullscreenHeight = 0;
-  private trash = new DisposableStack();
-
-  oncreate({dom, attrs}: m.CVnodeDOM<DragHandleAttrs>) {
-    this.resize = attrs.resize;
-    this.height = attrs.height;
-    this.isClosed = this.height <= 0;
-    this.fullscreenHeight = getFullScreenHeight();
-    const elem = dom as HTMLElement;
-    this.trash.use(
-      new DragGestureHandler(
-        elem,
-        this.onDrag.bind(this),
-        this.onDragStart.bind(this),
-        this.onDragEnd.bind(this),
-      ),
-    );
-    const cmd = globals.commandManager.registerCommand({
-      id: 'perfetto.ToggleDrawer',
-      name: 'Toggle drawer',
-      defaultHotkey: 'Q',
-      callback: () => {
-        this.toggleVisibility();
-      },
-    });
-    this.trash.use(cmd);
-  }
-
-  private toggleVisibility() {
-    if (this.height === 0) {
-      this.isClosed = false;
-      if (this.previousHeight === 0) {
-        this.previousHeight = getDefaultDetailsHeight();
-      }
-      this.resize(this.previousHeight);
-    } else {
-      this.isFullscreen = false;
-      this.isClosed = true;
-      this.previousHeight = this.height;
-      this.resize(0);
-    }
-    raf.scheduleFullRedraw();
-  }
-
-  onupdate({attrs}: m.CVnodeDOM<DragHandleAttrs>) {
-    this.resize = attrs.resize;
-    this.height = attrs.height;
-    this.isClosed = this.height <= 0;
-  }
-
-  onremove(_: m.CVnodeDOM<DragHandleAttrs>) {
-    this.trash.dispose();
-  }
-
-  onDrag(_x: number, y: number) {
-    const newHeight = Math.floor(
-      this.dragStartHeight + DRAG_HANDLE_HEIGHT_PX / 2 - y,
-    );
-    this.isClosed = newHeight <= 0;
-    this.isFullscreen = newHeight >= this.fullscreenHeight;
-    this.resize(newHeight);
-    raf.scheduleFullRedraw();
-  }
-
-  onDragStart(_x: number, _y: number) {
-    this.dragStartHeight = this.height;
-  }
-
-  onDragEnd() {}
-
-  view({attrs}: m.CVnode<DragHandleAttrs>) {
-    const {
-      tabDropdownEntries,
-      currentTabKey,
-      tabs,
-      onTabClick,
-      onTabClose = () => {},
-    } = attrs;
-
-    const icon = this.isClosed ? UP_ICON : DOWN_ICON;
-    const title = this.isClosed ? 'Show panel' : 'Hide panel';
-    const renderTab = (tab: Tab) => {
-      const {key, hasCloseButton = false} = tab;
-      const tag = currentTabKey === key ? '.tab[active]' : '.tab';
-      return m(
-        tag,
-        {
-          key,
-          onclick: (event: Event) => {
-            if (!event.defaultPrevented) {
-              onTabClick(key);
-            }
-          },
-          // Middle click to close
-          onauxclick: (event: MouseEvent) => {
-            if (!event.defaultPrevented) {
-              onTabClose(key);
-            }
-          },
-        },
-        m('span.pf-tab-title', tab.title),
-        hasCloseButton &&
-          m(Button, {
-            onclick: (event: Event) => {
-              onTabClose(key);
-              event.preventDefault();
-            },
-            compact: true,
-            icon: 'close',
-          }),
-      );
-    };
-
-    return m(
-      '.handle',
-      m(
-        '.buttons',
-        tabDropdownEntries && this.renderTabDropdown(tabDropdownEntries),
-      ),
-      m('.tabs', tabs.map(renderTab)),
-      m(
-        '.buttons',
-        m(Button, {
-          onclick: () => {
-            this.isClosed = false;
-            this.isFullscreen = true;
-            // Ensure fullscreenHeight is up to date.
-            this.fullscreenHeight = getFullScreenHeight();
-            this.resize(this.fullscreenHeight);
-            raf.scheduleFullRedraw();
-          },
-          title: 'Open fullscreen',
-          disabled: this.isFullscreen,
-          icon: 'vertical_align_top',
-          compact: true,
-        }),
-        m(Button, {
-          onclick: () => {
-            this.toggleVisibility();
-          },
-          title,
-          icon,
-          compact: true,
-        }),
-      ),
-    );
-  }
-
-  private renderTabDropdown(entries: TabDropdownEntry[]) {
-    return m(
-      PopupMenu2,
-      {
-        trigger: m(Button, {
-          compact: true,
-          icon: 'more_vert',
-          disabled: entries.length === 0,
-          title: 'More Tabs',
-        }),
-      },
-      entries.map((entry) => {
-        return m(MenuItem, {
-          key: entry.key,
-          label: entry.title,
-          onclick: () => entry.onClick(),
-          icon: entry.checked ? 'check_box' : 'check_box_outline_blank',
-        });
-      }),
-    );
-  }
-}
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index 143a53f..99d4157 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -13,16 +13,14 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {ErrorDetails} from '../base/logging';
-import {EXTENSION_URL} from '../common/recordingV2/recording_utils';
-import {GcsUploader} from '../common/gcs_uploader';
-import {RECORDING_V2_FLAG} from '../core/feature_flags';
+import {GcsUploader} from '../base/gcs_uploader';
 import {raf} from '../core/raf_scheduler';
 import {VERSION} from '../gen/perfetto_version';
 import {getCurrentModalKey, showModal} from '../widgets/modal';
 import {globals} from './globals';
-import {Router} from './router';
+import {AppImpl} from '../core/app_impl';
+import {Router} from '../core/router';
 
 const MODAL_KEY = 'crash_modal';
 
@@ -47,22 +45,20 @@
     return;
   }
 
-  if (!RECORDING_V2_FLAG.get()) {
-    if (err.message.includes('Unable to claim interface')) {
-      showWebUSBError();
-      timeLastReport = now;
-      return;
-    }
+  if (err.message.includes('Unable to claim interface')) {
+    showWebUSBError();
+    timeLastReport = now;
+    return;
+  }
 
-    if (
-      err.message.includes('A transfer error has occurred') ||
-      err.message.includes('The device was disconnected') ||
-      err.message.includes('The transfer was cancelled')
-    ) {
-      showConnectionLostError();
-      timeLastReport = now;
-      return;
-    }
+  if (
+    err.message.includes('A transfer error has occurred') ||
+    err.message.includes('The device was disconnected') ||
+    err.message.includes('The transfer was cancelled')
+  ) {
+    showConnectionLostError();
+    timeLastReport = now;
+    return;
   }
 
   if (err.message.includes('(ERR:fmt)')) {
@@ -75,6 +71,11 @@
     return;
   }
 
+  if (err.message.includes('(ERR:ws)')) {
+    showWebsocketConnectionIssue(err.message);
+    return;
+  }
+
   // This is only for older version of the UI and for ease of tracking across
   // cherry-picks. Newer versions don't have this exception anymore.
   if (err.message.includes('State hash does not match')) {
@@ -118,13 +119,13 @@
 
   constructor() {
     this.traceState = 'NOT_AVAILABLE';
-    const engine = globals.getCurrentEngine();
-    if (engine === undefined) return;
-    this.traceType = engine.source.type;
+    const traceSource = AppImpl.instance.trace?.traceInfo.source;
+    if (traceSource === undefined) return;
+    this.traceType = traceSource.type;
     // If the trace is either already uploaded, or comes from a postmessage+url
     // we don't need any re-upload.
-    if ('url' in engine.source && engine.source.url !== undefined) {
-      this.traceUrl = engine.source.url;
+    if ('url' in traceSource && traceSource.url !== undefined) {
+      this.traceUrl = traceSource.url;
       this.traceState = 'UPLOADED';
       // The trace is already uploaded, so assume the user is fine attaching to
       // the bugreport (this make the checkbox ticked by default).
@@ -135,12 +136,12 @@
     // If the user is not a googler, don't even offer the option to upload it.
     if (!globals.isInternalUser) return;
 
-    if (engine.source.type === 'FILE') {
+    if (traceSource.type === 'FILE') {
       this.traceState = 'NOT_UPLOADED';
-      this.traceData = engine.source.file;
+      this.traceData = traceSource.file;
       // this.traceSize = this.traceData.size;
-    } else if (engine.source.type === 'ARRAY_BUFFER') {
-      this.traceData = engine.source.buffer;
+    } else if (traceSource.type === 'ARRAY_BUFFER') {
+      this.traceData = traceSource.buffer;
       // this.traceSize = this.traceData.byteLength;
     } else {
       return; // Can't upload HTTP+RPC.
@@ -351,131 +352,6 @@
   });
 }
 
-export function showWebUSBErrorV2() {
-  showModal({
-    title: 'A WebUSB error occurred',
-    content: m(
-      'div',
-      m(
-        'span',
-        `Is adb already running on the host? Run this command and
-      try again.`,
-      ),
-      m('br'),
-      m('.modal-bash', '> adb kill-server'),
-      m('br'),
-      // The statement below covers the following edge case:
-      // 1. 'adb server' is running on the device.
-      // 2. The user selects the new Android target, so we try to fetch the
-      // OS version and do QSS.
-      // 3. The error modal is shown.
-      // 4. The user runs 'adb kill-server'.
-      // At this point we don't have a trigger to try fetching the OS version
-      // + QSS again. Therefore, the user will need to refresh the page.
-      m(
-        'span',
-        "If after running 'adb kill-server', you don't see " +
-          "a 'Start Recording' button on the page and you don't see " +
-          "'Allow USB debugging' on the device, " +
-          'you will need to reload this page.',
-      ),
-      m('br'),
-      m('br'),
-      m('span', 'For details see '),
-      m('a', {href: 'http://b/159048331', target: '_blank'}, 'b/159048331'),
-    ),
-  });
-}
-
-export function showConnectionLostError(): void {
-  showModal({
-    title: 'Connection with the ADB device lost',
-    content: m(
-      'div',
-      m('span', `Please connect the device again to restart the recording.`),
-      m('br'),
-    ),
-  });
-}
-
-export function showAllowUSBDebugging(): void {
-  showModal({
-    title: 'Could not connect to the device',
-    content: m(
-      'div',
-      m('span', 'Please allow USB debugging on the device.'),
-      m('br'),
-    ),
-  });
-}
-
-export function showNoDeviceSelected(): void {
-  showModal({
-    title: 'No device was selected for recording',
-    content: m(
-      'div',
-      m(
-        'span',
-        `If you want to connect to an ADB device,
-           please select it from the list.`,
-      ),
-      m('br'),
-    ),
-  });
-}
-
-export function showExtensionNotInstalled(): void {
-  showModal({
-    title: 'Perfetto Chrome extension not installed',
-    content: m(
-      'div',
-      m(
-        '.note',
-        `To trace Chrome from the Perfetto UI, you need to install our `,
-        m('a', {href: EXTENSION_URL, target: '_blank'}, 'Chrome extension'),
-        ' and then reload this page.',
-      ),
-      m('br'),
-    ),
-  });
-}
-
-export function showWebsocketConnectionIssue(message: string): void {
-  showModal({
-    title: 'Unable to connect to the device via websocket',
-    content: m('div', m('span', message), m('br')),
-  });
-}
-
-export function showIssueParsingTheTracedResponse(message: string): void {
-  showModal({
-    title:
-      'A problem was encountered while connecting to' +
-      ' the Perfetto tracing service',
-    content: m('div', m('span', message), m('br')),
-  });
-}
-
-export function showFailedToPushBinary(message: string): void {
-  showModal({
-    title: 'Failed to push a binary to the device',
-    content: m(
-      'div',
-      m(
-        'span',
-        'This can happen if your Android device has an OS version lower ' +
-          'than Q. Perfetto tried to push the latest version of its ' +
-          'embedded binary but failed.',
-      ),
-      m('br'),
-      m('br'),
-      m('span', 'Error message:'),
-      m('br'),
-      m('span', message),
-    ),
-  });
-}
-
 function showRpcSequencingError() {
   showModal({
     title: 'A TraceProcessor RPC error occurred',
@@ -525,3 +401,25 @@
     ],
   });
 }
+
+function showWebsocketConnectionIssue(message: string): void {
+  showModal({
+    title: 'Unable to connect to the device via websocket',
+    content: m(
+      'div',
+      m('div', 'trace_processor_shell --httpd is unreachable or crashed.'),
+      m('pre', message),
+    ),
+  });
+}
+
+function showConnectionLostError(): void {
+  showModal({
+    title: 'Connection with the ADB device lost',
+    content: m(
+      'div',
+      m('span', `Please connect the device again to restart the recording.`),
+      m('br'),
+    ),
+  });
+}
diff --git a/ui/src/frontend/file_drop_handler.ts b/ui/src/frontend/file_drop_handler.ts
index 1068af8..6d096be 100644
--- a/ui/src/frontend/file_drop_handler.ts
+++ b/ui/src/frontend/file_drop_handler.ts
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Actions} from '../common/actions';
-import {globals} from './globals';
+import {AppImpl} from '../core/app_impl';
 
 let lastDragTarget: EventTarget | null = null;
 
@@ -43,7 +42,7 @@
       const file = evt.dataTransfer.files[0];
       // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
       if (file) {
-        globals.dispatch(Actions.openTraceFromFile({file}));
+        AppImpl.instance.openTraceFromFile(file);
       }
     }
     evt.preventDefault();
diff --git a/ui/src/frontend/flags_page.ts b/ui/src/frontend/flags_page.ts
deleted file mode 100644
index 7b5af73..0000000
--- a/ui/src/frontend/flags_page.ts
+++ /dev/null
@@ -1,164 +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.
-
-import m from 'mithril';
-
-import {channelChanged, getNextChannel, setChannel} from '../common/channels';
-import {featureFlags, Flag, OverrideState} from '../core/feature_flags';
-import {raf} from '../core/raf_scheduler';
-
-import {createPage} from './pages';
-import {Router} from './router';
-
-const RELEASE_PROCESS_URL =
-  'https://perfetto.dev/docs/visualization/perfetto-ui-release-process';
-
-interface FlagOption {
-  id: string;
-  name: string;
-}
-
-interface SelectWidgetAttrs {
-  id: string;
-  label: string;
-  description: m.Children;
-  options: FlagOption[];
-  selected: string;
-  onSelect: (id: string) => void;
-}
-
-class SelectWidget implements m.ClassComponent<SelectWidgetAttrs> {
-  view(vnode: m.Vnode<SelectWidgetAttrs>) {
-    const route = Router.parseUrl(window.location.href);
-    const attrs = vnode.attrs;
-    const cssClass = route.subpage === `/${attrs.id}` ? '.focused' : '';
-    return m(
-      '.flag-widget' + cssClass,
-      {id: attrs.id},
-      m('label', attrs.label),
-      m(
-        'select',
-        {
-          onchange: (e: InputEvent) => {
-            const value = (e.target as HTMLSelectElement).value;
-            attrs.onSelect(value);
-            raf.scheduleFullRedraw();
-          },
-        },
-        attrs.options.map((o) => {
-          const selected = o.id === attrs.selected;
-          return m('option', {value: o.id, selected}, o.name);
-        }),
-      ),
-      m('.description', attrs.description),
-    );
-  }
-}
-
-interface FlagWidgetAttrs {
-  flag: Flag;
-}
-
-class FlagWidget implements m.ClassComponent<FlagWidgetAttrs> {
-  view(vnode: m.Vnode<FlagWidgetAttrs>) {
-    const flag = vnode.attrs.flag;
-    const defaultState = flag.defaultValue ? 'Enabled' : 'Disabled';
-    return m(SelectWidget, {
-      label: flag.name,
-      id: flag.id,
-      description: flag.description,
-      options: [
-        {id: OverrideState.DEFAULT, name: `Default (${defaultState})`},
-        {id: OverrideState.TRUE, name: 'Enabled'},
-        {id: OverrideState.FALSE, name: 'Disabled'},
-      ],
-      selected: flag.overriddenState(),
-      onSelect: (value: string) => {
-        switch (value) {
-          case OverrideState.TRUE:
-            flag.set(true);
-            break;
-          case OverrideState.FALSE:
-            flag.set(false);
-            break;
-          default:
-          case OverrideState.DEFAULT:
-            flag.reset();
-            break;
-        }
-      },
-    });
-  }
-}
-
-export const FlagsPage = createPage({
-  view() {
-    const needsReload = channelChanged();
-    return m(
-      '.flags-page',
-      m(
-        '.flags-content',
-        m('h1', 'Feature flags'),
-        needsReload && [
-          m('h2', 'Please reload for your changes to take effect'),
-        ],
-        m(SelectWidget, {
-          label: 'Release channel',
-          id: 'releaseChannel',
-          description: [
-            'Which release channel of the UI to use. See ',
-            m(
-              'a',
-              {
-                href: RELEASE_PROCESS_URL,
-              },
-              'Release Process',
-            ),
-            ' for more information.',
-          ],
-          options: [
-            {id: 'stable', name: 'Stable (default)'},
-            {id: 'canary', name: 'Canary'},
-            {id: 'autopush', name: 'Autopush'},
-          ],
-          selected: getNextChannel(),
-          onSelect: (id) => setChannel(id),
-        }),
-        m(
-          'button',
-          {
-            onclick: () => {
-              featureFlags.resetAll();
-              raf.scheduleFullRedraw();
-            },
-          },
-          'Reset all below',
-        ),
-
-        featureFlags.allFlags().map((flag) => m(FlagWidget, {flag})),
-      ),
-    );
-  },
-
-  oncreate(vnode: m.VnodeDOM) {
-    const route = Router.parseUrl(window.location.href);
-    const flagId = /[/](\w+)/.exec(route.subpage)?.slice(1, 2)[0];
-    if (flagId) {
-      const flag = vnode.dom.querySelector(`#${flagId}`);
-      if (flag) {
-        flag.scrollIntoView({block: 'center'});
-      }
-    }
-  },
-});
diff --git a/ui/src/frontend/flow_events_panel.ts b/ui/src/frontend/flow_events_panel.ts
index 83e848b..434d549 100644
--- a/ui/src/frontend/flow_events_panel.ts
+++ b/ui/src/frontend/flow_events_panel.ts
@@ -13,15 +13,10 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {Icons} from '../base/semantic_icons';
-import {Actions} from '../common/actions';
-import {getLegacySelection} from '../common/state';
 import {raf} from '../core/raf_scheduler';
-
-import {Flow, globals} from './globals';
-import {DurationWidget} from './widgets/duration';
-import {EmptyState} from '../widgets/empty_state';
+import {Flow} from '../core/flow_types';
+import {TraceImpl} from '../core/trace_impl';
 
 export const ALL_CATEGORIES = '_all_';
 
@@ -38,124 +33,15 @@
   return categories;
 }
 
-export class FlowEventsPanel implements m.ClassComponent {
-  view() {
-    const selection = getLegacySelection(globals.state);
-    if (!selection) {
-      return m(
-        EmptyState,
-        {
-          className: 'pf-noselection',
-          title: 'Nothing selected',
-        },
-        'Flow data will appear here',
-      );
-    }
-
-    if (selection.kind !== 'SLICE') {
-      return m(
-        EmptyState,
-        {
-          className: 'pf-noselection',
-          title: 'No flow data',
-          icon: 'warning',
-        },
-        `Flows are not applicable to the selection kind: '${selection.kind}'`,
-      );
-    }
-
-    const flowClickHandler = (sliceId: number, trackId: number) => {
-      const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
-      if (trackKey) {
-        globals.setLegacySelection(
-          {
-            kind: 'SLICE',
-            id: sliceId,
-            trackKey,
-            table: 'slice',
-          },
-          {
-            clearSearch: true,
-            pendingScrollId: undefined,
-            switchToCurrentSelectionTab: false,
-          },
-        );
-      }
-    };
-
-    // Can happen only for flow events version 1
-    const haveCategories =
-      globals.connectedFlows.filter((flow) => flow.category).length > 0;
-
-    const columns = [
-      m('th', 'Direction'),
-      m('th', 'Duration'),
-      m('th', 'Connected Slice ID'),
-      m('th', 'Connected Slice Name'),
-      m('th', 'Thread Out'),
-      m('th', 'Thread In'),
-      m('th', 'Process Out'),
-      m('th', 'Process In'),
-    ];
-
-    if (haveCategories) {
-      columns.push(m('th', 'Flow Category'));
-      columns.push(m('th', 'Flow Name'));
-    }
-
-    const rows = [m('tr', columns)];
-
-    // Fill the table with all the directly connected flow events
-    globals.connectedFlows.forEach((flow) => {
-      if (
-        selection.id !== flow.begin.sliceId &&
-        selection.id !== flow.end.sliceId
-      ) {
-        return;
-      }
-
-      const outgoing = selection.id === flow.begin.sliceId;
-      const otherEnd = outgoing ? flow.end : flow.begin;
-
-      const args = {
-        onclick: () => flowClickHandler(otherEnd.sliceId, otherEnd.trackId),
-        onmousemove: () =>
-          globals.dispatch(
-            Actions.setHighlightedSliceId({sliceId: otherEnd.sliceId}),
-          ),
-        onmouseleave: () =>
-          globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1})),
-      };
-
-      const data = [
-        m('td.flow-link', args, outgoing ? 'Outgoing' : 'Incoming'),
-        m('td.flow-link', args, m(DurationWidget, {dur: flow.dur})),
-        m('td.flow-link', args, otherEnd.sliceId.toString()),
-        m('td.flow-link', args, otherEnd.sliceName),
-        m('td.flow-link', args, flow.begin.threadName),
-        m('td.flow-link', args, flow.end.threadName),
-        m('td.flow-link', args, flow.begin.processName),
-        m('td.flow-link', args, flow.end.processName),
-      ];
-
-      if (haveCategories) {
-        data.push(m('td.flow-info', flow.category ?? '-'));
-        data.push(m('td.flow-info', flow.name ?? '-'));
-      }
-
-      rows.push(m('tr', data));
-    });
-
-    return m('.details-panel', [
-      m('.details-panel-heading', m('h2', `Flow events`)),
-      m('.flow-events-table', m('table', rows)),
-    ]);
-  }
+export interface FlowEventsAreaSelectedPanelAttrs {
+  trace: TraceImpl;
 }
 
-export class FlowEventsAreaSelectedPanel implements m.ClassComponent {
-  view() {
-    const selection = globals.state.selection;
+export class FlowEventsAreaSelectedPanel
+  implements m.ClassComponent<FlowEventsAreaSelectedPanelAttrs>
+{
+  view({attrs}: m.CVnode<FlowEventsAreaSelectedPanelAttrs>) {
+    const selection = attrs.trace.selection.selection;
     if (selection.kind !== 'area') {
       return;
     }
@@ -181,7 +67,8 @@
 
     const categoryToFlowsNum = new Map<string, number>();
 
-    globals.selectedFlows.forEach((flow) => {
+    const flows = attrs.trace.flows;
+    flows.selectedFlows.forEach((flow) => {
       const categories = getFlowCategories(flow);
       categories.forEach((cat) => {
         if (!categoryToFlowsNum.has(cat)) {
@@ -191,11 +78,11 @@
       });
     });
 
-    const allWasChecked = globals.visibleFlowCategories.get(ALL_CATEGORIES);
+    const allWasChecked = flows.visibleCategories.get(ALL_CATEGORIES);
     rows.push(
       m('tr.sum', [
         m('td.sum-data', 'All'),
-        m('td.sum-data', globals.selectedFlows.length),
+        m('td.sum-data', flows.selectedFlows.length),
         m(
           'td.sum-data',
           m(
@@ -203,17 +90,15 @@
             {
               onclick: () => {
                 if (allWasChecked) {
-                  globals.visibleFlowCategories.clear();
+                  for (const k of flows.visibleCategories.keys()) {
+                    flows.setCategoryVisible(k, false);
+                  }
                 } else {
                   categoryToFlowsNum.forEach((_, cat) => {
-                    globals.visibleFlowCategories.set(cat, true);
+                    flows.setCategoryVisible(cat, true);
                   });
                 }
-                globals.visibleFlowCategories.set(
-                  ALL_CATEGORIES,
-                  !allWasChecked,
-                );
-                raf.scheduleFullRedraw();
+                flows.setCategoryVisible(ALL_CATEGORIES, !allWasChecked);
               },
             },
             allWasChecked ? Icons.Checkbox : Icons.BlankCheckbox,
@@ -224,8 +109,8 @@
 
     categoryToFlowsNum.forEach((num, cat) => {
       const wasChecked =
-        globals.visibleFlowCategories.get(cat) ||
-        globals.visibleFlowCategories.get(ALL_CATEGORIES);
+        flows.visibleCategories.get(cat) ||
+        flows.visibleCategories.get(ALL_CATEGORIES);
       const data = [
         m('td.flow-info', cat),
         m('td.flow-info', num),
@@ -236,9 +121,9 @@
             {
               onclick: () => {
                 if (wasChecked) {
-                  globals.visibleFlowCategories.set(ALL_CATEGORIES, false);
+                  flows.setCategoryVisible(ALL_CATEGORIES, false);
                 }
-                globals.visibleFlowCategories.set(cat, !wasChecked);
+                flows.setCategoryVisible(cat, !wasChecked);
                 raf.scheduleFullRedraw();
               },
             },
diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index 16d28c3..28ee34b 100644
--- a/ui/src/frontend/flow_events_renderer.ts
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -12,15 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Size} from '../base/geom';
-import {exists} from '../base/utils';
-import {TrackState} from '../common/state';
-import {SliceRect} from '../public';
-
+import {ArrowHeadStyle, drawBezierArrow} from '../base/canvas/bezier_arrow';
+import {Size2D, Point2D, HorizontalBounds} from '../base/geom';
 import {ALL_CATEGORIES, getFlowCategories} from './flow_events_panel';
-import {Flow, FlowPoint, globals} from './globals';
-import {Panel} from './panel_container';
-import {PxSpan, TimeScale} from './time_scale';
+import {Flow} from '../core/flow_types';
+import {RenderedPanelInfo} from './panel_container';
+import {TimeScale} from '../base/time_scale';
+import {TrackNode} from '../public/workspace';
+import {TraceImpl} from '../core/trace_impl';
 
 const TRACK_GROUP_CONNECTION_OFFSET = 5;
 const TRIANGLE_SIZE = 5;
@@ -37,217 +36,80 @@
 const FOCUSED_FLOW_INTENSITY = 55;
 const DEFAULT_FLOW_INTENSITY = 70;
 
-type LineDirection = 'LEFT' | 'RIGHT' | 'UP' | 'DOWN';
-type ConnectionType = 'TRACK' | 'TRACK_GROUP';
+type VerticalEdgeOrPoint =
+  | ({kind: 'vertical_edge'} & Point2D)
+  | ({kind: 'point'} & Point2D);
 
-interface TrackPanelInfo {
-  panel: Panel;
-  yStart: number;
-}
+/**
+ * Renders the flows overlay on top of the timeline, given the set of panels and
+ * a canvas to draw on.
+ *
+ * Note: the actual flow data is retrieved from trace.flows, which are produced
+ * by FlowManager.
+ *
+ * @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.
+ */
+export function renderFlows(
+  trace: TraceImpl,
+  ctx: CanvasRenderingContext2D,
+  size: Size2D,
+  panels: ReadonlyArray<RenderedPanelInfo>,
+): void {
+  const timescale = new TimeScale(trace.timeline.visibleWindow, {
+    left: 0,
+    right: size.width,
+  });
 
-interface TrackGroupPanelInfo {
-  panel: Panel;
-  yStart: number;
-  height: number;
-}
+  // 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]),
+  );
 
-function getTrackIds(track: TrackState): ReadonlyArray<number> {
-  const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
-  return trackDesc?.tags?.trackIds ?? [];
-}
+  // Build a track index on trackIds. Note: We need to find the track nodes
+  // specifically here (not just the URIs) because we might need to navigate up
+  // the tree to find containing groups.
 
-export class FlowEventsRendererArgs {
-  trackIdToTrackPanel: Map<number, TrackPanelInfo>;
-  groupIdToTrackGroupPanel: Map<string, TrackGroupPanelInfo>;
+  const sqlTrackIdToTrack = new Map<number, TrackNode>();
+  trace.workspace.flatTracks.forEach((track) =>
+    track.uri
+      ? trace.tracks
+          .getTrack(track.uri)
+          ?.tags?.trackIds?.forEach((trackId) =>
+            sqlTrackIdToTrack.set(trackId, track),
+          )
+      : undefined,
+  );
 
-  constructor() {
-    this.trackIdToTrackPanel = new Map<number, TrackPanelInfo>();
-    this.groupIdToTrackGroupPanel = new Map<string, TrackGroupPanelInfo>();
-  }
+  const drawFlow = (flow: Flow, hue: number) => {
+    const flowStartTs =
+      flow.flowToDescendant || flow.begin.sliceStartTs >= flow.end.sliceStartTs
+        ? flow.begin.sliceStartTs
+        : flow.begin.sliceEndTs;
 
-  registerPanel(panel: Panel, yStart: number, height: number) {
-    if (exists(panel.trackKey)) {
-      const track = globals.state.tracks[panel.trackKey];
-      for (const trackId of getTrackIds(track)) {
-        this.trackIdToTrackPanel.set(trackId, {panel, yStart});
-      }
-    } else if (exists(panel.groupKey)) {
-      this.groupIdToTrackGroupPanel.set(panel.groupKey, {
-        panel,
-        yStart,
-        height,
-      });
-    }
-  }
-}
+    const flowEndTs = flow.end.sliceStartTs;
 
-export class FlowEventsRenderer {
-  private getTrackGroupIdByTrackId(trackId: number): string | undefined {
-    const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
-    return trackKey ? globals.state.tracks[trackKey].trackGroup : undefined;
-  }
+    const startX = timescale.timeToPx(flowStartTs);
+    const endX = timescale.timeToPx(flowEndTs);
 
-  private getTrackGroupYCoordinate(
-    args: FlowEventsRendererArgs,
-    trackId: number,
-  ): number | undefined {
-    const trackGroupId = this.getTrackGroupIdByTrackId(trackId);
-    if (!trackGroupId) {
-      return undefined;
-    }
-    const trackGroupInfo = args.groupIdToTrackGroupPanel.get(trackGroupId);
-    if (!trackGroupInfo) {
-      return undefined;
-    }
-    return (
-      trackGroupInfo.yStart +
-      trackGroupInfo.height -
-      TRACK_GROUP_CONNECTION_OFFSET
-    );
-  }
-
-  private getTrackYCoordinate(
-    args: FlowEventsRendererArgs,
-    trackId: number,
-  ): number | undefined {
-    return args.trackIdToTrackPanel.get(trackId)?.yStart;
-  }
-
-  private getYConnection(
-    args: FlowEventsRendererArgs,
-    trackId: number,
-    yMax: number,
-    rect?: SliceRect,
-  ): {y: number; connection: ConnectionType} | undefined {
-    if (!rect) {
-      const y = this.getTrackGroupYCoordinate(args, trackId);
-      if (y === undefined) {
-        return undefined;
-      }
-      return {y, connection: 'TRACK_GROUP'};
-    }
-    const y =
-      (this.getTrackYCoordinate(args, trackId) ?? 0) +
-      rect.top +
-      rect.height * 0.5;
-
-    return {
-      y: Math.min(Math.max(0, y), yMax),
-      connection: 'TRACK',
+    const flowBounds = {
+      left: Math.min(startX, endX),
+      right: Math.max(startX, endX),
     };
-  }
 
-  private getSliceRect(
-    args: FlowEventsRendererArgs,
-    point: FlowPoint,
-  ): SliceRect | undefined {
-    const trackPanel = args.trackIdToTrackPanel.get(point.trackId)?.panel;
-    if (!trackPanel) {
-      return undefined;
-    }
-    return trackPanel.getSliceRect?.(
-      point.sliceStartTs,
-      point.sliceEndTs,
-      point.depth,
-    );
-  }
-
-  /**
-   * Render the flows to the canvas.
-   *
-   * @param ctx Canvas rendering context.
-   * @param args Arg, e.g. definitions of where tracks live on the canvas.
-   * @param size The size of the drawable canvas region.
-   */
-  render(
-    ctx: CanvasRenderingContext2D,
-    args: FlowEventsRendererArgs,
-    size: Size,
-  ) {
-    const timescale = new TimeScale(
-      globals.timeline.visibleWindow,
-      new PxSpan(0, size.width),
-    );
-
-    globals.connectedFlows.forEach((flow) => {
-      this.drawFlow(ctx, timescale, size, args, flow, CONNECTED_FLOW_HUE);
-    });
-
-    globals.selectedFlows.forEach((flow) => {
-      const categories = getFlowCategories(flow);
-      for (const cat of categories) {
-        if (
-          globals.visibleFlowCategories.get(cat) ||
-          globals.visibleFlowCategories.get(ALL_CATEGORIES)
-        ) {
-          this.drawFlow(ctx, timescale, size, args, flow, SELECTED_FLOW_HUE);
-          break;
-        }
-      }
-    });
-  }
-
-  private drawFlow(
-    ctx: CanvasRenderingContext2D,
-    timescale: TimeScale,
-    size: Size,
-    args: FlowEventsRendererArgs,
-    flow: Flow,
-    hue: number,
-  ) {
-    const beginSliceRect = this.getSliceRect(args, flow.begin);
-    const endSliceRect = this.getSliceRect(args, flow.end);
-
-    const beginYConnection = this.getYConnection(
-      args,
-      flow.begin.trackId,
-      size.height,
-      beginSliceRect,
-    );
-    const endYConnection = this.getYConnection(
-      args,
-      flow.end.trackId,
-      size.height,
-      endSliceRect,
-    );
-
-    if (!beginYConnection || !endYConnection) {
+    if (!isInViewport(flowBounds, size)) {
       return;
     }
 
-    let beginDir: LineDirection = 'LEFT';
-    let endDir: LineDirection = 'RIGHT';
-    if (beginYConnection.connection === 'TRACK_GROUP') {
-      beginDir = beginYConnection.y > endYConnection.y ? 'DOWN' : 'UP';
-    }
-    if (endYConnection.connection === 'TRACK_GROUP') {
-      endDir = endYConnection.y > beginYConnection.y ? 'DOWN' : 'UP';
-    }
-
-    const begin = {
-      // If the flow goes to a descendant, we want to draw the arrow from the
-      // beginning of the slice
-      // rather from the end to avoid the flow arrow going backwards.
-      x: timescale.timeToPx(
-        flow.flowToDescendant ||
-          flow.begin.sliceStartTs >= flow.end.sliceStartTs
-          ? flow.begin.sliceStartTs
-          : flow.begin.sliceEndTs,
-      ),
-      y: beginYConnection.y,
-      dir: beginDir,
-    };
-    const end = {
-      x: timescale.timeToPx(flow.end.sliceStartTs),
-      y: endYConnection.y,
-      dir: endDir,
-    };
     const highlighted =
-      flow.end.sliceId === globals.state.highlightedSliceId ||
-      flow.begin.sliceId === globals.state.highlightedSliceId;
+      flow.end.sliceId === trace.timeline.highlightedSliceId ||
+      flow.begin.sliceId === trace.timeline.highlightedSliceId;
     const focused =
-      flow.id === globals.state.focusedFlowIdLeft ||
-      flow.id === globals.state.focusedFlowIdRight;
+      flow.id === trace.flows.focusedFlowIdLeft ||
+      flow.id === trace.flows.focusedFlowIdRight;
 
     let intensity = DEFAULT_FLOW_INTENSITY;
     let width = DEFAULT_FLOW_WIDTH;
@@ -258,106 +120,137 @@
     if (highlighted) {
       intensity = HIGHLIGHTED_FLOW_INTENSITY;
     }
-    this.drawFlowArrow(ctx, begin, end, hue, intensity, width);
-  }
 
-  private getDeltaX(dir: LineDirection, offset: number): number {
-    switch (dir) {
-      case 'LEFT':
-        return -offset;
-      case 'RIGHT':
-        return offset;
-      case 'UP':
-        return 0;
-      case 'DOWN':
-        return 0;
-      default:
-        return 0;
-    }
-  }
-
-  private getDeltaY(dir: LineDirection, offset: number): number {
-    switch (dir) {
-      case 'LEFT':
-        return 0;
-      case 'RIGHT':
-        return 0;
-      case 'UP':
-        return -offset;
-      case 'DOWN':
-        return offset;
-      default:
-        return 0;
-    }
-  }
-
-  private drawFlowArrow(
-    ctx: CanvasRenderingContext2D,
-    begin: {x: number; y: number; dir: LineDirection},
-    end: {x: number; y: number; dir: LineDirection},
-    hue: number,
-    intensity: number,
-    width: number,
-  ) {
-    const hasArrowHead = Math.abs(begin.x - end.x) > 3 * TRIANGLE_SIZE;
-    const END_OFFSET =
-      (end.dir === 'RIGHT' || end.dir === 'LEFT') && hasArrowHead
-        ? TRIANGLE_SIZE
-        : 0;
-    const color = `hsl(${hue}, 50%, ${intensity}%)`;
-    // draw curved line from begin to end (bezier curve)
-    ctx.strokeStyle = color;
-    ctx.lineWidth = width;
-    ctx.beginPath();
-    ctx.moveTo(begin.x, begin.y);
-    ctx.bezierCurveTo(
-      begin.x - this.getDeltaX(begin.dir, BEZIER_OFFSET),
-      begin.y - this.getDeltaY(begin.dir, BEZIER_OFFSET),
-      end.x - this.getDeltaX(end.dir, BEZIER_OFFSET + END_OFFSET),
-      end.y - this.getDeltaY(end.dir, BEZIER_OFFSET + END_OFFSET),
-      end.x - this.getDeltaX(end.dir, END_OFFSET),
-      end.y - this.getDeltaY(end.dir, END_OFFSET),
+    const start = getConnectionTarget(
+      flow.begin.trackId,
+      flow.begin.depth,
+      startX,
     );
-    ctx.stroke();
+    const end = getConnectionTarget(flow.end.trackId, flow.end.depth, endX);
 
-    // TODO (andrewbb): probably we should add a parameter 'MarkerType' to be
-    // able to choose what marker we want to draw _before_ the function call.
-    // e.g. triangle, circle, square?
-    if (begin.dir !== 'RIGHT' && begin.dir !== 'LEFT') {
-      // draw a circle if we the line has a vertical connection
-      ctx.fillStyle = color;
-      ctx.beginPath();
-      ctx.arc(begin.x, begin.y, 3, 0, 2 * Math.PI);
-      ctx.closePath();
-      ctx.fill();
+    if (start && end) {
+      drawArrow(ctx, start, end, intensity, hue, width);
+    }
+  };
+
+  const getConnectionTarget = (
+    trackId: number,
+    depth: number,
+    x: number,
+  ): VerticalEdgeOrPoint | undefined => {
+    const track = sqlTrackIdToTrack.get(trackId);
+    if (!track) {
+      return undefined;
     }
 
-    if (end.dir !== 'RIGHT' && end.dir !== 'LEFT') {
-      // draw a circle if we the line has a vertical connection
-      ctx.fillStyle = color;
-      ctx.beginPath();
-      ctx.arc(end.x, end.y, CIRCLE_RADIUS, 0, 2 * Math.PI);
-      ctx.closePath();
-      ctx.fill();
-    } else if (hasArrowHead) {
-      this.drawArrowHead(end, ctx, color);
+    const trackPanel = panelsByTrackNode.get(track);
+    if (trackPanel) {
+      const trackRect = trackPanel.rect;
+      const sliceRectRaw = trackPanel.panel.getSliceVerticalBounds?.(depth);
+      if (sliceRectRaw) {
+        const sliceRect = {
+          top: sliceRectRaw.top + trackRect.top,
+          bottom: sliceRectRaw.bottom + trackRect.top,
+        };
+        return {
+          kind: 'vertical_edge',
+          x,
+          y: (sliceRect.top + sliceRect.bottom) / 2,
+        };
+      } else {
+        // Slice bounds are not available for this track, so just put the target
+        // in the middle of the track
+        return {
+          kind: 'vertical_edge',
+          x,
+          y: (trackRect.top + trackRect.bottom) / 2,
+        };
+      }
+    } 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);
+      if (groupPanel) {
+        return {
+          kind: 'point',
+          x,
+          y: groupPanel.rect.bottom - TRACK_GROUP_CONNECTION_OFFSET,
+        };
+      }
     }
+
+    return undefined;
+  };
+
+  // Render the connected flows
+  trace.flows.connectedFlows.forEach((flow) => {
+    drawFlow(flow, CONNECTED_FLOW_HUE);
+  });
+
+  // Render the selected flows
+  trace.flows.selectedFlows.forEach((flow) => {
+    const categories = getFlowCategories(flow);
+    for (const cat of categories) {
+      if (
+        trace.flows.visibleCategories.get(cat) ||
+        trace.flows.visibleCategories.get(ALL_CATEGORIES)
+      ) {
+        drawFlow(flow, SELECTED_FLOW_HUE);
+        break;
+      }
+    }
+  });
+}
+
+// Check if an object defined by the horizontal bounds |bounds| is inside the
+// viewport defined by |viewportSizeZ.
+function isInViewport(bounds: HorizontalBounds, viewportSize: Size2D): boolean {
+  return bounds.right >= 0 && bounds.left < viewportSize.width;
+}
+
+function drawArrow(
+  ctx: CanvasRenderingContext2D,
+  start: VerticalEdgeOrPoint,
+  end: VerticalEdgeOrPoint,
+  intensity: number,
+  hue: number,
+  width: number,
+): void {
+  ctx.strokeStyle = `hsl(${hue}, 50%, ${intensity}%)`;
+  ctx.fillStyle = `hsl(${hue}, 50%, ${intensity}%)`;
+  ctx.lineWidth = width;
+
+  // TODO(stevegolton): Consider vertical distance too
+  const roomForArrowHead = Math.abs(start.x - end.x) > 3 * TRIANGLE_SIZE;
+
+  let startStyle: ArrowHeadStyle;
+  if (start.kind === 'vertical_edge') {
+    startStyle = {
+      orientation: 'east',
+      shape: 'none',
+    };
+  } else {
+    startStyle = {
+      orientation: 'auto_vertical',
+      shape: 'circle',
+      size: CIRCLE_RADIUS,
+    };
   }
 
-  private drawArrowHead(
-    end: {x: number; y: number; dir: LineDirection},
-    ctx: CanvasRenderingContext2D,
-    color: string,
-  ) {
-    const dx = this.getDeltaX(end.dir, TRIANGLE_SIZE);
-    const dy = this.getDeltaY(end.dir, TRIANGLE_SIZE);
-    // draw small triangle
-    ctx.fillStyle = color;
-    ctx.beginPath();
-    ctx.moveTo(end.x, end.y);
-    ctx.lineTo(end.x - dx - dy, end.y + dx - dy);
-    ctx.lineTo(end.x - dx + dy, end.y - dx - dy);
-    ctx.closePath();
-    ctx.fill();
+  let endStyle: ArrowHeadStyle;
+  if (end.kind === 'vertical_edge') {
+    endStyle = {
+      orientation: 'west',
+      shape: roomForArrowHead ? 'triangle' : 'none',
+      size: TRIANGLE_SIZE,
+    };
+  } else {
+    endStyle = {
+      orientation: 'auto_vertical',
+      shape: 'circle',
+      size: CIRCLE_RADIUS,
+    };
   }
+
+  drawBezierArrow(ctx, start, end, BEZIER_OFFSET, startStyle, endStyle);
 }
diff --git a/ui/src/frontend/generic_slice_details_tab.ts b/ui/src/frontend/generic_slice_details_tab.ts
index b104999..5595981 100644
--- a/ui/src/frontend/generic_slice_details_tab.ts
+++ b/ui/src/frontend/generic_slice_details_tab.ts
@@ -13,9 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-
-import {GenericSliceDetailsTabConfig} from '../core/generic_slice_details_types';
-import {raf} from '../core/raf_scheduler';
+import {TrackEventDetailsPanel} from '../public/details_panel';
 import {ColumnType} from '../trace_processor/query_result';
 import {sqlValueToReadableString} from '../trace_processor/sql_utils';
 import {DetailsShell} from '../widgets/details_shell';
@@ -23,54 +21,49 @@
 import {Section} from '../widgets/section';
 import {SqlRef} from '../widgets/sql_ref';
 import {dictToTree, Tree, TreeNode} from '../widgets/tree';
+import {Trace} from '../public/trace';
 
-import {BottomTab, NewBottomTabArgs} from './bottom_tab';
+export interface ColumnConfig {
+  readonly displayName?: string;
+}
 
-export {
-  ColumnConfig,
-  Columns,
-  GenericSliceDetailsTabConfig,
-  GenericSliceDetailsTabConfigBase,
-} from '../core/generic_slice_details_types';
+export type Columns = {
+  readonly [columnName: string]: ColumnConfig;
+};
 
 // A details tab, which fetches slice-like object from a given SQL table by id
 // and renders it according to the provided config, specifying which columns
 // need to be rendered and how.
-export class GenericSliceDetailsTab extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'dev.perfetto.GenericSliceDetailsTab';
+export class GenericSliceDetailsTab implements TrackEventDetailsPanel {
+  private data?: {[key: string]: ColumnType};
 
-  data: {[key: string]: ColumnType} | undefined;
+  constructor(
+    private readonly trace: Trace,
+    private readonly sqlTableName: string,
+    private readonly id: number,
+    private readonly title: string,
+    private readonly columns?: Columns,
+  ) {}
 
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): GenericSliceDetailsTab {
-    return new GenericSliceDetailsTab(args);
+  async load() {
+    const result = await this.trace.engine.query(
+      `select * from ${this.sqlTableName} where id = ${this.id}`,
+    );
+
+    this.data = result.firstRow({});
   }
 
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-
-    this.engine
-      .query(
-        `select * from ${this.config.sqlTableName} where id = ${this.config.id}`,
-      )
-      .then((queryResult) => {
-        this.data = queryResult.firstRow({});
-        raf.scheduleFullRedraw();
-      });
-  }
-
-  viewTab() {
-    if (this.data === undefined) {
+  render() {
+    if (!this.data) {
       return m('h2', 'Loading');
     }
 
     const args: {[key: string]: m.Child} = {};
-    if (this.config.columns !== undefined) {
-      for (const key of Object.keys(this.config.columns)) {
+    if (this.columns !== undefined) {
+      for (const key of Object.keys(this.columns)) {
         let argKey = key;
-        if (this.config.columns[key].displayName !== undefined) {
-          argKey = this.config.columns[key].displayName!;
+        if (this.columns[key].displayName !== undefined) {
+          argKey = this.columns[key].displayName!;
         }
         args[argKey] = sqlValueToReadableString(this.data[key]);
       }
@@ -85,7 +78,7 @@
     return m(
       DetailsShell,
       {
-        title: this.config.title,
+        title: this.title,
       },
       m(
         GridLayout,
@@ -97,8 +90,8 @@
             m(TreeNode, {
               left: 'SQL ID',
               right: m(SqlRef, {
-                table: this.config.sqlTableName,
-                id: this.config.id,
+                table: this.sqlTableName,
+                id: this.id,
               }),
             }),
           ]),
@@ -106,12 +99,4 @@
       ),
     );
   }
-
-  getTitle(): string {
-    return this.config.title;
-  }
-
-  isLoading() {
-    return this.data === undefined;
-  }
 }
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index f497a6b..f5be8b4 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -12,627 +12,22 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertExists, assertUnreachable} from '../base/logging';
-import {createStore, Store} from '../base/store';
-import {duration, Time, time, TimeSpan} from '../base/time';
-import {Actions, DeferredAction} from '../common/actions';
-import {AggregateData} from '../common/aggregation_data';
-import {Args} from '../common/arg_types';
-import {CommandManager} from '../common/commands';
-import {
-  ConversionJobName,
-  ConversionJobStatus,
-} from '../common/conversion_jobs';
-import {createEmptyState} from '../common/empty_state';
-import {MetricResult} from '../common/metric_data';
-import {CurrentSearchResults} from '../common/search_data';
-import {EngineConfig, State, getLegacySelection} from '../common/state';
-import {TabManager} from '../common/tab_registry';
-import {TimestampFormat, timestampFormat} from '../core/timestamp_format';
-import {TrackManager} from '../common/track_cache';
-import {setPerfHooks} from '../core/perf';
 import {raf} from '../core/raf_scheduler';
-import {ServiceWorkerController} from './service_worker_controller';
-import {Engine, EngineBase} from '../trace_processor/engine';
-import {HttpRpcState} from '../trace_processor/http_rpc_engine';
-import {Analytics, initAnalytics} from './analytics';
-import {Timeline} from './timeline';
-import {SliceSqlId} from '../trace_processor/sql_utils/core_types';
-import {SelectionManager, LegacySelection} from '../core/selection_manager';
-import {Optional, exists} from '../base/utils';
-import {OmniboxManager} from './omnibox_manager';
-import {CallsiteInfo} from '../common/legacy_flamegraph_util';
-import {LegacyFlamegraphCache} from '../core/legacy_flamegraph_cache';
-import {SerializedAppState} from '../common/state_serialization_schema';
-import {getServingRoot} from '../base/http_utils';
-import {
-  createSearchOverviewTrack,
-  SearchOverviewTrack,
-} from './search_overview_track';
-import {AppContext} from './app_context';
-import {TraceContext} from './trace_context';
-import {Registry} from '../base/registry';
-import {SidebarMenuItem} from '../public';
-
-const INSTANT_FOCUS_DURATION = 1n;
-const INCOMPLETE_SLICE_DURATION = 30_000n;
-
-type DispatchMultiple = (actions: DeferredAction[]) => void;
-type TrackDataStore = Map<string, {}>;
-type QueryResultsStore = Map<string, {} | undefined>;
-type AggregateDataStore = Map<string, AggregateData>;
-type Description = Map<string, string>;
-
-export interface SliceDetails {
-  ts?: time;
-  absTime?: string;
-  dur?: duration;
-  threadTs?: time;
-  threadDur?: duration;
-  priority?: number;
-  endState?: string | null;
-  cpu?: number;
-  id?: number;
-  threadStateId?: number;
-  utid?: number;
-  wakeupTs?: time;
-  wakerUtid?: number;
-  wakerCpu?: number;
-  category?: string;
-  name?: string;
-  tid?: number;
-  threadName?: string;
-  pid?: number;
-  processName?: string;
-  uid?: number;
-  packageName?: string;
-  versionCode?: number;
-  args?: Args;
-  description?: Description;
-}
-
-export interface FlowPoint {
-  trackId: number;
-
-  sliceName: string;
-  sliceCategory: string;
-  sliceId: SliceSqlId;
-  sliceStartTs: time;
-  sliceEndTs: time;
-  // Thread and process info. Only set in sliceSelected not in areaSelected as
-  // the latter doesn't display per-flow info and it'd be a waste to join
-  // additional tables for undisplayed info in that case. Nothing precludes
-  // adding this in a future iteration however.
-  threadName: string;
-  processName: string;
-
-  depth: number;
-
-  // TODO(altimin): Ideally we should have a generic mechanism for allowing to
-  // customise the name here, but for now we are hardcording a few
-  // Chrome-specific bits in the query here.
-  sliceChromeCustomName?: string;
-}
-
-export interface Flow {
-  id: number;
-
-  begin: FlowPoint;
-  end: FlowPoint;
-  dur: duration;
-
-  // Whether this flow connects a slice with its descendant.
-  flowToDescendant: boolean;
-
-  category?: string;
-  name?: string;
-}
-
-export interface ThreadStateDetails {
-  ts?: time;
-  dur?: duration;
-}
-
-export interface CpuProfileDetails {
-  id?: number;
-  ts?: time;
-  utid?: number;
-  stack?: CallsiteInfo[];
-}
-
-export interface QuantizedLoad {
-  start: time;
-  end: time;
-  load: number;
-}
-type OverviewStore = Map<string, QuantizedLoad[]>;
-
-export interface ThreadDesc {
-  utid: number;
-  tid: number;
-  threadName: string;
-  pid?: number;
-  procName?: string;
-  cmdline?: string;
-}
-type ThreadMap = Map<number, ThreadDesc>;
-
-// Options for globals.makeSelection().
-export interface MakeSelectionOpts {
-  // Whether to switch to the current selection tab or not. Default = true.
-  switchToCurrentSelectionTab?: boolean;
-
-  // Whether to cancel the current search selection. Default = true.
-  clearSearch?: boolean;
-}
-
-// All of these control additional things we can do when doing a
-// selection.
-export interface LegacySelectionArgs {
-  clearSearch: boolean;
-  switchToCurrentSelectionTab: boolean;
-  pendingScrollId: number | undefined;
-}
-
-export const defaultTraceContext: TraceContext = {
-  traceTitle: '',
-  traceUrl: '',
-  start: Time.ZERO,
-  end: Time.fromSeconds(10),
-  realtimeOffset: Time.ZERO,
-  utcOffset: Time.ZERO,
-  traceTzOffset: Time.ZERO,
-  cpus: [],
-  gpuCount: 0,
-};
+import {AppImpl} from '../core/app_impl';
 
 /**
  * Global accessors for state/dispatch in the frontend.
  */
-class Globals implements AppContext {
-  readonly root = getServingRoot();
-
-  private _testing = false;
-  private _dispatchMultiple?: DispatchMultiple = undefined;
-  private _store = createStore<State>(createEmptyState());
-  private _timeline?: Timeline = undefined;
-  private _serviceWorkerController?: ServiceWorkerController = undefined;
-  private _logging?: Analytics = undefined;
+class Globals {
+  // This is normally undefined is injected in via is_internal_user.js.
+  // WARNING: do not change/rename/move without considering impact on the
+  // internal_user script.
   private _isInternalUser: boolean | undefined = undefined;
 
-  // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
-  private _trackDataStore?: TrackDataStore = undefined;
-  private _queryResults?: QueryResultsStore = undefined;
-  private _overviewStore?: OverviewStore = undefined;
-  private _aggregateDataStore?: AggregateDataStore = undefined;
-  private _threadMap?: ThreadMap = undefined;
-  private _sliceDetails?: SliceDetails = undefined;
-  private _threadStateDetails?: ThreadStateDetails = undefined;
-  private _connectedFlows?: Flow[] = undefined;
-  private _selectedFlows?: Flow[] = undefined;
-  private _visibleFlowCategories?: Map<string, boolean> = undefined;
-  private _cpuProfileDetails?: CpuProfileDetails = undefined;
-  private _numQueriesQueued = 0;
-  private _bufferUsage?: number = undefined;
-  private _recordingLog?: string = undefined;
-  private _traceErrors?: number = undefined;
-  private _metricError?: string = undefined;
-  private _metricResult?: MetricResult = undefined;
-  private _jobStatus?: Map<ConversionJobName, ConversionJobStatus> = undefined;
-  private _embeddedMode?: boolean = undefined;
-  private _hideSidebar?: boolean = undefined;
-  private _cmdManager = new CommandManager();
-  private _tabManager = new TabManager();
-  private _trackManager = new TrackManager(this._store);
-  private _selectionManager = new SelectionManager(this._store);
-  private _hasFtrace: boolean = false;
-  private _searchOverviewTrack?: SearchOverviewTrack;
-
-  omnibox = new OmniboxManager();
-  areaFlamegraphCache = new LegacyFlamegraphCache('area');
-
-  scrollToTrackKey?: string | number;
-  httpRpcState: HttpRpcState = {connected: false};
-  showPanningHint = false;
-  permalinkHash?: string;
-  showTraceErrorPopup = true;
-
-  traceContext = defaultTraceContext;
-
-  readonly sidebarMenuItems = new Registry<SidebarMenuItem>((m) => m.commandId);
-
-  // This is the app's equivalent of a plugin's onTraceLoad() function.
-  // TODO(stevegolton): Eventually initialization that should be done on trace
-  // load should be moved into here, and then we can remove TraceController
-  // entirely
-  async onTraceLoad(engine: Engine, traceCtx: TraceContext): Promise<void> {
-    this.traceContext = traceCtx;
-
-    const {start, end} = traceCtx;
-    const traceSpan = new TimeSpan(start, end);
-    this._timeline = new Timeline(this._store, traceSpan);
-
-    // TODO(stevegolton): Even though createSearchOverviewTrack() returns a
-    // disposable, we completely ignore it as we assume the dispose action
-    // includes just dropping some tables, and seeing as this object will live
-    // for the duration of the trace/engine, there's no need to drop anything as
-    // the tables will be dropped along with the trace anyway.
-    //
-    // Note that this is no worse than a lot of the rest of the app where tables
-    // are created with no way to drop them.
-    //
-    // Once we fix the story around loading new traces, we should tidy this up.
-    // We could for example have a matching globals.onTraceUnload() that
-    // performs any tear-down before the old engine is dropped. This might seem
-    // pointless, but it could at least block until any currently running update
-    // cycles complete, to avoid leaving promises open on old engines that will
-    // never resolve.
-    //
-    // Alternatively we could decide that we don't want to support switching
-    // traces at all, in which case we can ignore tear down entirely.
-    this._searchOverviewTrack = await createSearchOverviewTrack(engine, this);
-  }
-
-  // Used for permalink load by trace_controller.ts.
-  restoreAppStateAfterTraceLoad?: SerializedAppState;
-
-  // TODO(hjd): Remove once we no longer need to update UUID on redraw.
-  private _publishRedraw?: () => void = undefined;
-
-  private _currentSearchResults: CurrentSearchResults = {
-    eventIds: new Float64Array(0),
-    tses: new BigInt64Array(0),
-    utids: new Float64Array(0),
-    trackKeys: [],
-    sources: [],
-    totalResults: 0,
-  };
-
-  engines = new Map<string, EngineBase>();
-
-  constructor() {
-    const {start, end} = defaultTraceContext;
-    this._timeline = new Timeline(this._store, new TimeSpan(start, end));
-  }
-
-  initialize(dispatchMultiple: DispatchMultiple) {
-    this._dispatchMultiple = dispatchMultiple;
-
-    setPerfHooks(
-      () => this.state.perfDebug,
-      () => this.dispatch(Actions.togglePerfDebug({})),
-    );
-
-    this._serviceWorkerController = new ServiceWorkerController(
-      getServingRoot(),
-    );
-    this._testing =
-      /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-      self.location && self.location.search.indexOf('testing=1') >= 0;
-    /* eslint-enable */
-    this._logging = initAnalytics();
-
-    // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
-    this._trackDataStore = new Map<string, {}>();
-    this._queryResults = new Map<string, {}>();
-    this._overviewStore = new Map<string, QuantizedLoad[]>();
-    this._aggregateDataStore = new Map<string, AggregateData>();
-    this._threadMap = new Map<number, ThreadDesc>();
-    this._sliceDetails = {};
-    this._connectedFlows = [];
-    this._selectedFlows = [];
-    this._visibleFlowCategories = new Map<string, boolean>();
-    this._threadStateDetails = {};
-    this._cpuProfileDetails = {};
-    this.engines.clear();
-    this._selectionManager.clear();
-  }
-
-  // Only initialises the store - useful for testing.
-  initStore(initialState: State) {
-    this._store = createStore(initialState);
-  }
-
-  get publishRedraw(): () => void {
-    return this._publishRedraw || (() => {});
-  }
-
-  set publishRedraw(f: () => void) {
-    this._publishRedraw = f;
-  }
-
-  get state(): State {
-    return assertExists(this._store).state;
-  }
-
-  get store(): Store<State> {
-    return assertExists(this._store);
-  }
-
-  dispatch(action: DeferredAction) {
-    this.dispatchMultiple([action]);
-  }
-
-  dispatchMultiple(actions: DeferredAction[]) {
-    assertExists(this._dispatchMultiple)(actions);
-  }
-
-  get timeline() {
-    return assertExists(this._timeline);
-  }
-
-  get logging() {
-    return assertExists(this._logging);
-  }
-
-  get serviceWorkerController() {
-    return assertExists(this._serviceWorkerController);
-  }
-
-  // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
-  get overviewStore(): OverviewStore {
-    return assertExists(this._overviewStore);
-  }
-
-  get trackDataStore(): TrackDataStore {
-    return assertExists(this._trackDataStore);
-  }
-
-  get queryResults(): QueryResultsStore {
-    return assertExists(this._queryResults);
-  }
-
-  get threads() {
-    return assertExists(this._threadMap);
-  }
-
-  get sliceDetails() {
-    return assertExists(this._sliceDetails);
-  }
-
-  set sliceDetails(click: SliceDetails) {
-    this._sliceDetails = assertExists(click);
-  }
-
-  get threadStateDetails() {
-    return assertExists(this._threadStateDetails);
-  }
-
-  set threadStateDetails(click: ThreadStateDetails) {
-    this._threadStateDetails = assertExists(click);
-  }
-
-  get connectedFlows() {
-    return assertExists(this._connectedFlows);
-  }
-
-  set connectedFlows(connectedFlows: Flow[]) {
-    this._connectedFlows = assertExists(connectedFlows);
-  }
-
-  get selectedFlows() {
-    return assertExists(this._selectedFlows);
-  }
-
-  set selectedFlows(selectedFlows: Flow[]) {
-    this._selectedFlows = assertExists(selectedFlows);
-  }
-
-  get visibleFlowCategories() {
-    return assertExists(this._visibleFlowCategories);
-  }
-
-  set visibleFlowCategories(visibleFlowCategories: Map<string, boolean>) {
-    this._visibleFlowCategories = assertExists(visibleFlowCategories);
-  }
-
-  get aggregateDataStore(): AggregateDataStore {
-    return assertExists(this._aggregateDataStore);
-  }
-
-  get traceErrors() {
-    return this._traceErrors;
-  }
-
-  setTraceErrors(arg: number) {
-    this._traceErrors = arg;
-  }
-
-  get metricError() {
-    return this._metricError;
-  }
-
-  setMetricError(arg: string) {
-    this._metricError = arg;
-  }
-
-  get metricResult() {
-    return this._metricResult;
-  }
-
-  setMetricResult(result: MetricResult) {
-    this._metricResult = result;
-  }
-
-  get cpuProfileDetails() {
-    return assertExists(this._cpuProfileDetails);
-  }
-
-  set cpuProfileDetails(click: CpuProfileDetails) {
-    this._cpuProfileDetails = assertExists(click);
-  }
-
-  set numQueuedQueries(value: number) {
-    this._numQueriesQueued = value;
-  }
-
-  get numQueuedQueries() {
-    return this._numQueriesQueued;
-  }
-
-  get bufferUsage() {
-    return this._bufferUsage;
-  }
-
-  get recordingLog() {
-    return this._recordingLog;
-  }
-
-  get currentSearchResults() {
-    return this._currentSearchResults;
-  }
-
-  set currentSearchResults(results: CurrentSearchResults) {
-    this._currentSearchResults = results;
-  }
-
-  set hasFtrace(value: boolean) {
-    this._hasFtrace = value;
-  }
-
-  get hasFtrace(): boolean {
-    return this._hasFtrace;
-  }
-
-  get searchOverviewTrack() {
-    return this._searchOverviewTrack;
-  }
-
-  getConversionJobStatus(name: ConversionJobName): ConversionJobStatus {
-    return this.getJobStatusMap().get(name) ?? ConversionJobStatus.NotRunning;
-  }
-
-  setConversionJobStatus(name: ConversionJobName, status: ConversionJobStatus) {
-    const map = this.getJobStatusMap();
-    if (status === ConversionJobStatus.NotRunning) {
-      map.delete(name);
-    } else {
-      map.set(name, status);
-    }
-  }
-
-  private getJobStatusMap(): Map<ConversionJobName, ConversionJobStatus> {
-    if (!this._jobStatus) {
-      this._jobStatus = new Map();
-    }
-    return this._jobStatus;
-  }
-
-  get embeddedMode(): boolean {
-    return !!this._embeddedMode;
-  }
-
-  set embeddedMode(value: boolean) {
-    this._embeddedMode = value;
-  }
-
-  get hideSidebar(): boolean {
-    return !!this._hideSidebar;
-  }
-
-  set hideSidebar(value: boolean) {
-    this._hideSidebar = value;
-  }
-
-  setBufferUsage(bufferUsage: number) {
-    this._bufferUsage = bufferUsage;
-  }
-
-  setTrackData(id: string, data: {}) {
-    this.trackDataStore.set(id, data);
-  }
-
-  setRecordingLog(recordingLog: string) {
-    this._recordingLog = recordingLog;
-  }
-
-  setAggregateData(kind: string, data: AggregateData) {
-    this.aggregateDataStore.set(kind, data);
-  }
-
-  getCurrentEngine(): EngineConfig | undefined {
-    return this.state.engine;
-  }
-
-  makeSelection(action: DeferredAction<{}>, opts: MakeSelectionOpts = {}) {
-    const {switchToCurrentSelectionTab = true, clearSearch = true} = opts;
-    const currentSelectionTabUri = 'current_selection';
-
-    // A new selection should cancel the current search selection.
-    clearSearch && globals.dispatch(Actions.setSearchIndex({index: -1}));
-
-    if (switchToCurrentSelectionTab) {
-      globals.dispatch(Actions.showTab({uri: currentSelectionTabUri}));
-    }
-    globals.dispatch(action);
-  }
-
-  setLegacySelection(
-    legacySelection: LegacySelection,
-    args: Partial<LegacySelectionArgs> = {},
-  ): void {
-    this._selectionManager.setLegacy(legacySelection);
-    this.handleSelectionArgs(args);
-  }
-
-  selectSingleEvent(
-    trackKey: string,
-    eventId: number,
-    args: Partial<LegacySelectionArgs> = {},
-  ): void {
-    this._selectionManager.setEvent(trackKey, eventId);
-    this.handleSelectionArgs(args);
-  }
-
-  private handleSelectionArgs(args: Partial<LegacySelectionArgs> = {}): void {
-    const {
-      clearSearch = true,
-      switchToCurrentSelectionTab = true,
-      pendingScrollId = undefined,
-    } = args;
-    if (clearSearch) {
-      globals.dispatch(Actions.setSearchIndex({index: -1}));
-    }
-    if (pendingScrollId !== undefined) {
-      globals.dispatch(
-        Actions.setPendingScrollId({
-          pendingScrollId,
-        }),
-      );
-    }
-    if (switchToCurrentSelectionTab) {
-      globals.dispatch(Actions.showTab({uri: 'current_selection'}));
-    }
-  }
-
-  clearSelection(): void {
-    globals.dispatch(Actions.setSearchIndex({index: -1}));
-    this._selectionManager.clear();
-  }
-
-  resetForTesting() {
-    this._dispatchMultiple = undefined;
-    this._timeline = undefined;
-    this._serviceWorkerController = undefined;
-
-    // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
-    this._trackDataStore = undefined;
-    this._queryResults = undefined;
-    this._overviewStore = undefined;
-    this._threadMap = undefined;
-    this._sliceDetails = undefined;
-    this._threadStateDetails = undefined;
-    this._aggregateDataStore = undefined;
-    this._numQueriesQueued = 0;
-    this._metricResult = undefined;
-    this._currentSearchResults = {
-      eventIds: new Float64Array(0),
-      tses: new BigInt64Array(0),
-      utids: new Float64Array(0),
-      trackKeys: [],
-      sources: [],
-      totalResults: 0,
-    };
+  // WARNING: do not change/rename/move without considering impact on the
+  // internal_user script.
+  get extraSqlPackages() {
+    return AppImpl.instance.extraSqlPackages;
   }
 
   // This variable is set by the is_internal_user.js script if the user is a
@@ -654,10 +49,6 @@
     raf.scheduleFullRedraw();
   }
 
-  get testing() {
-    return this._testing;
-  }
-
   // Used when switching to the legacy TraceViewer UI.
   // Most resources are cleaned up by replacing the current |window| object,
   // however pending RAFs and workers seem to outlive the |window| and need to
@@ -665,149 +56,6 @@
   shutdown() {
     raf.shutdown();
   }
-
-  get commandManager(): CommandManager {
-    return assertExists(this._cmdManager);
-  }
-
-  get tabManager() {
-    return this._tabManager;
-  }
-
-  get trackManager() {
-    return this._trackManager;
-  }
-
-  // Offset between t=0 and the configured time domain.
-  timestampOffset(): time {
-    const fmt = timestampFormat();
-    switch (fmt) {
-      case TimestampFormat.Timecode:
-      case TimestampFormat.Seconds:
-        return this.traceContext.start;
-      case TimestampFormat.Raw:
-      case TimestampFormat.RawLocale:
-        return Time.ZERO;
-      case TimestampFormat.UTC:
-        return this.traceContext.utcOffset;
-      case TimestampFormat.TraceTz:
-        return this.traceContext.traceTzOffset;
-      default:
-        const x: never = fmt;
-        throw new Error(`Unsupported format ${x}`);
-    }
-  }
-
-  // Convert absolute time to domain time.
-  toDomainTime(ts: time): time {
-    return Time.sub(ts, this.timestampOffset());
-  }
-
-  async findTimeRangeOfSelection(): Promise<
-    Optional<{start: time; end: time}>
-  > {
-    const sel = globals.state.selection;
-    if (sel.kind === 'area') {
-      return sel;
-    } else if (sel.kind === 'note') {
-      const selectedNote = this.state.notes[sel.id];
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      if (selectedNote) {
-        const kind = selectedNote.noteType;
-        switch (kind) {
-          case 'SPAN':
-            return {
-              start: selectedNote.start,
-              end: selectedNote.end,
-            };
-          case 'DEFAULT':
-            return {
-              start: selectedNote.timestamp,
-              end: Time.add(selectedNote.timestamp, INSTANT_FOCUS_DURATION),
-            };
-          default:
-            assertUnreachable(kind);
-        }
-      }
-    } else if (sel.kind === 'single') {
-      const uri = globals.state.tracks[sel.trackKey]?.uri;
-      if (uri) {
-        const bounds = await globals.trackManager
-          .resolveTrackInfo(uri)
-          ?.getEventBounds?.(sel.eventId);
-        if (bounds) {
-          return {
-            start: bounds.ts,
-            end: Time.add(bounds.ts, bounds.dur),
-          };
-        }
-      }
-      return undefined;
-    }
-
-    const selection = getLegacySelection(this.state);
-    if (selection === null) {
-      return undefined;
-    }
-
-    if (selection.kind === 'SCHED_SLICE' || selection.kind === 'SLICE') {
-      const slice = this.sliceDetails;
-      return findTimeRangeOfSlice(slice);
-    } else if (selection.kind === 'THREAD_STATE') {
-      const threadState = this.threadStateDetails;
-      return findTimeRangeOfSlice(threadState);
-    } else if (selection.kind === 'LOG') {
-      // TODO(hjd): Make focus selection work for logs.
-    } else if (selection.kind === 'GENERIC_SLICE') {
-      return findTimeRangeOfSlice({
-        ts: selection.start,
-        dur: selection.duration,
-      });
-    }
-
-    return undefined;
-  }
-}
-
-interface SliceLike {
-  ts: time;
-  dur: duration;
-}
-
-// Returns the start and end points of a slice-like object If slice is instant
-// or incomplete, dummy time will be returned which instead.
-function findTimeRangeOfSlice(slice: Partial<SliceLike>): {
-  start: time;
-  end: time;
-} {
-  if (exists(slice.ts) && exists(slice.dur)) {
-    if (slice.dur === -1n) {
-      return {
-        start: slice.ts,
-        end: Time.add(slice.ts, INCOMPLETE_SLICE_DURATION),
-      };
-    } else if (slice.dur === 0n) {
-      return {
-        start: slice.ts,
-        end: Time.add(slice.ts, INSTANT_FOCUS_DURATION),
-      };
-    } else {
-      return {start: slice.ts, end: Time.add(slice.ts, slice.dur)};
-    }
-  } else {
-    return {start: Time.INVALID, end: Time.INVALID};
-  }
-}
-
-// Returns the time span of the current selection, or the visible window if
-// there is no current selection.
-export async function getTimeSpanOfSelectionOrVisibleWindow(): Promise<TimeSpan> {
-  const range = await globals.findTimeRangeOfSelection();
-  if (exists(range)) {
-    return new TimeSpan(range.start, range.end);
-  } else {
-    return globals.timeline.visibleWindow.toTimeSpan();
-  }
 }
 
 export const globals = new Globals();
diff --git a/ui/src/frontend/gridline_helper_unittest.ts b/ui/src/frontend/gridline_helper_unittest.ts
index e5471ca..7b53587 100644
--- a/ui/src/frontend/gridline_helper_unittest.ts
+++ b/ui/src/frontend/gridline_helper_unittest.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import {Time, TimeSpan} from '../base/time';
-
 import {getPattern, generateTicks, TickType} from './gridline_helper';
 
 test('gridline helper to have sensible step sizes', () => {
diff --git a/ui/src/frontend/help_modal.ts b/ui/src/frontend/help_modal.ts
index d31188a..819f271 100644
--- a/ui/src/frontend/help_modal.ts
+++ b/ui/src/frontend/help_modal.ts
@@ -13,12 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-
-import {raf} from '../core/raf_scheduler';
 import {showModal} from '../widgets/modal';
 import {Spinner} from '../widgets/spinner';
-
-import {globals} from './globals';
 import {
   KeyboardLayoutMap,
   nativeKeyboardLayoutMap,
@@ -27,10 +23,15 @@
 import {KeyMapping} from './pan_and_zoom_handler';
 import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
 import {assertExists} from '../base/logging';
+import {AppImpl} from '../core/app_impl';
 
 export function toggleHelp() {
-  globals.logging.logEvent('User Actions', 'Show help');
-  showHelp();
+  AppImpl.instance.analytics.logEvent('User Actions', 'Show help');
+  showModal({
+    title: 'Perfetto Help',
+    content: () => m(KeyMappingsHelp),
+    buttons: [],
+  });
 }
 
 function keycap(glyph: m.Children): m.Children {
@@ -53,7 +54,7 @@
     nativeKeyboardLayoutMap()
       .then((keyMap: KeyboardLayoutMap) => {
         this.keyMap = keyMap;
-        raf.scheduleFullRedraw();
+        AppImpl.instance.scheduleFullRedraw();
       })
       .catch((e) => {
         if (
@@ -68,7 +69,7 @@
           // The alternative would be to show key mappings for all keyboard
           // layouts which is not feasible.
           this.keyMap = new EnglishQwertyKeyboardLayoutMap();
-          raf.scheduleFullRedraw();
+          AppImpl.instance.scheduleFullRedraw();
         } else {
           // Something unexpected happened. Either the browser doesn't conform
           // to the keyboard API spec, or the keyboard API spec has changed!
@@ -77,32 +78,7 @@
       });
   }
 
-  view(_: m.Vnode): m.Children {
-    const queryPageInstructions = globals.hideSidebar
-      ? []
-      : [
-          m('h2', 'Making SQL queries from the query page'),
-          m(
-            'table',
-            m(
-              'tr',
-              m('td', keycap('Ctrl'), ' + ', keycap('Enter')),
-              m('td', 'Execute query'),
-            ),
-            m(
-              'tr',
-              m(
-                'td',
-                keycap('Ctrl'),
-                ' + ',
-                keycap('Enter'),
-                ' (with selection)',
-              ),
-              m('td', 'Execute selection'),
-            ),
-          ),
-        ];
-
+  view(): m.Children {
     return m(
       '.help',
       m('h2', 'Navigation'),
@@ -165,11 +141,24 @@
           ),
         ),
       ),
-      ...queryPageInstructions,
+      m('h2', 'Making SQL queries from the query page'),
+      m(
+        'table',
+        m(
+          'tr',
+          m('td', keycap('Ctrl'), ' + ', keycap('Enter')),
+          m('td', 'Execute query'),
+        ),
+        m(
+          'tr',
+          m('td', keycap('Ctrl'), ' + ', keycap('Enter'), ' (with selection)'),
+          m('td', 'Execute selection'),
+        ),
+      ),
       m('h2', 'Command Hotkeys'),
       m(
         'table',
-        globals.commandManager.commands
+        AppImpl.instance.commands.commands
           .filter(({defaultHotkey}) => defaultHotkey)
           .sort((a, b) => a.name.localeCompare(b.name))
           .map(({defaultHotkey, name}) => {
@@ -191,11 +180,3 @@
     }
   }
 }
-
-function showHelp() {
-  showModal({
-    title: 'Perfetto Help',
-    content: () => m(KeyMappingsHelp),
-    buttons: [],
-  });
-}
diff --git a/ui/src/frontend/home_page.ts b/ui/src/frontend/home_page.ts
index 12612c0..e38f74c 100644
--- a/ui/src/frontend/home_page.ts
+++ b/ui/src/frontend/home_page.ts
@@ -13,13 +13,11 @@
 // limitations under the License.
 
 import m from 'mithril';
-
-import {channelChanged, getNextChannel, setChannel} from '../common/channels';
+import {channelChanged, getNextChannel, setChannel} from '../core/channels';
 import {Anchor} from '../widgets/anchor';
 import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
-
-import {globals} from './globals';
-import {createPage} from './pages';
+import {PageAttrs} from '../public/page';
+import {assetSrc} from '../base/assets';
 
 export class Hints implements m.ClassComponent {
   view() {
@@ -68,7 +66,7 @@
   }
 }
 
-export const HomePage = createPage({
+export class HomePage implements m.ClassComponent<PageAttrs> {
   view() {
     return m(
       '.page.home-page',
@@ -76,7 +74,7 @@
         '.home-page-center',
         m(
           '.home-page-title',
-          m(`img.logo[src=${globals.root}assets/logo-3d.png]`),
+          m(`img.logo[src=${assetSrc('assets/logo-3d.png')}]`),
           'Perfetto',
         ),
         m(Hints),
@@ -96,8 +94,8 @@
         'Privacy policy',
       ),
     );
-  },
-});
+  }
+}
 
 function mkChan(chan: string) {
   const checked = getNextChannel() === chan ? '[checked=true]' : '';
diff --git a/ui/src/frontend/idle_detector.ts b/ui/src/frontend/idle_detector.ts
new file mode 100644
index 0000000..d4e594f
--- /dev/null
+++ b/ui/src/frontend/idle_detector.ts
@@ -0,0 +1,80 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {defer} from '../base/deferred';
+import {raf} from '../core/raf_scheduler';
+import {AppImpl} from '../core/app_impl';
+
+/**
+ * This class is exposed by index.ts as window.waitForPerfettoIdle() and is used
+ * by tests, to detect when we reach quiescence.
+ */
+
+const IDLE_HYSTERESIS_MS = 100;
+const TIMEOUT_MS = 30_000;
+
+export class IdleDetector {
+  private promise = defer<void>();
+  private deadline = performance.now() + TIMEOUT_MS;
+  private idleSince?: number;
+  private idleHysteresisMs = IDLE_HYSTERESIS_MS;
+
+  waitForPerfettoIdle(idleHysteresisMs = IDLE_HYSTERESIS_MS): Promise<void> {
+    this.idleSince = undefined;
+    this.idleHysteresisMs = idleHysteresisMs;
+    this.scheduleNextTask();
+    return this.promise;
+  }
+
+  private onIdleCallback() {
+    const now = performance.now();
+    if (now > this.deadline) {
+      this.promise.reject(
+        `Didn't reach idle within ${TIMEOUT_MS} ms, giving up` +
+          ` ${this.idleIndicators()}`,
+      );
+      return;
+    }
+    if (this.idleIndicators().every((x) => x)) {
+      this.idleSince = this.idleSince ?? now;
+      const idleDur = now - this.idleSince;
+      if (idleDur >= this.idleHysteresisMs) {
+        // We have been idle for more than the threshold, success.
+        this.promise.resolve();
+        return;
+      }
+      // We are idle, but not for long enough. keep waiting
+      this.scheduleNextTask();
+      return;
+    }
+    // Not idle, reset and repeat.
+    this.idleSince = undefined;
+    this.scheduleNextTask();
+  }
+
+  private scheduleNextTask() {
+    requestIdleCallback(() => this.onIdleCallback());
+  }
+
+  private idleIndicators() {
+    const reqsPending = AppImpl.instance.trace?.engine.numRequestsPending ?? 0;
+    return [
+      reqsPending === 0,
+      !raf.hasPendingRedraws,
+      !document.getAnimations().some((a) => a.playState === 'running'),
+      document.querySelector('.progress.progress-anim') == null,
+      document.querySelector('.omnibox.message-mode') == null,
+    ];
+  }
+}
diff --git a/ui/src/frontend/idle_detector_interface.ts b/ui/src/frontend/idle_detector_interface.ts
new file mode 100644
index 0000000..49d1002
--- /dev/null
+++ b/ui/src/frontend/idle_detector_interface.ts
@@ -0,0 +1,17 @@
+// 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 IdleDetectorWindow {
+  waitForPerfettoIdle: (minIdleMs?: number) => Promise<void>;
+}
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index c8a078b..67aa000 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -15,53 +15,42 @@
 // Keep this import first.
 import '../base/disposable_polyfill';
 import '../base/static_initializers';
-import '../gen/all_plugins';
-import '../gen/all_core_plugins';
-
-import {Draft} from 'immer';
+import NON_CORE_PLUGINS from '../gen/all_plugins';
+import CORE_PLUGINS from '../gen/all_core_plugins';
 import m from 'mithril';
-
 import {defer} from '../base/deferred';
 import {addErrorHandler, reportError} from '../base/logging';
-import {Store} from '../base/store';
-import {Actions, DeferredAction, StateActions} from '../common/actions';
-import {flattenArgs, traceEvent} from '../common/metatracing';
-import {pluginManager} from '../common/plugins';
-import {State} from '../common/state';
-import {initController, runControllers} from '../controller';
-import {isGetCategoriesResponse} from '../controller/chrome_proxy_record_controller';
-import {RECORDING_V2_FLAG, featureFlags} from '../core/feature_flags';
+import {featureFlags} from '../core/feature_flags';
 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 {App} from './app';
+import {UiMain} from './ui_main';
 import {initCssConstants} from './css_constants';
 import {registerDebugGlobals} from './debug';
 import {maybeShowErrorDialog} from './error_dialog';
 import {installFileDropHandler} from './file_drop_handler';
-import {FlagsPage} from './flags_page';
 import {globals} from './globals';
 import {HomePage} from './home_page';
-import {InsightsPage} from './insights_page';
-import {MetricsPage} from './metrics_page';
-import {PluginsPage} from './plugins_page';
 import {postMessageHandler} from './post_message_handler';
-import {QueryPage} from './query_page';
-import {RecordPage, updateAvailableAdbDevices} from './record_page';
-import {RecordPageV2} from './record_page_v2';
-import {Route, Router} from './router';
+import {Route, Router} from '../core/router';
 import {CheckHttpRpcConnection} from './rpc_http_dialog';
-import {TraceInfoPage} from './trace_info_page';
 import {maybeOpenTraceFromRoute} from './trace_url_handler';
 import {ViewerPage} from './viewer_page';
-import {VizPage} from './viz_page';
-import {WidgetsPage} from './widgets_page';
 import {HttpRpcEngine} from '../trace_processor/http_rpc_engine';
 import {showModal} from '../widgets/modal';
-
-const EXTENSION_ID = 'lfmkphfpdbjijhpomgecfikhfohaoine';
+import {IdleDetector} from './idle_detector';
+import {IdleDetectorWindow} from './idle_detector_interface';
+import {AppImpl} from '../core/app_impl';
+import {addSqlTableTab} from './sql_table_tab';
+import {configureExtensions} from '../public/lib/extensions';
+import {
+  addDebugCounterTrack,
+  addDebugSliceTrack,
+} from '../public/lib/tracks/debug_tracks';
+import {addVisualizedArgTracks} from './visualized_args_tracks';
+import {addQueryResultsTab} from '../public/lib/query_table/query_result_tab';
+import {assetSrc, initAssets} from '../base/assets';
 
 const CSP_WS_PERMISSIVE_PORT = featureFlags.register({
   id: 'cspAllowAnyWebsocketPort',
@@ -73,14 +62,6 @@
   defaultValue: false,
 });
 
-function setExtensionAvailability(available: boolean) {
-  globals.dispatch(
-    Actions.setExtensionAvailable({
-      available,
-    }),
-  );
-}
-
 function routeChange(route: Route) {
   raf.scheduleFullRedraw();
   maybeOpenTraceFromRoute(route);
@@ -160,92 +141,54 @@
 }
 
 function main() {
+  // Setup content security policy before anything else.
+  setupContentSecurityPolicy();
+  initAssets();
+  AppImpl.initialize({
+    initialRouteArgs: Router.parseUrl(window.location.href).args,
+  });
+
   // 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
   // appendChild returns.
   const cssLoadPromise = defer<void>();
   const css = document.createElement('link');
   css.rel = 'stylesheet';
-  css.href = globals.root + 'perfetto.css';
+  css.href = assetSrc('perfetto.css');
   css.onload = () => cssLoadPromise.resolve();
   css.onerror = (err) => cssLoadPromise.reject(err);
   const favicon = document.head.querySelector('#favicon');
   if (favicon instanceof HTMLLinkElement) {
-    favicon.href = globals.root + 'assets/favicon.png';
+    favicon.href = assetSrc('assets/favicon.png');
   }
 
   // Load the script to detect if this is a Googler (see comments on globals.ts)
   // and initialize GA after that (or after a timeout if something goes wrong).
+  function initAnalyticsOnScriptLoad() {
+    AppImpl.instance.analytics.initialize(globals.isInternalUser);
+  }
   const script = document.createElement('script');
   script.src =
     'https://storage.cloud.google.com/perfetto-ui-internal/is_internal_user.js';
   script.async = true;
-  script.onerror = () => globals.logging.initialize();
-  script.onload = () => globals.logging.initialize();
-  setTimeout(() => globals.logging.initialize(), 5000);
+  script.onerror = () => initAnalyticsOnScriptLoad();
+  script.onload = () => initAnalyticsOnScriptLoad();
+  setTimeout(() => initAnalyticsOnScriptLoad(), 5000);
 
   document.head.append(script, css);
 
   // Route errors to both the UI bugreport dialog and Analytics (if enabled).
   addErrorHandler(maybeShowErrorDialog);
-  addErrorHandler((e) => globals.logging.logError(e));
+  addErrorHandler((e) => AppImpl.instance.analytics.logError(e));
 
   // Add Error handlers for JS error and for uncaught exceptions in promises.
   window.addEventListener('error', (e) => reportError(e));
   window.addEventListener('unhandledrejection', (e) => reportError(e));
 
-  const extensionLocalChannel = new MessageChannel();
-
-  initWasm(globals.root);
-  initController(extensionLocalChannel.port1);
-
-  // These need to be set before globals.initialize.
-  const route = Router.parseUrl(window.location.href);
-  globals.embeddedMode = route.args.mode === 'embedded';
-  globals.hideSidebar = route.args.hideSidebar === true;
-
-  globals.initialize(stateActionDispatcher);
-
-  globals.serviceWorkerController.install();
-
-  globals.store.subscribe(scheduleRafAndRunControllersOnStateChange);
-  globals.publishRedraw = () => raf.scheduleFullRedraw();
-
-  // We proxy messages between the extension and the controller because the
-  // controller's worker can't access chrome.runtime.
-  const extensionPort =
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    window.chrome && chrome.runtime
-      ? chrome.runtime.connect(EXTENSION_ID)
-      : undefined;
-
-  setExtensionAvailability(extensionPort !== undefined);
-
-  if (extensionPort) {
-    extensionPort.onDisconnect.addListener((_) => {
-      setExtensionAvailability(false);
-      void chrome.runtime.lastError; // Needed to not receive an error log.
-    });
-    // This forwards the messages from the extension to the controller.
-    extensionPort.onMessage.addListener(
-      (message: object, _port: chrome.runtime.Port) => {
-        if (isGetCategoriesResponse(message)) {
-          globals.dispatch(Actions.setChromeCategories(message));
-          return;
-        }
-        extensionLocalChannel.port2.postMessage(message);
-      },
-    );
-  }
-
-  // This forwards the messages from the controller to the extension
-  extensionLocalChannel.port2.onmessage = ({data}) => {
-    if (extensionPort) extensionPort.postMessage(data);
-  };
+  initWasm();
+  AppImpl.instance.serviceWorkerController.install();
 
   // Put debug variables in the global scope for better debugging.
   registerDebugGlobals();
@@ -261,9 +204,13 @@
 
   cssLoadPromise.then(() => onCssLoaded());
 
-  if (globals.testing) {
+  if (AppImpl.instance.testingMode) {
     document.body.classList.add('testing');
   }
+
+  (window as {} as IdleDetectorWindow).waitForPerfettoIdle = (ms?: number) => {
+    return new IdleDetector().waitForPerfettoIdle(ms);
+  };
 }
 
 function onCssLoaded() {
@@ -272,48 +219,29 @@
   // And replace it with the root <main> element which will be used by mithril.
   document.body.innerHTML = '';
 
-  const router = new Router({
-    '/': HomePage,
-    '/viewer': ViewerPage,
-    '/record': RECORDING_V2_FLAG.get() ? RecordPageV2 : RecordPage,
-    '/query': QueryPage,
-    '/insights': InsightsPage,
-    '/flags': FlagsPage,
-    '/metrics': MetricsPage,
-    '/info': TraceInfoPage,
-    '/widgets': WidgetsPage,
-    '/viz': VizPage,
-    '/plugins': PluginsPage,
-  });
+  const pages = AppImpl.instance.pages;
+  const traceless = true;
+  pages.registerPage({route: '/', traceless, page: HomePage});
+  pages.registerPage({route: '/viewer', page: ViewerPage});
+  const router = new Router();
   router.onRouteChanged = routeChange;
 
   raf.domRedraw = () => {
-    m.render(document.body, m(App, router.resolve()));
+    m.render(
+      document.body,
+      m(UiMain, pages.renderPageForCurrentRoute(AppImpl.instance.trace)),
+    );
   };
 
   if (
     (location.origin.startsWith('http://localhost:') ||
       location.origin.startsWith('http://127.0.0.1:')) &&
-    !globals.embeddedMode &&
-    !globals.testing
+    !AppImpl.instance.embeddedMode &&
+    !AppImpl.instance.testingMode
   ) {
     initLiveReload();
   }
 
-  if (!RECORDING_V2_FLAG.get()) {
-    updateAvailableAdbDevices();
-    try {
-      navigator.usb.addEventListener('connect', () =>
-        updateAvailableAdbDevices(),
-      );
-      navigator.usb.addEventListener('disconnect', () =>
-        updateAvailableAdbDevices(),
-      );
-    } catch (e) {
-      console.error('WebUSB API not supported');
-    }
-  }
-
   // Will update the chip on the sidebar footer that notifies that the RPC is
   // connected. Has no effect on the controller (which will repeat this check
   // before creating a new engine).
@@ -323,26 +251,14 @@
   maybeChangeRpcPortFromFragment();
   CheckHttpRpcConnection().then(() => {
     const route = Router.parseUrl(window.location.href);
-    globals.dispatch(
-      Actions.maybeSetPendingDeeplink({
-        ts: route.args.ts,
-        tid: route.args.tid,
-        dur: route.args.dur,
-        pid: route.args.pid,
-        query: route.args.query,
-        visStart: route.args.visStart,
-        visEnd: route.args.visEnd,
-      }),
-    );
-
-    if (!globals.embeddedMode) {
+    if (!AppImpl.instance.embeddedMode) {
       installFileDropHandler();
     }
 
     // Don't allow postMessage or opening trace from route when the user says
     // that they want to reuse the already loaded trace in trace processor.
-    const engine = globals.getCurrentEngine();
-    if (engine && engine.source.type === 'HTTP_RPC') {
+    const traceSource = AppImpl.instance.trace?.traceInfo.source;
+    if (traceSource && traceSource.type === 'HTTP_RPC') {
       return;
     }
 
@@ -355,10 +271,18 @@
   });
 
   // Force one initial render to get everything in place
-  m.render(document.body, m(App, router.resolve()));
+  m.render(
+    document.body,
+    m(UiMain, AppImpl.instance.pages.renderPageForCurrentRoute(undefined)),
+  );
 
-  // Initialize plugins, now that we are ready to go
-  pluginManager.initialize();
+  // Initialize plugins, now that we are ready to go.
+  const pluginManager = AppImpl.instance.plugins;
+  CORE_PLUGINS.forEach((p) => pluginManager.registerPlugin(p));
+  NON_CORE_PLUGINS.forEach((p) => pluginManager.registerPlugin(p));
+  const route = Router.parseUrl(window.location.href);
+  const overrides = (route.args.enablePlugins ?? '').split(',');
+  pluginManager.activatePlugins(overrides);
 }
 
 // If the URL is /#!?rpc_port=1234, change the default RPC port.
@@ -393,34 +317,15 @@
   }
 }
 
-function stateActionDispatcher(actions: DeferredAction[]) {
-  const edits = actions.map((action) => {
-    return traceEvent(
-      `action.${action.type}`,
-      () => {
-        return (draft: Draft<State>) => {
-          // eslint-disable-next-line @typescript-eslint/no-explicit-any
-          (StateActions as any)[action.type](draft, action.args);
-        };
-      },
-      {
-        args: flattenArgs(action.args),
-      },
-    );
-  });
-  globals.store.edit(edits);
-}
-
-function scheduleRafAndRunControllersOnStateChange(
-  store: Store<State>,
-  oldState: State,
-) {
-  // Only redraw if something actually changed
-  if (oldState !== store.state) {
-    raf.scheduleFullRedraw();
-  }
-  // Run in a separate task to avoid avoid reentry.
-  setTimeout(runControllers, 0);
-}
+// TODO(primiano): this injection is to break a cirular dependency. See
+// comment in sql_table_tab_interface.ts. Remove once we add an extension
+// point for context menus.
+configureExtensions({
+  addDebugCounterTrack,
+  addDebugSliceTrack,
+  addVisualizedArgTracks,
+  addSqlTableTab,
+  addQueryResultsTab,
+});
 
 main();
diff --git a/ui/src/frontend/insights_page.ts b/ui/src/frontend/insights_page.ts
deleted file mode 100644
index dfc1bd8..0000000
--- a/ui/src/frontend/insights_page.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {createPage} from './pages';
-
-export const InsightsPage = createPage({
-  view() {
-    return m('.insights-page');
-  },
-});
diff --git a/ui/src/frontend/keyboard_event_handler.ts b/ui/src/frontend/keyboard_event_handler.ts
deleted file mode 100644
index 53d3352..0000000
--- a/ui/src/frontend/keyboard_event_handler.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {Actions} from '../common/actions';
-import {getLegacySelection} from '../common/state';
-
-import {Flow, globals} from './globals';
-import {focusHorizontalRange, verticalScrollToTrack} from './scroll_helper';
-
-type Direction = 'Forward' | 'Backward';
-
-// Search |boundFlows| for |flowId| and return the id following it.
-// Returns the first flow id if nothing was found or |flowId| was the last flow
-// in |boundFlows|, and -1 if |boundFlows| is empty
-function findAnotherFlowExcept(boundFlows: Flow[], flowId: number): number {
-  let selectedFlowFound = false;
-
-  if (boundFlows.length === 0) {
-    return -1;
-  }
-
-  for (const flow of boundFlows) {
-    if (selectedFlowFound) {
-      return flow.id;
-    }
-
-    if (flow.id === flowId) {
-      selectedFlowFound = true;
-    }
-  }
-  return boundFlows[0].id;
-}
-
-// Change focus to the next flow event (matching the direction)
-export function focusOtherFlow(direction: Direction) {
-  const currentSelection = getLegacySelection(globals.state);
-  if (!currentSelection || currentSelection.kind !== 'SLICE') {
-    return;
-  }
-  const sliceId = currentSelection.id;
-  if (sliceId === -1) {
-    return;
-  }
-
-  const boundFlows = globals.connectedFlows.filter(
-    (flow) =>
-      (flow.begin.sliceId === sliceId && direction === 'Forward') ||
-      (flow.end.sliceId === sliceId && direction === 'Backward'),
-  );
-
-  if (direction === 'Backward') {
-    const nextFlowId = findAnotherFlowExcept(
-      boundFlows,
-      globals.state.focusedFlowIdLeft,
-    );
-    globals.dispatch(Actions.setHighlightedFlowLeftId({flowId: nextFlowId}));
-  } else {
-    const nextFlowId = findAnotherFlowExcept(
-      boundFlows,
-      globals.state.focusedFlowIdRight,
-    );
-    globals.dispatch(Actions.setHighlightedFlowRightId({flowId: nextFlowId}));
-  }
-}
-
-// Select the slice connected to the flow in focus
-export function moveByFocusedFlow(direction: Direction): void {
-  const currentSelection = getLegacySelection(globals.state);
-  if (!currentSelection || currentSelection.kind !== 'SLICE') {
-    return;
-  }
-
-  const sliceId = currentSelection.id;
-  const flowId =
-    direction === 'Backward'
-      ? globals.state.focusedFlowIdLeft
-      : globals.state.focusedFlowIdRight;
-
-  if (sliceId === -1 || flowId === -1) {
-    return;
-  }
-
-  // Find flow that is in focus and select corresponding slice
-  for (const flow of globals.connectedFlows) {
-    if (flow.id === flowId) {
-      const flowPoint = direction === 'Backward' ? flow.begin : flow.end;
-      const trackKeyByTrackId = globals.trackManager.trackKeyByTrackId;
-      const trackKey = trackKeyByTrackId.get(flowPoint.trackId);
-      if (trackKey) {
-        globals.setLegacySelection(
-          {
-            kind: 'SLICE',
-            id: flowPoint.sliceId,
-            trackKey,
-            table: 'slice',
-          },
-          {
-            clearSearch: true,
-            pendingScrollId: flowPoint.sliceId,
-            switchToCurrentSelectionTab: true,
-          },
-        );
-      }
-    }
-  }
-}
-
-export async function findCurrentSelection() {
-  const selection = getLegacySelection(globals.state);
-  if (selection === null) return;
-
-  const range = await globals.findTimeRangeOfSelection();
-  if (exists(range)) {
-    focusHorizontalRange(range.start, range.end);
-  }
-
-  if (selection.trackKey) {
-    verticalScrollToTrack(selection.trackKey, true);
-  }
-}
diff --git a/ui/src/frontend/legacy_flamegraph.ts b/ui/src/frontend/legacy_flamegraph.ts
deleted file mode 100644
index caa5e3e..0000000
--- a/ui/src/frontend/legacy_flamegraph.ts
+++ /dev/null
@@ -1,489 +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 {CallsiteInfo} from '../common/legacy_flamegraph_util';
-import {searchSegment} from '../base/binary_search';
-import {cropText} from '../base/string_utils';
-
-interface Node {
-  width: number;
-  x: number;
-  nextXForChildren: number;
-  size: number;
-}
-
-interface CallsiteInfoWidth {
-  callsite: CallsiteInfo;
-  width: number;
-}
-
-// Height of one 'row' on the flame chart including 1px of whitespace
-// below the box.
-const NODE_HEIGHT = 18;
-
-export const FLAMEGRAPH_HOVERED_COLOR = 'hsl(224, 45%, 55%)';
-
-export function findRootSize(data: ReadonlyArray<CallsiteInfo>) {
-  let totalSize = 0;
-  let i = 0;
-  while (i < data.length && data[i].depth === 0) {
-    totalSize += data[i].totalSize;
-    i++;
-  }
-  return totalSize;
-}
-
-export interface NodeRendering {
-  totalSize?: string;
-  selfSize?: string;
-}
-
-export class Flamegraph {
-  private nodeRendering: NodeRendering = {};
-  private flamegraphData: ReadonlyArray<CallsiteInfo>;
-  private highlightSomeNodes = false;
-  private maxDepth = -1;
-  private totalSize = -1;
-  // Initialised on first draw() call
-  private labelCharWidth = 0;
-  private labelFontStyle = '12px Roboto Mono';
-  private rolloverFontStyle = '12px Roboto Condensed';
-  // Key for the map is depth followed by x coordinate - `depth;x`
-  private graphData: Map<string, CallsiteInfoWidth> = new Map();
-  private xStartsPerDepth: Map<number, number[]> = new Map();
-
-  private hoveredX = -1;
-  private hoveredY = -1;
-  private hoveredCallsite?: CallsiteInfo;
-  private clickedCallsite?: CallsiteInfo;
-
-  private startingY = 0;
-
-  constructor(flamegraphData: CallsiteInfo[]) {
-    this.flamegraphData = flamegraphData;
-    this.findMaxDepth();
-  }
-
-  private findMaxDepth() {
-    this.maxDepth = Math.max(
-      ...this.flamegraphData.map((value) => value.depth),
-    );
-  }
-
-  // Instead of highlighting the interesting nodes, we actually want to
-  // de-emphasize the non-highlighted nodes. Returns true if there
-  // are any highlighted nodes in the flamegraph.
-  private highlightingExists() {
-    this.highlightSomeNodes = this.flamegraphData.some((e) => e.highlighted);
-  }
-
-  generateColor(
-    name: string,
-    isGreyedOut = false,
-    highlighted: boolean,
-  ): string {
-    if (isGreyedOut) {
-      return '#d9d9d9';
-    }
-    if (name === 'unknown' || name === 'root') {
-      return '#c0c0c0';
-    }
-    let x = 0;
-    for (let i = 0; i < name.length; i += 1) {
-      x += name.charCodeAt(i) % 64;
-    }
-    x = x % 360;
-    let l = '76';
-    // Make non-highlighted node lighter.
-    if (this.highlightSomeNodes && !highlighted) {
-      l = '90';
-    }
-    return `hsl(${x}deg, 45%, ${l}%)`;
-  }
-
-  // Caller will have to call draw method after updating data to have updated
-  // graph.
-  updateDataIfChanged(
-    nodeRendering: NodeRendering,
-    flamegraphData: ReadonlyArray<CallsiteInfo>,
-    clickedCallsite?: CallsiteInfo,
-  ) {
-    this.nodeRendering = nodeRendering;
-    this.clickedCallsite = clickedCallsite;
-    if (this.flamegraphData === flamegraphData) {
-      return;
-    }
-    this.flamegraphData = flamegraphData;
-    this.clickedCallsite = clickedCallsite;
-    this.findMaxDepth();
-    this.highlightingExists();
-    // Finding total size of roots.
-    this.totalSize = findRootSize(flamegraphData);
-  }
-
-  draw(
-    ctx: CanvasRenderingContext2D,
-    width: number,
-    height: number,
-    x = 0,
-    y = 0,
-    unit = 'B',
-  ) {
-    if (this.flamegraphData === undefined) {
-      return;
-    }
-
-    ctx.font = this.labelFontStyle;
-    ctx.textBaseline = 'middle';
-    if (this.labelCharWidth === 0) {
-      this.labelCharWidth = ctx.measureText('_').width;
-    }
-
-    this.startingY = y;
-
-    // For each node, we use this map to get information about its parent
-    // (total size of it, width and where it starts in graph) so we can
-    // calculate it's own position in graph.
-    const nodesMap = new Map<number, Node>();
-    let currentY = y;
-    nodesMap.set(-1, {width, nextXForChildren: x, size: this.totalSize, x});
-
-    // Initialize data needed for click/hover behavior.
-    this.graphData = new Map();
-    this.xStartsPerDepth = new Map();
-
-    // Draw root node.
-    ctx.fillStyle = this.generateColor('root', false, false);
-    ctx.fillRect(x, currentY, width, NODE_HEIGHT - 1);
-    const text = cropText(
-      `root: ${this.displaySize(
-        this.totalSize,
-        unit,
-        unit === 'B' ? 1024 : 1000,
-      )}`,
-      this.labelCharWidth,
-      width - 2,
-    );
-    ctx.fillStyle = 'black';
-    ctx.fillText(text, x + 5, currentY + (NODE_HEIGHT - 1) / 2);
-    currentY += NODE_HEIGHT;
-
-    // Set style for borders.
-    ctx.strokeStyle = 'white';
-    ctx.lineWidth = 0.5;
-
-    for (let i = 0; i < this.flamegraphData.length; i++) {
-      if (currentY > height) {
-        break;
-      }
-      const value = this.flamegraphData[i];
-      const parentNode = nodesMap.get(value.parentId);
-      if (parentNode === undefined) {
-        continue;
-      }
-
-      const isClicked = this.clickedCallsite !== undefined;
-      const isFullWidth =
-        isClicked && value.depth <= this.clickedCallsite!.depth;
-      const isGreyedOut =
-        isClicked && value.depth < this.clickedCallsite!.depth;
-
-      const parent = value.parentId;
-      const parentSize = parent === -1 ? this.totalSize : parentNode.size;
-      // Calculate node's width based on its proportion in parent.
-      const width =
-        (isFullWidth ? 1 : value.totalSize / parentSize) * parentNode.width;
-
-      const currentX = parentNode.nextXForChildren;
-      currentY = y + NODE_HEIGHT * (value.depth + 1);
-
-      // Draw node.
-      const name = this.getCallsiteName(value);
-      ctx.fillStyle = this.generateColor(name, isGreyedOut, value.highlighted);
-      ctx.fillRect(currentX, currentY, width, NODE_HEIGHT - 1);
-
-      // Set current node's data in map for children to use.
-      nodesMap.set(value.id, {
-        width,
-        nextXForChildren: currentX,
-        size: value.totalSize,
-        x: currentX,
-      });
-      // Update next x coordinate in parent.
-      nodesMap.set(value.parentId, {
-        width: parentNode.width,
-        nextXForChildren: currentX + width,
-        size: parentNode.size,
-        x: parentNode.x,
-      });
-
-      // Draw name.
-      const labelPaddingPx = 5;
-      const maxLabelWidth = width - labelPaddingPx * 2;
-      let text = cropText(name, this.labelCharWidth, maxLabelWidth);
-      // If cropped text and the original text are within 20% we keep the
-      // original text and just squish it a bit.
-      if (text.length * 1.2 > name.length) {
-        text = name;
-      }
-      ctx.fillStyle = 'black';
-      ctx.fillText(
-        text,
-        currentX + labelPaddingPx,
-        currentY + (NODE_HEIGHT - 1) / 2,
-        maxLabelWidth,
-      );
-
-      // Draw border on the right of node.
-      ctx.beginPath();
-      ctx.moveTo(currentX + width, currentY);
-      ctx.lineTo(currentX + width, currentY + NODE_HEIGHT);
-      ctx.stroke();
-
-      // Add this node for recognizing in click/hover.
-      // Map graphData contains one callsite which is on that depth and X
-      // start. Map xStartsPerDepth for each depth contains all X start
-      // coordinates that callsites on that level have.
-      this.graphData.set(`${value.depth};${currentX}`, {
-        callsite: value,
-        width,
-      });
-      const xStarts = this.xStartsPerDepth.get(value.depth);
-      if (xStarts === undefined) {
-        this.xStartsPerDepth.set(value.depth, [currentX]);
-      } else {
-        xStarts.push(currentX);
-      }
-    }
-
-    // Draw the tooltip.
-    if (this.hoveredX > -1 && this.hoveredY > -1 && this.hoveredCallsite) {
-      // Must set these before measureText below.
-      ctx.font = this.rolloverFontStyle;
-      ctx.textBaseline = 'top';
-
-      // Size in px of the border around the text and the edge of the rollover
-      // background.
-      const paddingPx = 8;
-      // Size in px of the x and y offset between the mouse and the top left
-      // corner of the rollover box.
-      const offsetPx = 4;
-
-      const lines: string[] = [];
-
-      let textWidth = this.addToTooltip(
-        this.getCallsiteName(this.hoveredCallsite),
-        width - paddingPx,
-        ctx,
-        lines,
-      );
-      if (this.hoveredCallsite.location != null) {
-        textWidth = Math.max(
-          textWidth,
-          this.addToTooltip(this.hoveredCallsite.location, width, ctx, lines),
-        );
-      }
-      textWidth = Math.max(
-        textWidth,
-        this.addToTooltip(this.hoveredCallsite.mapping, width, ctx, lines),
-      );
-
-      if (this.nodeRendering.totalSize !== undefined) {
-        const percentage =
-          (this.hoveredCallsite.totalSize / this.totalSize) * 100;
-        const totalSizeText = `${
-          this.nodeRendering.totalSize
-        }: ${this.displaySize(
-          this.hoveredCallsite.totalSize,
-          unit,
-          unit === 'B' ? 1024 : 1000,
-        )} (${percentage.toFixed(2)}%)`;
-        textWidth = Math.max(
-          textWidth,
-          this.addToTooltip(totalSizeText, width, ctx, lines),
-        );
-      }
-
-      if (
-        this.nodeRendering.selfSize !== undefined &&
-        this.hoveredCallsite.selfSize > 0
-      ) {
-        const selfPercentage =
-          (this.hoveredCallsite.selfSize / this.totalSize) * 100;
-        const selfSizeText = `${
-          this.nodeRendering.selfSize
-        }: ${this.displaySize(
-          this.hoveredCallsite.selfSize,
-          unit,
-          unit === 'B' ? 1024 : 1000,
-        )} (${selfPercentage.toFixed(2)}%)`;
-        textWidth = Math.max(
-          textWidth,
-          this.addToTooltip(selfSizeText, width, ctx, lines),
-        );
-      }
-
-      // Compute a line height as the bounding box height + 50%:
-      const heightSample = ctx.measureText(
-        'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
-      );
-      const lineHeight = Math.round(
-        heightSample.actualBoundingBoxDescent * 1.5,
-      );
-
-      const rectWidth = textWidth + 2 * paddingPx;
-      const rectHeight = lineHeight * lines.length + 2 * paddingPx;
-
-      let rectXStart = this.hoveredX + offsetPx;
-      let rectYStart = this.hoveredY + offsetPx;
-
-      if (rectXStart + rectWidth > width) {
-        rectXStart = width - rectWidth;
-      }
-
-      if (rectYStart + rectHeight > height) {
-        rectYStart = height - rectHeight;
-      }
-
-      ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
-      ctx.fillRect(rectXStart, rectYStart, rectWidth, rectHeight);
-      ctx.fillStyle = 'hsl(200, 50%, 40%)';
-      ctx.textAlign = 'left';
-      for (let i = 0; i < lines.length; i++) {
-        const line = lines[i];
-        ctx.fillText(
-          line,
-          rectXStart + paddingPx,
-          rectYStart + paddingPx + i * lineHeight,
-        );
-      }
-    }
-  }
-
-  private addToTooltip(
-    text: string,
-    width: number,
-    ctx: CanvasRenderingContext2D,
-    lines: string[],
-  ): number {
-    const lineSplitter: LineSplitter = splitIfTooBig(
-      text,
-      width,
-      ctx.measureText(text).width,
-    );
-    lines.push(...lineSplitter.lines);
-    return lineSplitter.lineWidth;
-  }
-
-  private getCallsiteName(value: CallsiteInfo): string {
-    return value.name === undefined || value.name === ''
-      ? 'unknown'
-      : value.name;
-  }
-
-  private displaySize(totalSize: number, unit: string, step = 1024): string {
-    if (unit === '') return totalSize.toLocaleString();
-    if (totalSize === 0) return `0 ${unit}`;
-    const units = [
-      ['', 1],
-      ['K', step],
-      ['M', Math.pow(step, 2)],
-      ['G', Math.pow(step, 3)],
-    ];
-    let unitsIndex = Math.trunc(Math.log(totalSize) / Math.log(step));
-    unitsIndex = unitsIndex > units.length - 1 ? units.length - 1 : unitsIndex;
-    const result = totalSize / +units[unitsIndex][1];
-    const resultString =
-      totalSize % +units[unitsIndex][1] === 0
-        ? result.toString()
-        : result.toFixed(2);
-    return `${resultString} ${units[unitsIndex][0]}${unit}`;
-  }
-
-  onMouseMove({x, y}: {x: number; y: number}) {
-    this.hoveredX = x;
-    this.hoveredY = y;
-    this.hoveredCallsite = this.findSelectedCallsite(x, y);
-    const isCallsiteSelected = this.hoveredCallsite !== undefined;
-    if (!isCallsiteSelected) {
-      this.onMouseOut();
-    }
-    return isCallsiteSelected;
-  }
-
-  onMouseOut() {
-    this.hoveredX = -1;
-    this.hoveredY = -1;
-    this.hoveredCallsite = undefined;
-  }
-
-  onMouseClick({x, y}: {x: number; y: number}): CallsiteInfo | undefined {
-    const clickedCallsite = this.findSelectedCallsite(x, y);
-    // TODO(b/148596659): Allow to expand [merged] callsites. Currently,
-    // this expands to the biggest of the nodes that were merged, which
-    // is confusing, so we disallow clicking on them.
-    if (clickedCallsite === undefined || clickedCallsite.merged) {
-      return undefined;
-    }
-    return clickedCallsite;
-  }
-
-  private findSelectedCallsite(x: number, y: number): CallsiteInfo | undefined {
-    const depth = Math.trunc((y - this.startingY) / NODE_HEIGHT) - 1; // at 0 is root
-    if (depth >= 0 && this.xStartsPerDepth.has(depth)) {
-      const startX = this.searchSmallest(this.xStartsPerDepth.get(depth)!, x);
-      const result = this.graphData.get(`${depth};${startX}`);
-      if (result !== undefined) {
-        const width = result.width;
-        return startX + width >= x ? result.callsite : undefined;
-      }
-    }
-    return undefined;
-  }
-
-  searchSmallest(haystack: number[], needle: number): number {
-    haystack = haystack.sort((n1, n2) => n1 - n2);
-    const [left] = searchSegment(haystack, needle);
-    return left === -1 ? -1 : haystack[left];
-  }
-
-  getHeight(): number {
-    return this.flamegraphData.length === 0
-      ? 0
-      : (this.maxDepth + 2) * NODE_HEIGHT;
-  }
-}
-
-export interface LineSplitter {
-  lineWidth: number;
-  lines: string[];
-}
-
-export function splitIfTooBig(
-  line: string,
-  width: number,
-  lineWidth: number,
-): LineSplitter {
-  if (line === '') return {lineWidth, lines: []};
-  const lines: string[] = [];
-  const charWidth = lineWidth / line.length;
-  const maxWidth = width - 32;
-  const maxLineLen = Math.trunc(maxWidth / charWidth);
-  while (line.length > 0) {
-    lines.push(line.slice(0, maxLineLen));
-    line = line.slice(maxLineLen);
-  }
-  lineWidth = Math.min(maxLineLen * charWidth, lineWidth);
-  return {lineWidth, lines};
-}
diff --git a/ui/src/frontend/legacy_flamegraph_panel.ts b/ui/src/frontend/legacy_flamegraph_panel.ts
deleted file mode 100644
index 9a476b8..0000000
--- a/ui/src/frontend/legacy_flamegraph_panel.ts
+++ /dev/null
@@ -1,902 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size 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, {Vnode} from 'mithril';
-
-import {findRef} from '../base/dom_utils';
-import {assertExists, assertTrue} from '../base/logging';
-import {time} from '../base/time';
-import {Actions} from '../common/actions';
-import {
-  CallsiteInfo,
-  FlamegraphViewingOption,
-  defaultViewingOption,
-  expandCallsites,
-  findRootSize,
-  mergeCallsites,
-  viewingOptions,
-} from '../common/legacy_flamegraph_util';
-import {ProfileType} from '../common/state';
-import {raf} from '../core/raf_scheduler';
-import {Button} from '../widgets/button';
-import {Icon} from '../widgets/icon';
-import {Modal, ModalAttrs} from '../widgets/modal';
-import {Popup} from '../widgets/popup';
-import {EmptyState} from '../widgets/empty_state';
-import {Spinner} from '../widgets/spinner';
-
-import {Flamegraph, NodeRendering} from './legacy_flamegraph';
-import {globals} from './globals';
-import {debounce} from './rate_limiters';
-import {Router} from './router';
-import {ButtonBar} from '../widgets/button';
-import {DurationWidget} from './widgets/duration';
-import {DetailsShell} from '../widgets/details_shell';
-import {Intent} from '../widgets/common';
-import {Engine, NUM, STR} from '../public';
-import {Monitor} from '../base/monitor';
-import {arrayEquals} from '../base/array_utils';
-import {getCurrentTrace} from './sidebar';
-import {convertTraceToPprofAndDownload} from './trace_converter';
-import {AsyncLimiter} from '../base/async_limiter';
-import {LegacyFlamegraphCache} from '../core/legacy_flamegraph_cache';
-
-const HEADER_HEIGHT = 30;
-
-export function profileType(s: string): ProfileType {
-  if (isProfileType(s)) {
-    return s;
-  }
-  if (s.startsWith('heap_profile')) {
-    return ProfileType.HEAP_PROFILE;
-  }
-  throw new Error('Unknown type ${s}');
-}
-
-function isProfileType(s: string): s is ProfileType {
-  return Object.values(ProfileType).includes(s as ProfileType);
-}
-
-function getFlamegraphType(type: ProfileType) {
-  switch (type) {
-    case ProfileType.HEAP_PROFILE:
-    case ProfileType.MIXED_HEAP_PROFILE:
-    case ProfileType.NATIVE_HEAP_PROFILE:
-    case ProfileType.JAVA_HEAP_SAMPLES:
-      return 'native';
-    case ProfileType.JAVA_HEAP_GRAPH:
-      return 'graph';
-    case ProfileType.PERF_SAMPLE:
-      return 'perf';
-    default:
-      const exhaustiveCheck: never = type;
-      throw new Error(`Unhandled case: ${exhaustiveCheck}`);
-  }
-}
-
-const HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS = [
-  FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY,
-  FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY,
-] as const;
-
-export type HeapGraphDominatorTreeViewingOption =
-  (typeof HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS)[number];
-
-export function isHeapGraphDominatorTreeViewingOption(
-  option: FlamegraphViewingOption,
-): option is HeapGraphDominatorTreeViewingOption {
-  return (
-    HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS as readonly FlamegraphViewingOption[]
-  ).includes(option);
-}
-
-const MIN_PIXEL_DISPLAYED = 1;
-
-function toSelectedCallsite(c: CallsiteInfo | undefined): string {
-  if (c !== undefined && c.name !== undefined) {
-    return c.name;
-  }
-  return '(none)';
-}
-
-const RENDER_SELF_AND_TOTAL: NodeRendering = {
-  selfSize: 'Self',
-  totalSize: 'Total',
-};
-const RENDER_OBJ_COUNT: NodeRendering = {
-  selfSize: 'Self objects',
-  totalSize: 'Subtree objects',
-};
-
-export interface FlamegraphSelectionParams {
-  readonly profileType: ProfileType;
-  readonly upids: number[];
-  readonly start: time;
-  readonly end: time;
-}
-
-interface FlamegraphDetailsPanelAttrs {
-  cache: LegacyFlamegraphCache;
-  selection: FlamegraphSelectionParams;
-}
-
-interface FlamegraphResult {
-  queryResults: ReadonlyArray<CallsiteInfo>;
-  incomplete: boolean;
-  renderResults?: ReadonlyArray<CallsiteInfo>;
-}
-
-interface FlamegraphState {
-  selection: FlamegraphSelectionParams;
-  viewingOption: FlamegraphViewingOption;
-  focusRegex: string;
-  result?: FlamegraphResult;
-  selectedCallsites: Readonly<{
-    [key: string]: CallsiteInfo | undefined;
-  }>;
-}
-
-export class LegacyFlamegraphDetailsPanel
-  implements m.ClassComponent<FlamegraphDetailsPanelAttrs>
-{
-  private undebouncedFocusRegex = '';
-  private updateFocusRegexDebounced = debounce(() => {
-    if (this.state === undefined) {
-      return;
-    }
-    this.state.focusRegex = this.undebouncedFocusRegex;
-    raf.scheduleFullRedraw();
-  }, 20);
-
-  private flamegraph: Flamegraph = new Flamegraph([]);
-  private queryLimiter = new AsyncLimiter();
-
-  private state?: FlamegraphState;
-  private queryMonitor = new Monitor([
-    () => this.state?.selection,
-    () => this.state?.focusRegex,
-    () => this.state?.viewingOption,
-  ]);
-  private selectedCallsitesMonitor = new Monitor([
-    () => this.state?.selection,
-    () => this.state?.focusRegex,
-  ]);
-  private renderResultMonitor = new Monitor([
-    () => this.state?.result?.queryResults,
-    () => this.state?.selectedCallsites,
-  ]);
-
-  view({attrs}: Vnode<FlamegraphDetailsPanelAttrs>) {
-    if (attrs.selection === undefined) {
-      this.state = undefined;
-    } else if (
-      attrs.selection.profileType !== this.state?.selection.profileType ||
-      attrs.selection.start !== this.state.selection.start ||
-      attrs.selection.end !== this.state.selection.end ||
-      !arrayEquals(attrs.selection.upids, this.state.selection.upids)
-    ) {
-      this.state = {
-        selection: attrs.selection,
-        focusRegex: '',
-        viewingOption: defaultViewingOption(attrs.selection.profileType),
-        selectedCallsites: {},
-      };
-    }
-    if (this.state === undefined) {
-      return m(
-        '.details-panel',
-        m('.details-panel-heading', m('h2', `Flamegraph Profile`)),
-      );
-    }
-
-    if (this.queryMonitor.ifStateChanged()) {
-      this.state.result = undefined;
-      const state = this.state;
-      this.queryLimiter.schedule(() => {
-        return LegacyFlamegraphDetailsPanel.fetchQueryResults(
-          assertExists(this.getCurrentEngine()),
-          attrs.cache,
-          state,
-        );
-      });
-    }
-
-    if (this.selectedCallsitesMonitor.ifStateChanged()) {
-      this.state.selectedCallsites = {};
-    }
-
-    if (
-      this.renderResultMonitor.ifStateChanged() &&
-      this.state.result !== undefined
-    ) {
-      const selected = this.state.selectedCallsites[this.state.viewingOption];
-      const expanded = expandCallsites(
-        this.state.result.queryResults,
-        selected?.id ?? -1,
-      );
-      this.state.result.renderResults = mergeCallsites(
-        expanded,
-        LegacyFlamegraphDetailsPanel.getMinSizeDisplayed(
-          expanded,
-          selected?.totalSize,
-        ),
-      );
-    }
-
-    let height: number | undefined;
-    if (this.state.result?.renderResults !== undefined) {
-      this.flamegraph.updateDataIfChanged(
-        this.nodeRendering(),
-        this.state.result.renderResults,
-        this.state.selectedCallsites[this.state.viewingOption],
-      );
-      height = this.flamegraph.getHeight() + HEADER_HEIGHT;
-    } else {
-      height = undefined;
-    }
-
-    return m(
-      '.flamegraph-profile',
-      this.maybeShowModal(),
-      m(
-        DetailsShell,
-        {
-          fillParent: true,
-          title: m(
-            'div.title',
-            this.getTitle(),
-            this.state.selection.profileType ===
-              ProfileType.MIXED_HEAP_PROFILE &&
-              m(
-                Popup,
-                {
-                  trigger: m(Icon, {icon: 'warning'}),
-                },
-                m(
-                  '',
-                  {style: {width: '300px'}},
-                  'This is a mixed java/native heap profile, free()s are not visualized. To visualize free()s, remove "all_heaps: true" from the config.',
-                ),
-              ),
-            ':',
-          ),
-          description: this.getViewingOptionButtons(),
-          buttons: [
-            m(
-              'div.selected',
-              `Selected function: ${toSelectedCallsite(
-                this.state.selectedCallsites[this.state.viewingOption],
-              )}`,
-            ),
-            m(
-              'div.time',
-              `Snapshot time: `,
-              m(DurationWidget, {
-                dur: this.state.selection.end - this.state.selection.start,
-              }),
-            ),
-            m('input[type=text][placeholder=Focus]', {
-              oninput: (e: Event) => {
-                const target = e.target as HTMLInputElement;
-                this.undebouncedFocusRegex = target.value;
-                this.updateFocusRegexDebounced();
-              },
-              // Required to stop hot-key handling:
-              onkeydown: (e: Event) => e.stopPropagation(),
-            }),
-            (this.state.selection.profileType ===
-              ProfileType.NATIVE_HEAP_PROFILE ||
-              this.state.selection.profileType ===
-                ProfileType.JAVA_HEAP_SAMPLES) &&
-              m(Button, {
-                icon: 'file_download',
-                intent: Intent.Primary,
-                onclick: () => {
-                  this.downloadPprof();
-                  raf.scheduleFullRedraw();
-                },
-              }),
-          ],
-        },
-        m(
-          '.flamegraph-content',
-          this.state.result === undefined
-            ? m(
-                '.loading-container',
-                m(
-                  EmptyState,
-                  {
-                    icon: 'bar_chart',
-                    title: 'Computing graph ...',
-                    className: 'flamegraph-loading',
-                  },
-                  m(Spinner, {easing: true}),
-                ),
-              )
-            : m(`canvas[ref=canvas]`, {
-                style: `height:${height}px; width:100%`,
-                onmousemove: (e: MouseEvent) => {
-                  const {offsetX, offsetY} = e;
-                  this.flamegraph.onMouseMove({x: offsetX, y: offsetY});
-                  raf.scheduleFullRedraw();
-                },
-                onmouseout: () => {
-                  this.flamegraph.onMouseOut();
-                  raf.scheduleFullRedraw();
-                },
-                onclick: (e: MouseEvent) => {
-                  if (
-                    this.state === undefined ||
-                    this.state.result === undefined
-                  ) {
-                    return;
-                  }
-                  const {offsetX, offsetY} = e;
-                  const cs = {...this.state.selectedCallsites};
-                  cs[this.state.viewingOption] = this.flamegraph.onMouseClick({
-                    x: offsetX,
-                    y: offsetY,
-                  });
-                  this.state.selectedCallsites = cs;
-                  raf.scheduleFullRedraw();
-                },
-              }),
-        ),
-      ),
-    );
-  }
-
-  private getTitle(): string {
-    const state = assertExists(this.state);
-    switch (state.selection.profileType) {
-      case ProfileType.MIXED_HEAP_PROFILE:
-        return 'Mixed heap profile';
-      case ProfileType.HEAP_PROFILE:
-        return 'Heap profile';
-      case ProfileType.NATIVE_HEAP_PROFILE:
-        return 'Native heap profile';
-      case ProfileType.JAVA_HEAP_SAMPLES:
-        return 'Java heap samples';
-      case ProfileType.JAVA_HEAP_GRAPH:
-        return 'Java heap graph';
-      case ProfileType.PERF_SAMPLE:
-        return 'Profile';
-      default:
-        throw new Error('unknown type');
-    }
-  }
-
-  private nodeRendering(): NodeRendering {
-    const state = assertExists(this.state);
-    const profileType = state.selection.profileType;
-    switch (profileType) {
-      case ProfileType.JAVA_HEAP_GRAPH:
-        if (
-          state.viewingOption ===
-            FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY ||
-          state.viewingOption ===
-            FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY
-        ) {
-          return RENDER_OBJ_COUNT;
-        } else {
-          return RENDER_SELF_AND_TOTAL;
-        }
-      case ProfileType.MIXED_HEAP_PROFILE:
-      case ProfileType.HEAP_PROFILE:
-      case ProfileType.NATIVE_HEAP_PROFILE:
-      case ProfileType.JAVA_HEAP_SAMPLES:
-      case ProfileType.PERF_SAMPLE:
-        return RENDER_SELF_AND_TOTAL;
-      default:
-        const exhaustiveCheck: never = profileType;
-        throw new Error(`Unhandled case: ${exhaustiveCheck}`);
-    }
-  }
-
-  private getViewingOptionButtons(): m.Children {
-    const ret = [];
-    const state = assertExists(this.state);
-    for (const {option, name} of viewingOptions(state.selection.profileType)) {
-      ret.push(
-        m(Button, {
-          label: name,
-          active: option === state.viewingOption,
-          onclick: () => {
-            const state = assertExists(this.state);
-            state.viewingOption = option;
-            raf.scheduleFullRedraw();
-          },
-        }),
-      );
-    }
-    return m(ButtonBar, ret);
-  }
-
-  onupdate({dom}: m.VnodeDOM<FlamegraphDetailsPanelAttrs>) {
-    const canvas = findRef(dom, 'canvas');
-    if (canvas === null || !(canvas instanceof HTMLCanvasElement)) {
-      return;
-    }
-    if (!this.state?.result?.renderResults) {
-      return;
-    }
-    canvas.width = canvas.offsetWidth * devicePixelRatio;
-    canvas.height = canvas.offsetHeight * devicePixelRatio;
-
-    const ctx = canvas.getContext('2d');
-    if (ctx === null) {
-      return;
-    }
-
-    ctx.clearRect(0, 0, canvas.width, canvas.height);
-    ctx.save();
-    ctx.scale(devicePixelRatio, devicePixelRatio);
-    const {offsetWidth: width, offsetHeight: height} = canvas;
-    const unit =
-      this.state.viewingOption ===
-        FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
-      this.state.viewingOption ===
-        FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY ||
-      this.state.viewingOption ===
-        FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY
-        ? 'B'
-        : '';
-    this.flamegraph.draw(ctx, width, height, 0, 0, unit);
-    ctx.restore();
-  }
-
-  private static async fetchQueryResults(
-    engine: Engine,
-    cache: LegacyFlamegraphCache,
-    state: FlamegraphState,
-  ) {
-    const table = await LegacyFlamegraphDetailsPanel.prepareViewsAndTables(
-      engine,
-      cache,
-      state,
-    );
-    const queryResults =
-      await LegacyFlamegraphDetailsPanel.getFlamegraphDataFromTables(
-        engine,
-        table,
-        state.viewingOption,
-        state.focusRegex,
-      );
-
-    let incomplete = false;
-    if (state.selection.profileType === ProfileType.JAVA_HEAP_GRAPH) {
-      const it = await engine.query(`
-        select value from stats
-        where severity = 'error' and name = 'heap_graph_non_finalized_graph'
-      `);
-      incomplete = it.firstRow({value: NUM}).value > 0;
-    }
-    state.result = {
-      queryResults,
-      incomplete,
-    };
-    raf.scheduleFullRedraw();
-  }
-
-  private static async prepareViewsAndTables(
-    engine: Engine,
-    cache: LegacyFlamegraphCache,
-    state: FlamegraphState,
-  ): Promise<string> {
-    const flamegraphType = getFlamegraphType(state.selection.profileType);
-    if (state.selection.profileType === ProfileType.PERF_SAMPLE) {
-      let upid: string;
-      let upidGroup: string;
-      if (state.selection.upids.length > 1) {
-        upid = `NULL`;
-        upidGroup = `'${this.serializeUpidGroup(state.selection.upids)}'`;
-      } else {
-        upid = `${state.selection.upids[0]}`;
-        upidGroup = `NULL`;
-      }
-      return cache.getTableName(
-        engine,
-        `
-          select
-            id,
-            name,
-            map_name,
-            parent_id,
-            depth,
-            cumulative_size,
-            cumulative_alloc_size,
-            cumulative_count,
-            cumulative_alloc_count,
-            size,
-            alloc_size,
-            count,
-            alloc_count,
-            source_file,
-            line_number
-          from experimental_flamegraph(
-            '${flamegraphType}',
-            NULL,
-            '>=${state.selection.start},<=${state.selection.end}',
-            ${upid},
-            ${upidGroup},
-            '${state.focusRegex}'
-          )
-        `,
-      );
-    }
-    if (
-      state.selection.profileType === ProfileType.JAVA_HEAP_GRAPH &&
-      isHeapGraphDominatorTreeViewingOption(state.viewingOption)
-    ) {
-      assertTrue(state.selection.start == state.selection.end);
-      return cache.getTableName(
-        engine,
-        await this.loadHeapGraphDominatorTreeQuery(
-          engine,
-          cache,
-          state.selection.upids[0],
-          state.selection.start,
-        ),
-      );
-    }
-    assertTrue(state.selection.start == state.selection.end);
-    return cache.getTableName(
-      engine,
-      `
-        select
-          id,
-          name,
-          map_name,
-          parent_id,
-          depth,
-          cumulative_size,
-          cumulative_alloc_size,
-          cumulative_count,
-          cumulative_alloc_count,
-          size,
-          alloc_size,
-          count,
-          alloc_count,
-          source_file,
-          line_number
-        from experimental_flamegraph(
-          '${flamegraphType}',
-          ${state.selection.start},
-          NULL,
-          ${state.selection.upids[0]},
-          NULL,
-          '${state.focusRegex}'
-        )
-      `,
-    );
-  }
-
-  private static async loadHeapGraphDominatorTreeQuery(
-    engine: Engine,
-    cache: LegacyFlamegraphCache,
-    upid: number,
-    timestamp: time,
-  ) {
-    const outputTableName = `heap_graph_type_dominated_${upid}_${timestamp}`;
-    const outputQuery = `SELECT * FROM ${outputTableName}`;
-    if (cache.hasQuery(outputQuery)) {
-      return outputQuery;
-    }
-
-    await engine.query(`
-      INCLUDE PERFETTO MODULE android.memory.heap_graph.dominator_tree;
-
-      -- heap graph dominator tree with objects as nodes and all relavant
-      -- object self stats and dominated stats
-      CREATE PERFETTO TABLE _heap_graph_object_dominated AS
-      SELECT
-      node.id,
-      node.idom_id,
-      node.dominated_obj_count,
-      node.dominated_size_bytes + node.dominated_native_size_bytes AS dominated_size,
-      node.depth,
-      obj.type_id,
-      obj.root_type,
-      obj.self_size + obj.native_size AS self_size
-      FROM heap_graph_dominator_tree node
-      JOIN heap_graph_object obj USING(id)
-      WHERE obj.upid = ${upid} AND obj.graph_sample_ts = ${timestamp}
-      -- required to accelerate the recursive cte below
-      ORDER BY idom_id;
-
-      -- calculate for each object node in the dominator tree the
-      -- HASH(path of type_id's from the super root to the object)
-      CREATE PERFETTO TABLE _dominator_tree_path_hash AS
-      WITH RECURSIVE _tree_visitor(id, path_hash) AS (
-        SELECT
-          id,
-          HASH(
-            CAST(type_id AS TEXT) || '-' || IFNULL(root_type, '')
-          ) AS path_hash
-        FROM _heap_graph_object_dominated
-        WHERE depth = 1
-        UNION ALL
-        SELECT
-          child.id,
-          HASH(CAST(parent.path_hash AS TEXT) || '/' || CAST(type_id AS TEXT)) AS path_hash
-        FROM _heap_graph_object_dominated child
-        JOIN _tree_visitor parent ON child.idom_id = parent.id
-      )
-      SELECT * from _tree_visitor
-      ORDER BY id;
-
-      -- merge object nodes with the same path into one "class type node", so the
-      -- end result is a tree where nodes are identified by their types and the
-      -- dominator relationships are preserved.
-      CREATE PERFETTO TABLE ${outputTableName} AS
-      SELECT
-        map.path_hash as id,
-        COALESCE(cls.deobfuscated_name, cls.name, '[NULL]') || IIF(
-          node.root_type IS NOT NULL,
-          ' [' || node.root_type || ']', ''
-        ) AS name,
-        IFNULL(parent_map.path_hash, -1) AS parent_id,
-        node.depth - 1 AS depth,
-        sum(dominated_size) AS cumulative_size,
-        -1 AS cumulative_alloc_size,
-        sum(dominated_obj_count) AS cumulative_count,
-        -1 AS cumulative_alloc_count,
-        '' as map_name,
-        '' as source_file,
-        -1 as line_number,
-        sum(self_size) AS size,
-        count(*) AS count
-      FROM _heap_graph_object_dominated node
-      JOIN _dominator_tree_path_hash map USING(id)
-      LEFT JOIN _dominator_tree_path_hash parent_map ON node.idom_id = parent_map.id
-      JOIN heap_graph_class cls ON node.type_id = cls.id
-      GROUP BY map.path_hash, name, parent_id, depth, map_name, source_file, line_number;
-
-      -- These are intermediates and not needed
-      DROP TABLE _heap_graph_object_dominated;
-      DROP TABLE _dominator_tree_path_hash;
-    `);
-
-    return outputQuery;
-  }
-
-  private static async getFlamegraphDataFromTables(
-    engine: Engine,
-    tableName: string,
-    viewingOption: FlamegraphViewingOption,
-    focusRegex: string,
-  ) {
-    let orderBy = '';
-    let totalColumnName:
-      | 'cumulativeSize'
-      | 'cumulativeAllocSize'
-      | 'cumulativeCount'
-      | 'cumulativeAllocCount' = 'cumulativeSize';
-    let selfColumnName: 'size' | 'count' = 'size';
-    // TODO(fmayer): Improve performance so this is no longer necessary.
-    // Alternatively consider collapsing frames of the same label.
-    const maxDepth = 100;
-    switch (viewingOption) {
-      case FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY:
-        orderBy = `where cumulative_alloc_size > 0 and depth < ${maxDepth} order by depth, parent_id,
-            cumulative_alloc_size desc, name`;
-        totalColumnName = 'cumulativeAllocSize';
-        selfColumnName = 'size';
-        break;
-      case FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY:
-        orderBy = `where cumulative_count > 0 and depth < ${maxDepth} order by depth, parent_id,
-            cumulative_count desc, name`;
-        totalColumnName = 'cumulativeCount';
-        selfColumnName = 'count';
-        break;
-      case FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY:
-        orderBy = `where cumulative_alloc_count > 0 and depth < ${maxDepth} order by depth, parent_id,
-            cumulative_alloc_count desc, name`;
-        totalColumnName = 'cumulativeAllocCount';
-        selfColumnName = 'count';
-        break;
-      case FlamegraphViewingOption.PERF_SAMPLES_KEY:
-      case FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY:
-        orderBy = `where cumulative_size > 0 and depth < ${maxDepth} order by depth, parent_id,
-            cumulative_size desc, name`;
-        totalColumnName = 'cumulativeSize';
-        selfColumnName = 'size';
-        break;
-      case FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY:
-        orderBy = `where depth < ${maxDepth} order by depth,
-          cumulativeCount desc, name`;
-        totalColumnName = 'cumulativeCount';
-        selfColumnName = 'count';
-        break;
-      case FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY:
-        orderBy = `where depth < ${maxDepth} order by depth,
-          cumulativeSize desc, name`;
-        totalColumnName = 'cumulativeSize';
-        selfColumnName = 'size';
-        break;
-      default:
-        const exhaustiveCheck: never = viewingOption;
-        throw new Error(`Unhandled case: ${exhaustiveCheck}`);
-        break;
-    }
-
-    const callsites = await engine.query(`
-      SELECT
-        id as hash,
-        IFNULL(IFNULL(DEMANGLE(name), name), '[NULL]') as name,
-        IFNULL(parent_id, -1) as parentHash,
-        depth,
-        cumulative_size as cumulativeSize,
-        cumulative_alloc_size as cumulativeAllocSize,
-        cumulative_count as cumulativeCount,
-        cumulative_alloc_count as cumulativeAllocCount,
-        map_name as mapping,
-        size,
-        count,
-        IFNULL(source_file, '') as sourceFile,
-        IFNULL(line_number, -1) as lineNumber
-      from ${tableName}
-      ${orderBy}
-    `);
-
-    const flamegraphData: CallsiteInfo[] = [];
-    const hashToindex: Map<number, number> = new Map();
-    const it = callsites.iter({
-      hash: NUM,
-      name: STR,
-      parentHash: NUM,
-      depth: NUM,
-      cumulativeSize: NUM,
-      cumulativeAllocSize: NUM,
-      cumulativeCount: NUM,
-      cumulativeAllocCount: NUM,
-      mapping: STR,
-      sourceFile: STR,
-      lineNumber: NUM,
-      size: NUM,
-      count: NUM,
-    });
-    for (let i = 0; it.valid(); ++i, it.next()) {
-      const hash = it.hash;
-      let name = it.name;
-      const parentHash = it.parentHash;
-      const depth = it.depth;
-      const totalSize = it[totalColumnName];
-      const selfSize = it[selfColumnName];
-      const mapping = it.mapping;
-      const highlighted =
-        focusRegex !== '' &&
-        name.toLocaleLowerCase().includes(focusRegex.toLocaleLowerCase());
-      const parentId = hashToindex.has(+parentHash)
-        ? hashToindex.get(+parentHash)!
-        : -1;
-
-      let location: string | undefined;
-      if (/[a-zA-Z]/i.test(it.sourceFile)) {
-        location = it.sourceFile;
-        if (it.lineNumber !== -1) {
-          location += `:${it.lineNumber}`;
-        }
-      }
-
-      if (depth === maxDepth - 1) {
-        name += ' [tree truncated]';
-      }
-      // Instead of hash, we will store index of callsite in this original array
-      // as an id of callsite. That way, we have quicker access to parent and it
-      // will stay unique:
-      hashToindex.set(hash, i);
-
-      flamegraphData.push({
-        id: i,
-        totalSize,
-        depth,
-        parentId,
-        name,
-        selfSize,
-        mapping,
-        merged: false,
-        highlighted,
-        location,
-      });
-    }
-    return flamegraphData;
-  }
-
-  private async downloadPprof() {
-    if (this.state === undefined) {
-      return;
-    }
-    const engine = this.getCurrentEngine();
-    if (engine === undefined) {
-      return;
-    }
-    try {
-      assertTrue(
-        this.state.selection.upids.length === 1,
-        'Native profiles can only contain one pid.',
-      );
-      const pid = await engine.query(
-        `select pid from process where upid = ${this.state.selection.upids[0]}`,
-      );
-      const trace = await getCurrentTrace();
-      convertTraceToPprofAndDownload(
-        trace,
-        pid.firstRow({pid: NUM}).pid,
-        this.state.selection.start,
-      );
-    } catch (error) {
-      throw new Error(`Failed to get current trace ${error}`);
-    }
-  }
-
-  private maybeShowModal() {
-    const state = assertExists(this.state);
-    if (state.result?.incomplete === undefined || !state.result.incomplete) {
-      return undefined;
-    }
-    if (globals.state.flamegraphModalDismissed) {
-      return undefined;
-    }
-    return m(Modal, {
-      title: 'The flamegraph is incomplete',
-      vAlign: 'TOP',
-      content: m(
-        'div',
-        'The current trace does not have a fully formed flamegraph',
-      ),
-      buttons: [
-        {
-          text: 'Show the errors',
-          primary: true,
-          action: () => Router.navigate('#!/info'),
-        },
-        {
-          text: 'Skip',
-          action: () => {
-            globals.dispatch(Actions.dismissFlamegraphModal({}));
-            raf.scheduleFullRedraw();
-          },
-        },
-      ],
-    } as ModalAttrs);
-  }
-
-  private static getMinSizeDisplayed(
-    flamegraphData: ReadonlyArray<CallsiteInfo>,
-    rootSize?: number,
-  ): number {
-    // Note: This is a hack. Really we should obtain the size of the canvas and
-    // use that to determine the number of buckets to display, but this code is
-    // legacy and going away soon, and the calculation before was just plain
-    // wrong anyway so this isn't really any worse.
-    //
-    // 800 buckets is a decent placeholder until the new flamegraph code lands.
-    const bucketCount = 800;
-    if (rootSize === undefined) {
-      rootSize = findRootSize(flamegraphData);
-    }
-    return (MIN_PIXEL_DISPLAYED * rootSize) / bucketCount;
-  }
-
-  private static serializeUpidGroup(upids: number[]) {
-    return new Array(upids).join();
-  }
-
-  private getCurrentEngine() {
-    const engineId = globals.getCurrentEngine()?.id;
-    if (engineId === undefined) return undefined;
-    return globals.engines.get(engineId);
-  }
-}
diff --git a/ui/src/frontend/legacy_flamegraph_unittest.ts b/ui/src/frontend/legacy_flamegraph_unittest.ts
deleted file mode 100644
index ddb006a..0000000
--- a/ui/src/frontend/legacy_flamegraph_unittest.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {splitIfTooBig} from './legacy_flamegraph';
-
-test('textGoingToMultipleLines', () => {
-  const text = 'Dummy text to go to multiple lines.';
-
-  const lineSplitter = splitIfTooBig(text, 7 + 32, text.length);
-
-  expect(lineSplitter).toEqual({
-    lines: ['Dummy t', 'ext to ', 'go to m', 'ultiple', ' lines.'],
-    lineWidth: 7,
-  });
-});
-
-test('emptyText', () => {
-  const text = '';
-
-  const lineSplitter = splitIfTooBig(text, 10, 5);
-
-  expect(lineSplitter).toEqual({lines: [], lineWidth: 5});
-});
-
-test('textEnoughForOneLine', () => {
-  const text = 'Dummy text to go to one lines.';
-
-  const lineSplitter = splitIfTooBig(text, text.length + 32, text.length);
-
-  expect(lineSplitter).toEqual({lines: [text], lineWidth: text.length});
-});
-
-test('textGoingToTwoLines', () => {
-  const text = 'Dummy text to go to two lines.';
-
-  const lineSplitter = splitIfTooBig(text, text.length / 2 + 32, text.length);
-
-  expect(lineSplitter).toEqual({
-    lines: ['Dummy text to g', 'o to two lines.'],
-    lineWidth: text.length / 2,
-  });
-});
diff --git a/ui/src/frontend/legacy_trace_viewer.ts b/ui/src/frontend/legacy_trace_viewer.ts
index f496712..25af38b 100644
--- a/ui/src/frontend/legacy_trace_viewer.ts
+++ b/ui/src/frontend/legacy_trace_viewer.ts
@@ -17,9 +17,9 @@
 import {assertTrue} from '../base/logging';
 import {isString} from '../base/object_utils';
 import {showModal} from '../widgets/modal';
-import {globals} from './globals';
 import {utf8Decode} from '../base/string_utils';
 import {convertToJson} from './trace_converter';
+import {assetSrc} from '../base/assets';
 
 const CTRACE_HEADER = 'TRACE:\n';
 
@@ -121,7 +121,7 @@
   }
 }
 
-export function openBufferWithLegacyTraceViewer(
+function openBufferWithLegacyTraceViewer(
   name: string,
   data: ArrayBuffer | string,
   size: number,
@@ -142,7 +142,7 @@
 
   // The location.pathname mangling is to make this code work also when hosted
   // in a non-root sub-directory, for the case of CI artifacts.
-  const catapultUrl = globals.root + 'assets/catapult_trace_viewer.html';
+  const catapultUrl = assetSrc('assets/catapult_trace_viewer.html');
   const newWin = window.open(catapultUrl);
   if (newWin) {
     // Popup succeedeed.
@@ -172,16 +172,21 @@
   });
 }
 
-export function openInOldUIWithSizeCheck(trace: Blob) {
+export async function openInOldUIWithSizeCheck(trace: Blob): Promise<void> {
   // Perfetto traces smaller than 50mb can be safely opened in the legacy UI.
   if (trace.size < 1024 * 1024 * 50) {
-    convertToJson(trace);
-    return;
+    return await convertToJson(trace, openBufferWithLegacyTraceViewer);
   }
 
   // Give the user the option to truncate larger perfetto traces.
   const size = Math.round(trace.size / (1024 * 1024));
-  showModal({
+
+  // If the user presses one of the buttons below, remember the promise that
+  // they trigger, so we await for it before returning.
+  let nextPromise: Promise<void> | undefined;
+  const setNextPromise = (p: Promise<void>) => (nextPromise = p);
+
+  await showModal({
     title: 'Legacy UI may fail to open this trace',
     content: m(
       'div',
@@ -206,20 +211,38 @@
     buttons: [
       {
         text: 'Open full trace (not recommended)',
-        action: () => convertToJson(trace),
+        action: () =>
+          setNextPromise(convertToJson(trace, openBufferWithLegacyTraceViewer)),
       },
       {
         text: 'Open beginning of trace',
-        action: () => convertToJson(trace, /* truncate*/ 'start'),
+        action: () =>
+          setNextPromise(
+            convertToJson(
+              trace,
+              openBufferWithLegacyTraceViewer,
+              /* truncate*/ 'start',
+            ),
+          ),
       },
       {
         text: 'Open end of trace',
         primary: true,
-        action: () => convertToJson(trace, /* truncate*/ 'end'),
+        action: () =>
+          setNextPromise(
+            convertToJson(
+              trace,
+              openBufferWithLegacyTraceViewer,
+              /* truncate*/ 'end',
+            ),
+          ),
       },
     ],
   });
-  return;
+  // nextPromise is undefined if the user just dimisses the dialog with ESC.
+  if (nextPromise !== undefined) {
+    await nextPromise;
+  }
 }
 
 // TraceViewer method that we wire up to trigger the file load.
diff --git a/ui/src/frontend/metrics_page.ts b/ui/src/frontend/metrics_page.ts
deleted file mode 100644
index 73797fb..0000000
--- a/ui/src/frontend/metrics_page.ts
+++ /dev/null
@@ -1,295 +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.
-
-import m from 'mithril';
-
-import {
-  error,
-  isError,
-  isPending,
-  pending,
-  Result,
-  success,
-} from '../base/result';
-import {pluginManager, PluginManager} from '../common/plugins';
-import {raf} from '../core/raf_scheduler';
-import {MetricVisualisation} from '../public';
-import {Engine} from '../trace_processor/engine';
-import {STR} from '../trace_processor/query_result';
-import {Select} from '../widgets/select';
-import {Spinner} from '../widgets/spinner';
-import {VegaView} from '../widgets/vega_view';
-
-import {globals} from './globals';
-import {createPage} from './pages';
-
-type Format = 'json' | 'prototext' | 'proto';
-const FORMATS: Format[] = ['json', 'prototext', 'proto'];
-
-function getEngine(): Engine | undefined {
-  const engineId = globals.getCurrentEngine()?.id;
-  if (engineId === undefined) {
-    return undefined;
-  }
-  const engine = globals.engines.get(engineId)?.getProxy('MetricsPage');
-  return engine;
-}
-
-async function getMetrics(engine: Engine): Promise<string[]> {
-  const metrics: string[] = [];
-  const metricsResult = await engine.query('select name from trace_metrics');
-  for (const it = metricsResult.iter({name: STR}); it.valid(); it.next()) {
-    metrics.push(it.name);
-  }
-  return metrics;
-}
-
-async function getMetric(
-  engine: Engine,
-  metric: string,
-  format: Format,
-): Promise<string> {
-  const result = await engine.computeMetric([metric], format);
-  if (result instanceof Uint8Array) {
-    return `Uint8Array<len=${result.length}>`;
-  } else {
-    return result;
-  }
-}
-
-class MetricsController {
-  engine: Engine;
-  plugins: PluginManager;
-  private _metrics: string[];
-  private _selected?: string;
-  private _result: Result<string>;
-  private _format: Format;
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  private _json: any;
-
-  constructor(plugins: PluginManager, engine: Engine) {
-    this.plugins = plugins;
-    this.engine = engine;
-    this._metrics = [];
-    this._result = success('');
-    this._json = {};
-    this._format = 'json';
-    getMetrics(this.engine).then((metrics) => {
-      this._metrics = metrics;
-    });
-  }
-
-  get metrics(): string[] {
-    return this._metrics;
-  }
-
-  get visualisations(): MetricVisualisation[] {
-    return this.plugins
-      .metricVisualisations()
-      .filter((v) => v.metric === this.selected);
-  }
-
-  set selected(metric: string | undefined) {
-    if (this._selected === metric) {
-      return;
-    }
-    this._selected = metric;
-    this.update();
-  }
-
-  get selected(): string | undefined {
-    return this._selected;
-  }
-
-  set format(format: Format) {
-    if (this._format === format) {
-      return;
-    }
-    this._format = format;
-    this.update();
-  }
-
-  get format(): Format {
-    return this._format;
-  }
-
-  get result(): Result<string> {
-    return this._result;
-  }
-
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  get resultAsJson(): any {
-    return this._json;
-  }
-
-  private update() {
-    const selected = this._selected;
-    const format = this._format;
-    if (selected === undefined) {
-      this._result = success('');
-      this._json = {};
-    } else {
-      this._result = pending();
-      this._json = {};
-      getMetric(this.engine, selected, format)
-        .then((result) => {
-          if (this._selected === selected && this._format === format) {
-            this._result = success(result);
-            if (format === 'json') {
-              this._json = JSON.parse(result);
-            }
-          }
-        })
-        .catch((e) => {
-          if (this._selected === selected && this._format === format) {
-            this._result = error(e);
-            this._json = {};
-          }
-        })
-        .finally(() => {
-          raf.scheduleFullRedraw();
-        });
-    }
-    raf.scheduleFullRedraw();
-  }
-}
-
-interface MetricResultAttrs {
-  result: Result<string>;
-}
-
-class MetricResultView implements m.ClassComponent<MetricResultAttrs> {
-  view({attrs}: m.CVnode<MetricResultAttrs>) {
-    const result = attrs.result;
-    if (isPending(result)) {
-      return m(Spinner);
-    }
-
-    if (isError(result)) {
-      return m('pre.metric-error', result.error);
-    }
-
-    return m('pre', result.data);
-  }
-}
-
-interface MetricPickerAttrs {
-  controller: MetricsController;
-}
-
-class MetricPicker implements m.ClassComponent<MetricPickerAttrs> {
-  view({attrs}: m.CVnode<MetricPickerAttrs>) {
-    const {controller} = attrs;
-    return m(
-      '.metrics-page-picker',
-      m(
-        Select,
-        {
-          value: controller.selected,
-          oninput: (e: Event) => {
-            if (!e.target) return;
-            controller.selected = (e.target as HTMLSelectElement).value;
-          },
-        },
-        controller.metrics.map((metric) =>
-          m(
-            'option',
-            {
-              value: metric,
-              key: metric,
-            },
-            metric,
-          ),
-        ),
-      ),
-      m(
-        Select,
-        {
-          oninput: (e: Event) => {
-            if (!e.target) return;
-            controller.format = (e.target as HTMLSelectElement).value as Format;
-          },
-        },
-        FORMATS.map((f) => {
-          return m('option', {
-            selected: controller.format === f,
-            key: f,
-            value: f,
-            label: f,
-          });
-        }),
-      ),
-    );
-  }
-}
-
-interface MetricVizViewAttrs {
-  visualisation: MetricVisualisation;
-  data: unknown;
-}
-
-class MetricVizView implements m.ClassComponent<MetricVizViewAttrs> {
-  view({attrs}: m.CVnode<MetricVizViewAttrs>) {
-    return m(
-      '',
-      m(VegaView, {
-        spec: attrs.visualisation.spec,
-        data: {
-          metric: attrs.data,
-        },
-      }),
-    );
-  }
-}
-
-class MetricPageContents implements m.ClassComponent {
-  controller?: MetricsController;
-
-  oncreate() {
-    const engine = getEngine();
-    if (engine !== undefined) {
-      this.controller = new MetricsController(pluginManager, engine);
-    }
-  }
-
-  view() {
-    const controller = this.controller;
-    if (controller === undefined) {
-      return m('');
-    }
-
-    const json = controller.resultAsJson;
-
-    return [
-      m(MetricPicker, {
-        controller,
-      }),
-      controller.format === 'json' &&
-        controller.visualisations.map((visualisation) => {
-          let data = json;
-          for (const p of visualisation.path) {
-            data = data[p] ?? [];
-          }
-          return m(MetricVizView, {visualisation, data});
-        }),
-      m(MetricResultView, {result: controller.result}),
-    ];
-  }
-}
-
-export const MetricsPage = createPage({
-  view() {
-    return m('.metrics-page', m(MetricPageContents));
-  },
-});
diff --git a/ui/src/frontend/named_slice_track.ts b/ui/src/frontend/named_slice_track.ts
index 24fa2cf..ed9b5f0 100644
--- a/ui/src/frontend/named_slice_track.ts
+++ b/ui/src/frontend/named_slice_track.ts
@@ -12,10 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {getColorForSlice} from '../core/colorizer';
-import {Slice} from '../public';
+import {getColorForSlice} from '../public/lib/colorizer';
+import {TrackEventDetailsPanel} from '../public/details_panel';
+import {TrackEventSelection} from '../public/selection';
+import {Slice} from '../public/track';
 import {STR_NULL} from '../trace_processor/query_result';
-
 import {
   BASE_ROW,
   BaseSliceTrack,
@@ -24,9 +25,11 @@
   SLICE_FLAGS_INCOMPLETE,
   SLICE_FLAGS_INSTANT,
 } from './base_slice_track';
-import {globals} from './globals';
+import {ThreadSliceDetailsPanel} from './thread_slice_details_tab';
 import {NewTrackArgs} from './track';
 import {renderDuration} from './widgets/duration';
+import {TraceImpl} from '../core/trace_impl';
+import {assertIsInstance} from '../base/logging';
 
 export const NAMED_ROW = {
   // Base columns (tsq, ts, dur, id, depth).
@@ -68,18 +71,13 @@
   }
 
   onSliceClick(args: OnSliceClickArgs<SliceType>) {
-    globals.setLegacySelection(
-      {
-        kind: 'SLICE',
-        id: args.slice.id,
-        trackKey: this.trackKey,
-        table: 'slice',
-      },
-      {
-        clearSearch: true,
-        pendingScrollId: undefined,
-        switchToCurrentSelectionTab: true,
-      },
-    );
+    this.trace.selection.selectTrackEvent(this.uri, args.slice.id);
+  }
+
+  detailsPanel(_sel: TrackEventSelection): TrackEventDetailsPanel {
+    // Rationale for the assertIsInstance: ThreadSliceDetailsPanel requires a
+    // TraceImpl (because of flows) but here we must take a Trace interface,
+    // because this class is exposed to plugins (which see only Trace).
+    return new ThreadSliceDetailsPanel(assertIsInstance(this.trace, TraceImpl));
   }
 }
diff --git a/ui/src/frontend/notes.ts b/ui/src/frontend/notes.ts
deleted file mode 100644
index 3c53753..0000000
--- a/ui/src/frontend/notes.ts
+++ /dev/null
@@ -1,51 +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 {globals} from './globals';
-import {NotesManager} from './notes_manager';
-import {NotesEditorTab} from './notes_panel';
-import {DisposableStack} from '../base/disposable_stack';
-
-/**
- * Registers with the tab manager to show notes details panels when notes are
- * selected.
- *
- * Notes are core functionality thus don't really belong in a plugin.
- */
-export class Notes implements Disposable {
-  private trash = new DisposableStack();
-
-  constructor() {
-    this.trash.use(
-      globals.tabManager.registerDetailsPanel(new NotesEditorTab()),
-    );
-
-    this.trash.use(
-      globals.tabManager.registerTab({
-        uri: 'notes.manager',
-        isEphemeral: false,
-        content: {
-          getTitle: () => 'Notes & markers',
-          render: () => m(NotesManager),
-        },
-      }),
-    );
-  }
-
-  [Symbol.dispose]() {
-    this.trash.dispose();
-  }
-}
diff --git a/ui/src/frontend/notes_list_editor.ts b/ui/src/frontend/notes_list_editor.ts
new file mode 100644
index 0000000..e747ac9
--- /dev/null
+++ b/ui/src/frontend/notes_list_editor.ts
@@ -0,0 +1,63 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {Button} from '../widgets/button';
+import {Icons} from '../base/semantic_icons';
+import {TraceImplAttrs} from '../core/trace_impl';
+
+export class NotesListEditor implements m.ClassComponent<TraceImplAttrs> {
+  view({attrs}: m.CVnode<TraceImplAttrs>) {
+    const notes = attrs.trace.notes.notes;
+    if (notes.size === 0) {
+      return 'No notes found';
+    }
+
+    return m(
+      'table',
+      m(
+        'thead',
+        m(
+          'tr',
+          m('td', 'ID'),
+          m('td', 'Color'),
+          m('td', 'Type'),
+          m('td', 'Text'),
+          m('td', 'Delete'),
+        ),
+      ),
+      m(
+        'tbody',
+        Array.from(notes.entries()).map(([id, note]) => {
+          return m(
+            'tr',
+            m('td', id),
+            m('td', note.color),
+            m('td', note.noteType),
+            m('td', note.text),
+            m(
+              'td',
+              m(Button, {
+                icon: Icons.Delete,
+                onclick: () => {
+                  attrs.trace.notes.removeNote(id);
+                },
+              }),
+            ),
+          );
+        }),
+      ),
+    );
+  }
+}
diff --git a/ui/src/frontend/notes_manager.ts b/ui/src/frontend/notes_manager.ts
deleted file mode 100644
index caa3ed0..0000000
--- a/ui/src/frontend/notes_manager.ts
+++ /dev/null
@@ -1,64 +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 {globals} from './globals';
-import {Actions} from '../common/actions';
-import {Button} from '../widgets/button';
-import {Icons} from '../base/semantic_icons';
-
-export class NotesManager implements m.ClassComponent {
-  view(_: m.CVnode) {
-    const notesList = Object.entries(globals.state.notes);
-    if (notesList.length === 0) {
-      return 'No notes found';
-    }
-
-    return m(
-      'table',
-      m(
-        'thead',
-        m(
-          'tr',
-          m('td', 'ID'),
-          m('td', 'Color'),
-          m('td', 'Type'),
-          m('td', 'Text'),
-          m('td', 'Delete'),
-        ),
-      ),
-      m(
-        'tbody',
-        notesList.map(([id, note]) => {
-          return m(
-            'tr',
-            m('td', id),
-            m('td', note.color),
-            m('td', note.noteType),
-            m('td', note.text),
-            m(
-              'td',
-              m(Button, {
-                icon: Icons.Delete,
-                onclick: () => {
-                  globals.dispatch(Actions.removeNote({id}));
-                },
-              }),
-            ),
-          );
-        }),
-      ),
-    );
-  }
-}
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 29c9aa6..21dc29a 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -13,29 +13,23 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {currentTargetOffset} from '../base/dom_utils';
 import {Icons} from '../base/semantic_icons';
-import {Time} from '../base/time';
-import {Actions} from '../common/actions';
-import {randomColor} from '../core/colorizer';
-import {SpanNote, Note, Selection} from '../common/state';
+import {randomColor} from '../public/lib/colorizer';
+import {SpanNote, Note} from '../public/note';
 import {raf} from '../core/raf_scheduler';
 import {Button, ButtonBar} from '../widgets/button';
-import {TextInput} from '../widgets/text_input';
-
 import {TRACK_SHELL_WIDTH} from './css_constants';
-import {globals} from './globals';
 import {getMaxMajorTicks, generateTicks, TickType} from './gridline_helper';
-import {Size} from '../base/geom';
+import {Size2D} from '../base/geom';
 import {Panel} from './panel_container';
-import {isTraceLoaded} from './sidebar';
 import {Timestamp} from './widgets/timestamp';
-import {uuidv4} from '../base/uuid';
 import {assertUnreachable} from '../base/logging';
-import {DetailsPanel} from '../public';
-import {PxSpan, TimeScale} from './time_scale';
-import {canvasClip} from '../common/canvas_utils';
+import {DetailsPanel} from '../public/details_panel';
+import {TimeScale} from '../base/time_scale';
+import {canvasClip} from '../base/canvas_utils';
+import {Selection} from '../public/selection';
+import {TraceImpl} from '../core/trace_impl';
 
 const FLAG_WIDTH = 16;
 const AREA_TRIANGLE_WIDTH = 10;
@@ -61,23 +55,39 @@
 export class NotesPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = false;
+  private readonly trace: TraceImpl;
   private timescale?: TimeScale; // The timescale from the last render()
   private hoveredX: null | number = null;
+  private mouseDragging = false;
+
+  constructor(trace: TraceImpl) {
+    this.trace = trace;
+  }
 
   render(): m.Children {
-    const allCollapsed = Object.values(globals.state.trackGroups).every(
-      (group) => group.collapsed,
+    const allCollapsed = this.trace.workspace.flatTracks.every(
+      (n) => n.collapsed,
     );
 
     return m(
       '.notes-panel',
       {
+        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
+          // avoid setting accidental flags like measuring the time on the brush
+          // timeline.
+          this.mouseDragging = false;
+        },
         onclick: (e: MouseEvent) => {
-          const {x, y} = currentTargetOffset(e);
-          this.onClick(x - TRACK_SHELL_WIDTH, y);
-          e.stopPropagation();
+          if (!this.mouseDragging) {
+            const x = currentTargetOffset(e).x - TRACK_SHELL_WIDTH;
+            this.onClick(x);
+            e.stopPropagation();
+          }
         },
         onmousemove: (e: MouseEvent) => {
+          this.mouseDragging = true;
           this.hoveredX = currentTargetOffset(e).x - TRACK_SHELL_WIDTH;
           raf.scheduleRedraw();
         },
@@ -87,64 +97,67 @@
         },
         onmouseout: () => {
           this.hoveredX = null;
-          globals.dispatch(Actions.setHoveredNoteTimestamp({ts: Time.INVALID}));
+          this.trace.timeline.hoveredNoteTimestamp = undefined;
         },
       },
-      isTraceLoaded() &&
-        m(
-          ButtonBar,
-          {className: 'pf-toolbar'},
-          m(Button, {
-            onclick: (e: Event) => {
-              e.preventDefault();
-              if (allCollapsed) {
-                globals.commandManager.runCommand(
-                  'perfetto.CoreCommands#ExpandAllGroups',
-                );
-              } else {
-                globals.commandManager.runCommand(
-                  'perfetto.CoreCommands#CollapseAllGroups',
-                );
-              }
-            },
-            title: allCollapsed ? 'Expand all' : 'Collapse all',
-            icon: allCollapsed ? 'unfold_more' : 'unfold_less',
-            compact: true,
-          }),
-          m(Button, {
-            onclick: (e: Event) => {
-              e.preventDefault();
-              globals.dispatch(Actions.clearAllPinnedTracks({}));
-            },
-            title: 'Clear all pinned tracks',
-            icon: 'clear_all',
-            compact: true,
-          }),
-          m(TextInput, {
-            placeholder: 'Filter tracks...',
-            title:
-              'Track filter - enter one or more comma-separated search terms',
-            value: globals.state.trackFilterTerm,
-            oninput: (e: Event) => {
-              const filterTerm = (e.target as HTMLInputElement).value;
-              globals.dispatch(Actions.setTrackFilterTerm({filterTerm}));
-            },
-          }),
-          m(Button, {
-            type: 'reset',
-            icon: 'backspace',
-            onclick: () => {
-              globals.dispatch(
-                Actions.setTrackFilterTerm({filterTerm: undefined}),
+      m(
+        ButtonBar,
+        {className: 'pf-toolbar'},
+        m(Button, {
+          onclick: (e: Event) => {
+            e.preventDefault();
+            if (allCollapsed) {
+              this.trace.commands.runCommand(
+                'perfetto.CoreCommands#ExpandAllGroups',
               );
-            },
-            title: 'Clear track filter',
-          }),
-        ),
+            } else {
+              this.trace.commands.runCommand(
+                'perfetto.CoreCommands#CollapseAllGroups',
+              );
+            }
+          },
+          title: allCollapsed ? 'Expand all' : 'Collapse all',
+          icon: allCollapsed ? 'unfold_more' : 'unfold_less',
+          compact: true,
+        }),
+        m(Button, {
+          onclick: (e: Event) => {
+            e.preventDefault();
+            this.trace.workspace.pinnedTracks.forEach((t) =>
+              this.trace.workspace.unpinTrack(t),
+            );
+            raf.scheduleFullRedraw();
+          },
+          title: 'Clear all pinned tracks',
+          icon: 'clear_all',
+          compact: true,
+        }),
+        // TODO(stevegolton): Re-introduce this when we fix track filtering
+        // m(TextInput, {
+        //   placeholder: 'Filter tracks...',
+        //   title:
+        //     'Track filter - enter one or more comma-separated search terms',
+        //   value: this.trace.state.trackFilterTerm,
+        //   oninput: (e: Event) => {
+        //     const filterTerm = (e.target as HTMLInputElement).value;
+        //     this.trace.dispatch(Actions.setTrackFilterTerm({filterTerm}));
+        //   },
+        // }),
+        // m(Button, {
+        //   type: 'reset',
+        //   icon: 'backspace',
+        //   onclick: () => {
+        //     this.trace.dispatch(
+        //       Actions.setTrackFilterTerm({filterTerm: undefined}),
+        //     );
+        //   },
+        //   title: 'Clear track filter',
+        // }),
+      ),
     );
   }
 
-  renderCanvas(ctx: CanvasRenderingContext2D, size: Size) {
+  renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
     ctx.fillStyle = '#999';
     ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
 
@@ -157,18 +170,21 @@
     ctx.restore();
   }
 
-  private renderPanel(ctx: CanvasRenderingContext2D, size: Size): void {
+  private renderPanel(ctx: CanvasRenderingContext2D, size: Size2D): void {
     let aNoteIsHovered = false;
 
-    const visibleWindow = globals.timeline.visibleWindow;
-    const timescale = new TimeScale(visibleWindow, new PxSpan(0, size.width));
+    const visibleWindow = this.trace.timeline.visibleWindow;
+    const timescale = new TimeScale(visibleWindow, {
+      left: 0,
+      right: size.width,
+    });
     const timespan = visibleWindow.toTimeSpan();
 
     this.timescale = timescale;
 
     if (size.width > 0 && timespan.duration > 0n) {
       const maxMajorTicks = getMaxMajorTicks(size.width);
-      const offset = globals.timestampOffset();
+      const offset = this.trace.timeline.timestampOffset();
       const tickGen = generateTicks(timespan, maxMajorTicks, offset);
       for (const {type, time} of tickGen) {
         const px = Math.floor(timescale.timeToPx(time));
@@ -181,7 +197,7 @@
     ctx.textBaseline = 'bottom';
     ctx.font = '10px Helvetica';
 
-    for (const note of Object.values(globals.state.notes)) {
+    for (const note of this.trace.notes.notes.values()) {
       const timestamp = getStartTimestamp(note);
       // TODO(hjd): We should still render area selection marks in viewport is
       // *within* the area (e.g. both lhs and rhs are out of bounds).
@@ -197,7 +213,7 @@
         this.hoveredX !== null && this.hitTestNote(this.hoveredX, note);
       if (currentIsHovered) aNoteIsHovered = true;
 
-      const selection = globals.state.selection;
+      const selection = this.trace.selection.selection;
       const isSelected = selection.kind === 'note' && selection.id === note.id;
       const x = timescale.timeToPx(timestamp);
       const left = Math.floor(x);
@@ -234,14 +250,14 @@
     // A real note is hovered so we don't need to see the preview line.
     // TODO(hjd): Change cursor to pointer here.
     if (aNoteIsHovered) {
-      globals.dispatch(Actions.setHoveredNoteTimestamp({ts: Time.INVALID}));
+      this.trace.timeline.hoveredNoteTimestamp = undefined;
     }
 
     // View preview note flag when hovering on notes panel.
     if (!aNoteIsHovered && this.hoveredX !== null) {
       const timestamp = timescale.pxToHpTime(this.hoveredX).toTime();
       if (visibleWindow.contains(timestamp)) {
-        globals.dispatch(Actions.setHoveredNoteTimestamp({ts: timestamp}));
+        this.trace.timeline.hoveredNoteTimestamp = timestamp;
         const x = timescale.timeToPx(timestamp);
         const left = Math.floor(x);
         this.drawFlag(ctx, left, size.height, '#aaa', /* fill */ true);
@@ -315,26 +331,23 @@
     ctx.textBaseline = prevBaseline;
   }
 
-  private onClick(x: number, _: number) {
+  private onClick(x: number) {
     if (!this.timescale) {
       return;
     }
 
     // Select the hovered note, or create a new single note & select it
     if (x < 0) return;
-    for (const note of Object.values(globals.state.notes)) {
+    for (const note of this.trace.notes.notes.values()) {
       if (this.hoveredX !== null && this.hitTestNote(this.hoveredX, note)) {
-        globals.makeSelection(Actions.selectNote({id: note.id}));
+        this.trace.selection.selectNote({id: note.id});
         return;
       }
     }
     const timestamp = this.timescale.pxToHpTime(x).toTime();
-    const id = uuidv4();
     const color = randomColor();
-    globals.dispatchMultiple([
-      Actions.addNote({id, timestamp, color}),
-      Actions.selectNote({id}),
-    ]);
+    const noteId = this.trace.notes.addNote({timestamp, color});
+    this.trace.selection.selectNote({id: noteId});
   }
 
   private hitTestNote(x: number, note: SpanNote | Note): boolean {
@@ -358,6 +371,8 @@
 }
 
 export class NotesEditorTab implements DetailsPanel {
+  constructor(private trace: TraceImpl) {}
+
   render(selection: Selection) {
     if (selection.kind !== 'note') {
       return undefined;
@@ -365,13 +380,16 @@
 
     const id = selection.id;
 
-    const note = globals.state.notes[id];
+    const note = this.trace.notes.getNote(id);
     if (note === undefined) {
       return m('.', `No Note with id ${id}`);
     }
     const startTime = getStartTimestamp(note);
     return m(
       '.notes-editor-panel',
+      {
+        key: id, // Every note shoul get its own brand new DOM.
+      },
       m(
         '.notes-editor-panel-heading-bar',
         m(
@@ -380,15 +398,18 @@
           m(Timestamp, {ts: startTime}),
         ),
         m('input[type=text]', {
-          value: note.text,
+          oncreate: (v: m.VnodeDOM) => {
+            // NOTE: due to bad design decisions elsewhere this component is
+            // rendered every time the mouse moves on the canvas. We cannot set
+            // `value: note.text` as an input as that will clobber the input
+            // value as we move the mouse.
+            const inputElement = v.dom as HTMLInputElement;
+            inputElement.value = note.text;
+            inputElement.focus();
+          },
           onchange: (e: InputEvent) => {
             const newText = (e.target as HTMLInputElement).value;
-            globals.dispatch(
-              Actions.changeNoteText({
-                id,
-                newText,
-              }),
-            );
+            this.trace.notes.changeNote(id, {text: newText});
           },
         }),
         m(
@@ -398,22 +419,14 @@
             value: note.color,
             onchange: (e: Event) => {
               const newColor = (e.target as HTMLInputElement).value;
-              globals.dispatch(
-                Actions.changeNoteColor({
-                  id,
-                  newColor,
-                }),
-              );
+              this.trace.notes.changeNote(id, {color: newColor});
             },
           }),
         ),
         m(Button, {
           label: 'Remove',
           icon: Icons.Delete,
-          onclick: () => {
-            globals.dispatch(Actions.removeNote({id}));
-            raf.scheduleFullRedraw();
-          },
+          onclick: () => this.trace.notes.removeNote(id),
         }),
       ),
     );
diff --git a/ui/src/frontend/omnibox.ts b/ui/src/frontend/omnibox.ts
index 16ada06..5f1e29a 100644
--- a/ui/src/frontend/omnibox.ts
+++ b/ui/src/frontend/omnibox.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {classNames} from '../base/classnames';
 import {FuzzySegment} from '../base/fuzzy';
 import {isString} from '../base/object_utils';
diff --git a/ui/src/frontend/omnibox_manager.ts b/ui/src/frontend/omnibox_manager.ts
deleted file mode 100644
index 41d795b..0000000
--- a/ui/src/frontend/omnibox_manager.ts
+++ /dev/null
@@ -1,156 +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 {raf} from '../core/raf_scheduler';
-
-export enum OmniboxMode {
-  Search,
-  Query,
-  Command,
-  Prompt,
-}
-
-export interface PromptOption {
-  key: string;
-  displayName: string;
-}
-
-interface Prompt {
-  text: string;
-  options?: PromptOption[];
-  resolve(result: string): void;
-  reject(): void;
-}
-
-const defaultMode = OmniboxMode.Search;
-
-export class OmniboxManager {
-  private _omniboxMode = defaultMode;
-  private _focusOmniboxNextRender = false;
-  private _pendingCursorPlacement?: number;
-  private _pendingPrompt?: Prompt;
-  private _text = '';
-  private _omniboxSelectionIndex = 0;
-
-  get omniboxMode(): OmniboxMode {
-    return this._omniboxMode;
-  }
-
-  get pendingPrompt(): Prompt | undefined {
-    return this._pendingPrompt;
-  }
-
-  get text(): string {
-    return this._text;
-  }
-
-  get omniboxSelectionIndex(): number {
-    return this._omniboxSelectionIndex;
-  }
-
-  get focusOmniboxNextRender(): boolean {
-    return this._focusOmniboxNextRender;
-  }
-
-  get pendingCursorPlacement(): number | undefined {
-    return this._pendingCursorPlacement;
-  }
-
-  setText(value: string): void {
-    this._text = value;
-  }
-
-  setOmniboxSelectionIndex(index: number): void {
-    this._omniboxSelectionIndex = index;
-  }
-
-  focusOmnibox(cursorPlacement?: number): void {
-    this._focusOmniboxNextRender = true;
-    this._pendingCursorPlacement = cursorPlacement;
-    raf.scheduleFullRedraw();
-  }
-
-  clearOmniboxFocusFlag(): void {
-    this._focusOmniboxNextRender = false;
-    this._pendingCursorPlacement = undefined;
-  }
-
-  setMode(mode: OmniboxMode): void {
-    this._omniboxMode = mode;
-    this._focusOmniboxNextRender = true;
-    this.resetOmniboxText();
-    this.rejectPendingPrompt();
-    raf.scheduleFullRedraw();
-  }
-
-  // Start a prompt. If options are supplied, the user must pick one from the
-  // list, otherwise the input is free-form text.
-  prompt(text: string, options?: PromptOption[]): Promise<string> {
-    this._omniboxMode = OmniboxMode.Prompt;
-    this.resetOmniboxText();
-    this.rejectPendingPrompt();
-
-    const promise = new Promise<string>((resolve, reject) => {
-      this._pendingPrompt = {
-        text,
-        options,
-        resolve,
-        reject,
-      };
-    });
-
-    this._focusOmniboxNextRender = true;
-    raf.scheduleFullRedraw();
-
-    return promise;
-  }
-
-  // Resolve the pending prompt with a value to return to the prompter.
-  resolvePrompt(value: string): void {
-    if (this._pendingPrompt) {
-      this._pendingPrompt.resolve(value);
-      this._pendingPrompt = undefined;
-    }
-    this.setMode(OmniboxMode.Search);
-  }
-
-  // Reject the prompt outright. Doing this will force the owner of the prompt
-  // promise to catch, so only do this when things go seriously wrong.
-  // Use |resolvePrompt(null)| to indicate cancellation.
-  rejectPrompt(): void {
-    if (this._pendingPrompt) {
-      this._pendingPrompt.reject();
-      this._pendingPrompt = undefined;
-    }
-    this.setMode(OmniboxMode.Search);
-  }
-
-  reset(): void {
-    this.setMode(defaultMode);
-    this.resetOmniboxText();
-    raf.scheduleFullRedraw();
-  }
-
-  private rejectPendingPrompt() {
-    if (this._pendingPrompt) {
-      this._pendingPrompt.reject();
-      this._pendingPrompt = undefined;
-    }
-  }
-
-  private resetOmniboxText() {
-    this._text = '';
-    this._omniboxSelectionIndex = 0;
-  }
-}
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index 4252974..e3798e1 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -13,11 +13,9 @@
 // limitations under the License.
 
 import m from 'mithril';
-
-import {Time, TimeSpan, time} from '../base/time';
-import {colorForCpu} from '../core/colorizer';
+import {Duration, Time, TimeSpan, duration, time} from '../base/time';
+import {colorForCpu} from '../public/lib/colorizer';
 import {timestampFormat, TimestampFormat} from '../core/timestamp_format';
-
 import {
   OVERVIEW_TIMELINE_NON_VISIBLE_COLOR,
   TRACK_SHELL_WIDTH,
@@ -26,42 +24,55 @@
 import {DragStrategy} from './drag/drag_strategy';
 import {InnerDragStrategy} from './drag/inner_drag_strategy';
 import {OuterDragStrategy} from './drag/outer_drag_strategy';
-import {DragGestureHandler} from './drag_gesture_handler';
-import {globals} from './globals';
+import {DragGestureHandler} from '../base/drag_gesture_handler';
 import {
   getMaxMajorTicks,
   MIN_PX_PER_STEP,
   generateTicks,
   TickType,
 } from './gridline_helper';
-import {Size} from '../base/geom';
+import {Size2D} from '../base/geom';
 import {Panel} from './panel_container';
-import {PxSpan, TimeScale} from './time_scale';
-import {HighPrecisionTimeSpan} from '../common/high_precision_time_span';
+import {TimeScale} from '../base/time_scale';
+import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
+import {TraceImpl} from '../core/trace_impl';
+import {LONG, NUM} from '../trace_processor/query_result';
+import {raf} from '../core/raf_scheduler';
+import {getOrCreate} from '../base/utils';
+
+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);
+  private readonly overviewData: OverviewDataLoader;
+
+  constructor(private trace: TraceImpl) {
+    this.overviewData = getOrCreate(
+      tracesData,
+      trace,
+      () => new OverviewDataLoader(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 = globals.traceContext;
+    const traceTime = this.trace.traceInfo;
     if (this.width > TRACK_SHELL_WIDTH) {
-      const pxSpan = new PxSpan(TRACK_SHELL_WIDTH, this.width);
+      const pxBounds = {left: TRACK_SHELL_WIDTH, right: this.width};
       const hpTraceTime = HighPrecisionTimeSpan.fromTime(
         traceTime.start,
         traceTime.end,
       );
-      this.timeScale = new TimeScale(hpTraceTime, pxSpan);
+      this.timeScale = new TimeScale(hpTraceTime, pxBounds);
       if (this.gesture === undefined) {
         this.gesture = new DragGestureHandler(
           dom as HTMLElement,
@@ -102,19 +113,20 @@
     });
   }
 
-  renderCanvas(ctx: CanvasRenderingContext2D, size: Size) {
+  renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
     if (this.width === undefined) return;
     if (this.timeScale === undefined) return;
+
     const headerHeight = 20;
     const tracksHeight = size.height - headerHeight;
     const traceContext = new TimeSpan(
-      globals.traceContext.start,
-      globals.traceContext.end,
+      this.trace.traceInfo.start,
+      this.trace.traceInfo.end,
     );
 
     if (size.width > TRACK_SHELL_WIDTH && traceContext.duration > 0n) {
       const maxMajorTicks = getMaxMajorTicks(this.width - TRACK_SHELL_WIDTH);
-      const offset = globals.timestampOffset();
+      const offset = this.trace.timeline.timestampOffset();
       const tickGen = generateTicks(traceContext, maxMajorTicks, offset);
 
       // Draw time labels
@@ -126,7 +138,7 @@
         if (xPos > this.width) break;
         if (type === TickType.MAJOR) {
           ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
-          const domainTime = globals.toDomainTime(time);
+          const domainTime = this.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);
@@ -137,12 +149,13 @@
     }
 
     // Draw mini-tracks with quanitzed density for each process.
-    if (globals.overviewStore.size > 0) {
-      const numTracks = globals.overviewStore.size;
+    const overviewData = this.overviewData.overviewData;
+    if (overviewData.size > 0) {
+      const numTracks = overviewData.size;
       let y = 0;
       const trackHeight = (tracksHeight - 1) / numTracks;
-      for (const key of globals.overviewStore.keys()) {
-        const loads = globals.overviewStore.get(key)!;
+      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));
@@ -161,9 +174,7 @@
     ctx.fillRect(0, size.height - 1, this.width, 1);
 
     // Draw semi-opaque rects that occlude the non-visible time range.
-    const [vizStartPx, vizEndPx] = OverviewTimelinePanel.extractBounds(
-      this.timeScale,
-    );
+    const [vizStartPx, vizEndPx] = this.extractBounds(this.timeScale);
 
     ctx.fillStyle = OVERVIEW_TIMELINE_NON_VISIBLE_COLOR;
     ctx.fillRect(
@@ -205,9 +216,7 @@
 
   private chooseCursor(x: number) {
     if (this.timeScale === undefined) return 'default';
-    const [startBound, endBound] = OverviewTimelinePanel.extractBounds(
-      this.timeScale,
-    );
+    const [startBound, endBound] = this.extractBounds(this.timeScale);
     if (
       OverviewTimelinePanel.inBorderRange(x, startBound) ||
       OverviewTimelinePanel.inBorderRange(x, endBound)
@@ -229,16 +238,22 @@
 
   onDragStart(x: number) {
     if (this.timeScale === undefined) return;
-    const pixelBounds = OverviewTimelinePanel.extractBounds(this.timeScale);
+
+    const cb = (vizTime: HighPrecisionTimeSpan) => {
+      this.trace.timeline.updateVisibleTimeHP(vizTime);
+      raf.scheduleRedraw();
+    };
+    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(this.timeScale, pixelBounds);
+      this.dragStrategy = new BorderDragStrategy(timeScale, pixelBounds, cb);
     } else if (x < pixelBounds[0] || pixelBounds[1] < x) {
-      this.dragStrategy = new OuterDragStrategy(this.timeScale);
+      this.dragStrategy = new OuterDragStrategy(timeScale, cb);
     } else {
-      this.dragStrategy = new InnerDragStrategy(this.timeScale, pixelBounds);
+      this.dragStrategy = new InnerDragStrategy(timeScale, pixelBounds, cb);
     }
     this.dragStrategy.onDragStart(x);
   }
@@ -247,8 +262,8 @@
     this.dragStrategy = undefined;
   }
 
-  private static extractBounds(timeScale: TimeScale): [number, number] {
-    const vizTime = globals.timeline.visibleWindow;
+  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)),
@@ -275,15 +290,21 @@
     case TimestampFormat.Timecode:
       renderTimecode(ctx, time, x, y, minWidth);
       break;
-    case TimestampFormat.Raw:
+    case TimestampFormat.TraceNs:
       ctx.fillText(time.toString(), x, y, minWidth);
       break;
-    case TimestampFormat.RawLocale:
+    case TimestampFormat.TraceNsLocale:
       ctx.fillText(time.toLocaleString(), x, y, minWidth);
       break;
     case TimestampFormat.Seconds:
       ctx.fillText(Time.formatSeconds(time), x, y, minWidth);
       break;
+    case TimestampFormat.Milliseoncds:
+      ctx.fillText(Time.formatMilliseconds(time), x, y, minWidth);
+      break;
+    case TimestampFormat.Microseconds:
+      ctx.fillText(Time.formatMicroseconds(time), x, y, minWidth);
+      break;
     default:
       const z: never = fmt;
       throw new Error(`Invalid timestamp ${z}`);
@@ -304,3 +325,126 @@
   const {dhhmmss} = timecode;
   ctx.fillText(dhhmmss, x, y, minWidth);
 }
+
+interface QuantizedLoad {
+  start: time;
+  end: time;
+  load: number;
+}
+
+// Kicks of a sequence of promises that load the overiew data in steps.
+// Each step schedules an animation frame.
+class OverviewDataLoader {
+  overviewData = new Map<string, QuantizedLoad[]>();
+
+  constructor(private trace: TraceImpl) {
+    this.beginLoad();
+  }
+
+  async beginLoad() {
+    const traceSpan = new TimeSpan(
+      this.trace.traceInfo.start,
+      this.trace.traceInfo.end,
+    );
+    const engine = this.trace.engine;
+    const stepSize = Duration.max(1n, traceSpan.duration / 100n);
+    const hasSchedSql = 'select ts from sched limit 1';
+    const hasSchedOverview = (await engine.query(hasSchedSql)).numRows() > 0;
+    if (hasSchedOverview) {
+      await this.loadSchedOverview(traceSpan, stepSize);
+    } else {
+      await this.loadSliceOverview(traceSpan, stepSize);
+    }
+  }
+
+  async loadSchedOverview(traceSpan: TimeSpan, stepSize: duration) {
+    const stepPromises = [];
+    for (
+      let start = traceSpan.start;
+      start < traceSpan.end;
+      start = Time.add(start, stepSize)
+    ) {
+      const progress = start - traceSpan.start;
+      const ratio = Number(progress) / Number(traceSpan.duration);
+      this.trace.omnibox.showStatusMessage(
+        'Loading overview ' + `${Math.round(ratio * 100)}%`,
+      );
+      const end = Time.add(start, stepSize);
+      // The (async() => {})() queues all the 100 async promises in one batch.
+      // Without that, we would wait for each step to be rendered before
+      // kicking off the next one. That would interleave an animation frame
+      // between each step, slowing down significantly the overall process.
+      stepPromises.push(
+        (async () => {
+          const schedResult = await this.trace.engine.query(
+            `select cast(sum(dur) as float)/${stepSize} as load, cpu from sched ` +
+              `where ts >= ${start} and ts < ${end} and utid != 0 ` +
+              'group by cpu order by cpu',
+          );
+          const schedData: {[key: string]: QuantizedLoad} = {};
+          const it = schedResult.iter({load: NUM, cpu: NUM});
+          for (; it.valid(); it.next()) {
+            const load = it.load;
+            const cpu = it.cpu;
+            schedData[cpu] = {start, end, load};
+          }
+          this.appendData(schedData);
+        })(),
+      );
+    } // for(start = ...)
+    await Promise.all(stepPromises);
+  }
+
+  async loadSliceOverview(traceSpan: TimeSpan, stepSize: duration) {
+    // Slices overview.
+    const sliceResult = await this.trace.engine.query(`select
+            bucket,
+            upid,
+            ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load
+          from thread
+          inner join (
+            select
+              ifnull(cast((ts - ${traceSpan.start})/${stepSize} as int), 0) as bucket,
+              sum(dur) as utid_sum,
+              utid
+            from slice
+            inner join thread_track on slice.track_id = thread_track.id
+            group by bucket, utid
+          ) using(utid)
+          where upid is not null
+          group by bucket, upid`);
+
+    const slicesData: {[key: string]: QuantizedLoad[]} = {};
+    const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM});
+    for (; it.valid(); it.next()) {
+      const bucket = it.bucket;
+      const upid = it.upid;
+      const load = it.load;
+
+      const start = Time.add(traceSpan.start, stepSize * bucket);
+      const end = Time.add(start, stepSize);
+
+      const upidStr = upid.toString();
+      let loadArray = slicesData[upidStr];
+      if (loadArray === undefined) {
+        loadArray = slicesData[upidStr] = [];
+      }
+      loadArray.push({start, end, load});
+    }
+    this.appendData(slicesData);
+  }
+
+  appendData(data: {[key: string]: QuantizedLoad | QuantizedLoad[]}) {
+    for (const [key, value] of Object.entries(data)) {
+      if (!this.overviewData.has(key)) {
+        this.overviewData.set(key, []);
+      }
+      if (value instanceof Array) {
+        this.overviewData.get(key)!.push(...value);
+      } else {
+        this.overviewData.get(key)!.push(value);
+      }
+    }
+    raf.scheduleRedraw();
+  }
+}
diff --git a/ui/src/frontend/pages.ts b/ui/src/frontend/pages.ts
deleted file mode 100644
index a27a7ae..0000000
--- a/ui/src/frontend/pages.ts
+++ /dev/null
@@ -1,26 +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';
-
-// Wrap component with common UI elements (nav bar etc).
-export function createPage(
-  component: m.Component<PageAttrs>,
-): m.Component<PageAttrs> {
-  return component;
-}
-
-export interface PageAttrs {
-  subpage?: string;
-}
diff --git a/ui/src/frontend/pan_and_zoom_handler.ts b/ui/src/frontend/pan_and_zoom_handler.ts
index 954a350..4536b9e 100644
--- a/ui/src/frontend/pan_and_zoom_handler.ts
+++ b/ui/src/frontend/pan_and_zoom_handler.ts
@@ -15,9 +15,8 @@
 import {DisposableStack} from '../base/disposable_stack';
 import {currentTargetOffset, elementIsEditable} from '../base/dom_utils';
 import {raf} from '../core/raf_scheduler';
-
 import {Animation} from './animation';
-import {DragGestureHandler} from './drag_gesture_handler';
+import {DragGestureHandler} from '../base/drag_gesture_handler';
 
 // When first starting to pan or zoom, move at least this many units.
 const INITIAL_PAN_STEP_PX = 50;
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 872d931..ab6de73 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -13,10 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {findRef, toHTMLElement} from '../base/dom_utils';
 import {assertExists, assertFalse} from '../base/logging';
-import {time} from '../base/time';
 import {
   PerfStatsSource,
   RunningStatistics,
@@ -26,24 +24,16 @@
   runningStatStr,
 } from '../core/perf';
 import {raf} from '../core/raf_scheduler';
-import {SliceRect} from '../public';
-
 import {SimpleResizeObserver} from '../base/resize_observer';
-import {canvasClip} from '../common/canvas_utils';
-import {
-  SELECTION_STROKE_COLOR,
-  TOPBAR_HEIGHT,
-  TRACK_SHELL_WIDTH,
-} from './css_constants';
-import {
-  FlowEventsRenderer,
-  FlowEventsRendererArgs,
-} from './flow_events_renderer';
-import {globals} from './globals';
-import {Size} from '../base/geom';
+import {canvasClip} from '../base/canvas_utils';
+import {SELECTION_STROKE_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
+import {Bounds2D, Size2D, VerticalBounds} from '../base/geom';
 import {VirtualCanvas} from './virtual_canvas';
 import {DisposableStack} from '../base/disposable_stack';
-import {PxSpan, TimeScale} from './time_scale';
+import {TimeScale} from '../base/time_scale';
+import {TrackNode} from '../public/workspace';
+import {HTMLAttrs} from '../widgets/common';
+import {TraceImpl, TraceImplAttrs} from '../core/trace_impl';
 
 const CANVAS_OVERDRAW_PX = 100;
 
@@ -51,43 +41,63 @@
   readonly kind: 'panel';
   render(): m.Children;
   readonly selectable: boolean;
-  readonly trackKey?: string; // Defined if this panel represents are track
-  readonly groupKey?: string; // Defined if this panel represents a group - i.e. a group summary track
-  renderCanvas(ctx: CanvasRenderingContext2D, size: Size): void;
-  getSliceRect?(tStart: time, tDur: time, depth: number): SliceRect | undefined;
+  // 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 childPanels: Panel[];
+  readonly header?: Panel;
+  readonly topOffsetPx: number;
+  readonly sticky: boolean;
+  readonly childPanels: PanelOrGroup[];
 }
 
 export type PanelOrGroup = Panel | PanelGroup;
 
-export interface PanelContainerAttrs {
+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 {
-  trackOrGroupKey: string; // Can be == '' for singleton panels.
+  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>, PerfStatsSource
 {
-  // These values are updated with proper values in oncreate.
-  // Y position of the panel container w.r.t. the client
-  private panelContainerTop = 0;
-  private panelContainerHeight = 0;
+  private readonly trace: TraceImpl;
+  private attrs: PanelContainerAttrs;
 
   // Updated every render cycle in the view() hook
   private panelById = new Map<string, Panel>();
@@ -109,6 +119,21 @@
   private readonly OVERLAY_REF = 'overlay';
   private readonly PANEL_STACK_REF = 'panel-stack';
 
+  constructor({attrs}: m.CVnode<PanelContainerAttrs>) {
+    this.attrs = attrs;
+    this.trace = attrs.trace;
+    const onRedraw = () => this.renderCanvas();
+    raf.addRedrawCallback(onRedraw);
+    this.trash.defer(() => {
+      raf.removeRedrawCallback(onRedraw);
+    });
+
+    perfDisplay.addContainer(this);
+    this.trash.defer(() => {
+      perfDisplay.removeContainer(this);
+    });
+  }
+
   getPanelsInRegion(
     startX: number,
     endX: number,
@@ -126,8 +151,8 @@
       if (
         realPosX + pos.width >= minX &&
         realPosX <= maxX &&
-        pos.clientY + pos.height >= minY &&
-        pos.clientY <= maxY &&
+        pos.absY + pos.height >= minY &&
+        pos.absY <= maxY &&
         pos.panel.selectable
       ) {
         panels.push(pos.panel);
@@ -139,77 +164,52 @@
   // 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 area = globals.timeline.selectedArea;
+    const {selectedYRange} = this.attrs;
+    const area = this.trace.timeline.selectedArea;
     if (
       area === undefined ||
-      globals.timeline.areaY.start === undefined ||
-      globals.timeline.areaY.end === undefined ||
+      selectedYRange === undefined ||
       this.panelInfos.length === 0
     ) {
       return;
     }
-    // Only get panels from the current panel container if the selection began
-    // in this container.
-    const panelContainerTop = this.panelInfos[0].clientY;
-    const panelContainerBottom =
-      this.panelInfos[this.panelInfos.length - 1].clientY +
-      this.panelInfos[this.panelInfos.length - 1].height;
-    if (
-      globals.timeline.areaY.start + TOPBAR_HEIGHT < panelContainerTop ||
-      globals.timeline.areaY.start + TOPBAR_HEIGHT > panelContainerBottom
-    ) {
-      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(
-      globals.timeline.visibleWindow,
-      new PxSpan(0, this.virtualCanvas!.size.width - TRACK_SHELL_WIDTH),
-    );
+    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),
-      globals.timeline.areaY.start + TOPBAR_HEIGHT,
-      globals.timeline.areaY.end + TOPBAR_HEIGHT,
+      selectedYRange.top,
+      selectedYRange.bottom,
     );
+
     // Get the track ids from the panels.
-    const tracks = [];
+    const trackUris: string[] = [];
     for (const panel of panels) {
-      if (panel.trackKey !== undefined) {
-        tracks.push(panel.trackKey);
-        continue;
-      }
-      if (panel.groupKey !== undefined) {
-        const trackGroup = globals.state.trackGroups[panel.groupKey];
-        // Only select a track group and all child tracks if it is closed.
-        if (trackGroup.collapsed) {
-          tracks.push(panel.groupKey);
-          for (const track of trackGroup.tracks) {
-            tracks.push(track);
+      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);
         }
       }
     }
-    globals.timeline.selectArea(area.start, area.end, tracks);
-  }
-
-  constructor() {
-    const onRedraw = () => this.renderCanvas();
-    raf.addRedrawCallback(onRedraw);
-    this.trash.defer(() => {
-      raf.removeRedrawCallback(onRedraw);
-    });
-
-    perfDisplay.addContainer(this);
-    this.trash.defer(() => {
-      perfDisplay.removeContainer(this);
-    });
+    this.trace.timeline.selectArea(area.start, area.end, trackUris);
   }
 
   private virtualCanvas?: VirtualCanvas;
@@ -264,12 +264,12 @@
     this.trash.dispose();
   }
 
-  renderPanel(node: Panel, panelId: string, extraClass = ''): m.Vnode {
+  renderPanel(node: Panel, panelId: string, htmlAttrs?: HTMLAttrs): m.Vnode {
     assertFalse(this.panelById.has(panelId));
     this.panelById.set(panelId, node);
     return m(
-      `.pf-panel${extraClass}`,
-      {'data-panel-id': panelId},
+      `.pf-panel`,
+      {...htmlAttrs, 'data-panel-id': panelId},
       node.render(),
     );
   }
@@ -279,13 +279,17 @@
   // 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',
-        this.renderPanel(
-          node.header,
-          `${panelId}-header`,
-          node.collapsed ? '' : '.pf-sticky',
-        ),
+        node.header &&
+          this.renderPanel(node.header, `${panelId}-header`, {
+            style: !node.collapsed && node.sticky ? style : {},
+          }),
         ...node.childPanels.map((child, index) =>
           this.renderTree(child, `${panelId}-${index}`),
         ),
@@ -295,6 +299,7 @@
   }
 
   view({attrs}: m.CVnode<PanelContainerAttrs>) {
+    this.attrs = attrs;
     this.panelById.clear();
     const children = attrs.panels.map((panel, index) =>
       this.renderTree(panel, `${index}`),
@@ -319,25 +324,23 @@
   private readPanelRectsFromDom(dom: Element): void {
     this.panelInfos = [];
 
+    const panel = dom.querySelectorAll('.pf-panel');
     const panels = assertExists(findRef(dom, this.PANEL_STACK_REF));
-    const domRect = panels.getBoundingClientRect();
-    this.panelContainerTop = domRect.y;
-    this.panelContainerHeight = domRect.height;
-
-    dom.querySelectorAll('.pf-panel').forEach((panelElement) => {
+    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 key = panel.trackKey || panel.groupKey || '';
       const rect = panelElement.getBoundingClientRect();
       this.panelInfos.push({
-        trackOrGroupKey: key,
+        trackNode: panel.trackNode,
         height: rect.height,
         width: rect.width,
         clientX: rect.x,
         clientY: rect.y,
+        absY: rect.y - top,
         panel,
       });
     });
@@ -361,7 +364,6 @@
     this.handleAreaSelection();
 
     const totalRenderedPanels = this.renderPanels(ctx, vc);
-
     this.drawTopLayerOnCanvas(ctx, vc);
 
     // Collect performance as the last thing we do.
@@ -377,10 +379,12 @@
     ctx: CanvasRenderingContext2D,
     vc: VirtualCanvas,
   ): number {
+    this.attrs.renderUnderlay?.(ctx, vc.size);
+
     let panelTop = 0;
     let totalOnCanvas = 0;
 
-    const flowEventsRendererArgs = new FlowEventsRendererArgs();
+    const renderedPanels = Array<RenderedPanelInfo>();
 
     for (let i = 0; i < this.panelInfos.length; i++) {
       const {
@@ -397,8 +401,6 @@
       };
       const panelSize = {width: panelWidth, height: panelHeight};
 
-      flowEventsRendererArgs.registerPanel(panel, panelTop, panelHeight);
-
       if (vc.overlapsCanvas(panelRect)) {
         totalOnCanvas++;
 
@@ -417,20 +419,20 @@
         ctx.restore();
       }
 
+      renderedPanels.push({
+        panel,
+        rect: {
+          top: panelTop,
+          bottom: panelTop + panelHeight,
+          left: 0,
+          right: panelWidth,
+        },
+      });
+
       panelTop += panelHeight;
     }
 
-    const flowEventsRenderer = new FlowEventsRenderer();
-
-    ctx.save();
-    ctx.translate(TRACK_SHELL_WIDTH, 0);
-    const trackSize = {
-      width: vc.size.width - TRACK_SHELL_WIDTH,
-      height: vc.size.height,
-    };
-    canvasClip(ctx, 0, 0, trackSize.width, trackSize.height);
-    flowEventsRenderer.render(ctx, flowEventsRendererArgs, trackSize);
-    ctx.restore();
+    this.attrs.renderOverlay?.(ctx, vc.size, renderedPanels);
 
     return totalOnCanvas;
   }
@@ -441,54 +443,43 @@
     ctx: CanvasRenderingContext2D,
     vc: VirtualCanvas,
   ): void {
-    const area = globals.timeline.selectedArea;
-    if (
-      area === undefined ||
-      globals.timeline.areaY.start === undefined ||
-      globals.timeline.areaY.end === undefined
-    ) {
+    const {selectedYRange} = this.attrs;
+    const area = this.trace.timeline.selectedArea;
+    if (area === undefined || selectedYRange === undefined) {
       return;
     }
-    if (this.panelInfos.length === 0 || area.tracks.length === 0) 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 = this.panelContainerHeight + this.panelContainerTop;
-    let selectedTracksMaxY = this.panelContainerTop;
-    let trackFromCurrentContainerSelected = false;
+    let selectedTracksMinY = selectedYRange.top;
+    let selectedTracksMaxY = selectedYRange.bottom;
     for (let i = 0; i < this.panelInfos.length; i++) {
-      if (area.tracks.includes(this.panelInfos[i].trackOrGroupKey)) {
-        trackFromCurrentContainerSelected = true;
+      const trackUri = this.panelInfos[i].trackNode?.uri;
+      if (trackUri && area.trackUris.includes(trackUri)) {
         selectedTracksMinY = Math.min(
           selectedTracksMinY,
-          this.panelInfos[i].clientY,
+          this.panelInfos[i].absY,
         );
         selectedTracksMaxY = Math.max(
           selectedTracksMaxY,
-          this.panelInfos[i].clientY + this.panelInfos[i].height,
+          this.panelInfos[i].absY + this.panelInfos[i].height,
         );
       }
     }
 
-    // No box should be drawn if there are no selected tracks in the current
-    // container.
-    if (!trackFromCurrentContainerSelected) {
-      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(
-      globals.timeline.visibleWindow,
-      new PxSpan(0, vc.size.width - TRACK_SHELL_WIDTH),
-    );
+    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);
-    // To align with where to draw on the canvas subtract the first panel Y.
-    selectedTracksMinY -= this.panelContainerTop;
-    selectedTracksMaxY -= this.panelContainerTop;
     ctx.save();
     ctx.strokeStyle = SELECTION_STROKE_COLOR;
     ctx.lineWidth = 1;
@@ -512,7 +503,7 @@
     panel: Panel,
     renderTime: number,
     ctx: CanvasRenderingContext2D,
-    size: Size,
+    size: Size2D,
   ) {
     if (!perfDebug()) return;
     let renderStats = this.panelPerfStats.get(panel);
diff --git a/ui/src/frontend/permalink.ts b/ui/src/frontend/permalink.ts
index 4d0aacb..b69916f 100644
--- a/ui/src/frontend/permalink.ts
+++ b/ui/src/frontend/permalink.ts
@@ -13,34 +13,26 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {assertExists} from '../base/logging';
-import {Actions} from '../common/actions';
-import {ConversionJobStatus} from '../common/conversion_jobs';
 import {
   JsonSerialize,
   parseAppState,
   serializeAppState,
-} from '../common/state_serialization';
+} from '../core/state_serialization';
 import {
   BUCKET_NAME,
   MIME_BINARY,
   MIME_JSON,
   GcsUploader,
-} from '../common/gcs_uploader';
-import {globals} from './globals';
-import {
-  publishConversionJobStatusUpdate,
-  publishPermalinkHash,
-} from './publish';
-import {Router} from './router';
-import {Optional} from '../base/utils';
+} from '../base/gcs_uploader';
 import {
   SERIALIZED_STATE_VERSION,
   SerializedAppState,
-} from '../common/state_serialization_schema';
+} from '../core/state_serialization_schema';
 import {z} from 'zod';
 import {showModal} from '../widgets/modal';
+import {AppImpl} from '../core/app_impl';
+import {CopyableLink} from '../widgets/copyable_link';
 
 // Permalink serialization has two layers:
 // 1. Serialization of the app state (state_serialization.ts):
@@ -63,81 +55,54 @@
   // 1. parseAppState() does further semantic checks (e.g. version checking).
   // 2. We want to still load the traceUrl even if the app state is invalid.
   appState: z.any().optional(),
-
-  // This is for the very unusual case of clicking on "Share settings" in the
-  // recording page. In this case there is no trace or app state. We just
-  // create a permalink with the recording state.
-  recordingOpts: z.any().optional(),
 });
 
 type PermalinkState = z.infer<typeof PERMALINK_SCHEMA>;
 
-export interface PermalinkOptions {
-  mode: 'APP_STATE' | 'RECORDING_OPTS';
-}
-
-export async function createPermalink(opts: PermalinkOptions): Promise<void> {
-  const jobName = 'create_permalink';
-  publishConversionJobStatusUpdate({
-    jobName,
-    jobStatus: ConversionJobStatus.InProgress,
-  });
-
-  try {
-    const hash = await createPermalinkInternal(opts);
-    publishPermalinkHash(hash);
-  } finally {
-    publishConversionJobStatusUpdate({
-      jobName,
-      jobStatus: ConversionJobStatus.NotRunning,
-    });
-  }
+export async function createPermalink(): Promise<void> {
+  const hash = await createPermalinkInternal();
+  showPermalinkDialog(hash);
 }
 
 // Returns the file name, not the full url (i.e. the name of the GCS object).
-async function createPermalinkInternal(
-  opts: PermalinkOptions,
-): Promise<string> {
+async function createPermalinkInternal(): Promise<string> {
   const permalinkData: PermalinkState = {};
 
-  if (opts.mode === 'RECORDING_OPTS') {
-    permalinkData.recordingOpts = globals.state.recordConfig;
-  } else if (opts.mode === 'APP_STATE') {
-    // Check if we need to upload the trace file, before serializing the app
-    // state.
-    let alreadyUploadedUrl = '';
-    const engine = assertExists(globals.getCurrentEngine());
-    let dataToUpload: File | ArrayBuffer | undefined = undefined;
-    let traceName = `trace ${engine.id}`;
-    if (engine.source.type === 'FILE') {
-      dataToUpload = engine.source.file;
-      traceName = dataToUpload.name;
-    } else if (engine.source.type === 'ARRAY_BUFFER') {
-      dataToUpload = engine.source.buffer;
-    } else if (engine.source.type === 'URL') {
-      alreadyUploadedUrl = engine.source.url;
-    } else {
-      throw new Error(`Cannot share trace ${JSON.stringify(engine.source)}`);
-    }
-
-    // Upload the trace file, unless it's already uploaded (type == 'URL').
-    // Internally TraceGcsUploader will skip the upload if an object with the
-    // same hash exists already.
-    if (alreadyUploadedUrl) {
-      permalinkData.traceUrl = alreadyUploadedUrl;
-    } else if (dataToUpload !== undefined) {
-      updateStatus(`Uploading ${traceName}`);
-      const uploader: GcsUploader = new GcsUploader(dataToUpload, {
-        mimeType: MIME_BINARY,
-        onProgress: () => reportUpdateProgress(uploader),
-      });
-      await uploader.waitForCompletion();
-      permalinkData.traceUrl = uploader.uploadedUrl;
-    }
-
-    permalinkData.appState = serializeAppState();
+  // 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';
+  if (traceSource.type === 'FILE') {
+    dataToUpload = traceSource.file;
+    traceName = dataToUpload.name;
+  } else if (traceSource.type === 'ARRAY_BUFFER') {
+    dataToUpload = traceSource.buffer;
+  } else if (traceSource.type === 'URL') {
+    alreadyUploadedUrl = traceSource.url;
+  } else {
+    throw new Error(`Cannot share trace ${JSON.stringify(traceSource)}`);
   }
 
+  // Upload the trace file, unless it's already uploaded (type == 'URL').
+  // Internally TraceGcsUploader will skip the upload if an object with the
+  // same hash exists already.
+  if (alreadyUploadedUrl) {
+    permalinkData.traceUrl = alreadyUploadedUrl;
+  } else if (dataToUpload !== undefined) {
+    updateStatus(`Uploading ${traceName}`);
+    const uploader: GcsUploader = new GcsUploader(dataToUpload, {
+      mimeType: MIME_BINARY,
+      onProgress: () => reportUpdateProgress(uploader),
+    });
+    await uploader.waitForCompletion();
+    permalinkData.traceUrl = uploader.uploadedUrl;
+  }
+
+  permalinkData.appState = serializeAppState(trace);
+
   // Serialize the permalink with the app state (or recording state) and upload.
   updateStatus(`Creating permalink...`);
   const permalinkJson = JsonSerialize(permalinkData);
@@ -184,28 +149,19 @@
     }
   }
 
-  if (permalink.recordingOpts !== undefined) {
-    // This permalink state only contains a RecordConfig. Show the
-    // recording page with the config, but keep other state as-is.
-    globals.dispatch(
-      Actions.setRecordConfig({config: permalink.recordingOpts}),
-    );
-    Router.navigate('#!/record');
-    return;
-  }
+  let serializedAppState: SerializedAppState | undefined = undefined;
   if (permalink.appState !== undefined) {
     // This is the most common case where the permalink contains the app state
-    // (and optionally a traceUrl, below). globals.restoreAppStateAfterTraceLoad
-    // will be processed by trace_controller.ts after the trace has loaded.
+    // (and optionally a traceUrl, below).
     const parseRes = parseAppState(permalink.appState);
     if (parseRes.success) {
-      globals.restoreAppStateAfterTraceLoad = parseRes.data;
+      serializedAppState = parseRes.data;
     } else {
       error = parseRes.error;
     }
   }
   if (permalink.traceUrl) {
-    globals.dispatch(Actions.openTraceFromUrl({url: permalink.traceUrl}));
+    AppImpl.instance.openTraceFromUrl(permalink.traceUrl, serializedAppState);
   }
 
   if (error) {
@@ -244,7 +200,7 @@
 // the trace URL.
 // If we suceed, convert it to a new-style JSON object preserving some minimal
 // information (really just vieport and pinned tracks).
-function tryLoadLegacyPermalink(data: unknown): Optional<PermalinkState> {
+function tryLoadLegacyPermalink(data: unknown): PermalinkState | undefined {
   const legacyData = data as {
     version?: number;
     engine?: {source?: {url?: string}};
@@ -282,11 +238,12 @@
 }
 
 function updateStatus(msg: string): void {
-  // TODO(hjd): Unify loading updates.
-  globals.dispatch(
-    Actions.updateStatus({
-      msg,
-      timestamp: Date.now() / 1000,
-    }),
-  );
+  AppImpl.instance.omnibox.showStatusMessage(msg);
+}
+
+function showPermalinkDialog(hash: string) {
+  showModal({
+    title: 'Permalink',
+    content: m(CopyableLink, {url: `${self.location.origin}/#!/?s=${hash}`}),
+  });
 }
diff --git a/ui/src/frontend/pivot_table.ts b/ui/src/frontend/pivot_table.ts
index f083fae..a096140 100644
--- a/ui/src/frontend/pivot_table.ts
+++ b/ui/src/frontend/pivot_table.ts
@@ -1,51 +1,54 @@
-/*
- * 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.
- */
+// 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 {SortDirection} from '../base/comparison_utils';
 import {sqliteString} from '../base/string_utils';
-import {Actions} from '../common/actions';
-import {DropDirection} from '../common/dragndrop_logic';
-import {COUNT_AGGREGATION} from '../common/empty_state';
-import {Area, PivotTableResult} from '../common/state';
-import {raf} from '../core/raf_scheduler';
-import {ColumnType} from '../trace_processor/query_result';
-
-import {globals} from './globals';
+import {DropDirection} from '../core/pivot_table_manager';
 import {
-  aggregationIndex,
-  areaFilters,
-  extractArgumentExpression,
-  sliceAggregationColumns,
-  tables,
-} from './pivot_table_query_generator';
-import {
+  PivotTableResult,
   Aggregation,
   AggregationFunction,
   columnKey,
   PivotTree,
   TableColumn,
-} from './pivot_table_types';
-import {PopupMenuButton, popupMenuIcon, PopupMenuItem} from './popup_menu';
+  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,
+  areaFilters,
+  sliceAggregationColumns,
+  tables,
+} from '../core/pivot_table_query_generator';
+import {
+  PopupMenuButton,
+  popupMenuIcon,
+  PopupMenuItem,
+} from '../widgets/popup_menu';
 import {ReorderableCell, ReorderableCellGroup} from './reorderable_cells';
 import {AttributeModalHolder} from './tables/attribute_modal_holder';
 import {DurationWidget} from './widgets/duration';
-import {addSqlTableTab} from './sql_table_tab';
-import {SqlTables} from './widgets/sql/table/well_known_sql_tables';
+import {getSqlTableDescription} from './widgets/sql/table/sql_table_registry';
+import {assertExists, assertFalse} from '../base/logging';
+import {Filter, SqlColumn} from './widgets/sql/table/column';
+import {argSqlColumn} from './widgets/sql/table/well_known_columns';
+import {TraceImpl} from '../core/trace_impl';
+import {PivotTableManager} from '../core/pivot_table_manager';
+import {extensions} from '../public/lib/extensions';
 
 interface PathItem {
   tree: PivotTree;
@@ -53,7 +56,8 @@
 }
 
 interface PivotTableAttrs {
-  selectionArea: Area;
+  trace: TraceImpl;
+  selectionArea: AreaSelection;
 }
 
 interface DrillFilter {
@@ -61,28 +65,30 @@
   value: ColumnType;
 }
 
-function drillFilterColumnName(column: TableColumn): string {
+function drillFilterColumnName(column: TableColumn): SqlColumn {
   switch (column.kind) {
     case 'argument':
-      return extractArgumentExpression(column.argument, SqlTables.slice.name);
+      return argSqlColumn('arg_set_id', column.argument);
     case 'regular':
       return `${column.column}`;
   }
 }
 
 // Convert DrillFilter to SQL condition to be used in WHERE clause.
-function renderDrillFilter(filter: DrillFilter): string {
+function renderDrillFilter(filter: DrillFilter): Filter {
   const column = drillFilterColumnName(filter.column);
-  if (filter.value === null) {
-    return `${column} IS NULL`;
-  } else if (typeof filter.value === 'number') {
-    return `${column} = ${filter.value}`;
-  } else if (filter.value instanceof Uint8Array) {
+  const value = filter.value;
+  if (value === null) {
+    return {op: (cols) => `${cols[0]} IS NULL`, columns: [column]};
+  } else if (typeof value === 'number' || typeof value === 'bigint') {
+    return {op: (cols) => `${cols[0]} = ${filter.value}`, columns: [column]};
+  } else if (value instanceof Uint8Array) {
     throw new Error(`BLOB as DrillFilter not implemented`);
-  } else if (typeof filter.value === 'bigint') {
-    return `${column} = ${filter.value}`;
   }
-  return `${column} = ${sqliteString(filter.value)}`;
+  return {
+    op: (cols) => `${cols[0]} = ${sqliteString(value)}`,
+    columns: [column],
+  };
 }
 
 function readableColumnName(column: TableColumn) {
@@ -102,28 +108,23 @@
 }
 
 export class PivotTable implements m.ClassComponent<PivotTableAttrs> {
-  constructor() {
-    this.attributeModalHolder = new AttributeModalHolder((arg) => {
-      globals.dispatch(
-        Actions.setPivotTablePivotSelected({
-          column: {kind: 'argument', argument: arg},
-          selected: true,
-        }),
-      );
-      globals.dispatch(
-        Actions.setPivotTableQueryRequested({queryRequested: true}),
-      );
-    });
+  private pivotMgr: PivotTableManager;
+
+  constructor({attrs}: m.CVnode<PivotTableAttrs>) {
+    this.pivotMgr = attrs.trace.pivotTable;
+    this.attributeModalHolder = new AttributeModalHolder((arg) =>
+      this.pivotMgr.setPivotSelected({
+        column: {kind: 'argument', argument: arg},
+        selected: true,
+      }),
+    );
   }
 
   get pivotState() {
-    return globals.state.nonSerializableState.pivotTable;
-  }
-  get constrainToArea() {
-    return globals.state.nonSerializableState.pivotTable.constrainToArea;
+    return this.pivotMgr.state;
   }
 
-  renderDrillDownCell(area: Area, filters: DrillFilter[]) {
+  renderDrillDownCell(attrs: PivotTableAttrs, filters: DrillFilter[]) {
     return m(
       'td',
       m(
@@ -132,11 +133,12 @@
           title: 'All corresponding slices',
           onclick: () => {
             const queryFilters = filters.map(renderDrillFilter);
-            if (this.constrainToArea) {
-              queryFilters.push(...areaFilters(area));
+            if (this.pivotState.constrainToArea) {
+              queryFilters.push(...areaFilters(attrs.selectionArea));
             }
-            addSqlTableTab({
-              table: SqlTables.slice,
+            extensions.addSqlTableTab(attrs.trace, {
+              table: assertExists(getSqlTableDescription('slice')),
+              // TODO(altimin): this should properly reference the required columns, but it works for now (until the pivot table is going to be rewritten to be more flexible).
               filters: queryFilters,
             });
           },
@@ -147,7 +149,7 @@
   }
 
   renderSectionRow(
-    area: Area,
+    attrs: PivotTableAttrs,
     path: PathItem[],
     tree: PivotTree,
     result: PivotTableResult,
@@ -190,7 +192,7 @@
       });
     }
 
-    renderedCells.push(this.renderDrillDownCell(area, drillFilters));
+    renderedCells.push(this.renderDrillDownCell(attrs, drillFilters));
     return m('tr', renderedCells);
   }
 
@@ -201,31 +203,33 @@
     ) {
       if (typeof value === 'bigint') {
         return m(DurationWidget, {dur: value});
+      } else if (typeof value === 'number') {
+        return m(DurationWidget, {dur: BigInt(Math.round(value))});
       }
     }
     return `${value}`;
   }
 
   renderTree(
-    area: Area,
+    attrs: PivotTableAttrs,
     path: PathItem[],
     tree: PivotTree,
     result: PivotTableResult,
     sink: m.Vnode[],
   ) {
     if (tree.isCollapsed) {
-      sink.push(this.renderSectionRow(area, path, tree, result));
+      sink.push(this.renderSectionRow(attrs, path, tree, result));
       return;
     }
     if (tree.children.size > 0) {
       // Avoid rendering the intermediate results row for the root of tree
       // and in case there's only one child subtree.
       if (!tree.isCollapsed && path.length > 0 && tree.children.size !== 1) {
-        sink.push(this.renderSectionRow(area, path, tree, result));
+        sink.push(this.renderSectionRow(attrs, path, tree, result));
       }
       for (const [key, childTree] of tree.children.entries()) {
         path.push({tree: childTree, nextKey: key});
-        this.renderTree(area, path, childTree, result, sink);
+        this.renderTree(attrs, path, childTree, result, sink);
         path.pop();
       }
       return;
@@ -234,7 +238,7 @@
     // Avoid rendering the intermediate results row if it has only one leaf
     // row.
     if (!tree.isCollapsed && path.length > 0 && tree.rows.length > 1) {
-      sink.push(this.renderSectionRow(area, path, tree, result));
+      sink.push(this.renderSectionRow(attrs, path, tree, result));
     }
     for (const row of tree.rows) {
       const renderedCells = [];
@@ -261,7 +265,7 @@
         renderedCells.push(m('td.aggregation' + markFirst(j), renderedValue));
       }
 
-      renderedCells.push(this.renderDrillDownCell(area, drillFilters));
+      renderedCells.push(this.renderDrillDownCell(attrs, drillFilters));
       sink.push(m('tr', renderedCells));
     }
   }
@@ -290,16 +294,12 @@
   }
 
   sortingItem(aggregationIndex: number, order: SortDirection): PopupMenuItem {
+    const pivotMgr = this.pivotMgr;
     return {
       itemType: 'regular',
       text: order === 'DESC' ? 'Highest first' : 'Lowest first',
       callback() {
-        globals.dispatch(
-          Actions.setPivotTableSortColumn({aggregationIndex, order}),
-        );
-        globals.dispatch(
-          Actions.setPivotTableQueryRequested({queryRequested: true}),
-        );
+        pivotMgr.setSortColumn(aggregationIndex, order);
       },
     };
   }
@@ -321,14 +321,7 @@
     return {
       itemType: 'regular',
       text: nameOverride ?? readableColumnName(aggregation.column),
-      callback: () => {
-        globals.dispatch(
-          Actions.addPivotTableAggregation({aggregation, after: index}),
-        );
-        globals.dispatch(
-          Actions.setPivotTableQueryRequested({queryRequested: true}),
-        );
-      },
+      callback: () => this.pivotMgr.addAggregation(aggregation, index),
     };
   }
 
@@ -366,7 +359,6 @@
     removeItem: boolean,
   ): ReorderableCell {
     const popupItems: PopupMenuItem[] = [];
-    const state = globals.state.nonSerializableState.pivotTable;
     if (aggregation.sortDirection === undefined) {
       popupItems.push(
         this.sortingItem(index, 'DESC'),
@@ -388,20 +380,12 @@
         if (aggregation.aggregationFunction === otherAgg) {
           continue;
         }
-
+        const pivotMgr = this.pivotMgr;
         popupItems.push({
           itemType: 'regular',
           text: otherAgg,
           callback() {
-            globals.dispatch(
-              Actions.setPivotTableAggregationFunction({
-                index,
-                function: otherAgg,
-              }),
-            );
-            globals.dispatch(
-              Actions.setPivotTableQueryRequested({queryRequested: true}),
-            );
+            pivotMgr.setAggregationFunction(index, otherAgg);
           },
         });
       }
@@ -411,17 +395,12 @@
       popupItems.push({
         itemType: 'regular',
         text: 'Remove',
-        callback: () => {
-          globals.dispatch(Actions.removePivotTableAggregation({index}));
-          globals.dispatch(
-            Actions.setPivotTableQueryRequested({queryRequested: true}),
-          );
-        },
+        callback: () => this.pivotMgr.removeAggregation(index),
       });
     }
 
     let hasCount = false;
-    for (const agg of state.selectedAggregations.values()) {
+    for (const agg of this.pivotState.selectedAggregations.values()) {
       if (agg.aggregationFunction === 'COUNT') {
         hasCount = true;
       }
@@ -438,7 +417,7 @@
     }
 
     const sliceAggregationsItem = this.aggregationPopupTableGroup(
-      SqlTables.slice.name,
+      assertExists(getSqlTableDescription('slice')).name,
       sliceAggregationColumns,
       index,
     );
@@ -465,6 +444,7 @@
     pivot: TableColumn,
     selectedPivots: Set<string>,
   ): ReorderableCell {
+    const pivotMgr = this.pivotMgr;
     const items: PopupMenuItem[] = [
       {
         itemType: 'regular',
@@ -479,15 +459,7 @@
         itemType: 'regular',
         text: 'Remove',
         callback() {
-          globals.dispatch(
-            Actions.setPivotTablePivotSelected({
-              column: pivot,
-              selected: false,
-            }),
-          );
-          globals.dispatch(
-            Actions.setPivotTableQueryRequested({queryRequested: true}),
-          );
+          pivotMgr.setPivotSelected({column: pivot, selected: false});
         },
       });
     }
@@ -503,17 +475,11 @@
         if (selectedPivots.has(columnKey(column))) {
           continue;
         }
-
         group.push({
           itemType: 'regular',
           text: columnName,
           callback() {
-            globals.dispatch(
-              Actions.setPivotTablePivotSelected({column, selected: true}),
-            );
-            globals.dispatch(
-              Actions.setPivotTableQueryRequested({queryRequested: true}),
-            );
+            pivotMgr.setPivotSelected({column, selected: true});
           },
         });
       }
@@ -534,27 +500,19 @@
   }
 
   renderResultsTable(attrs: PivotTableAttrs) {
-    const state = globals.state.nonSerializableState.pivotTable;
-    if (state.queryResult === null) {
+    const state = this.pivotState;
+    const queryResult = state.queryResult;
+    if (queryResult === undefined) {
       return m('div', 'Loading...');
     }
-    const queryResult: PivotTableResult = state.queryResult;
 
     const renderedRows: m.Vnode[] = [];
-    const tree = state.queryResult.tree;
 
-    if (tree.children.size === 0 && tree.rows.length === 0) {
-      // Empty result, render a special message
-      return m('.empty-result', 'No slices in the current selection.');
-    }
+    // We should not even be showing the tab if there's no results.
+    const tree = queryResult.tree;
+    assertFalse(tree.children.size === 0 && tree.rows.length === 0);
 
-    this.renderTree(
-      attrs.selectionArea,
-      [],
-      tree,
-      state.queryResult,
-      renderedRows,
-    );
+    this.renderTree(attrs, [], tree, queryResult, renderedRows);
 
     const selectedPivots = new Set(
       this.pivotState.selectedPivots.map(columnKey),
@@ -563,11 +521,11 @@
       this.renderPivotColumnHeader(queryResult, pivot, selectedPivots),
     );
 
-    const removeItem = state.queryResult.metadata.aggregationColumns.length > 1;
-    const aggregationTableHeaders =
-      state.queryResult.metadata.aggregationColumns.map((aggregation, index) =>
+    const removeItem = queryResult.metadata.aggregationColumns.length > 1;
+    const aggregationTableHeaders = queryResult.metadata.aggregationColumns.map(
+      (aggregation, index) =>
         this.renderAggregationHeaderCell(aggregation, index, removeItem),
-      );
+    );
 
     return m(
       'table.pivot-table',
@@ -582,23 +540,13 @@
           m(ReorderableCellGroup, {
             cells: pivotTableHeaders,
             onReorder: (from: number, to: number, direction: DropDirection) => {
-              globals.dispatch(
-                Actions.changePivotTablePivotOrder({from, to, direction}),
-              );
-              globals.dispatch(
-                Actions.setPivotTableQueryRequested({queryRequested: true}),
-              );
+              this.pivotMgr.setOrder(from, to, direction);
             },
           }),
           m(ReorderableCellGroup, {
             cells: aggregationTableHeaders,
             onReorder: (from: number, to: number, direction: DropDirection) => {
-              globals.dispatch(
-                Actions.changePivotTableAggregationOrder({from, to, direction}),
-              );
-              globals.dispatch(
-                Actions.setPivotTableQueryRequested({queryRequested: true}),
-              );
+              this.pivotMgr.setAggregationOrder(from, to, direction);
             },
           }),
           m(
@@ -612,16 +560,7 @@
                     ? 'Query data for the whole timeline'
                     : 'Constrain to selected area',
                   callback: () => {
-                    globals.dispatch(
-                      Actions.setPivotTableConstrainToArea({
-                        constrain: !state.constrainToArea,
-                      }),
-                    );
-                    globals.dispatch(
-                      Actions.setPivotTableQueryRequested({
-                        queryRequested: true,
-                      }),
-                    );
+                    this.pivotMgr.setConstrainedToArea(!state.constrainToArea);
                   },
                 },
               ],
@@ -629,7 +568,7 @@
           ),
         ),
       ),
-      m('tbody', this.renderTotalsRow(state.queryResult), renderedRows),
+      m('tbody', this.renderTotalsRow(queryResult), renderedRows),
     );
   }
 
diff --git a/ui/src/frontend/pivot_table_argument_popup.ts b/ui/src/frontend/pivot_table_argument_popup.ts
index 949200e..a4d67c2 100644
--- a/ui/src/frontend/pivot_table_argument_popup.ts
+++ b/ui/src/frontend/pivot_table_argument_popup.ts
@@ -1,21 +1,18 @@
-/*
- * 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.
- */
+// 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 {raf} from '../core/raf_scheduler';
 
 interface ArgumentPopupArgs {
diff --git a/ui/src/frontend/pivot_table_query_generator.ts b/ui/src/frontend/pivot_table_query_generator.ts
deleted file mode 100644
index f721165..0000000
--- a/ui/src/frontend/pivot_table_query_generator.ts
+++ /dev/null
@@ -1,176 +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 {sqliteString} from '../base/string_utils';
-import {Area, PivotTableQuery, PivotTableState} from '../common/state';
-import {getSelectedTrackKeys} from '../controller/aggregation/slice_aggregation_controller';
-
-import {Aggregation, TableColumn} from './pivot_table_types';
-import {SqlTables} from './widgets/sql/table/well_known_sql_tables';
-
-export interface Table {
-  name: string;
-  displayName: string;
-  columns: string[];
-}
-
-export const sliceTable = {
-  name: SqlTables.slice.name,
-  displayName: 'slice',
-  columns: [
-    'type',
-    'ts',
-    'dur',
-    'category',
-    'name',
-    'depth',
-    'pid',
-    'process_name',
-    'tid',
-    'thread_name',
-  ],
-};
-
-// Columns of `slice` table available for aggregation.
-export const sliceAggregationColumns = [
-  'ts',
-  'dur',
-  'depth',
-  'thread_ts',
-  'thread_dur',
-  'thread_instruction_count',
-  'thread_instruction_delta',
-];
-
-// List of available tables to query, used to populate selectors of pivot
-// columns in the UI.
-export const tables: Table[] = [sliceTable];
-
-// Queried "table column" is either:
-// 1. A real one, represented as object with table and column name.
-// 2. Pseudo-column 'count' that's rendered as '1' in SQL to use in queries like
-// `select sum(1), name from slice group by name`.
-
-export interface RegularColumn {
-  kind: 'regular';
-  table: string;
-  column: string;
-}
-
-export interface ArgumentColumn {
-  kind: 'argument';
-  argument: string;
-}
-
-// Exception thrown by query generator in case incoming parameters are not
-// suitable in order to build a correct query; these are caught by the UI and
-// displayed to the user.
-export class QueryGeneratorError extends Error {}
-
-// Internal column name for different rollover levels of aggregate columns.
-function aggregationAlias(aggregationIndex: number): string {
-  return `agg_${aggregationIndex}`;
-}
-
-export function areaFilters(area: Area): string[] {
-  return [
-    `ts + dur > ${area.start}`,
-    `ts < ${area.end}`,
-    `track_id in (${getSelectedTrackKeys(area).join(', ')})`,
-  ];
-}
-
-export function expression(column: TableColumn): string {
-  switch (column.kind) {
-    case 'regular':
-      return `${column.table}.${column.column}`;
-    case 'argument':
-      return extractArgumentExpression(column.argument, SqlTables.slice.name);
-  }
-}
-
-function aggregationExpression(aggregation: Aggregation): string {
-  if (aggregation.aggregationFunction === 'COUNT') {
-    return 'COUNT()';
-  }
-  return `${aggregation.aggregationFunction}(${expression(
-    aggregation.column,
-  )})`;
-}
-
-export function extractArgumentExpression(argument: string, table?: string) {
-  const prefix = table === undefined ? '' : `${table}.`;
-  return `extract_arg(${prefix}arg_set_id, ${sqliteString(argument)})`;
-}
-
-export function aggregationIndex(pivotColumns: number, aggregationNo: number) {
-  return pivotColumns + aggregationNo;
-}
-
-export function generateQueryFromState(
-  state: PivotTableState,
-): PivotTableQuery {
-  if (state.selectionArea === undefined) {
-    throw new QueryGeneratorError('Should not be called without area');
-  }
-
-  const sliceTableAggregations = [...state.selectedAggregations.values()];
-  if (sliceTableAggregations.length === 0) {
-    throw new QueryGeneratorError('No aggregations selected');
-  }
-
-  const pivots = state.selectedPivots;
-
-  const aggregations = sliceTableAggregations.map(
-    (agg, index) =>
-      `${aggregationExpression(agg)} as ${aggregationAlias(index)}`,
-  );
-  const countIndex = aggregations.length;
-  // Extra count aggregation, needed in order to compute combined averages.
-  aggregations.push('COUNT() as hidden_count');
-
-  const renderedPivots = pivots.map(expression);
-  const sortClauses: string[] = [];
-  for (let i = 0; i < sliceTableAggregations.length; i++) {
-    const sortDirection = sliceTableAggregations[i].sortDirection;
-    if (sortDirection !== undefined) {
-      sortClauses.push(`${aggregationAlias(i)} ${sortDirection}`);
-    }
-  }
-
-  const whereClause = state.constrainToArea
-    ? `where ${areaFilters(state.selectionArea).join(' and\n')}`
-    : '';
-  const text = `
-    INCLUDE PERFETTO MODULE slices.slices;
-
-    select
-      ${renderedPivots.concat(aggregations).join(',\n')}
-    from ${SqlTables.slice.name}
-    ${whereClause}
-    group by ${renderedPivots.join(', ')}
-    ${sortClauses.length > 0 ? 'order by ' + sortClauses.join(', ') : ''}
-  `;
-
-  return {
-    text,
-    metadata: {
-      pivotColumns: pivots,
-      aggregationColumns: sliceTableAggregations,
-      countIndex,
-    },
-  };
-}
diff --git a/ui/src/frontend/pivot_table_types.ts b/ui/src/frontend/pivot_table_types.ts
deleted file mode 100644
index 76cecb4..0000000
--- a/ui/src/frontend/pivot_table_types.ts
+++ /dev/null
@@ -1,118 +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 {SortDirection} from '../base/comparison_utils';
-import {EqualsBuilder} from '../common/comparator_builder';
-import {ColumnType} from '../trace_processor/query_result';
-
-// Node in the hierarchical pivot tree. Only leaf nodes contain data from the
-// query result.
-export interface PivotTree {
-  // Whether the node should be collapsed in the UI, false by default and can
-  // be toggled with the button.
-  isCollapsed: boolean;
-
-  // Non-empty only in internal nodes.
-  children: Map<ColumnType, PivotTree>;
-  aggregates: ColumnType[];
-
-  // Non-empty only in leaf nodes.
-  rows: ColumnType[][];
-}
-
-export type AggregationFunction = 'COUNT' | 'SUM' | 'MIN' | 'MAX' | 'AVG';
-
-// Queried "table column" is either:
-// 1. A real one, represented as object with table and column name.
-// 2. Pseudo-column 'count' that's rendered as '1' in SQL to use in queries like
-// `select sum(1), name from slice group by name`.
-
-export interface RegularColumn {
-  kind: 'regular';
-  table: string;
-  column: string;
-}
-
-export interface ArgumentColumn {
-  kind: 'argument';
-  argument: string;
-}
-
-export type TableColumn = RegularColumn | ArgumentColumn;
-
-export function tableColumnEquals(t1: TableColumn, t2: TableColumn): boolean {
-  switch (t1.kind) {
-    case 'argument': {
-      return t2.kind === 'argument' && t1.argument === t2.argument;
-    }
-    case 'regular': {
-      return (
-        t2.kind === 'regular' &&
-        t1.table === t2.table &&
-        t1.column === t2.column
-      );
-    }
-  }
-}
-
-export function toggleEnabled<T>(
-  compare: (fst: T, snd: T) => boolean,
-  arr: T[],
-  column: T,
-  enabled: boolean,
-): void {
-  if (enabled && arr.find((value) => compare(column, value)) === undefined) {
-    arr.push(column);
-  }
-  if (!enabled) {
-    const index = arr.findIndex((value) => compare(column, value));
-    if (index !== -1) {
-      arr.splice(index, 1);
-    }
-  }
-}
-
-export interface Aggregation {
-  aggregationFunction: AggregationFunction;
-  column: TableColumn;
-
-  // If the aggregation is sorted, the field contains a sorting direction.
-  sortDirection?: SortDirection;
-}
-
-export function aggregationEquals(agg1: Aggregation, agg2: Aggregation) {
-  return new EqualsBuilder(agg1, agg2)
-    .comparePrimitive((agg) => agg.aggregationFunction)
-    .compare(tableColumnEquals, (agg) => agg.column)
-    .equals();
-}
-
-// Used to convert TableColumn to a string in order to store it in a Map, as
-// ES6 does not support compound Set/Map keys. This function should only be used
-// for interning keys, and does not have any requirements beyond different
-// TableColumn objects mapping to different strings.
-export function columnKey(tableColumn: TableColumn): string {
-  switch (tableColumn.kind) {
-    case 'argument': {
-      return `argument:${tableColumn.argument}`;
-    }
-    case 'regular': {
-      return `${tableColumn.table}.${tableColumn.column}`;
-    }
-  }
-}
-
-export function aggregationKey(aggregation: Aggregation): string {
-  return `${aggregation.aggregationFunction}:${columnKey(aggregation.column)}`;
-}
diff --git a/ui/src/frontend/plugins_page.ts b/ui/src/frontend/plugins_page.ts
deleted file mode 100644
index ed2b2f5..0000000
--- a/ui/src/frontend/plugins_page.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {pluginManager, pluginRegistry} from '../common/plugins';
-import {raf} from '../core/raf_scheduler';
-import {Button} from '../widgets/button';
-
-import {exists} from '../base/utils';
-import {PluginDescriptor} from '../public';
-import {createPage} from './pages';
-import {defaultPlugins} from '../core/default_plugins';
-import {Intent} from '../widgets/common';
-
-export const PluginsPage = createPage({
-  view() {
-    return m(
-      '.pf-plugins-page',
-      m('h1', 'Plugins'),
-      m(
-        '.pf-plugins-topbar',
-        m(Button, {
-          intent: Intent.Primary,
-          label: 'Disable All',
-          onclick: async () => {
-            for (const plugin of pluginRegistry.values()) {
-              await pluginManager.disablePlugin(plugin.pluginId, true);
-              raf.scheduleFullRedraw();
-            }
-          },
-        }),
-        m(Button, {
-          intent: Intent.Primary,
-          label: 'Enable All',
-          onclick: async () => {
-            for (const plugin of pluginRegistry.values()) {
-              await pluginManager.enablePlugin(plugin.pluginId, true);
-              raf.scheduleFullRedraw();
-            }
-          },
-        }),
-        m(Button, {
-          intent: Intent.Primary,
-          label: 'Restore Defaults',
-          onclick: async () => {
-            await pluginManager.restoreDefaults(true);
-            raf.scheduleFullRedraw();
-          },
-        }),
-      ),
-      m(
-        '.pf-plugins-grid',
-        [
-          m('span', 'Plugin'),
-          m('span', 'Default?'),
-          m('span', 'Enabled?'),
-          m('span', 'Active?'),
-          m('span', 'Control'),
-          m('span', 'Load Time'),
-        ],
-        Array.from(pluginRegistry.values()).map((plugin) => {
-          return renderPluginRow(plugin);
-        }),
-      ),
-    );
-  },
-});
-
-function renderPluginRow(plugin: PluginDescriptor): m.Children {
-  const pluginId = plugin.pluginId;
-  const isDefault = defaultPlugins.includes(pluginId);
-  const pluginDetails = pluginManager.plugins.get(pluginId);
-  const isActive = pluginManager.isActive(pluginId);
-  const isEnabled = pluginManager.isEnabled(pluginId);
-  const loadTime = pluginDetails?.previousOnTraceLoadTimeMillis;
-  return [
-    m('span', pluginId),
-    m('span', isDefault ? 'Yes' : 'No'),
-    isEnabled
-      ? m('.pf-tag.pf-active', 'Enabled')
-      : m('.pf-tag.pf-inactive', 'Disabled'),
-    isActive
-      ? m('.pf-tag.pf-active', 'Active')
-      : m('.pf-tag.pf-inactive', 'Inactive'),
-    m(Button, {
-      label: isActive ? 'Disable' : 'Enable',
-      intent: Intent.Primary,
-      onclick: async () => {
-        if (isActive) {
-          await pluginManager.disablePlugin(pluginId, true);
-        } else {
-          await pluginManager.enablePlugin(pluginId, true);
-        }
-        raf.scheduleFullRedraw();
-      },
-    }),
-    exists(loadTime) ? m('span', `${loadTime.toFixed(1)} ms`) : m('span', `-`),
-  ];
-}
diff --git a/ui/src/frontend/popup_menu.ts b/ui/src/frontend/popup_menu.ts
deleted file mode 100644
index ada362f..0000000
--- a/ui/src/frontend/popup_menu.ts
+++ /dev/null
@@ -1,199 +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 {SortDirection} from '../base/comparison_utils';
-import {raf} from '../core/raf_scheduler';
-
-export interface RegularPopupMenuItem {
-  itemType: 'regular';
-  // Display text
-  text: string;
-  // Action on menu item click
-  callback: () => void;
-}
-
-// Helper function for simplifying defining menus.
-export function menuItem(
-  text: string,
-  action: () => void,
-): RegularPopupMenuItem {
-  return {
-    itemType: 'regular',
-    text,
-    callback: action,
-  };
-}
-
-export interface GroupPopupMenuItem {
-  itemType: 'group';
-  text: string;
-  itemId: string;
-  children: PopupMenuItem[];
-}
-
-export type PopupMenuItem = RegularPopupMenuItem | GroupPopupMenuItem;
-
-export interface PopupMenuButtonAttrs {
-  // Icon for button opening a menu
-  icon: string;
-  // List of popup menu items
-  items: PopupMenuItem[];
-}
-
-// To ensure having at most one popup menu on the screen at a time, we need to
-// listen to click events on the whole page and close currently opened popup, if
-// there's any. This class, used as a singleton, does exactly that.
-class PopupHolder {
-  // Invariant: global listener should be register if and only if this.popup is
-  // not undefined.
-  popup: PopupMenuButton | undefined = undefined;
-  initialized = false;
-  listener: (e: MouseEvent) => void;
-
-  constructor() {
-    this.listener = (e: MouseEvent) => {
-      // Only handle those events that are not part of dropdown menu themselves.
-      const hasDropdown =
-        e.composedPath().find(PopupHolder.isDropdownElement) !== undefined;
-      if (!hasDropdown) {
-        this.ensureHidden();
-      }
-    };
-  }
-
-  static isDropdownElement(target: EventTarget) {
-    if (target instanceof HTMLElement) {
-      return target.tagName === 'DIV' && target.classList.contains('dropdown');
-    }
-    return false;
-  }
-
-  ensureHidden() {
-    if (this.popup !== undefined) {
-      this.popup.setVisible(false);
-    }
-  }
-
-  clear() {
-    if (this.popup !== undefined) {
-      this.popup = undefined;
-      window.removeEventListener('click', this.listener);
-    }
-  }
-
-  showPopup(popup: PopupMenuButton) {
-    this.ensureHidden();
-    this.popup = popup;
-    window.addEventListener('click', this.listener);
-  }
-}
-
-// Singleton instance of PopupHolder
-const popupHolder = new PopupHolder();
-
-// For a table column that can be sorted; the standard popup icon should
-// reflect the current sorting direction. This function returns an icon
-// corresponding to optional SortDirection according to which the column is
-// sorted. (Optional because column might be unsorted)
-export function popupMenuIcon(sortDirection?: SortDirection) {
-  switch (sortDirection) {
-    case undefined:
-      return 'more_horiz';
-    case 'DESC':
-      return 'arrow_drop_down';
-    case 'ASC':
-      return 'arrow_drop_up';
-  }
-}
-
-// Component that displays a button that shows a popup menu on click.
-export class PopupMenuButton implements m.ClassComponent<PopupMenuButtonAttrs> {
-  popupShown = false;
-  expandedGroups: Set<string> = new Set();
-
-  setVisible(visible: boolean) {
-    this.popupShown = visible;
-    if (this.popupShown) {
-      popupHolder.showPopup(this);
-    } else {
-      popupHolder.clear();
-    }
-    raf.scheduleFullRedraw();
-  }
-
-  renderItem(item: PopupMenuItem): m.Child {
-    switch (item.itemType) {
-      case 'regular':
-        return m(
-          'button.open-menu',
-          {
-            onclick: () => {
-              item.callback();
-              // Hide the menu item after the action has been invoked
-              this.setVisible(false);
-            },
-          },
-          item.text,
-        );
-      case 'group':
-        const isExpanded = this.expandedGroups.has(item.itemId);
-        return m(
-          'div',
-          m(
-            'button.open-menu.disallow-selection',
-            {
-              onclick: () => {
-                if (this.expandedGroups.has(item.itemId)) {
-                  this.expandedGroups.delete(item.itemId);
-                } else {
-                  this.expandedGroups.add(item.itemId);
-                }
-                raf.scheduleFullRedraw();
-              },
-            },
-            // Show text with up/down arrow, depending on expanded state.
-            item.text + (isExpanded ? ' \u25B2' : ' \u25BC'),
-          ),
-          isExpanded
-            ? m(
-                'div.nested-menu',
-                item.children.map((item) => this.renderItem(item)),
-              )
-            : null,
-        );
-    }
-  }
-
-  view(vnode: m.Vnode<PopupMenuButtonAttrs, this>) {
-    return m(
-      '.dropdown',
-      m(
-        '.dropdown-button',
-        {
-          onclick: () => {
-            this.setVisible(!this.popupShown);
-          },
-        },
-        vnode.children,
-        m('i.material-icons', vnode.attrs.icon),
-      ),
-      m(
-        this.popupShown ? '.popup-menu.opened' : '.popup-menu.closed',
-        vnode.attrs.items.map((item) => this.renderItem(item)),
-      ),
-    );
-  }
-}
diff --git a/ui/src/frontend/post_message_handler.ts b/ui/src/frontend/post_message_handler.ts
index cb82ddd..f04bea1 100644
--- a/ui/src/frontend/post_message_handler.ts
+++ b/ui/src/frontend/post_message_handler.ts
@@ -13,15 +13,13 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {Time} from '../base/time';
-import {Actions, PostedScrollToRange, PostedTrace} from '../common/actions';
+import {PostedTrace} from '../core/trace_source';
 import {showModal} from '../widgets/modal';
-
 import {initCssConstants} from './css_constants';
-import {globals} from './globals';
 import {toggleHelp} from './help_modal';
-import {focusHorizontalRange} from './scroll_helper';
+import {scrollTo} from '../public/scroll_helper';
+import {AppImpl} from '../core/app_impl';
 
 const TRUSTED_ORIGINS_KEY = 'trustedOrigins';
 
@@ -33,6 +31,12 @@
   perfetto: PostedScrollToRange;
 }
 
+interface PostedScrollToRange {
+  timeStart: number;
+  timeEnd: number;
+  viewPercentage?: number;
+}
+
 // Returns whether incoming traces should be opened automatically or should
 // instead require a user interaction.
 export function isTrustedOrigin(origin: string): boolean {
@@ -206,7 +210,7 @@
     // For external traces, we need to disable other features such as
     // downloading and sharing a trace.
     postedTrace.localOnly = true;
-    globals.dispatch(Actions.openTraceFromBuffer(postedTrace));
+    AppImpl.instance.openTraceFromBuffer(postedTrace);
   };
 
   const trustAndOpenTrace = () => {
@@ -262,16 +266,12 @@
   return str.replace(/[^A-Za-z0-9.\-_#:/?=&;%+$ ]/g, ' ');
 }
 
-function isTraceViewerReady(): boolean {
-  return !!globals.getCurrentEngine()?.ready;
-}
-
 const _maxScrollToRangeAttempts = 20;
 async function scrollToTimeRange(
   postedScrollToRange: PostedScrollToRange,
   maxAttempts?: number,
 ) {
-  const ready = isTraceViewerReady();
+  const ready = AppImpl.instance.trace && !AppImpl.instance.isLoadingTrace;
   if (!ready) {
     if (maxAttempts === undefined) {
       maxAttempts = 0;
@@ -284,7 +284,9 @@
   } else {
     const start = Time.fromSeconds(postedScrollToRange.timeStart);
     const end = Time.fromSeconds(postedScrollToRange.timeEnd);
-    focusHorizontalRange(start, end, postedScrollToRange.viewPercentage);
+    scrollTo({
+      time: {start, end, viewPercentage: postedScrollToRange.viewPercentage},
+    });
   }
 }
 
diff --git a/ui/src/frontend/process_details_tab.ts b/ui/src/frontend/process_details_tab.ts
new file mode 100644
index 0000000..c457408
--- /dev/null
+++ b/ui/src/frontend/process_details_tab.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 m from 'mithril';
+import {Tab} from '../public/tab';
+import {Upid} from '../trace_processor/sql_utils/core_types';
+import {DetailsShell} from '../widgets/details_shell';
+import {GridLayout, GridLayoutColumn} from '../widgets/grid_layout';
+import {Section} from '../widgets/section';
+import {Details, DetailsSchema} from './widgets/sql/details/details';
+import d = DetailsSchema;
+import {Trace} from '../public/trace';
+
+export class ProcessDetailsTab implements Tab {
+  private data: Details;
+
+  // TODO(altimin): Ideally, we would not require the pid to be passed in, but
+  // fetch it from the underlying data instead.
+  //
+  // However, the only place which creates `ProcessDetailsTab` currently is `renderProcessRef`,
+  // which already has `pid` available (note that Details is already fetching the data, including
+  // the `pid` from the trace processor, but it doesn't expose it for now).
+  constructor(private args: {trace: Trace; upid: Upid; pid?: number}) {
+    this.data = new Details(args.trace, 'process', args.upid, {
+      'pid': d.Value('pid'),
+      'Name': d.Value('name'),
+      'Start time': d.Timestamp('start_ts', {skipIfNull: true}),
+      'End time': d.Timestamp('end_ts', {skipIfNull: true}),
+      'Parent process': d.SqlIdRef('process', 'parent_upid', {
+        skipIfNull: true,
+      }),
+      'User ID': d.Value('uid', {skipIfNull: true}),
+      'Android app ID': d.Value('android_appid', {skipIfNull: true}),
+      'Command line': d.Value('cmdline', {skipIfNull: true}),
+      'Machine id': d.Value('machine_id', {skipIfNull: true}),
+      'Args': d.ArgSetId('arg_set_id'),
+    });
+  }
+
+  render() {
+    return m(
+      DetailsShell,
+      {
+        title: this.getTitle(),
+      },
+      m(
+        GridLayout,
+        m(GridLayoutColumn, m(Section, {title: 'Details'}, this.data.render())),
+      ),
+    );
+  }
+
+  getTitle(): string {
+    if (this.args.pid !== undefined) {
+      return `Process ${this.args.pid}`;
+    }
+    return `Process upid:${this.args.upid}`;
+  }
+}
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
deleted file mode 100644
index 84158c0..0000000
--- a/ui/src/frontend/publish.ts
+++ /dev/null
@@ -1,192 +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 {Actions} from '../common/actions';
-import {AggregateData} from '../common/aggregation_data';
-import {ConversionJobStatusUpdate} from '../common/conversion_jobs';
-import {MetricResult} from '../common/metric_data';
-import {CurrentSearchResults} from '../common/search_data';
-import {raf} from '../core/raf_scheduler';
-import {HttpRpcState} from '../trace_processor/http_rpc_engine';
-import {getLegacySelection} from '../common/state';
-
-import {
-  CpuProfileDetails,
-  Flow,
-  globals,
-  QuantizedLoad,
-  SliceDetails,
-  ThreadDesc,
-  ThreadStateDetails,
-} from './globals';
-import {findCurrentSelection} from './keyboard_event_handler';
-
-export function publishOverviewData(data: {
-  [key: string]: QuantizedLoad | QuantizedLoad[];
-}) {
-  for (const [key, value] of Object.entries(data)) {
-    if (!globals.overviewStore.has(key)) {
-      globals.overviewStore.set(key, []);
-    }
-    if (value instanceof Array) {
-      globals.overviewStore.get(key)!.push(...value);
-    } else {
-      globals.overviewStore.get(key)!.push(value);
-    }
-  }
-  raf.scheduleRedraw();
-}
-
-export function clearOverviewData() {
-  globals.overviewStore.clear();
-  raf.scheduleRedraw();
-}
-
-export function publishTrackData(args: {id: string; data: {}}) {
-  globals.setTrackData(args.id, args.data);
-  raf.scheduleRedraw();
-}
-
-export function publishMetricResult(metricResult: MetricResult) {
-  globals.setMetricResult(metricResult);
-  globals.publishRedraw();
-}
-
-export function publishSelectedFlows(selectedFlows: Flow[]) {
-  globals.selectedFlows = selectedFlows;
-  globals.publishRedraw();
-}
-
-export function publishHttpRpcState(httpRpcState: HttpRpcState) {
-  globals.httpRpcState = httpRpcState;
-  raf.scheduleFullRedraw();
-}
-
-export function publishCpuProfileDetails(details: CpuProfileDetails) {
-  globals.cpuProfileDetails = details;
-  globals.publishRedraw();
-}
-
-export function publishHasFtrace(value: boolean): void {
-  globals.hasFtrace = value;
-  globals.publishRedraw();
-}
-
-export function publishConversionJobStatusUpdate(
-  job: ConversionJobStatusUpdate,
-) {
-  globals.setConversionJobStatus(job.jobName, job.jobStatus);
-  globals.publishRedraw();
-}
-
-export function publishLoading(numQueuedQueries: number) {
-  globals.numQueuedQueries = numQueuedQueries;
-  // TODO(hjd): Clean up loadingAnimation given that this now causes a full
-  // redraw anyways. Also this should probably just go via the global state.
-  raf.scheduleFullRedraw();
-}
-
-export function publishBufferUsage(args: {percentage: number}) {
-  globals.setBufferUsage(args.percentage);
-  globals.publishRedraw();
-}
-
-export function publishSearchResult(args: CurrentSearchResults) {
-  globals.currentSearchResults = args;
-  globals.publishRedraw();
-}
-
-export function publishRecordingLog(args: {logs: string}) {
-  globals.setRecordingLog(args.logs);
-  globals.publishRedraw();
-}
-
-export function publishTraceErrors(numErrors: number) {
-  globals.setTraceErrors(numErrors);
-  globals.publishRedraw();
-}
-
-export function publishMetricError(error: string) {
-  globals.setMetricError(error);
-  globals.publishRedraw();
-}
-
-export function publishAggregateData(args: {
-  data: AggregateData;
-  kind: string;
-}) {
-  globals.setAggregateData(args.kind, args.data);
-  globals.publishRedraw();
-}
-
-export function publishQueryResult(args: {id: string; data?: {}}) {
-  globals.queryResults.set(args.id, args.data);
-  globals.publishRedraw();
-}
-
-export function publishThreads(data: ThreadDesc[]) {
-  globals.threads.clear();
-  data.forEach((thread) => {
-    globals.threads.set(thread.utid, thread);
-  });
-  globals.publishRedraw();
-}
-
-export function publishSliceDetails(click: SliceDetails) {
-  globals.sliceDetails = click;
-  const id = click.id;
-  if (id !== undefined && id === globals.state.pendingScrollId) {
-    findCurrentSelection();
-    globals.dispatch(Actions.clearPendingScrollId({id: undefined}));
-  }
-  globals.publishRedraw();
-}
-
-export function publishThreadStateDetails(click: ThreadStateDetails) {
-  globals.threadStateDetails = click;
-  globals.publishRedraw();
-}
-
-export function publishConnectedFlows(connectedFlows: Flow[]) {
-  globals.connectedFlows = connectedFlows;
-  // If a chrome slice is selected and we have any flows in connectedFlows
-  // we will find the flows on the right and left of that slice to set a default
-  // focus. In all other cases the focusedFlowId(Left|Right) will be set to -1.
-  globals.dispatch(Actions.setHighlightedFlowLeftId({flowId: -1}));
-  globals.dispatch(Actions.setHighlightedFlowRightId({flowId: -1}));
-  const currentSelection = getLegacySelection(globals.state);
-  if (currentSelection?.kind === 'SLICE') {
-    const sliceId = currentSelection.id;
-    for (const flow of globals.connectedFlows) {
-      if (flow.begin.sliceId === sliceId) {
-        globals.dispatch(Actions.setHighlightedFlowRightId({flowId: flow.id}));
-      }
-      if (flow.end.sliceId === sliceId) {
-        globals.dispatch(Actions.setHighlightedFlowLeftId({flowId: flow.id}));
-      }
-    }
-  }
-
-  globals.publishRedraw();
-}
-
-export function publishShowPanningHint() {
-  globals.showPanningHint = true;
-  globals.publishRedraw();
-}
-
-export function publishPermalinkHash(hash: string | undefined): void {
-  globals.permalinkHash = hash;
-  globals.publishRedraw();
-}
diff --git a/ui/src/frontend/query_history.ts b/ui/src/frontend/query_history.ts
deleted file mode 100644
index c16d720..0000000
--- a/ui/src/frontend/query_history.ts
+++ /dev/null
@@ -1,193 +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 {Icons} from '../base/semantic_icons';
-import {assertTrue} from '../base/logging';
-import {Icon} from '../widgets/icon';
-import {raf} from '../core/raf_scheduler';
-import {z} from 'zod';
-
-const QUERY_HISTORY_KEY = 'queryHistory';
-
-export interface QueryHistoryComponentAttrs {
-  runQuery: (query: string) => void;
-  setQuery: (query: string) => void;
-}
-
-export class QueryHistoryComponent
-  implements m.ClassComponent<QueryHistoryComponentAttrs>
-{
-  view({attrs}: m.CVnode<QueryHistoryComponentAttrs>): m.Child {
-    const runQuery = attrs.runQuery;
-    const setQuery = attrs.setQuery;
-    const unstarred: HistoryItemComponentAttrs[] = [];
-    const starred: HistoryItemComponentAttrs[] = [];
-    for (let i = queryHistoryStorage.data.length - 1; i >= 0; i--) {
-      const entry = queryHistoryStorage.data[i];
-      const arr = entry.starred ? starred : unstarred;
-      arr.push({index: i, entry, runQuery, setQuery});
-    }
-    return m(
-      '.query-history',
-      m(
-        'header.overview',
-        `Query history (${queryHistoryStorage.data.length} queries)`,
-      ),
-      starred.map((attrs) => m(HistoryItemComponent, attrs)),
-      unstarred.map((attrs) => m(HistoryItemComponent, attrs)),
-    );
-  }
-}
-
-export interface HistoryItemComponentAttrs {
-  index: number;
-  entry: QueryHistoryEntry;
-  runQuery: (query: string) => void;
-  setQuery: (query: string) => void;
-}
-
-export class HistoryItemComponent
-  implements m.ClassComponent<HistoryItemComponentAttrs>
-{
-  view(vnode: m.Vnode<HistoryItemComponentAttrs>): m.Child {
-    const query = vnode.attrs.entry.query;
-    return m(
-      '.history-item',
-      m(
-        '.history-item-buttons',
-        m(
-          'button',
-          {
-            onclick: () => {
-              queryHistoryStorage.setStarred(
-                vnode.attrs.index,
-                !vnode.attrs.entry.starred,
-              );
-              raf.scheduleFullRedraw();
-            },
-          },
-          m(Icon, {icon: Icons.Star, filled: vnode.attrs.entry.starred}),
-        ),
-        m(
-          'button',
-          {
-            onclick: () => vnode.attrs.setQuery(query),
-          },
-          m(Icon, {icon: 'edit'}),
-        ),
-        m(
-          'button',
-          {
-            onclick: () => vnode.attrs.runQuery(query),
-          },
-          m(Icon, {icon: 'play_arrow'}),
-        ),
-        m(
-          'button',
-          {
-            onclick: () => {
-              queryHistoryStorage.remove(vnode.attrs.index);
-              raf.scheduleFullRedraw();
-            },
-          },
-          m(Icon, {icon: 'delete'}),
-        ),
-      ),
-      m(
-        'pre',
-        {
-          onclick: () => vnode.attrs.setQuery(query),
-          ondblclick: () => vnode.attrs.runQuery(query),
-        },
-        query,
-      ),
-    );
-  }
-}
-
-class HistoryStorage {
-  data: QueryHistory;
-  maxItems = 50;
-
-  constructor() {
-    this.data = this.load();
-  }
-
-  saveQuery(query: string) {
-    const items = this.data;
-    let firstUnstarred = -1;
-    let countUnstarred = 0;
-    for (let i = 0; i < items.length; i++) {
-      if (!items[i].starred) {
-        countUnstarred++;
-        if (firstUnstarred === -1) {
-          firstUnstarred = i;
-        }
-      }
-
-      if (items[i].query === query) {
-        // Query is already in the history, no need to save
-        return;
-      }
-    }
-
-    if (countUnstarred >= this.maxItems) {
-      assertTrue(firstUnstarred !== -1);
-      items.splice(firstUnstarred, 1);
-    }
-
-    items.push({query, starred: false});
-    this.save();
-  }
-
-  setStarred(index: number, starred: boolean) {
-    assertTrue(index >= 0 && index < this.data.length);
-    this.data[index].starred = starred;
-    this.save();
-  }
-
-  remove(index: number) {
-    assertTrue(index >= 0 && index < this.data.length);
-    this.data.splice(index, 1);
-    this.save();
-  }
-
-  private load(): QueryHistory {
-    const value = window.localStorage.getItem(QUERY_HISTORY_KEY);
-    if (value === null) {
-      return [];
-    }
-    const res = QUERY_HISTORY_SCHEMA.safeParse(JSON.parse(value));
-    return res.success ? res.data : [];
-  }
-
-  private save() {
-    window.localStorage.setItem(QUERY_HISTORY_KEY, JSON.stringify(this.data));
-  }
-}
-
-const QUERY_HISTORY_ENTRY_SCHEMA = z.object({
-  query: z.string(),
-  starred: z.boolean().default(false),
-});
-
-type QueryHistoryEntry = z.infer<typeof QUERY_HISTORY_ENTRY_SCHEMA>;
-
-const QUERY_HISTORY_SCHEMA = z.array(QUERY_HISTORY_ENTRY_SCHEMA);
-
-type QueryHistory = z.infer<typeof QUERY_HISTORY_SCHEMA>;
-
-export const queryHistoryStorage = new HistoryStorage();
diff --git a/ui/src/frontend/query_page.ts b/ui/src/frontend/query_page.ts
deleted file mode 100644
index 6d006d9..0000000
--- a/ui/src/frontend/query_page.ts
+++ /dev/null
@@ -1,142 +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.
-
-import m from 'mithril';
-
-import {SimpleResizeObserver} from '../base/resize_observer';
-import {undoCommonChatAppReplacements} from '../base/string_utils';
-import {QueryResponse, runQuery} from '../common/queries';
-import {raf} from '../core/raf_scheduler';
-import {Engine} from '../trace_processor/engine';
-import {Callout} from '../widgets/callout';
-import {Editor} from '../widgets/editor';
-
-import {globals} from './globals';
-import {createPage} from './pages';
-import {QueryHistoryComponent, queryHistoryStorage} from './query_history';
-import {addQueryResultsTab} from './query_result_tab';
-import {QueryTable} from './query_table';
-
-interface QueryPageState {
-  enteredText: string;
-  executedQuery?: string;
-  queryResult?: QueryResponse;
-  heightPx: string;
-  generation: number;
-}
-
-const state: QueryPageState = {
-  enteredText: '',
-  heightPx: '100px',
-  generation: 0,
-};
-
-function runManualQuery(query: string) {
-  state.executedQuery = query;
-  state.queryResult = undefined;
-  const engine = getEngine();
-  if (engine) {
-    runQuery(undoCommonChatAppReplacements(query), engine).then(
-      (resp: QueryResponse) => {
-        addQueryResultsTab(
-          {
-            query: query,
-            title: 'Standalone Query',
-            prefetchedResponse: resp,
-          },
-          'analyze_page_query',
-        );
-        // We might have started to execute another query. Ignore it in that
-        // case.
-        if (state.executedQuery !== query) {
-          return;
-        }
-        state.queryResult = resp;
-        raf.scheduleFullRedraw();
-      },
-    );
-  }
-  raf.scheduleDelayedFullRedraw();
-}
-
-function getEngine(): Engine | undefined {
-  const engineId = globals.getCurrentEngine()?.id;
-  if (engineId === undefined) {
-    return undefined;
-  }
-  const engine = globals.engines.get(engineId)?.getProxy('QueryPage');
-  return engine;
-}
-
-class QueryInput implements m.ClassComponent {
-  private resize?: Disposable;
-
-  oncreate({dom}: m.CVnodeDOM): void {
-    this.resize = new SimpleResizeObserver(dom, () => {
-      state.heightPx = (dom as HTMLElement).style.height;
-    });
-    (dom as HTMLElement).style.height = state.heightPx;
-  }
-
-  onremove(): void {
-    if (this.resize) {
-      this.resize[Symbol.dispose]();
-      this.resize = undefined;
-    }
-  }
-
-  view() {
-    return m(Editor, {
-      generation: state.generation,
-      initialText: state.enteredText,
-
-      onExecute: (text: string) => {
-        if (!text) {
-          return;
-        }
-        queryHistoryStorage.saveQuery(text);
-        runManualQuery(text);
-      },
-
-      onUpdate: (text: string) => {
-        state.enteredText = text;
-      },
-    });
-  }
-}
-
-export const QueryPage = createPage({
-  view() {
-    return m(
-      '.query-page',
-      m(Callout, 'Enter query and press Cmd/Ctrl + Enter'),
-      m(QueryInput),
-      state.executedQuery === undefined
-        ? null
-        : m(QueryTable, {
-            query: state.executedQuery,
-            resp: state.queryResult,
-            fillParent: false,
-          }),
-      m(QueryHistoryComponent, {
-        runQuery: runManualQuery,
-        setQuery: (q: string) => {
-          state.enteredText = q;
-          state.generation++;
-          raf.scheduleFullRedraw();
-        },
-      }),
-    );
-  },
-});
diff --git a/ui/src/frontend/query_result_tab.ts b/ui/src/frontend/query_result_tab.ts
deleted file mode 100644
index c85df5d..0000000
--- a/ui/src/frontend/query_result_tab.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-import {v4 as uuidv4} from 'uuid';
-
-import {assertExists} from '../base/logging';
-import {QueryResponse, runQuery} from '../common/queries';
-import {raf} from '../core/raf_scheduler';
-import {QueryError} from '../trace_processor/query_result';
-import {
-  AddDebugTrackMenu,
-  uuidToViewName,
-} from './debug_tracks/add_debug_track_menu';
-import {Button} from '../widgets/button';
-import {PopupMenu2} from '../widgets/menu';
-import {PopupPosition} from '../widgets/popup';
-
-import {BottomTab, NewBottomTabArgs} from './bottom_tab';
-import {QueryTable} from './query_table';
-import {globals} from './globals';
-import {Actions} from '../common/actions';
-import {BottomTabToTabAdapter} from '../public/utils';
-import {Engine} from '../public';
-
-interface QueryResultTabConfig {
-  readonly query: string;
-  readonly title: string;
-  // Optional data to display in this tab instead of fetching it again
-  // (e.g. when duplicating an existing tab which already has the data).
-  readonly prefetchedResponse?: QueryResponse;
-}
-
-// External interface for adding a new query results tab
-// Automatically decided whether to add v1 or v2 tab
-export function addQueryResultsTab(
-  config: QueryResultTabConfig,
-  tag?: string,
-): void {
-  const queryResultsTab = new QueryResultTab({
-    config,
-    engine: getEngine(),
-    uuid: uuidv4(),
-  });
-
-  const uri = 'queryResults#' + (tag ?? uuidv4());
-
-  globals.tabManager.registerTab({
-    uri,
-    content: new BottomTabToTabAdapter(queryResultsTab),
-    isEphemeral: true,
-  });
-
-  globals.dispatch(Actions.showTab({uri}));
-}
-
-// TODO(stevegolton): Find a way to make this more elegant.
-function getEngine(): Engine {
-  const engConfig = globals.getCurrentEngine();
-  const engineId = assertExists(engConfig).id;
-  return assertExists(globals.engines.get(engineId)).getProxy('QueryResult');
-}
-
-export class QueryResultTab extends BottomTab<QueryResultTabConfig> {
-  static readonly kind = 'dev.perfetto.QueryResultTab';
-
-  queryResponse?: QueryResponse;
-  sqlViewName?: string;
-
-  static create(args: NewBottomTabArgs<QueryResultTabConfig>): QueryResultTab {
-    return new QueryResultTab(args);
-  }
-
-  constructor(args: NewBottomTabArgs<QueryResultTabConfig>) {
-    super(args);
-
-    this.initTrack(args);
-  }
-
-  async initTrack(args: NewBottomTabArgs<QueryResultTabConfig>) {
-    let uuid = '';
-    if (this.config.prefetchedResponse !== undefined) {
-      this.queryResponse = this.config.prefetchedResponse;
-      uuid = args.uuid;
-    } else {
-      const result = await runQuery(this.config.query, this.engine);
-      this.queryResponse = result;
-      raf.scheduleFullRedraw();
-      if (result.error !== undefined) {
-        return;
-      }
-
-      uuid = uuidv4();
-    }
-
-    if (uuid !== '') {
-      this.sqlViewName = await this.createViewForDebugTrack(uuid);
-      if (this.sqlViewName) {
-        raf.scheduleFullRedraw();
-      }
-    }
-  }
-
-  getTitle(): string {
-    const suffix = this.queryResponse
-      ? ` (${this.queryResponse.rows.length})`
-      : '';
-    return `${this.config.title}${suffix}`;
-  }
-
-  viewTab(): m.Child {
-    return m(QueryTable, {
-      query: this.config.query,
-      resp: this.queryResponse,
-      fillParent: true,
-      contextButtons: [
-        this.sqlViewName === undefined
-          ? null
-          : m(
-              PopupMenu2,
-              {
-                trigger: m(Button, {label: 'Show debug track'}),
-                popupPosition: PopupPosition.Top,
-              },
-              m(AddDebugTrackMenu, {
-                dataSource: {
-                  sqlSource: `select * from ${this.sqlViewName}`,
-                  columns: assertExists(this.queryResponse).columns,
-                },
-                engine: this.engine,
-              }),
-            ),
-      ],
-    });
-  }
-
-  isLoading() {
-    return this.queryResponse === undefined;
-  }
-
-  async createViewForDebugTrack(uuid: string): Promise<string> {
-    const viewId = uuidToViewName(uuid);
-    // Assuming that the query results come from a SELECT query, try creating a
-    // view to allow us to reuse it for further queries.
-    const hasValidQueryResponse =
-      this.queryResponse && this.queryResponse.error === undefined;
-    const sqlQuery = hasValidQueryResponse
-      ? this.queryResponse!.lastStatementSql
-      : this.config.query;
-    try {
-      const createViewResult = await this.engine.query(
-        `create view ${viewId} as ${sqlQuery}`,
-      );
-      if (createViewResult.error()) {
-        // If it failed, do nothing.
-        return '';
-      }
-    } catch (e) {
-      if (e instanceof QueryError) {
-        // If it failed, do nothing.
-        return '';
-      }
-      throw e;
-    }
-    return viewId;
-  }
-}
diff --git a/ui/src/frontend/query_table.ts b/ui/src/frontend/query_table.ts
deleted file mode 100644
index dd7cc19..0000000
--- a/ui/src/frontend/query_table.ts
+++ /dev/null
@@ -1,284 +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.
-
-import m from 'mithril';
-
-import {BigintMath} from '../base/bigint_math';
-import {copyToClipboard} from '../base/clipboard';
-import {isString} from '../base/object_utils';
-import {Time} from '../base/time';
-import {Actions} from '../common/actions';
-import {QueryResponse} from '../common/queries';
-import {Row} from '../trace_processor/query_result';
-import {Anchor} from '../widgets/anchor';
-import {Button} from '../widgets/button';
-import {Callout} from '../widgets/callout';
-import {DetailsShell} from '../widgets/details_shell';
-
-import {queryResponseToClipboard} from './clipboard';
-import {downloadData} from './download_utils';
-import {globals} from './globals';
-import {Router} from './router';
-import {reveal} from './scroll_helper';
-
-interface QueryTableRowAttrs {
-  row: Row;
-  columns: string[];
-}
-
-type Numeric = bigint | number;
-
-function isIntegral(x: Row[string]): x is Numeric {
-  return (
-    typeof x === 'bigint' || (typeof x === 'number' && Number.isInteger(x))
-  );
-}
-
-function hasTs(row: Row): row is Row & {ts: Numeric} {
-  return 'ts' in row && isIntegral(row.ts);
-}
-
-function hasDur(row: Row): row is Row & {dur: Numeric} {
-  return 'dur' in row && isIntegral(row.dur);
-}
-
-function hasTrackId(row: Row): row is Row & {track_id: Numeric} {
-  return 'track_id' in row && isIntegral(row.track_id);
-}
-
-function hasType(row: Row): row is Row & {type: string} {
-  return 'type' in row && isString(row.type);
-}
-
-function hasId(row: Row): row is Row & {id: Numeric} {
-  return 'id' in row && isIntegral(row.id);
-}
-
-function hasSliceId(row: Row): row is Row & {slice_id: Numeric} {
-  return 'slice_id' in row && isIntegral(row.slice_id);
-}
-
-// These are properties that a row should have in order to be "slice-like",
-// insofar as it represents a time range and a track id which can be revealed
-// or zoomed-into on the timeline.
-type Sliceish = {
-  ts: Numeric;
-  dur: Numeric;
-  track_id: Numeric;
-};
-
-export function isSliceish(row: Row): row is Row & Sliceish {
-  return hasTs(row) && hasDur(row) && hasTrackId(row);
-}
-
-// Attempts to extract a slice ID from a row, or undefined if none can be found
-export function getSliceId(row: Row): number | undefined {
-  if (hasType(row) && row.type.includes('slice')) {
-    if (hasId(row)) {
-      return Number(row.id);
-    }
-  } else {
-    if (hasSliceId(row)) {
-      return Number(row.slice_id);
-    }
-  }
-  return undefined;
-}
-
-class QueryTableRow implements m.ClassComponent<QueryTableRowAttrs> {
-  view(vnode: m.Vnode<QueryTableRowAttrs>) {
-    const {row, columns} = vnode.attrs;
-    const cells = columns.map((col) => this.renderCell(col, row[col]));
-
-    // TODO(dproy): Make click handler work from analyze page.
-    if (
-      Router.parseUrl(window.location.href).page === '/viewer' &&
-      isSliceish(row)
-    ) {
-      return m(
-        'tr',
-        {
-          onclick: () => this.selectAndRevealSlice(row, false),
-          // TODO(altimin): Consider improving the logic here (e.g. delay?) to
-          // account for cases when dblclick fires late.
-          ondblclick: () => this.selectAndRevealSlice(row, true),
-          clickable: true,
-          title: 'Go to slice',
-        },
-        cells,
-      );
-    } else {
-      return m('tr', cells);
-    }
-  }
-
-  private renderCell(name: string, value: Row[string]) {
-    if (value instanceof Uint8Array) {
-      return m('td', this.renderBlob(name, value));
-    } else {
-      return m('td', `${value}`);
-    }
-  }
-
-  private renderBlob(name: string, value: Uint8Array) {
-    return m(
-      Anchor,
-      {
-        onclick: () => downloadData(`${name}.blob`, value),
-      },
-      `Blob (${value.length} bytes)`,
-    );
-  }
-
-  private selectAndRevealSlice(
-    row: Row & Sliceish,
-    switchToCurrentSelectionTab: boolean,
-  ) {
-    const trackId = Number(row.track_id);
-    const sliceStart = Time.fromRaw(BigInt(row.ts));
-    // row.dur can be negative. Clamp to 1ns.
-    const sliceDur = BigintMath.max(BigInt(row.dur), 1n);
-    const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
-    if (trackKey !== undefined) {
-      reveal(trackKey, sliceStart, Time.add(sliceStart, sliceDur), true);
-      const sliceId = getSliceId(row);
-      if (sliceId !== undefined) {
-        this.selectSlice(sliceId, trackKey, switchToCurrentSelectionTab);
-      }
-    }
-  }
-
-  private selectSlice(
-    sliceId: number,
-    trackKey: string,
-    switchToCurrentSelectionTab: boolean,
-  ) {
-    const action = Actions.selectSlice({
-      id: sliceId,
-      trackKey,
-      table: 'slice',
-    });
-    globals.makeSelection(action, {switchToCurrentSelectionTab});
-  }
-}
-
-interface QueryTableContentAttrs {
-  resp: QueryResponse;
-}
-
-class QueryTableContent implements m.ClassComponent<QueryTableContentAttrs> {
-  private previousResponse?: QueryResponse;
-
-  onbeforeupdate(vnode: m.CVnode<QueryTableContentAttrs>) {
-    return vnode.attrs.resp !== this.previousResponse;
-  }
-
-  view(vnode: m.CVnode<QueryTableContentAttrs>) {
-    const resp = vnode.attrs.resp;
-    this.previousResponse = resp;
-    const cols = [];
-    for (const col of resp.columns) {
-      cols.push(m('td', col));
-    }
-    const tableHeader = m('tr', cols);
-
-    const rows = resp.rows.map((row) =>
-      m(QueryTableRow, {row, columns: resp.columns}),
-    );
-
-    if (resp.error) {
-      return m('.query-error', `SQL error: ${resp.error}`);
-    } else {
-      return m(
-        'table.pf-query-table',
-        m('thead', tableHeader),
-        m('tbody', rows),
-      );
-    }
-  }
-}
-
-interface QueryTableAttrs {
-  query: string;
-  resp?: QueryResponse;
-  contextButtons?: m.Child[];
-  fillParent: boolean;
-}
-
-export class QueryTable implements m.ClassComponent<QueryTableAttrs> {
-  view({attrs}: m.CVnode<QueryTableAttrs>) {
-    const {resp, query, contextButtons = [], fillParent} = attrs;
-
-    return m(
-      DetailsShell,
-      {
-        title: this.renderTitle(resp),
-        description: query,
-        buttons: this.renderButtons(query, contextButtons, resp),
-        fillParent,
-      },
-      resp && this.renderTableContent(resp),
-    );
-  }
-
-  renderTitle(resp?: QueryResponse) {
-    if (!resp) {
-      return 'Query - running';
-    }
-    const result = resp.error ? 'error' : `${resp.rows.length} rows`;
-    return `Query result (${result}) - ${resp.durationMs.toLocaleString()}ms`;
-  }
-
-  renderButtons(
-    query: string,
-    contextButtons: m.Child[],
-    resp?: QueryResponse,
-  ) {
-    return [
-      contextButtons,
-      m(Button, {
-        label: 'Copy query',
-        onclick: () => {
-          copyToClipboard(query);
-        },
-      }),
-      resp &&
-        resp.error === undefined &&
-        m(Button, {
-          label: 'Copy result (.tsv)',
-          onclick: () => {
-            queryResponseToClipboard(resp);
-          },
-        }),
-    ];
-  }
-
-  renderTableContent(resp: QueryResponse) {
-    return m(
-      '.pf-query-panel',
-      resp.statementWithOutputCount > 1 &&
-        m(
-          '.pf-query-warning',
-          m(
-            Callout,
-            {icon: 'warning'},
-            `${resp.statementWithOutputCount} out of ${resp.statementCount} `,
-            'statements returned a result. ',
-            'Only the results for the last statement are displayed.',
-          ),
-        ),
-      m(QueryTableContent, {resp}),
-    );
-  }
-}
diff --git a/ui/src/frontend/record_config.ts b/ui/src/frontend/record_config.ts
deleted file mode 100644
index 0201dc1..0000000
--- a/ui/src/frontend/record_config.ts
+++ /dev/null
@@ -1,231 +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.
-
-import {exists} from '../base/utils';
-import {getDefaultRecordingTargets, RecordingTarget} from '../common/state';
-import {
-  createEmptyRecordConfig,
-  NamedRecordConfig,
-  NAMED_RECORD_CONFIG_SCHEMA,
-  RecordConfig,
-  RECORD_CONFIG_SCHEMA,
-} from '../controller/record_config_types';
-
-const LOCAL_STORAGE_RECORD_CONFIGS_KEY = 'recordConfigs';
-const LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY = 'autosaveConfig';
-const LOCAL_STORAGE_RECORD_TARGET_OS_KEY = 'recordTargetOS';
-
-export class RecordConfigStore {
-  recordConfigs: Array<NamedRecordConfig>;
-  recordConfigNames: Set<string>;
-
-  constructor() {
-    this.recordConfigs = [];
-    this.recordConfigNames = new Set();
-    this.reloadFromLocalStorage();
-  }
-
-  private _save() {
-    window.localStorage.setItem(
-      LOCAL_STORAGE_RECORD_CONFIGS_KEY,
-      JSON.stringify(this.recordConfigs),
-    );
-  }
-
-  save(recordConfig: RecordConfig, title?: string): void {
-    // We reload from local storage in case of concurrent
-    // modifications of local storage from a different tab.
-    this.reloadFromLocalStorage();
-
-    const savedTitle = title ?? new Date().toJSON();
-    const config: NamedRecordConfig = {
-      title: savedTitle,
-      config: recordConfig,
-      key: new Date().toJSON(),
-    };
-
-    this.recordConfigs.push(config);
-    this.recordConfigNames.add(savedTitle);
-
-    this._save();
-  }
-
-  overwrite(recordConfig: RecordConfig, key: string) {
-    // We reload from local storage in case of concurrent
-    // modifications of local storage from a different tab.
-    this.reloadFromLocalStorage();
-
-    const found = this.recordConfigs.find((e) => e.key === key);
-    if (found === undefined) {
-      throw new Error('trying to overwrite non-existing config');
-    }
-
-    found.config = recordConfig;
-
-    this._save();
-  }
-
-  delete(key: string): void {
-    // We reload from local storage in case of concurrent
-    // modifications of local storage from a different tab.
-    this.reloadFromLocalStorage();
-
-    let idx = -1;
-    for (let i = 0; i < this.recordConfigs.length; ++i) {
-      if (this.recordConfigs[i].key === key) {
-        idx = i;
-        break;
-      }
-    }
-
-    if (idx !== -1) {
-      this.recordConfigNames.delete(this.recordConfigs[idx].title);
-      this.recordConfigs.splice(idx, 1);
-      this._save();
-    } else {
-      // TODO(bsebastien): Show a warning message to the user in the UI.
-      console.warn("The config selected doesn't exist any more");
-    }
-  }
-
-  private clearRecordConfigs(): void {
-    this.recordConfigs = [];
-    this.recordConfigNames.clear();
-    this._save();
-  }
-
-  reloadFromLocalStorage(): void {
-    const configsLocalStorage = window.localStorage.getItem(
-      LOCAL_STORAGE_RECORD_CONFIGS_KEY,
-    );
-
-    if (exists(configsLocalStorage)) {
-      this.recordConfigNames.clear();
-
-      try {
-        const validConfigLocalStorage: Array<NamedRecordConfig> = [];
-        const parsedConfigsLocalStorage = JSON.parse(configsLocalStorage);
-
-        // Check if it's an array.
-        if (!Array.isArray(parsedConfigsLocalStorage)) {
-          this.clearRecordConfigs();
-          return;
-        }
-
-        for (let i = 0; i < parsedConfigsLocalStorage.length; ++i) {
-          const serConfig = parsedConfigsLocalStorage[i];
-          const res = NAMED_RECORD_CONFIG_SCHEMA.safeParse(serConfig);
-          if (res.success) {
-            validConfigLocalStorage.push(res.data);
-          } else {
-            console.log(
-              'Validation of saved record config has failed: ',
-              res.error.toString(),
-            );
-          }
-        }
-
-        this.recordConfigs = validConfigLocalStorage;
-        this._save();
-      } catch (e) {
-        this.clearRecordConfigs();
-      }
-    } else {
-      this.clearRecordConfigs();
-    }
-  }
-
-  canSave(title: string) {
-    return !this.recordConfigNames.has(title);
-  }
-}
-
-// This class is a singleton to avoid many instances
-// conflicting as they attempt to edit localStorage.
-export const recordConfigStore = new RecordConfigStore();
-
-export class AutosaveConfigStore {
-  config: RecordConfig;
-
-  // Whether the current config is a default one or has been saved before.
-  // Used to determine whether the button to load "last started config" should
-  // be present in the recording profiles list.
-  hasSavedConfig: boolean;
-
-  constructor() {
-    this.hasSavedConfig = false;
-    this.config = createEmptyRecordConfig();
-    const savedItem = window.localStorage.getItem(
-      LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY,
-    );
-    if (savedItem === null) {
-      return;
-    }
-    const parsed = JSON.parse(savedItem);
-    if (parsed !== null && typeof parsed === 'object') {
-      const res = RECORD_CONFIG_SCHEMA.safeParse(parsed);
-      if (res.success) {
-        this.config = res.data;
-        this.hasSavedConfig = true;
-      }
-    }
-  }
-
-  get(): RecordConfig {
-    return this.config;
-  }
-
-  save(newConfig: RecordConfig) {
-    window.localStorage.setItem(
-      LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY,
-      JSON.stringify(newConfig),
-    );
-    this.config = newConfig;
-    this.hasSavedConfig = true;
-  }
-}
-
-export const autosaveConfigStore = new AutosaveConfigStore();
-
-export class RecordTargetStore {
-  recordTargetOS: string | null;
-
-  constructor() {
-    this.recordTargetOS = window.localStorage.getItem(
-      LOCAL_STORAGE_RECORD_TARGET_OS_KEY,
-    );
-  }
-
-  get(): string | null {
-    return this.recordTargetOS;
-  }
-
-  getValidTarget(): RecordingTarget {
-    const validTargets = getDefaultRecordingTargets();
-    const savedOS = this.get();
-
-    const validSavedTarget = validTargets.find((el) => el.os === savedOS);
-    return validSavedTarget || validTargets[0];
-  }
-
-  save(newTargetOS: string) {
-    window.localStorage.setItem(
-      LOCAL_STORAGE_RECORD_TARGET_OS_KEY,
-      newTargetOS,
-    );
-    this.recordTargetOS = newTargetOS;
-  }
-}
-
-export const recordTargetStore = new RecordTargetStore();
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
deleted file mode 100644
index 0baebf5..0000000
--- a/ui/src/frontend/record_page.ts
+++ /dev/null
@@ -1,943 +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 {Actions} from '../common/actions';
-import {
-  AdbRecordingTarget,
-  getDefaultRecordingTargets,
-  hasActiveProbes,
-  isAdbTarget,
-  isAndroidP,
-  isAndroidTarget,
-  isChromeTarget,
-  isCrOSTarget,
-  isLinuxTarget,
-  isWindowsTarget,
-  LoadedConfig,
-  MAX_TIME,
-  RecordingTarget,
-} from '../common/state';
-import {AdbOverWebUsb} from '../controller/adb';
-import {
-  createEmptyRecordConfig,
-  RecordConfig,
-} from '../controller/record_config_types';
-import {featureFlags} from '../core/feature_flags';
-import {raf} from '../core/raf_scheduler';
-
-import {globals} from './globals';
-import {createPage, PageAttrs} from './pages';
-import {
-  autosaveConfigStore,
-  recordConfigStore,
-  recordTargetStore,
-} from './record_config';
-import {CodeSnippet} from './record_widgets';
-import {AdvancedSettings} from './recording/advanced_settings';
-import {AndroidSettings} from './recording/android_settings';
-import {ChromeSettings} from './recording/chrome_settings';
-import {CpuSettings} from './recording/cpu_settings';
-import {GpuSettings} from './recording/gpu_settings';
-import {LinuxPerfSettings} from './recording/linux_perf_settings';
-import {MemorySettings} from './recording/memory_settings';
-import {PowerSettings} from './recording/power_settings';
-import {RecordingSectionAttrs} from './recording/recording_sections';
-import {RecordingSettings} from './recording/recording_settings';
-import {EtwSettings} from './recording/etw_settings';
-import {createPermalink} from './permalink';
-
-export const PERSIST_CONFIG_FLAG = featureFlags.register({
-  id: 'persistConfigsUI',
-  name: 'Config persistence UI',
-  description: 'Show experimental config persistence UI on the record page.',
-  defaultValue: true,
-});
-
-export const RECORDING_SECTIONS = [
-  'buffers',
-  'instructions',
-  'config',
-  'cpu',
-  'etw',
-  'gpu',
-  'power',
-  'memory',
-  'android',
-  'chrome',
-  'tracePerf',
-  'advanced',
-];
-
-function RecordHeader() {
-  return m(
-    '.record-header',
-    m(
-      '.top-part',
-      m(
-        '.target-and-status',
-        RecordingPlatformSelection(),
-        RecordingStatusLabel(),
-        ErrorLabel(),
-      ),
-      recordingButtons(),
-    ),
-    RecordingNotes(),
-  );
-}
-
-function RecordingPlatformSelection() {
-  if (globals.state.recordingInProgress) return [];
-
-  const availableAndroidDevices = globals.state.availableAdbDevices;
-  const recordingTarget = globals.state.recordingTarget;
-
-  const targets = [];
-  for (const {os, name} of getDefaultRecordingTargets()) {
-    targets.push(m('option', {value: os}, name));
-  }
-  for (const d of availableAndroidDevices) {
-    targets.push(m('option', {value: d.serial}, d.name));
-  }
-
-  const selectedIndex = isAdbTarget(recordingTarget)
-    ? targets.findIndex((node) => node.attrs.value === recordingTarget.serial)
-    : targets.findIndex((node) => node.attrs.value === recordingTarget.os);
-
-  return m(
-    '.target',
-    m(
-      'label',
-      'Target platform:',
-      m(
-        'select',
-        {
-          selectedIndex,
-          onchange: (e: Event) => {
-            onTargetChange((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;
-          },
-        },
-        ...targets,
-      ),
-    ),
-    m(
-      '.chip',
-      {onclick: addAndroidDevice},
-      m('button', 'Add ADB Device'),
-      m('i.material-icons', 'add'),
-    ),
-  );
-}
-
-// |target| can be the TargetOs or the android serial.
-function onTargetChange(target: string) {
-  const recordingTarget: RecordingTarget =
-    globals.state.availableAdbDevices.find((d) => d.serial === target) ||
-    getDefaultRecordingTargets().find((t) => t.os === target) ||
-    getDefaultRecordingTargets()[0];
-
-  if (isChromeTarget(recordingTarget)) {
-    globals.dispatch(Actions.setFetchChromeCategories({fetch: true}));
-  }
-
-  globals.dispatch(Actions.setRecordingTarget({target: recordingTarget}));
-  recordTargetStore.save(target);
-  raf.scheduleFullRedraw();
-}
-
-function Instructions(cssClass: string) {
-  return m(
-    `.record-section.instructions${cssClass}`,
-    m('header', 'Recording command'),
-    PERSIST_CONFIG_FLAG.get()
-      ? m(
-          'button.permalinkconfig',
-          {
-            onclick: () => createPermalink({mode: 'RECORDING_OPTS'}),
-          },
-          'Share recording settings',
-        )
-      : null,
-    RecordingSnippet(),
-    BufferUsageProgressBar(),
-    m('.buttons', StopCancelButtons()),
-    recordingLog(),
-  );
-}
-
-export function loadedConfigEqual(
-  cfg1: LoadedConfig,
-  cfg2: LoadedConfig,
-): boolean {
-  return cfg1.type === 'NAMED' && cfg2.type === 'NAMED'
-    ? cfg1.name === cfg2.name
-    : cfg1.type === cfg2.type;
-}
-
-export function loadConfigButton(
-  config: RecordConfig,
-  configType: LoadedConfig,
-): m.Vnode {
-  return m(
-    'button',
-    {
-      class: 'config-button',
-      title: 'Apply configuration settings',
-      disabled: loadedConfigEqual(configType, globals.state.lastLoadedConfig),
-      onclick: () => {
-        globals.dispatch(Actions.setRecordConfig({config, configType}));
-        raf.scheduleFullRedraw();
-      },
-    },
-    m('i.material-icons', 'file_upload'),
-  );
-}
-
-export function displayRecordConfigs() {
-  const configs = [];
-  if (autosaveConfigStore.hasSavedConfig) {
-    configs.push(
-      m('.config', [
-        m('span.title-config', m('strong', 'Latest started recording')),
-        loadConfigButton(autosaveConfigStore.get(), {type: 'AUTOMATIC'}),
-      ]),
-    );
-  }
-  for (const item of recordConfigStore.recordConfigs) {
-    configs.push(
-      m('.config', [
-        m('span.title-config', item.title),
-        loadConfigButton(item.config, {type: 'NAMED', name: item.title}),
-        m(
-          'button',
-          {
-            class: 'config-button',
-            title: 'Overwrite configuration with current settings',
-            onclick: () => {
-              if (
-                confirm(
-                  `Overwrite config "${item.title}" with current settings?`,
-                )
-              ) {
-                recordConfigStore.overwrite(
-                  globals.state.recordConfig,
-                  item.key,
-                );
-                globals.dispatch(
-                  Actions.setRecordConfig({
-                    config: item.config,
-                    configType: {type: 'NAMED', name: item.title},
-                  }),
-                );
-                raf.scheduleFullRedraw();
-              }
-            },
-          },
-          m('i.material-icons', 'save'),
-        ),
-        m(
-          'button',
-          {
-            class: 'config-button',
-            title: 'Remove configuration',
-            onclick: () => {
-              recordConfigStore.delete(item.key);
-              raf.scheduleFullRedraw();
-            },
-          },
-          m('i.material-icons', 'delete'),
-        ),
-      ]),
-    );
-  }
-  return configs;
-}
-
-export const ConfigTitleState = {
-  title: '',
-  getTitle: () => {
-    return ConfigTitleState.title;
-  },
-  setTitle: (value: string) => {
-    ConfigTitleState.title = value;
-  },
-  clearTitle: () => {
-    ConfigTitleState.title = '';
-  },
-};
-
-export function Configurations(cssClass: string) {
-  const canSave = recordConfigStore.canSave(ConfigTitleState.getTitle());
-  return m(
-    `.record-section${cssClass}`,
-    m('header', 'Save and load configurations'),
-    m('.input-config', [
-      m('input', {
-        value: ConfigTitleState.title,
-        placeholder: 'Title for config',
-        oninput() {
-          ConfigTitleState.setTitle(this.value);
-          raf.scheduleFullRedraw();
-        },
-      }),
-      m(
-        'button',
-        {
-          class: 'config-button',
-          disabled: !canSave,
-          title: canSave
-            ? 'Save current config'
-            : 'Duplicate name, saving disabled',
-          onclick: () => {
-            recordConfigStore.save(
-              globals.state.recordConfig,
-              ConfigTitleState.getTitle(),
-            );
-            raf.scheduleFullRedraw();
-            ConfigTitleState.clearTitle();
-          },
-        },
-        m('i.material-icons', 'save'),
-      ),
-      m(
-        'button',
-        {
-          class: 'config-button',
-          title: 'Clear current configuration',
-          onclick: () => {
-            if (
-              confirm(
-                'Current configuration will be cleared. ' + 'Are you sure?',
-              )
-            ) {
-              globals.dispatch(
-                Actions.setRecordConfig({
-                  config: createEmptyRecordConfig(),
-                  configType: {type: 'NONE'},
-                }),
-              );
-              raf.scheduleFullRedraw();
-            }
-          },
-        },
-        m('i.material-icons', 'delete_forever'),
-      ),
-    ]),
-    displayRecordConfigs(),
-  );
-}
-
-function BufferUsageProgressBar() {
-  if (!globals.state.recordingInProgress) return [];
-
-  const bufferUsage = globals.bufferUsage ?? 0.0;
-  // Buffer usage is not available yet on Android.
-  if (bufferUsage === 0) return [];
-
-  return m(
-    'label',
-    'Buffer usage: ',
-    m('progress', {max: 100, value: bufferUsage * 100}),
-  );
-}
-
-function RecordingNotes() {
-  const sideloadUrl =
-    'https://perfetto.dev/docs/contributing/build-instructions#get-the-code';
-  const linuxUrl = 'https://perfetto.dev/docs/quickstart/linux-tracing';
-  const cmdlineUrl =
-    'https://perfetto.dev/docs/quickstart/android-tracing#perfetto-cmdline';
-  const extensionURL = `https://chrome.google.com/webstore/detail/perfetto-ui/lfmkphfpdbjijhpomgecfikhfohaoine`;
-
-  const notes: m.Children = [];
-
-  const msgFeatNotSupported = m(
-    'span',
-    `Some probes are only supported in Perfetto versions running
-      on Android Q+. `,
-  );
-
-  const msgPerfettoNotSupported = m(
-    'span',
-    `Perfetto is not supported natively before Android P. `,
-  );
-
-  const msgSideload = m(
-    'span',
-    `If you have a rooted device you can `,
-    m(
-      'a',
-      {href: sideloadUrl, target: '_blank'},
-      `sideload the latest version of
-         Perfetto.`,
-    ),
-  );
-
-  const msgRecordingNotSupported = m(
-    '.note',
-    `Recording Perfetto traces from the UI is not supported natively
-     before Android Q. If you are using a P device, please select 'Android P'
-     as the 'Target Platform' and `,
-    m(
-      'a',
-      {href: cmdlineUrl, target: '_blank'},
-      `collect the trace using ADB.`,
-    ),
-  );
-
-  const msgChrome = m(
-    '.note',
-    `To trace Chrome from the Perfetto UI, you need to install our `,
-    m('a', {href: extensionURL, target: '_blank'}, 'Chrome extension'),
-    ' and then reload this page. ',
-  );
-
-  const msgWinEtw = m(
-    '.note',
-    `To trace with Etw on Windows from the Perfetto UI, you to run chrome with`,
-    ` administrator permission and you need to install our `,
-    m('a', {href: extensionURL, target: '_blank'}, 'Chrome extension'),
-    ' and then reload this page.',
-  );
-
-  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.`,
-    ),
-  );
-
-  const msgZeroProbes = m(
-    '.note',
-    "It looks like you didn't add any probes. " +
-      'Please add at least one to get a non-empty trace.',
-  );
-
-  if (!hasActiveProbes(globals.state.recordConfig)) {
-    notes.push(msgZeroProbes);
-  }
-
-  if (isAdbTarget(globals.state.recordingTarget)) {
-    notes.push(msgRecordingNotSupported);
-  }
-  switch (globals.state.recordingTarget.os) {
-    case 'Q':
-      break;
-    case 'P':
-      notes.push(m('.note', msgFeatNotSupported, msgSideload));
-      break;
-    case 'O':
-      notes.push(m('.note', msgPerfettoNotSupported, msgSideload));
-      break;
-    case 'L':
-      notes.push(msgLinux);
-      break;
-    case 'C':
-      if (!globals.state.extensionInstalled) notes.push(msgChrome);
-      break;
-    case 'CrOS':
-      if (!globals.state.extensionInstalled) notes.push(msgChrome);
-      break;
-    case 'Win':
-      if (!globals.state.extensionInstalled) notes.push(msgWinEtw);
-      break;
-    default:
-  }
-  if (globals.state.recordConfig.mode === 'LONG_TRACE') {
-    notes.unshift(msgLongTraces);
-  }
-
-  return notes.length > 0 ? m('div', notes) : [];
-}
-
-function RecordingSnippet() {
-  const target = globals.state.recordingTarget;
-
-  // We don't need commands to start tracing on chrome
-  if (isChromeTarget(target)) {
-    return globals.state.extensionInstalled &&
-      !globals.state.recordingInProgress
-      ? m(
-          'div',
-          m(
-            'label',
-            `To trace Chrome from the Perfetto UI you just have to press
-         'Start Recording'.`,
-          ),
-        )
-      : [];
-  }
-  return m(CodeSnippet, {text: getRecordCommand(target)});
-}
-
-function getRecordCommand(target: RecordingTarget) {
-  const data = globals.trackDataStore.get('config') as {
-    commandline: string;
-    pbtxt: string;
-    pbBase64: string;
-  } | null;
-
-  const cfg = globals.state.recordConfig;
-  let time = cfg.durationMs / 1000;
-
-  if (time > MAX_TIME) {
-    time = MAX_TIME;
-  }
-
-  const pbBase64 = data ? data.pbBase64 : '';
-  const pbtx = data ? data.pbtxt : '';
-  let cmd = '';
-  if (isAndroidP(target)) {
-    cmd += `echo '${pbBase64}' | \n`;
-    cmd += 'base64 --decode | \n';
-    cmd += 'adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace"\n';
-  } else {
-    cmd += isAndroidTarget(target)
-      ? '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 recordingButtons() {
-  const state = globals.state;
-  const target = state.recordingTarget;
-  const recInProgress = state.recordingInProgress;
-
-  const start = m(
-    `button`,
-    {
-      class: recInProgress ? '' : 'selected',
-      onclick: onStartRecordingPressed,
-    },
-    'Start Recording',
-  );
-
-  const buttons: m.Children = [];
-
-  if (isAndroidTarget(target)) {
-    if (
-      !recInProgress &&
-      isAdbTarget(target) &&
-      globals.state.recordConfig.mode !== 'LONG_TRACE'
-    ) {
-      buttons.push(start);
-    }
-  } else if (
-    (isWindowsTarget(target) || isChromeTarget(target)) &&
-    state.extensionInstalled
-  ) {
-    buttons.push(start);
-  }
-  return m('.button', buttons);
-}
-
-function StopCancelButtons() {
-  if (!globals.state.recordingInProgress) return [];
-
-  const stop = m(
-    `button.selected`,
-    {onclick: () => globals.dispatch(Actions.stopRecording({}))},
-    'Stop',
-  );
-
-  const cancel = m(
-    `button`,
-    {onclick: () => globals.dispatch(Actions.cancelRecording({}))},
-    'Cancel',
-  );
-
-  return [stop, cancel];
-}
-
-function onStartRecordingPressed() {
-  location.href = '#!/record/instructions';
-  raf.scheduleFullRedraw();
-  autosaveConfigStore.save(globals.state.recordConfig);
-
-  const target = globals.state.recordingTarget;
-  if (
-    isAndroidTarget(target) ||
-    isChromeTarget(target) ||
-    isWindowsTarget(target)
-  ) {
-    globals.logging.logEvent('Record Trace', `Record trace (${target.os})`);
-    globals.dispatch(Actions.startRecording({}));
-  }
-}
-
-function RecordingStatusLabel() {
-  const recordingStatus = globals.state.recordingStatus;
-  if (!recordingStatus) return [];
-  return m('label', recordingStatus);
-}
-
-export function ErrorLabel() {
-  const lastRecordingError = globals.state.lastRecordingError;
-  if (!lastRecordingError) return [];
-  return m('label.error-label', `Error:  ${lastRecordingError}`);
-}
-
-function recordingLog() {
-  const logs = globals.recordingLog;
-  if (logs === undefined) return [];
-  return m('.code-snippet.no-top-bar', m('code', logs));
-}
-
-// The connection must be done in the frontend. After it, the serial ID will
-// be inserted in the state, and the worker will be able to connect to the
-// correct device.
-async function addAndroidDevice() {
-  let device: USBDevice;
-  try {
-    device = await new AdbOverWebUsb().findDevice();
-  } catch (e) {
-    const err = `No device found: ${e.name}: ${e.message}`;
-    console.error(err, e);
-    alert(err);
-    return;
-  }
-
-  if (!device.serialNumber) {
-    console.error('serial number undefined');
-    return;
-  }
-
-  // After the user has selected a device with the chrome UI, it will be
-  // available when listing all the available device from WebUSB. Therefore,
-  // we update the list of available devices.
-  await updateAvailableAdbDevices(device.serialNumber);
-}
-
-// We really should be getting the API version from the adb target, but
-// currently its too complicated to do that (== most likely, we need to finish
-// recordingV2 migration). For now, add an escape hatch to use Android S as a
-// default, given that the main features we want are gated by API level 31 and S
-// is old enough to be the default most of the time.
-const USE_ANDROID_S_AS_DEFAULT_FLAG = featureFlags.register({
-  id: 'recordingPageUseSAsDefault',
-  name: 'Use Android S as a default recording target',
-  description: 'Use Android S as a default recording target instead of Q',
-  defaultValue: false,
-});
-
-export async function updateAvailableAdbDevices(
-  preferredDeviceSerial?: string,
-) {
-  const devices = await new AdbOverWebUsb().getPairedDevices();
-
-  let recordingTarget: AdbRecordingTarget | undefined = undefined;
-
-  const availableAdbDevices: AdbRecordingTarget[] = [];
-  devices.forEach((d) => {
-    if (d.productName && d.serialNumber) {
-      // TODO(nicomazz): At this stage, we can't know the OS version, so we
-      // assume it is 'Q'. This can create problems with devices with an old
-      // version of perfetto. The os detection should be done after the adb
-      // connection, from adb_record_controller
-      availableAdbDevices.push({
-        name: d.productName,
-        serial: d.serialNumber,
-        os: USE_ANDROID_S_AS_DEFAULT_FLAG.get() ? 'S' : 'Q',
-      });
-      if (preferredDeviceSerial && preferredDeviceSerial === d.serialNumber) {
-        recordingTarget = availableAdbDevices[availableAdbDevices.length - 1];
-      }
-    }
-  });
-
-  globals.dispatch(
-    Actions.setAvailableAdbDevices({devices: availableAdbDevices}),
-  );
-  selectAndroidDeviceIfAvailable(availableAdbDevices, recordingTarget);
-  raf.scheduleFullRedraw();
-  return availableAdbDevices;
-}
-
-function selectAndroidDeviceIfAvailable(
-  availableAdbDevices: AdbRecordingTarget[],
-  recordingTarget?: RecordingTarget,
-) {
-  if (!recordingTarget) {
-    recordingTarget = globals.state.recordingTarget;
-  }
-  const deviceConnected = isAdbTarget(recordingTarget);
-  const connectedDeviceDisconnected =
-    deviceConnected &&
-    availableAdbDevices.find(
-      (e) => e.serial === (recordingTarget as AdbRecordingTarget).serial,
-    ) === undefined;
-
-  if (availableAdbDevices.length) {
-    // If there's an Android device available and the current selection isn't
-    // one, select the Android device by default. If the current device isn't
-    // available anymore, but another Android device is, select the other
-    // Android device instead.
-    if (!deviceConnected || connectedDeviceDisconnected) {
-      recordingTarget = availableAdbDevices[0];
-    }
-
-    globals.dispatch(Actions.setRecordingTarget({target: recordingTarget}));
-    return;
-  }
-
-  // If the currently selected device was disconnected, reset the recording
-  // target to the default one.
-  if (connectedDeviceDisconnected) {
-    globals.dispatch(
-      Actions.setRecordingTarget({target: getDefaultRecordingTargets()[0]}),
-    );
-  }
-}
-
-function recordMenu(routePage: string) {
-  const target = globals.state.recordingTarget;
-  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'),
-    ),
-  );
-  const recInProgress = globals.state.recordingInProgress;
-
-  const probes = [];
-  if (isLinuxTarget(target)) {
-    probes.push(cpuProbe, powerProbe, memoryProbe, chromeProbe, advancedProbe);
-  } else if (isChromeTarget(target) && !isCrOSTarget(target)) {
-    probes.push(chromeProbe);
-  } else if (isWindowsTarget(target)) {
-    probes.push(chromeProbe, etwProbe);
-  } else {
-    probes.push(
-      cpuProbe,
-      gpuProbe,
-      powerProbe,
-      memoryProbe,
-      androidProbe,
-      chromeProbe,
-      tracePerfProbe,
-      advancedProbe,
-    );
-  }
-
-  return m(
-    '.record-menu',
-    {
-      class: recInProgress ? 'disabled' : '',
-      onclick: () => raf.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'),
-        ),
-      ),
-      PERSIST_CONFIG_FLAG.get()
-        ? 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'),
-            ),
-          )
-        : null,
-    ),
-    m('header', 'Probes'),
-    m('ul', probes),
-  );
-}
-
-export function maybeGetActiveCss(routePage: string, section: string): string {
-  return routePage === section ? '.active' : '';
-}
-
-export const RecordPage = createPage({
-  view({attrs}: m.Vnode<PageAttrs>) {
-    const pages: m.Children = [];
-    // we need to remove the `/` character from the route
-    let routePage = attrs.subpage ? attrs.subpage.substr(1) : '';
-    if (!RECORDING_SECTIONS.includes(routePage)) {
-      routePage = 'buffers';
-    }
-    pages.push(recordMenu(routePage));
-
-    pages.push(
-      m(RecordingSettings, {
-        dataSources: [],
-        cssClass: maybeGetActiveCss(routePage, 'buffers'),
-      } as RecordingSectionAttrs),
-    );
-    pages.push(Instructions(maybeGetActiveCss(routePage, 'instructions')));
-    pages.push(Configurations(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: [],
-          cssClass: maybeGetActiveCss(routePage, section),
-        } as RecordingSectionAttrs),
-      );
-    }
-
-    if (isChromeTarget(globals.state.recordingTarget)) {
-      globals.dispatch(Actions.setFetchChromeCategories({fetch: true}));
-    }
-
-    return m(
-      '.record-page',
-      globals.state.recordingInProgress ? m('.hider') : [],
-      m(
-        '.record-container',
-        RecordHeader(),
-        m('.record-container-content', recordMenu(routePage), pages),
-      ),
-    );
-  },
-});
diff --git a/ui/src/frontend/record_page_v2.ts b/ui/src/frontend/record_page_v2.ts
deleted file mode 100644
index 8b38c14..0000000
--- a/ui/src/frontend/record_page_v2.ts
+++ /dev/null
@@ -1,658 +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 '../common/recordingV2/recording_config_utils';
-import {
-  ChromeTargetInfo,
-  RecordingTargetV2,
-  TargetInfo,
-} from '../common/recordingV2/recording_interfaces_v2';
-import {
-  RecordingPageController,
-  RecordingState,
-} from '../common/recordingV2/recording_page_controller';
-import {
-  EXTENSION_NAME,
-  EXTENSION_URL,
-} from '../common/recordingV2/recording_utils';
-import {targetFactoryRegistry} from '../common/recordingV2/target_factory_registry';
-import {raf} from '../core/raf_scheduler';
-
-import {globals} from './globals';
-import {createPage, PageAttrs} from './pages';
-import {recordConfigStore} from './record_config';
-import {
-  Configurations,
-  maybeGetActiveCss,
-  PERSIST_CONFIG_FLAG,
-  RECORDING_SECTIONS,
-} from './record_page';
-import {CodeSnippet} from './record_widgets';
-import {AdvancedSettings} from './recording/advanced_settings';
-import {AndroidSettings} from './recording/android_settings';
-import {ChromeSettings} from './recording/chrome_settings';
-import {CpuSettings} from './recording/cpu_settings';
-import {EtwSettings} from './recording/etw_settings';
-import {GpuSettings} from './recording/gpu_settings';
-import {LinuxPerfSettings} from './recording/linux_perf_settings';
-import {MemorySettings} from './recording/memory_settings';
-import {PowerSettings} from './recording/power_settings';
-import {RecordingSectionAttrs} from './recording/recording_sections';
-import {RecordingSettings} from './recording/recording_settings';
-import {FORCE_RESET_MESSAGE} from './recording/recording_ui_utils';
-import {showAddNewTargetModal} from './recording/reset_target_modal';
-import {createPermalink} from './permalink';
-
-const START_RECORDING_MESSAGE = 'Start Recording';
-
-const controller = new RecordingPageController();
-const recordConfigUtils = new 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() {
-  const platformSelection = RecordingPlatformSelection();
-  const statusLabel = RecordingStatusLabel();
-  const buttons = RecordingButton();
-  const notes = RecordingNotes();
-  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() {
-  const recordingStatus = globals.state.recordingStatus;
-  if (!recordingStatus) return undefined;
-  return m('label', recordingStatus);
-}
-
-function Instructions(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'),
-    PERSIST_CONFIG_FLAG.get()
-      ? m(
-          'button.permalinkconfig',
-          {
-            onclick: () => createPermalink({mode: 'RECORDING_OPTS'}),
-          },
-          'Share recording settings',
-        )
-      : null,
-    RecordingSnippet(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() {
-  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(
-      globals.state.recordConfig,
-      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 (globals.state.recordConfig.mode === 'LONG_TRACE') {
-    notes.unshift(msgLongTraces);
-  }
-
-  return notes.length > 0 ? m('div', notes) : undefined;
-}
-
-function RecordingSnippet(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(targetInfo)});
-}
-
-function getRecordCommand(targetInfo: TargetInfo): string {
-  const recordCommand = recordConfigUtils.fetchLatestRecordCommand(
-    globals.state.recordConfig,
-    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() {
-  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(
-    globals.state.recordConfig,
-    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: () => raf.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'),
-        ),
-      ),
-      PERSIST_CONFIG_FLAG.get()
-        ? 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'),
-            ),
-          )
-        : null,
-    ),
-    m('header', 'Probes'),
-    m('ul', probes),
-  );
-}
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-function getRecordContainer(subpage?: string): m.Vnode<any, any> {
-  const components: m.Children[] = [RecordHeader()];
-  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'),
-    } as RecordingSectionAttrs),
-  );
-  pages.push(Instructions(maybeGetActiveCss(routePage, 'instructions')));
-  pages.push(Configurations(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),
-      } as RecordingSectionAttrs),
-    );
-  }
-
-  components.push(m('.record-container-content', pages));
-  return m('.record-container', components);
-}
-
-export const RecordPageV2 = createPage({
-  oninit(): void {
-    controller.initFactories();
-  },
-
-  view({attrs}: m.Vnode<PageAttrs>) {
-    return m(
-      '.record-page',
-      controller.getState() > RecordingState.TARGET_INFO_DISPLAYED
-        ? m('.hider')
-        : [],
-      getRecordContainer(attrs.subpage),
-    );
-  },
-});
diff --git a/ui/src/frontend/record_widgets.ts b/ui/src/frontend/record_widgets.ts
deleted file mode 100644
index e6e68f2..0000000
--- a/ui/src/frontend/record_widgets.ts
+++ /dev/null
@@ -1,469 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {Draft, produce} from 'immer';
-import m from 'mithril';
-
-import {copyToClipboard} from '../base/clipboard';
-import {assertExists} from '../base/logging';
-import {Actions} from '../common/actions';
-import {RecordConfig} from '../controller/record_config_types';
-
-import {globals} from './globals';
-
-export declare type Setter<T> = (draft: Draft<RecordConfig>, val: T) => void;
-export declare type Getter<T> = (cfg: RecordConfig) => T;
-
-function defaultSort(a: string, b: string) {
-  return a.localeCompare(b);
-}
-
-// +---------------------------------------------------------------------------+
-// | Docs link with 'i' in circle icon.                                        |
-// +---------------------------------------------------------------------------+
-
-interface DocsChipAttrs {
-  href: string;
-}
-
-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',
-    );
-  }
-}
-
-// +---------------------------------------------------------------------------+
-// | Probe: the rectangular box on the right-hand-side with a toggle box.      |
-// +---------------------------------------------------------------------------+
-
-export interface ProbeAttrs {
-  title: string;
-  img: string | null;
-  compact?: boolean;
-  descr: m.Children;
-  isEnabled: Getter<boolean>;
-  setEnabled: Setter<boolean>;
-}
-
-export class Probe implements m.ClassComponent<ProbeAttrs> {
-  view({attrs, children}: m.CVnode<ProbeAttrs>) {
-    const onToggle = (enabled: boolean) => {
-      const traceCfg = produce(globals.state.recordConfig, (draft) => {
-        attrs.setEnabled(draft, enabled);
-      });
-      globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
-    };
-
-    const enabled = attrs.isEnabled(globals.state.recordConfig);
-
-    return m(
-      `.probe${attrs.compact ? '.compact' : ''}${enabled ? '.enabled' : ''}`,
-      attrs.img &&
-        m('img', {
-          src: `${globals.root}assets/${attrs.img}`,
-          onclick: () => onToggle(!enabled),
-        }),
-      m(
-        'label',
-        m(`input[type=checkbox]`, {
-          checked: enabled,
-          oninput: (e: InputEvent) => {
-            onToggle((e.target as HTMLInputElement).checked);
-          },
-        }),
-        m('span', attrs.title),
-      ),
-      attrs.compact
-        ? ''
-        : m(
-            `div${attrs.img ? '' : '.extended-desc'}`,
-            m('div', attrs.descr),
-            m('.probe-config', children),
-          ),
-    );
-  }
-}
-
-export function CompactProbe(args: {
-  title: string;
-  isEnabled: Getter<boolean>;
-  setEnabled: Setter<boolean>;
-}) {
-  return m(Probe, {
-    title: args.title,
-    img: null,
-    compact: true,
-    descr: '',
-    isEnabled: args.isEnabled,
-    setEnabled: args.setEnabled,
-  } as ProbeAttrs);
-}
-
-// +-------------------------------------------------------------+
-// | Toggle: an on/off switch.
-// +-------------------------------------------------------------+
-
-export interface ToggleAttrs {
-  title: string;
-  descr: string;
-  cssClass?: string;
-  isEnabled: Getter<boolean>;
-  setEnabled: Setter<boolean>;
-}
-
-export class Toggle implements m.ClassComponent<ToggleAttrs> {
-  view({attrs}: m.CVnode<ToggleAttrs>) {
-    const onToggle = (enabled: boolean) => {
-      const traceCfg = produce(globals.state.recordConfig, (draft) => {
-        attrs.setEnabled(draft, enabled);
-      });
-      globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
-    };
-
-    const enabled = attrs.isEnabled(globals.state.recordConfig);
-
-    return m(
-      `.toggle${enabled ? '.enabled' : ''}${attrs.cssClass ?? ''}`,
-      m(
-        'label',
-        m(`input[type=checkbox]`, {
-          checked: enabled,
-          oninput: (e: InputEvent) => {
-            onToggle((e.target as HTMLInputElement).checked);
-          },
-        }),
-        m('span', attrs.title),
-      ),
-      m('.descr', attrs.descr),
-    );
-  }
-}
-
-// +---------------------------------------------------------------------------+
-// | Slider: draggable horizontal slider with numeric spinner.                 |
-// +---------------------------------------------------------------------------+
-
-export interface SliderAttrs {
-  title: string;
-  icon?: string;
-  cssClass?: string;
-  isTime?: boolean;
-  unit: string;
-  values: number[];
-  get: Getter<number>;
-  set: Setter<number>;
-  min?: number;
-  description?: string;
-  disabled?: boolean;
-  zeroIsDefault?: boolean;
-}
-
-export class Slider implements m.ClassComponent<SliderAttrs> {
-  onValueChange(attrs: SliderAttrs, newVal: number) {
-    const traceCfg = produce(globals.state.recordConfig, (draft) => {
-      attrs.set(draft, newVal);
-    });
-    globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
-  }
-
-  onTimeValueChange(attrs: SliderAttrs, hms: string) {
-    try {
-      const date = new Date(`1970-01-01T${hms}.000Z`);
-      if (isNaN(date.getTime())) return;
-      this.onValueChange(attrs, date.getTime());
-    } catch {}
-  }
-
-  onSliderChange(attrs: SliderAttrs, newIdx: number) {
-    this.onValueChange(attrs, attrs.values[newIdx]);
-  }
-
-  view({attrs}: m.CVnode<SliderAttrs>) {
-    const id = attrs.title.replace(/[^a-z0-9]/gim, '_').toLowerCase();
-    const maxIdx = attrs.values.length - 1;
-    const val = attrs.get(globals.state.recordConfig);
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    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().substr(11, 8),
-        oninput: (e: InputEvent) => {
-          this.onTimeValueChange(attrs, (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(attrs, +(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(attrs, +(e.target as HTMLInputElement).value);
-        },
-      }),
-      m(`input.spinner[min=${min}][for=${id}]`, spinnerCfg),
-      m('.unit', attrs.unit),
-    );
-  }
-}
-
-// +---------------------------------------------------------------------------+
-// | Dropdown: wrapper around <select>. Supports single an multiple selection. |
-// +---------------------------------------------------------------------------+
-
-export interface DropdownAttrs {
-  title: string;
-  cssClass?: string;
-  options: Map<string, string>;
-  sort?: (a: string, b: string) => number;
-  get: Getter<string[]>;
-  set: Setter<string[]>;
-}
-
-export class Dropdown implements m.ClassComponent<DropdownAttrs> {
-  resetScroll(dom: HTMLSelectElement) {
-    // Chrome seems to override the scroll offset on creationa, b without this,
-    // even though we call it after having marked the options as selected.
-    setTimeout(() => {
-      // Don't reset the scroll position if the element is still focused.
-      if (dom !== document.activeElement) dom.scrollTop = 0;
-    }, 0);
-  }
-
-  onChange(attrs: DropdownAttrs, e: Event) {
-    const dom = e.target as HTMLSelectElement;
-    const selKeys: string[] = [];
-    for (let i = 0; i < dom.selectedOptions.length; i++) {
-      const item = assertExists(dom.selectedOptions.item(i));
-      selKeys.push(item.value);
-    }
-    const traceCfg = produce(globals.state.recordConfig, (draft) => {
-      attrs.set(draft, selKeys);
-    });
-    globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
-  }
-
-  view({attrs}: m.CVnode<DropdownAttrs>) {
-    const options: m.Children = [];
-    const selItems = attrs.get(globals.state.recordConfig);
-    let numSelected = 0;
-    const entries = [...attrs.options.entries()];
-    const f = attrs.sort === undefined ? defaultSort : attrs.sort;
-    entries.sort((a, b) => f(a[1], b[1]));
-    for (const [key, label] of entries) {
-      const opts = {value: key, selected: false};
-      if (selItems.includes(key)) {
-        opts.selected = true;
-        numSelected++;
-      }
-      options.push(m('option', opts, label));
-    }
-    const label = `${attrs.title} ${numSelected ? `(${numSelected})` : ''}`;
-    return m(
-      `select.dropdown${attrs.cssClass ?? ''}[multiple=multiple]`,
-      {
-        onblur: (e: Event) => this.resetScroll(e.target as HTMLSelectElement),
-        onmouseleave: (e: Event) =>
-          this.resetScroll(e.target as HTMLSelectElement),
-        oninput: (e: Event) => this.onChange(attrs, e),
-        oncreate: (vnode) => this.resetScroll(vnode.dom as HTMLSelectElement),
-      },
-      m('optgroup', {label}, options),
-    );
-  }
-}
-
-// +---------------------------------------------------------------------------+
-// | Textarea: wrapper around <textarea>.                                      |
-// +---------------------------------------------------------------------------+
-
-export interface TextareaAttrs {
-  placeholder: string;
-  docsLink?: string;
-  cssClass?: string;
-  get: Getter<string>;
-  set: Setter<string>;
-  title?: string;
-}
-
-export class Textarea implements m.ClassComponent<TextareaAttrs> {
-  onChange(attrs: TextareaAttrs, dom: HTMLTextAreaElement) {
-    const traceCfg = produce(globals.state.recordConfig, (draft) => {
-      attrs.set(draft, dom.value);
-    });
-    globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
-  }
-
-  view({attrs}: m.CVnode<TextareaAttrs>) {
-    return m(
-      '.textarea-holder',
-      m(
-        'header',
-        attrs.title,
-        attrs.docsLink && [' ', m(DocsChip, {href: attrs.docsLink})],
-      ),
-      m(`textarea.extra-input${attrs.cssClass ?? ''}`, {
-        onchange: (e: Event) =>
-          this.onChange(attrs, e.target as HTMLTextAreaElement),
-        placeholder: attrs.placeholder,
-        value: attrs.get(globals.state.recordConfig),
-      }),
-    );
-  }
-}
-
-// +---------------------------------------------------------------------------+
-// | CodeSnippet: command-prompt-like box with code snippets to copy/paste.    |
-// +---------------------------------------------------------------------------+
-
-export interface CodeSnippetAttrs {
-  text: string;
-  hardWhitespace?: boolean;
-}
-
-export class CodeSnippet implements m.ClassComponent<CodeSnippetAttrs> {
-  view({attrs}: m.CVnode<CodeSnippetAttrs>) {
-    return m(
-      '.code-snippet',
-      m(
-        'button',
-        {
-          title: 'Copy to clipboard',
-          onclick: () => copyToClipboard(attrs.text),
-        },
-        m('i.material-icons', 'assignment'),
-      ),
-      m('code', attrs.text),
-    );
-  }
-}
-
-export interface CategoryGetter {
-  get: Getter<string[]>;
-  set: Setter<string[]>;
-}
-
-type CategoriesCheckboxListParams = CategoryGetter & {
-  categories: Map<string, string>;
-  title: string;
-};
-
-export class CategoriesCheckboxList
-  implements m.ClassComponent<CategoriesCheckboxListParams>
-{
-  updateValue(
-    attrs: CategoriesCheckboxListParams,
-    value: string,
-    enabled: boolean,
-  ) {
-    const traceCfg = produce(globals.state.recordConfig, (draft) => {
-      const values = attrs.get(draft);
-      const index = values.indexOf(value);
-      if (enabled && index === -1) {
-        values.push(value);
-      }
-      if (!enabled && index !== -1) {
-        values.splice(index, 1);
-      }
-    });
-    globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
-  }
-
-  view({attrs}: m.CVnode<CategoriesCheckboxListParams>) {
-    const enabled = new Set(attrs.get(globals.state.recordConfig));
-    return m(
-      '.categories-list',
-      m(
-        'h3',
-        attrs.title,
-        m(
-          'button.config-button',
-          {
-            onclick: () => {
-              const config = produce(globals.state.recordConfig, (draft) => {
-                attrs.set(draft, Array.from(attrs.categories.keys()));
-              });
-              globals.dispatch(Actions.setRecordConfig({config}));
-            },
-          },
-          'All',
-        ),
-        m(
-          'button.config-button',
-          {
-            onclick: () => {
-              const config = produce(globals.state.recordConfig, (draft) => {
-                attrs.set(draft, []);
-              });
-              globals.dispatch(Actions.setRecordConfig({config}));
-            },
-          },
-          'None',
-        ),
-      ),
-      m(
-        'ul.checkboxes',
-        Array.from(attrs.categories.entries()).map(([key, value]) => {
-          const id = `category-checkbox-${key}`;
-          return m(
-            'label',
-            {for: id},
-            m(
-              'li',
-              m('input[type=checkbox]', {
-                id,
-                checked: enabled.has(key),
-                onclick: (e: InputEvent) => {
-                  const target = e.target as HTMLInputElement;
-                  this.updateValue(attrs, key, target.checked);
-                },
-              }),
-              value,
-            ),
-          );
-        }),
-      ),
-    );
-  }
-}
diff --git a/ui/src/frontend/recording/advanced_settings.ts b/ui/src/frontend/recording/advanced_settings.ts
deleted file mode 100644
index ad2fc9c..0000000
--- a/ui/src/frontend/recording/advanced_settings.ts
+++ /dev/null
@@ -1,114 +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 {
-  Dropdown,
-  DropdownAttrs,
-  Probe,
-  ProbeAttrs,
-  Slider,
-  SliderAttrs,
-  Textarea,
-  TextareaAttrs,
-  Toggle,
-  ToggleAttrs,
-} from '../record_widgets';
-import {RecordingSectionAttrs} from './recording_sections';
-
-const FTRACE_CATEGORIES = new Map<string, string>();
-FTRACE_CATEGORIES.set('binder/*', 'binder');
-FTRACE_CATEGORIES.set('block/*', 'block');
-FTRACE_CATEGORIES.set('clk/*', 'clk');
-FTRACE_CATEGORIES.set('ext4/*', 'ext4');
-FTRACE_CATEGORIES.set('f2fs/*', 'f2fs');
-FTRACE_CATEGORIES.set('i2c/*', 'i2c');
-FTRACE_CATEGORIES.set('irq/*', 'irq');
-FTRACE_CATEGORIES.set('kmem/*', 'kmem');
-FTRACE_CATEGORIES.set('memory_bus/*', 'memory_bus');
-FTRACE_CATEGORIES.set('mmc/*', 'mmc');
-FTRACE_CATEGORIES.set('oom/*', 'oom');
-FTRACE_CATEGORIES.set('power/*', 'power');
-FTRACE_CATEGORIES.set('regulator/*', 'regulator');
-FTRACE_CATEGORIES.set('sched/*', 'sched');
-FTRACE_CATEGORIES.set('sync/*', 'sync');
-FTRACE_CATEGORIES.set('task/*', 'task');
-FTRACE_CATEGORIES.set('task/*', 'task');
-FTRACE_CATEGORIES.set('vmscan/*', 'vmscan');
-FTRACE_CATEGORIES.set('fastrpc/*', 'fastrpc');
-
-export class AdvancedSettings
-  implements m.ClassComponent<RecordingSectionAttrs>
-{
-  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
-    return m(
-      `.record-section${attrs.cssClass}`,
-      m(
-        Probe,
-        {
-          title: 'Advanced ftrace config',
-          img: 'rec_ftrace.png',
-          descr: `Enable individual events and tune the kernel-tracing (ftrace)
-                  module. The events enabled here are in addition to those from
-                  enabled by other probes.`,
-          setEnabled: (cfg, val) => (cfg.ftrace = val),
-          isEnabled: (cfg) => cfg.ftrace,
-        } as ProbeAttrs,
-        m(Toggle, {
-          title: 'Resolve kernel symbols',
-          cssClass: '.thin',
-          descr: `Enables lookup via /proc/kallsyms for workqueue,
-              sched_blocked_reason and other events
-              (userdebug/eng builds only).`,
-          setEnabled: (cfg, val) => (cfg.symbolizeKsyms = val),
-          isEnabled: (cfg) => cfg.symbolizeKsyms,
-        } as ToggleAttrs),
-        m(Slider, {
-          title: 'Buf size',
-          cssClass: '.thin',
-          values: [0, 512, 1024, 2 * 1024, 4 * 1024, 16 * 1024, 32 * 1024],
-          unit: 'KB',
-          zeroIsDefault: true,
-          set: (cfg, val) => (cfg.ftraceBufferSizeKb = val),
-          get: (cfg) => cfg.ftraceBufferSizeKb,
-        } as SliderAttrs),
-        m(Slider, {
-          title: 'Drain rate',
-          cssClass: '.thin',
-          values: [0, 100, 250, 500, 1000, 2500, 5000],
-          unit: 'ms',
-          zeroIsDefault: true,
-          set: (cfg, val) => (cfg.ftraceDrainPeriodMs = val),
-          get: (cfg) => cfg.ftraceDrainPeriodMs,
-        } as SliderAttrs),
-        m(Dropdown, {
-          title: 'Event groups',
-          cssClass: '.multicolumn.ftrace-events',
-          options: FTRACE_CATEGORIES,
-          set: (cfg, val) => (cfg.ftraceEvents = val),
-          get: (cfg) => cfg.ftraceEvents,
-        } as DropdownAttrs),
-        m(Textarea, {
-          placeholder:
-            'Add extra events, one per line, e.g.:\n' +
-            'sched/sched_switch\n' +
-            'kmem/*',
-          set: (cfg, val) => (cfg.ftraceExtraEvents = val),
-          get: (cfg) => cfg.ftraceExtraEvents,
-        } as TextareaAttrs),
-      ),
-    );
-  }
-}
diff --git a/ui/src/frontend/recording/android_settings.ts b/ui/src/frontend/recording/android_settings.ts
deleted file mode 100644
index 5d5cd70..0000000
--- a/ui/src/frontend/recording/android_settings.ts
+++ /dev/null
@@ -1,208 +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 {DataSourceDescriptor} from '../../protos';
-import {globals} from '../globals';
-import {
-  Dropdown,
-  DropdownAttrs,
-  Probe,
-  ProbeAttrs,
-  Slider,
-  SliderAttrs,
-  Textarea,
-  TextareaAttrs,
-  Toggle,
-  ToggleAttrs,
-} from '../record_widgets';
-
-import {RecordingSectionAttrs} from './recording_sections';
-
-const LOG_BUFFERS = new Map<string, string>();
-LOG_BUFFERS.set('LID_CRASH', 'Crash');
-LOG_BUFFERS.set('LID_DEFAULT', 'Main');
-LOG_BUFFERS.set('LID_EVENTS', 'Binary events');
-LOG_BUFFERS.set('LID_KERNEL', 'Kernel');
-LOG_BUFFERS.set('LID_RADIO', 'Radio');
-LOG_BUFFERS.set('LID_SECURITY', 'Security');
-LOG_BUFFERS.set('LID_STATS', 'Stats');
-LOG_BUFFERS.set('LID_SYSTEM', 'System');
-
-const DEFAULT_ATRACE_CATEGORIES = new Map<string, string>();
-DEFAULT_ATRACE_CATEGORIES.set('adb', 'ADB');
-DEFAULT_ATRACE_CATEGORIES.set('aidl', 'AIDL calls');
-DEFAULT_ATRACE_CATEGORIES.set('am', 'Activity Manager');
-DEFAULT_ATRACE_CATEGORIES.set('audio', 'Audio');
-DEFAULT_ATRACE_CATEGORIES.set('binder_driver', 'Binder Kernel driver');
-DEFAULT_ATRACE_CATEGORIES.set('binder_lock', 'Binder global lock trace');
-DEFAULT_ATRACE_CATEGORIES.set('bionic', 'Bionic C library');
-DEFAULT_ATRACE_CATEGORIES.set('camera', 'Camera');
-DEFAULT_ATRACE_CATEGORIES.set('dalvik', 'ART & Dalvik');
-DEFAULT_ATRACE_CATEGORIES.set('database', 'Database');
-DEFAULT_ATRACE_CATEGORIES.set('gfx', 'Graphics');
-DEFAULT_ATRACE_CATEGORIES.set('hal', 'Hardware Modules');
-DEFAULT_ATRACE_CATEGORIES.set('input', 'Input');
-DEFAULT_ATRACE_CATEGORIES.set('network', 'Network');
-DEFAULT_ATRACE_CATEGORIES.set('nnapi', 'Neural Network API');
-DEFAULT_ATRACE_CATEGORIES.set('pm', 'Package Manager');
-DEFAULT_ATRACE_CATEGORIES.set('power', 'Power Management');
-DEFAULT_ATRACE_CATEGORIES.set('res', 'Resource Loading');
-DEFAULT_ATRACE_CATEGORIES.set('rro', 'Resource Overlay');
-DEFAULT_ATRACE_CATEGORIES.set('rs', 'RenderScript');
-DEFAULT_ATRACE_CATEGORIES.set('sm', 'Sync Manager');
-DEFAULT_ATRACE_CATEGORIES.set('ss', 'System Server');
-DEFAULT_ATRACE_CATEGORIES.set('vibrator', 'Vibrator');
-DEFAULT_ATRACE_CATEGORIES.set('video', 'Video');
-DEFAULT_ATRACE_CATEGORIES.set('view', 'View System');
-DEFAULT_ATRACE_CATEGORIES.set('webview', 'WebView');
-DEFAULT_ATRACE_CATEGORIES.set('wm', 'Window Manager');
-
-function isDataSourceDescriptor(
-  descriptor: unknown,
-): descriptor is DataSourceDescriptor {
-  if (descriptor instanceof Object) {
-    return (descriptor as DataSourceDescriptor).name !== undefined;
-  }
-  return false;
-}
-
-class AtraceAppsList implements m.ClassComponent {
-  view() {
-    if (globals.state.recordConfig.allAtraceApps) {
-      return m('div');
-    }
-
-    return m(Textarea, {
-      placeholder:
-        'Apps to profile, one per line, e.g.:\n' +
-        'com.android.phone\n' +
-        'lmkd\n' +
-        'com.android.nfc',
-      cssClass: '.record-apps-list',
-      set: (cfg, val) => (cfg.atraceApps = val),
-      get: (cfg) => cfg.atraceApps,
-    } as TextareaAttrs);
-  }
-}
-
-export class AndroidSettings
-  implements m.ClassComponent<RecordingSectionAttrs>
-{
-  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
-    let atraceCategories = DEFAULT_ATRACE_CATEGORIES;
-    for (const dataSource of attrs.dataSources) {
-      if (
-        dataSource.name !== 'linux.ftrace' ||
-        !isDataSourceDescriptor(dataSource.descriptor)
-      ) {
-        continue;
-      }
-      const atraces = dataSource.descriptor.ftraceDescriptor?.atraceCategories;
-      if (!atraces || atraces.length === 0) {
-        break;
-      }
-
-      atraceCategories = new Map<string, string>();
-      for (const atrace of atraces) {
-        if (atrace.name) {
-          atraceCategories.set(atrace.name, atrace.description ?? '');
-        }
-      }
-    }
-
-    return m(
-      `.record-section${attrs.cssClass}`,
-      m(
-        Probe,
-        {
-          title: 'Atrace userspace annotations',
-          img: 'rec_atrace.png',
-          descr: `Enables C++ / Java codebase annotations (ATRACE_BEGIN() /
-                      os.Trace())`,
-          setEnabled: (cfg, val) => (cfg.atrace = val),
-          isEnabled: (cfg) => cfg.atrace,
-        } as ProbeAttrs,
-        m(Dropdown, {
-          title: 'Categories',
-          cssClass: '.multicolumn.atrace-categories',
-          options: atraceCategories,
-          set: (cfg, val) => (cfg.atraceCats = val),
-          get: (cfg) => cfg.atraceCats,
-        } as DropdownAttrs),
-        m(Toggle, {
-          title: 'Record events from all Android apps and services',
-          descr: '',
-          setEnabled: (cfg, val) => (cfg.allAtraceApps = val),
-          isEnabled: (cfg) => cfg.allAtraceApps,
-        } as ToggleAttrs),
-        m(AtraceAppsList),
-      ),
-      m(
-        Probe,
-        {
-          title: 'Event log (logcat)',
-          img: 'rec_logcat.png',
-          descr: `Streams the event log into the trace. If no buffer filter is
-                      specified, all buffers are selected.`,
-          setEnabled: (cfg, val) => (cfg.androidLogs = val),
-          isEnabled: (cfg) => cfg.androidLogs,
-        } as ProbeAttrs,
-        m(Dropdown, {
-          title: 'Buffers',
-          cssClass: '.multicolumn',
-          options: LOG_BUFFERS,
-          set: (cfg, val) => (cfg.androidLogBuffers = val),
-          get: (cfg) => cfg.androidLogBuffers,
-        } as DropdownAttrs),
-      ),
-      m(Probe, {
-        title: 'Frame timeline',
-        img: 'rec_frame_timeline.png',
-        descr: `Records expected/actual frame timings from surface_flinger.
-                      Requires Android 12 (S) or above.`,
-        setEnabled: (cfg, val) => (cfg.androidFrameTimeline = val),
-        isEnabled: (cfg) => cfg.androidFrameTimeline,
-      } as ProbeAttrs),
-      m(Probe, {
-        title: 'Game intervention list',
-        img: '',
-        descr: `List game modes and interventions.
-                    Requires Android 13 (T) or above.`,
-        setEnabled: (cfg, val) => (cfg.androidGameInterventionList = val),
-        isEnabled: (cfg) => cfg.androidGameInterventionList,
-      } as ProbeAttrs),
-      m(
-        Probe,
-        {
-          title: 'Network Tracing',
-          img: '',
-          descr: `Records detailed information on network packets.
-                      Requires Android 14 (U) or above.`,
-          setEnabled: (cfg, val) => (cfg.androidNetworkTracing = val),
-          isEnabled: (cfg) => cfg.androidNetworkTracing,
-        } as ProbeAttrs,
-        m(Slider, {
-          title: 'Poll interval',
-          cssClass: '.thin',
-          values: [100, 250, 500, 1000, 2500],
-          unit: 'ms',
-          set: (cfg, val) => (cfg.androidNetworkTracingPollMs = val),
-          get: (cfg) => cfg.androidNetworkTracingPollMs,
-        } as SliderAttrs),
-      ),
-    );
-  }
-}
diff --git a/ui/src/frontend/recording/chrome_settings.ts b/ui/src/frontend/recording/chrome_settings.ts
deleted file mode 100644
index ed51954..0000000
--- a/ui/src/frontend/recording/chrome_settings.ts
+++ /dev/null
@@ -1,239 +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 {produce} from 'immer';
-import m from 'mithril';
-
-import {Actions} from '../../common/actions';
-import {DataSource} from '../../common/recordingV2/recording_interfaces_v2';
-import {getBuiltinChromeCategoryList, isChromeTarget} from '../../common/state';
-import {
-  MultiSelect,
-  MultiSelectDiff,
-  Option as MultiSelectOption,
-} from '../../widgets/multiselect';
-import {Section} from '../../widgets/section';
-import {globals} from '../globals';
-import {
-  CategoryGetter,
-  CompactProbe,
-  Toggle,
-  ToggleAttrs,
-} from '../record_widgets';
-
-import {RecordingSectionAttrs} from './recording_sections';
-
-function extractChromeCategories(
-  dataSources: DataSource[],
-): string[] | undefined {
-  for (const dataSource of dataSources) {
-    if (dataSource.name === 'chromeCategories') {
-      return dataSource.descriptor as string[];
-    }
-  }
-  return undefined;
-}
-
-class ChromeCategoriesSelection
-  implements m.ClassComponent<RecordingSectionAttrs>
-{
-  private defaultCategoryOptions: MultiSelectOption[] | undefined = undefined;
-  private disabledByDefaultCategoryOptions: MultiSelectOption[] | undefined =
-    undefined;
-
-  static updateValue(attrs: CategoryGetter, diffs: MultiSelectDiff[]) {
-    const traceCfg = produce(globals.state.recordConfig, (draft) => {
-      const values = attrs.get(draft);
-      for (const diff of diffs) {
-        const value = diff.id;
-        const index = values.indexOf(value);
-        const enabled = diff.checked;
-        if (enabled && index === -1) {
-          values.push(value);
-        }
-        if (!enabled && index !== -1) {
-          values.splice(index, 1);
-        }
-      }
-    });
-    globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
-  }
-
-  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
-    const categoryConfigGetter: CategoryGetter = {
-      get: (cfg) => cfg.chromeCategoriesSelected,
-      set: (cfg, val) => (cfg.chromeCategoriesSelected = val),
-    };
-
-    if (
-      this.defaultCategoryOptions === undefined ||
-      this.disabledByDefaultCategoryOptions === undefined
-    ) {
-      // If we are attempting to record via the Chrome extension, we receive the
-      // list of actually supported categories via DevTools. Otherwise, we fall
-      // back to an integrated list of categories from a recent version of
-      // Chrome.
-      const enabled = new Set(
-        categoryConfigGetter.get(globals.state.recordConfig),
-      );
-      let categories =
-        globals.state.chromeCategories ||
-        extractChromeCategories(attrs.dataSources);
-      if (!categories || !isChromeTarget(globals.state.recordingTarget)) {
-        categories = getBuiltinChromeCategoryList();
-      }
-      this.defaultCategoryOptions = [];
-      this.disabledByDefaultCategoryOptions = [];
-      const disabledPrefix = 'disabled-by-default-';
-      categories.forEach((cat) => {
-        const checked = enabled.has(cat);
-
-        if (
-          cat.startsWith(disabledPrefix) &&
-          this.disabledByDefaultCategoryOptions !== undefined
-        ) {
-          this.disabledByDefaultCategoryOptions.push({
-            id: cat,
-            name: cat.replace(disabledPrefix, ''),
-            checked: checked,
-          });
-        } else if (
-          !cat.startsWith(disabledPrefix) &&
-          this.defaultCategoryOptions !== undefined
-        ) {
-          this.defaultCategoryOptions.push({
-            id: cat,
-            name: cat,
-            checked: checked,
-          });
-        }
-      });
-    }
-
-    return m(
-      'div.chrome-categories',
-      m(
-        Section,
-        {title: 'Additional Categories'},
-        m(MultiSelect, {
-          options: this.defaultCategoryOptions,
-          repeatCheckedItemsAtTop: false,
-          fixedSize: false,
-          onChange: (diffs: MultiSelectDiff[]) => {
-            diffs.forEach(({id, checked}) => {
-              if (this.defaultCategoryOptions === undefined) {
-                return;
-              }
-              for (const option of this.defaultCategoryOptions) {
-                if (option.id == id) {
-                  option.checked = checked;
-                }
-              }
-            });
-            ChromeCategoriesSelection.updateValue(categoryConfigGetter, diffs);
-          },
-        }),
-      ),
-      m(
-        Section,
-        {title: 'High Overhead Categories'},
-        m(MultiSelect, {
-          options: this.disabledByDefaultCategoryOptions,
-          repeatCheckedItemsAtTop: false,
-          fixedSize: false,
-          onChange: (diffs: MultiSelectDiff[]) => {
-            diffs.forEach(({id, checked}) => {
-              if (this.disabledByDefaultCategoryOptions === undefined) {
-                return;
-              }
-              for (const option of this.disabledByDefaultCategoryOptions) {
-                if (option.id == id) {
-                  option.checked = checked;
-                }
-              }
-            });
-            ChromeCategoriesSelection.updateValue(categoryConfigGetter, diffs);
-          },
-        }),
-      ),
-    );
-  }
-}
-
-export class ChromeSettings implements m.ClassComponent<RecordingSectionAttrs> {
-  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
-    return m(
-      `.record-section${attrs.cssClass}`,
-      CompactProbe({
-        title: 'Task scheduling',
-        setEnabled: (cfg, val) => (cfg.taskScheduling = val),
-        isEnabled: (cfg) => cfg.taskScheduling,
-      }),
-      CompactProbe({
-        title: 'IPC flows',
-        setEnabled: (cfg, val) => (cfg.ipcFlows = val),
-        isEnabled: (cfg) => cfg.ipcFlows,
-      }),
-      CompactProbe({
-        title: 'Javascript execution',
-        setEnabled: (cfg, val) => (cfg.jsExecution = val),
-        isEnabled: (cfg) => cfg.jsExecution,
-      }),
-      CompactProbe({
-        title: 'Web content rendering, layout and compositing',
-        setEnabled: (cfg, val) => (cfg.webContentRendering = val),
-        isEnabled: (cfg) => cfg.webContentRendering,
-      }),
-      CompactProbe({
-        title: 'UI rendering & surface compositing',
-        setEnabled: (cfg, val) => (cfg.uiRendering = val),
-        isEnabled: (cfg) => cfg.uiRendering,
-      }),
-      CompactProbe({
-        title: 'Input events',
-        setEnabled: (cfg, val) => (cfg.inputEvents = val),
-        isEnabled: (cfg) => cfg.inputEvents,
-      }),
-      CompactProbe({
-        title: 'Navigation & Loading',
-        setEnabled: (cfg, val) => (cfg.navigationAndLoading = val),
-        isEnabled: (cfg) => cfg.navigationAndLoading,
-      }),
-      CompactProbe({
-        title: 'Chrome Logs',
-        setEnabled: (cfg, val) => (cfg.chromeLogs = val),
-        isEnabled: (cfg) => cfg.chromeLogs,
-      }),
-      CompactProbe({
-        title: 'Audio',
-        setEnabled: (cfg, val) => (cfg.audio = val),
-        isEnabled: (cfg) => cfg.audio,
-      }),
-      CompactProbe({
-        title: 'Video',
-        setEnabled: (cfg, val) => (cfg.video = val),
-        isEnabled: (cfg) => cfg.video,
-      }),
-      m(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.',
-        setEnabled: (cfg, val) => (cfg.chromePrivacyFiltering = val),
-        isEnabled: (cfg) => cfg.chromePrivacyFiltering,
-      } as ToggleAttrs),
-      m(ChromeCategoriesSelection, attrs),
-    );
-  }
-}
diff --git a/ui/src/frontend/recording/cpu_settings.ts b/ui/src/frontend/recording/cpu_settings.ts
deleted file mode 100644
index 5799881..0000000
--- a/ui/src/frontend/recording/cpu_settings.ts
+++ /dev/null
@@ -1,79 +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 {Probe, ProbeAttrs, Slider, SliderAttrs} from '../record_widgets';
-import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
-
-export class CpuSettings implements m.ClassComponent<RecordingSectionAttrs> {
-  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
-    return m(
-      `.record-section${attrs.cssClass}`,
-      m(
-        Probe,
-        {
-          title: 'Coarse CPU usage counter',
-          img: 'rec_cpu_coarse.png',
-          descr: `Lightweight polling of CPU usage counters via /proc/stat.
-                    Allows to periodically monitor CPU usage.`,
-          setEnabled: (cfg, val) => (cfg.cpuCoarse = val),
-          isEnabled: (cfg) => cfg.cpuCoarse,
-        } as ProbeAttrs,
-        m(Slider, {
-          title: 'Poll interval',
-          cssClass: '.thin',
-          values: POLL_INTERVAL_MS,
-          unit: 'ms',
-          set: (cfg, val) => (cfg.cpuCoarsePollMs = val),
-          get: (cfg) => cfg.cpuCoarsePollMs,
-        } as SliderAttrs),
-      ),
-      m(Probe, {
-        title: 'Scheduling details',
-        img: 'rec_cpu_fine.png',
-        descr: 'Enables high-detailed tracking of scheduling events',
-        setEnabled: (cfg, val) => (cfg.cpuSched = val),
-        isEnabled: (cfg) => cfg.cpuSched,
-      } as ProbeAttrs),
-      m(
-        Probe,
-        {
-          title: 'CPU frequency and idle states',
-          img: 'rec_cpu_freq.png',
-          descr:
-            'Records cpu frequency and idle state changes via ftrace and sysfs',
-          setEnabled: (cfg, val) => (cfg.cpuFreq = val),
-          isEnabled: (cfg) => cfg.cpuFreq,
-        } as ProbeAttrs,
-        m(Slider, {
-          title: 'Sysfs poll interval',
-          cssClass: '.thin',
-          values: POLL_INTERVAL_MS,
-          unit: 'ms',
-          set: (cfg, val) => (cfg.cpuFreqPollMs = val),
-          get: (cfg) => cfg.cpuFreqPollMs,
-        } as SliderAttrs),
-      ),
-      m(Probe, {
-        title: 'Syscalls',
-        img: 'rec_syscalls.png',
-        descr: `Tracks the enter and exit of all syscalls. On Android
-                requires a userdebug or eng build.`,
-        setEnabled: (cfg, val) => (cfg.cpuSyscall = val),
-        isEnabled: (cfg) => cfg.cpuSyscall,
-      } as ProbeAttrs),
-    );
-  }
-}
diff --git a/ui/src/frontend/recording/etw_settings.ts b/ui/src/frontend/recording/etw_settings.ts
deleted file mode 100644
index 64ffc51..0000000
--- a/ui/src/frontend/recording/etw_settings.ts
+++ /dev/null
@@ -1,41 +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 {Probe, ProbeAttrs} from '../record_widgets';
-
-import {RecordingSectionAttrs} from './recording_sections';
-
-export class EtwSettings implements m.ClassComponent<RecordingSectionAttrs> {
-  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
-    return m(
-      `.record-section${attrs.cssClass}`,
-      m(Probe, {
-        title: 'CSwitch',
-        img: null,
-        descr: `Enables to recording of context switches.`,
-        setEnabled: (cfg, val) => (cfg.etwCSwitch = val),
-        isEnabled: (cfg) => cfg.etwCSwitch,
-      } as ProbeAttrs),
-      m(Probe, {
-        title: 'Dispatcher',
-        img: null,
-        descr: 'Enables to get thread state.',
-        setEnabled: (cfg, val) => (cfg.etwThreadState = val),
-        isEnabled: (cfg) => cfg.etwThreadState,
-      } as ProbeAttrs),
-    );
-  }
-}
diff --git a/ui/src/frontend/recording/gpu_settings.ts b/ui/src/frontend/recording/gpu_settings.ts
deleted file mode 100644
index 1f3f201..0000000
--- a/ui/src/frontend/recording/gpu_settings.ts
+++ /dev/null
@@ -1,49 +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 {Probe, ProbeAttrs} from '../record_widgets';
-import {RecordingSectionAttrs} from './recording_sections';
-
-export class GpuSettings implements m.ClassComponent<RecordingSectionAttrs> {
-  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
-    return m(
-      `.record-section${attrs.cssClass}`,
-      m(Probe, {
-        title: 'GPU frequency',
-        img: 'rec_cpu_freq.png',
-        descr: 'Records gpu frequency via ftrace',
-        setEnabled: (cfg, val) => (cfg.gpuFreq = val),
-        isEnabled: (cfg) => cfg.gpuFreq,
-      } as ProbeAttrs),
-      m(Probe, {
-        title: 'GPU memory',
-        img: 'rec_gpu_mem_total.png',
-        descr: `Allows to track per process and global total GPU memory usages.
-                (Available on recent Android 12+ kernels)`,
-        setEnabled: (cfg, val) => (cfg.gpuMemTotal = val),
-        isEnabled: (cfg) => cfg.gpuMemTotal,
-      } as ProbeAttrs),
-      m(Probe, {
-        title: 'GPU work period',
-        img: 'rec_cpu_voltage.png',
-        descr: `Allows to track per package GPU work.
-                (Available on recent Android 14+ kernels)`,
-        setEnabled: (cfg, val) => (cfg.gpuWorkPeriod = val),
-        isEnabled: (cfg) => cfg.gpuWorkPeriod,
-      } as ProbeAttrs),
-    );
-  }
-}
diff --git a/ui/src/frontend/recording/linux_perf_settings.ts b/ui/src/frontend/recording/linux_perf_settings.ts
deleted file mode 100644
index 96e0b71..0000000
--- a/ui/src/frontend/recording/linux_perf_settings.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {
-  Probe,
-  ProbeAttrs,
-  Slider,
-  SliderAttrs,
-  Textarea,
-  TextareaAttrs,
-} from '../record_widgets';
-
-import {RecordingSectionAttrs} from './recording_sections';
-
-const PLACEHOLDER_TEXT = `Filters for processes to profile, one per line e.g.:
-com.android.phone
-lmkd
-com.android.webview:sandboxed_process*`;
-
-export interface LinuxPerfConfiguration {
-  targets: string[];
-}
-
-export class LinuxPerfSettings
-  implements m.ClassComponent<RecordingSectionAttrs>
-{
-  config = {targets: []} as LinuxPerfConfiguration;
-  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
-    return m(
-      `.record-section${attrs.cssClass}`,
-      m(
-        Probe,
-        {
-          title: 'Callstack sampling',
-          img: 'rec_profiling.png',
-          descr: `Periodically records the current callstack (chain of 
-              function calls) of processes.`,
-          setEnabled: (cfg, val) => (cfg.tracePerf = val),
-          isEnabled: (cfg) => cfg.tracePerf,
-        } as ProbeAttrs,
-        m(Slider, {
-          title: 'Sampling Frequency',
-          cssClass: '.thin',
-          values: [20, 40, 60, 80, 100, 120, 140, 160, 180, 200],
-          unit: 'hz',
-          set: (cfg, val) => (cfg.timebaseFrequency = val),
-          get: (cfg) => cfg.timebaseFrequency,
-        } as SliderAttrs),
-        m(Textarea, {
-          placeholder: PLACEHOLDER_TEXT,
-          cssClass: '.record-apps-list',
-          set: (cfg, val) => {
-            cfg.targetCmdLine = val.split('\n');
-          },
-          get: (cfg) => cfg.targetCmdLine.join('\n'),
-        } as TextareaAttrs),
-      ),
-    );
-  }
-}
diff --git a/ui/src/frontend/recording/memory_settings.ts b/ui/src/frontend/recording/memory_settings.ts
deleted file mode 100644
index 330866b..0000000
--- a/ui/src/frontend/recording/memory_settings.ts
+++ /dev/null
@@ -1,342 +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 {MeminfoCounters, VmstatCounters} from '../../protos';
-import {globals} from '../globals';
-import {
-  Dropdown,
-  DropdownAttrs,
-  Probe,
-  ProbeAttrs,
-  Slider,
-  SliderAttrs,
-  Textarea,
-  TextareaAttrs,
-  Toggle,
-  ToggleAttrs,
-} from '../record_widgets';
-
-import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
-
-class HeapSettings implements m.ClassComponent<RecordingSectionAttrs> {
-  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
-    const valuesForMS = [
-      0,
-      1000,
-      10 * 1000,
-      30 * 1000,
-      60 * 1000,
-      5 * 60 * 1000,
-      10 * 60 * 1000,
-      30 * 60 * 1000,
-      60 * 60 * 1000,
-    ];
-    const valuesForShMemBuff = [
-      0,
-      512,
-      1024,
-      2 * 1024,
-      4 * 1024,
-      8 * 1024,
-      16 * 1024,
-      32 * 1024,
-      64 * 1024,
-      128 * 1024,
-      256 * 1024,
-      512 * 1024,
-      1024 * 1024,
-      64 * 1024 * 1024,
-      128 * 1024 * 1024,
-      256 * 1024 * 1024,
-      512 * 1024 * 1024,
-    ];
-
-    return m(
-      `.${attrs.cssClass}`,
-      m(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',
-        set: (cfg, val) => (cfg.hpProcesses = val),
-        get: (cfg) => cfg.hpProcesses,
-      } as TextareaAttrs),
-      m(Slider, {
-        title: 'Sampling interval',
-        cssClass: '.thin',
-        values: [
-          0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192,
-          16384, 32768, 65536, 131072, 262144, 524288, 1048576,
-        ],
-        unit: 'B',
-        min: 0,
-        set: (cfg, val) => (cfg.hpSamplingIntervalBytes = val),
-        get: (cfg) => cfg.hpSamplingIntervalBytes,
-      } as SliderAttrs),
-      m(Slider, {
-        title: 'Continuous dumps interval ',
-        description: 'Time between following dumps (0 = disabled)',
-        cssClass: '.thin',
-        values: valuesForMS,
-        unit: 'ms',
-        min: 0,
-        set: (cfg, val) => {
-          cfg.hpContinuousDumpsInterval = val;
-        },
-        get: (cfg) => cfg.hpContinuousDumpsInterval,
-      } as SliderAttrs),
-      m(Slider, {
-        title: 'Continuous dumps phase',
-        description: 'Time before first dump',
-        cssClass: `.thin${
-          globals.state.recordConfig.hpContinuousDumpsInterval === 0
-            ? '.greyed-out'
-            : ''
-        }`,
-        values: valuesForMS,
-        unit: 'ms',
-        min: 0,
-        disabled: globals.state.recordConfig.hpContinuousDumpsInterval === 0,
-        set: (cfg, val) => (cfg.hpContinuousDumpsPhase = val),
-        get: (cfg) => cfg.hpContinuousDumpsPhase,
-      } as SliderAttrs),
-      m(Slider, {
-        title: `Shared memory buffer`,
-        cssClass: '.thin',
-        values: valuesForShMemBuff.filter(
-          (value) => value === 0 || (value >= 8192 && value % 4096 === 0),
-        ),
-        unit: 'B',
-        min: 0,
-        set: (cfg, val) => (cfg.hpSharedMemoryBuffer = val),
-        get: (cfg) => cfg.hpSharedMemoryBuffer,
-      } as SliderAttrs),
-      m(Toggle, {
-        title: 'Block client',
-        cssClass: '.thin',
-        descr: `Slow down target application if profiler cannot keep up.`,
-        setEnabled: (cfg, val) => (cfg.hpBlockClient = val),
-        isEnabled: (cfg) => cfg.hpBlockClient,
-      } as ToggleAttrs),
-      m(Toggle, {
-        title: 'All custom allocators (Q+)',
-        cssClass: '.thin',
-        descr: `If the target application exposes custom allocators, also
-sample from those.`,
-        setEnabled: (cfg, val) => (cfg.hpAllHeaps = val),
-        isEnabled: (cfg) => cfg.hpAllHeaps,
-      } as ToggleAttrs),
-      // TODO(hjd): Add advanced options.
-    );
-  }
-}
-
-class JavaHeapDumpSettings implements m.ClassComponent<RecordingSectionAttrs> {
-  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
-    const valuesForMS = [
-      0,
-      1000,
-      10 * 1000,
-      30 * 1000,
-      60 * 1000,
-      5 * 60 * 1000,
-      10 * 60 * 1000,
-      30 * 60 * 1000,
-      60 * 60 * 1000,
-    ];
-
-    return m(
-      `.${attrs.cssClass}`,
-      m(Textarea, {
-        title: 'Names or pids of the processes to track (required)',
-        placeholder: 'One per line, e.g.:\n' + 'com.android.vending\n' + '1503',
-        set: (cfg, val) => (cfg.jpProcesses = val),
-        get: (cfg) => cfg.jpProcesses,
-      } as TextareaAttrs),
-      m(Slider, {
-        title: 'Continuous dumps interval ',
-        description: 'Time between following dumps (0 = disabled)',
-        cssClass: '.thin',
-        values: valuesForMS,
-        unit: 'ms',
-        min: 0,
-        set: (cfg, val) => {
-          cfg.jpContinuousDumpsInterval = val;
-        },
-        get: (cfg) => cfg.jpContinuousDumpsInterval,
-      } as SliderAttrs),
-      m(Slider, {
-        title: 'Continuous dumps phase',
-        description: 'Time before first dump',
-        cssClass: `.thin${
-          globals.state.recordConfig.jpContinuousDumpsInterval === 0
-            ? '.greyed-out'
-            : ''
-        }`,
-        values: valuesForMS,
-        unit: 'ms',
-        min: 0,
-        disabled: globals.state.recordConfig.jpContinuousDumpsInterval === 0,
-        set: (cfg, val) => (cfg.jpContinuousDumpsPhase = val),
-        get: (cfg) => cfg.jpContinuousDumpsPhase,
-      } as SliderAttrs),
-    );
-  }
-}
-
-export class MemorySettings implements m.ClassComponent<RecordingSectionAttrs> {
-  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
-    const meminfoOpts = new Map<string, string>();
-    for (const x in MeminfoCounters) {
-      if (
-        typeof MeminfoCounters[x] === 'number' &&
-        !`${x}`.endsWith('_UNSPECIFIED')
-      ) {
-        meminfoOpts.set(x, x.replace('MEMINFO_', '').toLowerCase());
-      }
-    }
-    const vmstatOpts = new Map<string, string>();
-    for (const x in VmstatCounters) {
-      if (
-        typeof VmstatCounters[x] === 'number' &&
-        !`${x}`.endsWith('_UNSPECIFIED')
-      ) {
-        vmstatOpts.set(x, x.replace('VMSTAT_', '').toLowerCase());
-      }
-    }
-    return m(
-      `.record-section${attrs.cssClass}`,
-      m(
-        Probe,
-        {
-          title: 'Native heap profiling',
-          img: 'rec_native_heap_profiler.png',
-          descr: `Track native heap allocations & deallocations of an Android
-               process. (Available on Android 10+)`,
-          setEnabled: (cfg, val) => (cfg.heapProfiling = val),
-          isEnabled: (cfg) => cfg.heapProfiling,
-        } as ProbeAttrs,
-        m(HeapSettings, attrs),
-      ),
-      m(
-        Probe,
-        {
-          title: 'Java heap dumps',
-          img: 'rec_java_heap_dump.png',
-          descr: `Dump information about the Java object graph of an
-          Android app. (Available on Android 11+)`,
-          setEnabled: (cfg, val) => (cfg.javaHeapDump = val),
-          isEnabled: (cfg) => cfg.javaHeapDump,
-        } as ProbeAttrs,
-        m(JavaHeapDumpSettings, attrs),
-      ),
-      m(
-        Probe,
-        {
-          title: 'Kernel meminfo',
-          img: 'rec_meminfo.png',
-          descr: 'Polling of /proc/meminfo',
-          setEnabled: (cfg, val) => (cfg.meminfo = val),
-          isEnabled: (cfg) => cfg.meminfo,
-        } as ProbeAttrs,
-        m(Slider, {
-          title: 'Poll interval',
-          cssClass: '.thin',
-          values: POLL_INTERVAL_MS,
-          unit: 'ms',
-          set: (cfg, val) => (cfg.meminfoPeriodMs = val),
-          get: (cfg) => cfg.meminfoPeriodMs,
-        } as SliderAttrs),
-        m(Dropdown, {
-          title: 'Select counters',
-          cssClass: '.multicolumn',
-          options: meminfoOpts,
-          set: (cfg, val) => (cfg.meminfoCounters = val),
-          get: (cfg) => cfg.meminfoCounters,
-        } as DropdownAttrs),
-      ),
-      m(Probe, {
-        title: 'High-frequency memory events',
-        img: 'rec_mem_hifreq.png',
-        descr: `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`,
-        setEnabled: (cfg, val) => (cfg.memHiFreq = val),
-        isEnabled: (cfg) => cfg.memHiFreq,
-      } as ProbeAttrs),
-      m(Probe, {
-        title: 'Low memory killer',
-        img: 'rec_lmk.png',
-        descr: `Record LMK events. Works both with the old in-kernel LMK
-                and the newer userspace lmkd. It also tracks OOM score
-                adjustments.`,
-        setEnabled: (cfg, val) => (cfg.memLmk = val),
-        isEnabled: (cfg) => cfg.memLmk,
-      } as ProbeAttrs),
-      m(
-        Probe,
-        {
-          title: 'Per process stats',
-          img: 'rec_ps_stats.png',
-          descr: `Periodically samples all processes in the system tracking:
-                    their thread list, memory counters (RSS, swap and other
-                    /proc/status counters) and oom_score_adj.`,
-          setEnabled: (cfg, val) => (cfg.procStats = val),
-          isEnabled: (cfg) => cfg.procStats,
-        } as ProbeAttrs,
-        m(Slider, {
-          title: 'Poll interval',
-          cssClass: '.thin',
-          values: POLL_INTERVAL_MS,
-          unit: 'ms',
-          set: (cfg, val) => (cfg.procStatsPeriodMs = val),
-          get: (cfg) => cfg.procStatsPeriodMs,
-        } as SliderAttrs),
-      ),
-      m(
-        Probe,
-        {
-          title: 'Virtual memory stats',
-          img: 'rec_vmstat.png',
-          descr: `Periodically polls virtual memory stats from /proc/vmstat.
-                    Allows to gather statistics about swap, eviction,
-                    compression and pagecache efficiency`,
-          setEnabled: (cfg, val) => (cfg.vmstat = val),
-          isEnabled: (cfg) => cfg.vmstat,
-        } as ProbeAttrs,
-        m(Slider, {
-          title: 'Poll interval',
-          cssClass: '.thin',
-          values: POLL_INTERVAL_MS,
-          unit: 'ms',
-          set: (cfg, val) => (cfg.vmstatPeriodMs = val),
-          get: (cfg) => cfg.vmstatPeriodMs,
-        } as SliderAttrs),
-        m(Dropdown, {
-          title: 'Select counters',
-          cssClass: '.multicolumn',
-          options: vmstatOpts,
-          set: (cfg, val) => (cfg.vmstatCounters = val),
-          get: (cfg) => cfg.vmstatCounters,
-        } as DropdownAttrs),
-      ),
-    );
-  }
-}
diff --git a/ui/src/frontend/recording/power_settings.ts b/ui/src/frontend/recording/power_settings.ts
deleted file mode 100644
index b449229..0000000
--- a/ui/src/frontend/recording/power_settings.ts
+++ /dev/null
@@ -1,84 +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 {globals} from '../globals';
-import {Probe, ProbeAttrs, Slider, SliderAttrs} from '../record_widgets';
-import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
-
-export class PowerSettings implements m.ClassComponent<RecordingSectionAttrs> {
-  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
-    const DOC_URL = 'https://perfetto.dev/docs/data-sources/battery-counters';
-    const descr = [
-      m(
-        'div',
-        m(
-          'span',
-          `Polls charge counters and instantaneous power draw from
-                    the battery power management IC and the power rails from
-                    the PowerStats HAL (`,
-        ),
-        m('a', {href: DOC_URL, target: '_blank'}, 'see docs for more'),
-        m('span', ')'),
-      ),
-    ];
-    if (globals.isInternalUser) {
-      descr.push(
-        m(
-          'div',
-          m('span', 'Googlers: See '),
-          m(
-            'a',
-            {href: 'http://go/power-rails-internal-doc', target: '_blank'},
-            'this doc',
-          ),
-          m(
-            'span',
-            ` for instructions on how to change the default rail selection
-                  on internal devices.`,
-          ),
-        ),
-      );
-    }
-    return m(
-      `.record-section${attrs.cssClass}`,
-      m(
-        Probe,
-        {
-          title: 'Battery drain & power rails',
-          img: 'rec_battery_counters.png',
-          descr,
-          setEnabled: (cfg, val) => (cfg.batteryDrain = val),
-          isEnabled: (cfg) => cfg.batteryDrain,
-        } as ProbeAttrs,
-        m(Slider, {
-          title: 'Poll interval',
-          cssClass: '.thin',
-          values: POLL_INTERVAL_MS,
-          unit: 'ms',
-          set: (cfg, val) => (cfg.batteryDrainPollMs = val),
-          get: (cfg) => cfg.batteryDrainPollMs,
-        } as SliderAttrs),
-      ),
-      m(Probe, {
-        title: 'Board voltages & frequencies',
-        img: 'rec_board_voltage.png',
-        descr: 'Tracks voltage and frequency changes from board sensors',
-        setEnabled: (cfg, val) => (cfg.boardSensors = val),
-        isEnabled: (cfg) => cfg.boardSensors,
-      } as ProbeAttrs),
-    );
-  }
-}
diff --git a/ui/src/frontend/recording/recording_multiple_choice.ts b/ui/src/frontend/recording/recording_multiple_choice.ts
deleted file mode 100644
index 9f01753..0000000
--- a/ui/src/frontend/recording/recording_multiple_choice.ts
+++ /dev/null
@@ -1,116 +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 '../../common/recordingV2/recording_interfaces_v2';
-import {RecordingPageController} from '../../common/recordingV2/recording_page_controller';
-import {RECORDING_MODAL_DIALOG_KEY} from '../../common/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/frontend/recording/recording_sections.ts b/ui/src/frontend/recording/recording_sections.ts
deleted file mode 100644
index f0e3fa1..0000000
--- a/ui/src/frontend/recording/recording_sections.ts
+++ /dev/null
@@ -1,22 +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 {DataSource} from '../../common/recordingV2/recording_interfaces_v2';
-
-export interface RecordingSectionAttrs {
-  dataSources: DataSource[];
-  cssClass: string;
-}
-
-export const POLL_INTERVAL_MS = [250, 500, 1000, 2500, 5000, 30000, 60000];
diff --git a/ui/src/frontend/recording/recording_settings.ts b/ui/src/frontend/recording/recording_settings.ts
deleted file mode 100644
index 64b11ae..0000000
--- a/ui/src/frontend/recording/recording_settings.ts
+++ /dev/null
@@ -1,103 +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 {produce} from 'immer';
-import m from 'mithril';
-
-import {Actions} from '../../common/actions';
-import {RecordMode} from '../../common/state';
-import {globals} from '../globals';
-import {Slider, SliderAttrs} from '../record_widgets';
-
-import {RecordingSectionAttrs} from './recording_sections';
-
-export class RecordingSettings
-  implements m.ClassComponent<RecordingSectionAttrs>
-{
-  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
-    const S = (x: number) => x * 1000;
-    const M = (x: number) => x * 1000 * 60;
-    const H = (x: number) => x * 1000 * 60 * 60;
-
-    const cfg = globals.state.recordConfig;
-
-    const recButton = (mode: RecordMode, title: string, img: string) => {
-      const checkboxArgs = {
-        checked: cfg.mode === mode,
-        onchange: (e: InputEvent) => {
-          const checked = (e.target as HTMLInputElement).checked;
-          if (!checked) return;
-          const traceCfg = produce(globals.state.recordConfig, (draft) => {
-            draft.mode = mode;
-          });
-          globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
-        },
-      };
-      return m(
-        `label${cfg.mode === mode ? '.selected' : ''}`,
-        m(`input[type=radio][name=rec_mode]`, checkboxArgs),
-        m(`img[src=${globals.root}assets/${img}]`),
-        m('span', title),
-      );
-    };
-
-    return m(
-      `.record-section${attrs.cssClass}`,
-      m('header', 'Recording mode'),
-      m(
-        '.record-mode',
-        recButton('STOP_WHEN_FULL', 'Stop when full', 'rec_one_shot.png'),
-        recButton('RING_BUFFER', 'Ring buffer', 'rec_ring_buf.png'),
-        recButton('LONG_TRACE', 'Long trace', 'rec_long_trace.png'),
-      ),
-
-      m(Slider, {
-        title: 'In-memory buffer size',
-        icon: '360',
-        values: [4, 8, 16, 32, 64, 128, 256, 512],
-        unit: 'MB',
-        set: (cfg, val) => (cfg.bufferSizeMb = val),
-        get: (cfg) => cfg.bufferSizeMb,
-      } as SliderAttrs),
-
-      m(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)],
-        isTime: true,
-        unit: 'h:m:s',
-        set: (cfg, val) => (cfg.durationMs = val),
-        get: (cfg) => cfg.durationMs,
-      } as SliderAttrs),
-      m(Slider, {
-        title: 'Max file size',
-        icon: 'save',
-        cssClass: cfg.mode !== 'LONG_TRACE' ? '.hide' : '',
-        values: [5, 25, 50, 100, 500, 1000, 1000 * 5, 1000 * 10],
-        unit: 'MB',
-        set: (cfg, val) => (cfg.maxFileSizeMb = val),
-        get: (cfg) => cfg.maxFileSizeMb,
-      } as SliderAttrs),
-      m(Slider, {
-        title: 'Flush on disk every',
-        cssClass: cfg.mode !== 'LONG_TRACE' ? '.hide' : '',
-        icon: 'av_timer',
-        values: [100, 250, 500, 1000, 2500, 5000],
-        unit: 'ms',
-        set: (cfg, val) => (cfg.fileWritePeriodMs = val),
-        get: (cfg) => cfg.fileWritePeriodMs || 0,
-      } as SliderAttrs),
-    );
-  }
-}
diff --git a/ui/src/frontend/recording/reset_interface_modal.ts b/ui/src/frontend/recording/reset_interface_modal.ts
deleted file mode 100644
index 961ac07..0000000
--- a/ui/src/frontend/recording/reset_interface_modal.ts
+++ /dev/null
@@ -1,71 +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 {showModal} from '../../widgets/modal';
-
-import {FORCE_RESET_MESSAGE} from './recording_ui_utils';
-
-export function couldNotClaimInterface(
-  onReset: () => Promise<void>,
-  onCancel: () => void,
-) {
-  let hasPressedAButton = false;
-  showModal({
-    title: 'Could not claim the USB interface',
-    content: m(
-      'div',
-      m(
-        'text',
-        'This can happen if you have the Android Debug Bridge ' +
-          '(adb) running on your workstation or any other tool which is ' +
-          'taking exclusive access of the USB interface.',
-      ),
-      m('br'),
-      m('br'),
-      m(
-        'text.small-font',
-        'Resetting will cause the ADB server to disconnect and ' +
-          'will try to reassign the interface to the current browser.',
-      ),
-    ),
-    buttons: [
-      {
-        text: FORCE_RESET_MESSAGE,
-        primary: true,
-        id: 'force_USB_interface',
-        action: () => {
-          hasPressedAButton = true;
-          onReset();
-        },
-      },
-      {
-        text: 'Cancel',
-        primary: false,
-        id: 'cancel_USB_interface',
-        action: () => {
-          hasPressedAButton = true;
-          onCancel();
-        },
-      },
-    ],
-  }).then(() => {
-    // If the user has clicked away from the modal, we interpret that as a
-    // 'Cancel'.
-    if (!hasPressedAButton) {
-      onCancel();
-    }
-  });
-}
diff --git a/ui/src/frontend/recording/reset_target_modal.ts b/ui/src/frontend/recording/reset_target_modal.ts
deleted file mode 100644
index a73aa44..0000000
--- a/ui/src/frontend/recording/reset_target_modal.ts
+++ /dev/null
@@ -1,188 +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 '../../common/recordingV2/recording_page_controller';
-import {
-  EXTENSION_URL,
-  RECORDING_MODAL_DIALOG_KEY,
-} from '../../common/recordingV2/recording_utils';
-import {
-  CHROME_TARGET_FACTORY,
-  ChromeTargetFactory,
-} from '../../common/recordingV2/target_factories/chrome_target_factory';
-import {targetFactoryRegistry} from '../../common/recordingV2/target_factory_registry';
-import {WebsocketMenuController} from '../../common/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/frontend/reorderable_cells.ts b/ui/src/frontend/reorderable_cells.ts
index 1e67616..e8e87f9 100644
--- a/ui/src/frontend/reorderable_cells.ts
+++ b/ui/src/frontend/reorderable_cells.ts
@@ -1,22 +1,19 @@
-/*
- * 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.
- */
+// 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 {DropDirection} from '../common/dragndrop_logic';
+import {DropDirection} from '../core/pivot_table_manager';
 import {raf} from '../core/raf_scheduler';
 
 export interface ReorderableCell {
diff --git a/ui/src/frontend/router.ts b/ui/src/frontend/router.ts
deleted file mode 100644
index 0a51519..0000000
--- a/ui/src/frontend/router.ts
+++ /dev/null
@@ -1,345 +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 {assertExists, assertTrue} from '../base/logging';
-import {PageAttrs} from './pages';
-import {z} from 'zod';
-
-export const ROUTE_PREFIX = '#!';
-const DEFAULT_ROUTE = '/';
-
-// The set of args that can be set on the route via #!/page?a=1&b2.
-// Route args are orthogonal to pages (i.e. should NOT make sense only in a
-// only within a specific page, use /page/subpages for that).
-// Args are !== the querystring (location.search) which is sent to the
-// server. The route args are NOT sent to the HTTP server.
-// Given this URL:
-// http://host/?foo=1&bar=2#!/page/subpage?local_cache_key=a0b1&baz=3.
-//
-// location.search = 'foo=1&bar=2'.
-//   This is seen by the HTTP server. We really don't use querystrings as the
-//   perfetto UI is client only.
-//
-// location.hash = '#!/page/subpage?local_cache_key=a0b1'.
-//   This is client-only. All the routing logic in the Perfetto UI uses only
-//   this.
-
-// We use .catch(undefined) on every field below to make sure that passing an
-// invalid value doesn't invalidate the other keys which might be valid.
-// Zod default behaviour is atomic: either everything validates correctly or
-// the whole parsing fails.
-const ROUTE_SCHEMA = z
-  .object({
-    // The local_cache_key is special and is persisted across navigations.
-    local_cache_key: z.string().optional().catch(undefined),
-
-    // These are transient and are really set only on startup.
-
-    // Are we loading a trace via ABT.
-    openFromAndroidBugTool: z.boolean().optional().catch(undefined),
-
-    // For permalink hash.
-    s: z.string().optional().catch(undefined),
-
-    // DEPRECATED: for #!/record?p=cpu subpages (b/191255021).
-    p: z.string().optional().catch(undefined),
-
-    // For fetching traces from Cloud Storage or local servers
-    // as with record_android_trace.
-    url: z.string().optional().catch(undefined),
-
-    // For connecting to a trace_processor_shell --httpd instance running on a
-    // non-standard port. This requires the CSP_WS_PERMISSIVE_PORT flag to relax
-    // the Content Security Policy.
-    rpc_port: z.string().regex(/\d+/).optional().catch(undefined),
-
-    // Override the referrer. Useful for scripts such as
-    // record_android_trace to record where the trace is coming from.
-    referrer: z.string().optional().catch(undefined),
-
-    // For the 'mode' of the UI. For example when the mode is 'embedded'
-    // some features are disabled.
-    mode: z.enum(['embedded']).optional().catch(undefined),
-
-    // Should we hide the sidebar?
-    hideSidebar: z.boolean().optional().catch(undefined),
-
-    // Deep link support
-    ts: z.string().optional().catch(undefined),
-    dur: z.string().optional().catch(undefined),
-    tid: z.string().optional().catch(undefined),
-    pid: z.string().optional().catch(undefined),
-    query: z.string().optional().catch(undefined),
-    visStart: z.string().optional().catch(undefined),
-    visEnd: z.string().optional().catch(undefined),
-  })
-  // default({}) ensures at compile-time that every entry is either optional or
-  // has a default value.
-  .default({});
-
-type RouteArgs = z.infer<typeof ROUTE_SCHEMA>;
-
-function safeParseRoute(rawRoute: unknown): RouteArgs {
-  const res = ROUTE_SCHEMA.safeParse(rawRoute);
-  return res.success ? res.data : {};
-}
-
-// A broken down representation of a route.
-// For instance: #!/record/gpu?local_cache_key=a0b1
-// becomes: {page: '/record', subpage: '/gpu', args: {local_cache_key: 'a0b1'}}
-export interface Route {
-  page: string;
-  subpage: string;
-  fragment: string;
-  args: RouteArgs;
-}
-
-export interface RoutesMap {
-  [key: string]: m.Component<PageAttrs>;
-}
-
-// This router does two things:
-// 1) Maps fragment paths (#!/page/subpage) to Mithril components.
-// The route map is passed to the ctor and is later used when calling the
-// resolve() method.
-//
-// 2) Handles the (optional) args, e.g. #!/page?arg=1&arg2=2.
-// Route args are carry information that is orthogonal to the page (e.g. the
-// trace id).
-// local_cache_key has some special treatment: once a URL has a local_cache_key,
-// it gets automatically appended to further navigations that don't have one.
-// For instance if the current url is #!/viewer?local_cache_key=1234 and a later
-// action (either user-initiated or code-initited) navigates to #!/info, the
-// rotuer will automatically replace the history entry with
-// #!/info?local_cache_key=1234.
-// This is to keep propagating the trace id across page changes, for handling
-// tab discards (b/175041881).
-//
-// This class does NOT deal with the "load a trace when the url contains ?url=
-// or ?local_cache_key=". That logic lives in trace_url_handler.ts, which is
-// triggered by Router.onRouteChanged().
-export class Router {
-  private readonly recentChanges: number[] = [];
-  private routes: RoutesMap;
-
-  // frontend/index.ts calls maybeOpenTraceFromRoute() + redraw here.
-  // This event is decoupled for testing and to avoid circular deps.
-  onRouteChanged: (route: Route) => void = () => {};
-
-  constructor(routes: RoutesMap) {
-    assertExists(routes[DEFAULT_ROUTE]);
-    this.routes = routes;
-    window.onhashchange = (e: HashChangeEvent) => this.onHashChange(e);
-    const route = Router.parseUrl(window.location.href);
-    this.onRouteChanged(route);
-  }
-
-  private onHashChange(e: HashChangeEvent) {
-    this.crashIfLivelock();
-
-    const oldRoute = Router.parseUrl(e.oldURL);
-    const newRoute = Router.parseUrl(e.newURL);
-
-    if (
-      newRoute.args.local_cache_key === undefined &&
-      oldRoute.args.local_cache_key
-    ) {
-      // Propagate `local_cache_key across` navigations. When a trace is loaded,
-      // the URL becomes #!/viewer?local_cache_key=123. `local_cache_key` allows
-      // reopening the trace from cache in the case of a reload or discard.
-      // When using the UI we can hit "bare" links (e.g. just '#!/info') which
-      // don't have the trace_uuid:
-      // - When clicking on an <a> element from the sidebar.
-      // - When the code calls Router.navigate().
-      // - When the user pastes a URL from docs page.
-      // In all these cases we want to keep propagating the `local_cache_key`.
-      // We do so by re-setting the `local_cache_key` and doing a
-      // location.replace which overwrites the history entry (note
-      // location.replace is NOT just a String.replace operation).
-      newRoute.args.local_cache_key = oldRoute.args.local_cache_key;
-    }
-
-    const args = m.buildQueryString(newRoute.args);
-    let normalizedFragment = `#!${newRoute.page}${newRoute.subpage}`;
-    if (args.length) {
-      normalizedFragment += `?${args}`;
-    }
-    if (newRoute.fragment) {
-      normalizedFragment += `#${newRoute.fragment}`;
-    }
-
-    if (!e.newURL.endsWith(normalizedFragment)) {
-      location.replace(normalizedFragment);
-      return;
-    }
-
-    this.onRouteChanged(newRoute);
-  }
-
-  // Returns the component for the current route in the URL.
-  // If no route matches the URL, returns a component corresponding to
-  // |this.defaultRoute|.
-  resolve(): m.Vnode<PageAttrs> {
-    const route = Router.parseFragment(location.hash);
-    let component = this.routes[route.page];
-    if (component === undefined) {
-      component = assertExists(this.routes[DEFAULT_ROUTE]);
-    }
-    return m(component, {subpage: route.subpage} as PageAttrs);
-  }
-
-  static navigate(newHash: string) {
-    assertTrue(newHash.startsWith(ROUTE_PREFIX));
-    window.location.hash = newHash;
-  }
-
-  // Breaks down a fragment into a Route object.
-  // Sample input:
-  // '#!/record/gpu?local_cache_key=abcd-1234#myfragment'
-  // Sample output:
-  // {
-  //  page: '/record',
-  //  subpage: '/gpu',
-  //  fragment: 'myfragment',
-  //  args: {local_cache_key: 'abcd-1234'}
-  // }
-  static parseFragment(hash: string): Route {
-    if (hash.startsWith(ROUTE_PREFIX)) {
-      hash = hash.substring(ROUTE_PREFIX.length);
-    } else {
-      hash = '';
-    }
-
-    const url = new URL(`https://example.com${hash}`);
-
-    const path = url.pathname;
-    let page = path;
-    let subpage = '';
-    const splittingPoint = path.indexOf('/', 1);
-    if (splittingPoint > 0) {
-      page = path.substring(0, splittingPoint);
-      subpage = path.substring(splittingPoint);
-    }
-    if (page === '/') {
-      page = '';
-    }
-
-    let rawArgs = {};
-    if (url.search) {
-      rawArgs = Router.parseQueryString(url.search);
-    }
-
-    const args = safeParseRoute(rawArgs);
-
-    // Javascript sadly distinguishes between foo[bar] === undefined
-    // and foo[bar] is not set at all. Here we need the second case to
-    // avoid making the URL ugly.
-    for (const key of Object.keys(args)) {
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      if ((args as any)[key] === undefined) {
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        delete (args as any)[key];
-      }
-    }
-
-    let fragment = url.hash;
-    if (fragment.startsWith('#')) {
-      fragment = fragment.substring(1);
-    }
-
-    return {page, subpage, args, fragment};
-  }
-
-  private static parseQueryString(query: string) {
-    query = query.replaceAll('+', ' ');
-    return m.parseQueryString(query);
-  }
-
-  private static parseSearchParams(url: string): RouteArgs {
-    const query = new URL(url).search;
-    const rawArgs = Router.parseQueryString(query);
-    const args = safeParseRoute(rawArgs);
-    return args;
-  }
-
-  // Like parseFragment() but takes a full URL.
-  static parseUrl(url: string): Route {
-    const searchArgs = Router.parseSearchParams(url);
-
-    const hashPos = url.indexOf('#');
-    const fragment = hashPos < 0 ? '' : url.substring(hashPos);
-    const route = Router.parseFragment(fragment);
-    route.args = Object.assign({}, searchArgs, route.args);
-
-    return route;
-  }
-
-  // Throws if EVENT_LIMIT onhashchange events occur within WINDOW_MS.
-  private crashIfLivelock() {
-    const WINDOW_MS = 1000;
-    const EVENT_LIMIT = 20;
-    const now = Date.now();
-    while (
-      this.recentChanges.length > 0 &&
-      now - this.recentChanges[0] > WINDOW_MS
-    ) {
-      this.recentChanges.shift();
-    }
-    this.recentChanges.push(now);
-    if (this.recentChanges.length > EVENT_LIMIT) {
-      throw new Error('History rewriting livelock');
-    }
-  }
-
-  static getUrlForVersion(versionCode: string): string {
-    const url = `${window.location.origin}/${versionCode}/`;
-    return url;
-  }
-
-  static async isVersionAvailable(
-    versionCode: string,
-  ): Promise<string | undefined> {
-    if (versionCode === '') {
-      return undefined;
-    }
-    const controller = new AbortController();
-    const timeoutId = setTimeout(() => controller.abort(), 1000);
-    const url = Router.getUrlForVersion(versionCode);
-    let r;
-    try {
-      r = await fetch(url, {signal: controller.signal});
-    } catch (e) {
-      console.error(
-        `No UI version for ${versionCode} at ${url}. This is an error if ${versionCode} is a released Perfetto version`,
-      );
-      return undefined;
-    } finally {
-      clearTimeout(timeoutId);
-    }
-    if (!r.ok) {
-      return undefined;
-    }
-    return url;
-  }
-
-  static navigateToVersion(versionCode: string): void {
-    const url = Router.getUrlForVersion(versionCode);
-    if (url === undefined) {
-      throw new Error(`No URL known for UI version ${versionCode}.`);
-    }
-    window.location.replace(url);
-  }
-}
diff --git a/ui/src/frontend/router_unittest.ts b/ui/src/frontend/router_unittest.ts
deleted file mode 100644
index ec65425..0000000
--- a/ui/src/frontend/router_unittest.ts
+++ /dev/null
@@ -1,185 +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 {Router} from './router';
-
-const mockComponent = {
-  view() {},
-};
-
-describe('Router#resolve', () => {
-  beforeEach(() => {
-    window.location.hash = '';
-  });
-
-  test('Default route must be defined', () => {
-    expect(() => new Router({'/a': mockComponent})).toThrow();
-  });
-
-  test('Resolves empty route to default component', () => {
-    const router = new Router({'/': mockComponent});
-    window.location.hash = '';
-    expect(router.resolve().tag).toBe(mockComponent);
-  });
-
-  test('Resolves subpage route to component of main page', () => {
-    const nonDefaultComponent = {view() {}};
-    const router = new Router({
-      '/': mockComponent,
-      '/a': nonDefaultComponent,
-    });
-    window.location.hash = '#!/a/subpage';
-    expect(router.resolve().tag).toBe(nonDefaultComponent);
-    expect(router.resolve().attrs.subpage).toBe('/subpage');
-  });
-
-  test('Pass empty subpage if not found in URL', () => {
-    const nonDefaultComponent = {view() {}};
-    const router = new Router({
-      '/': mockComponent,
-      '/a': nonDefaultComponent,
-    });
-    window.location.hash = '#!/a';
-    expect(router.resolve().tag).toBe(nonDefaultComponent);
-    expect(router.resolve().attrs.subpage).toBe('');
-  });
-});
-
-describe('Router.parseUrl', () => {
-  // Can parse arguments from the search string.
-  test('Search parsing', () => {
-    const url = 'http://localhost?p=123&s=42&url=a?b?c';
-    const route = Router.parseUrl(url);
-    const args = route.args;
-    expect(args.p).toBe('123');
-    expect(args.s).toBe('42');
-    expect(args.url).toBe('a?b?c');
-    expect(route.fragment).toBe('');
-  });
-
-  // Or from the fragment string.
-  test('Fragment parsing', () => {
-    const url = 'http://localhost/#!/foo?p=123&s=42&url=a?b?c';
-    const route = Router.parseUrl(url);
-    const args = route.args;
-    expect(args.p).toBe('123');
-    expect(args.s).toBe('42');
-    expect(args.url).toBe('a?b?c');
-    expect(route.fragment).toBe('');
-  });
-
-  // Or both in which case fragment overrides the search.
-  test('Fragment parsing', () => {
-    const url =
-      'http://localhost/?p=1&s=2&hideSidebar=true#!/foo?s=3&url=4&hideSidebar=false';
-    const route = Router.parseUrl(url);
-    const args = route.args;
-    expect(args.p).toBe('1');
-    expect(args.s).toBe('3');
-    expect(args.url).toBe('4');
-    expect(args.hideSidebar).toBe(false);
-    expect(route.fragment).toBe('');
-  });
-
-  // + is also space
-  test('plus is space query', () => {
-    const url = 'http://localhost?query=(foo+%2B+bar),';
-    const route = Router.parseUrl(url);
-    const args = route.args;
-    expect(args.query).toBe('(foo + bar),');
-  });
-
-  // + is also space
-  test('plus is space hash', () => {
-    const url = 'http://localhost#!/foo?query=(foo+%2B+bar),';
-    const route = Router.parseUrl(url);
-    const args = route.args;
-    expect(args.query).toBe('(foo + bar),');
-  });
-
-  test('Nested fragment', () => {
-    const url =
-      'http://localhost/?p=1&s=2&hideSidebar=true#!/foo?s=3&url=4&hideSidebar=false#myfragment';
-    const route = Router.parseUrl(url);
-    expect(route.fragment).toBe('myfragment');
-  });
-});
-
-describe('Router.parseFragment', () => {
-  test('empty route broken into empty components', () => {
-    const {page, subpage, args} = Router.parseFragment('');
-    expect(page).toBe('');
-    expect(subpage).toBe('');
-    expect(args.mode).toBe(undefined);
-  });
-
-  test('by default args are undefined', () => {
-    // This prevents the url from becoming messy.
-    const {args} = Router.parseFragment('');
-    expect(args).toEqual({});
-  });
-
-  test('invalid route broken into empty components', () => {
-    const {page, subpage} = Router.parseFragment('/bla');
-    expect(page).toBe('');
-    expect(subpage).toBe('');
-  });
-
-  test('simple route has page defined', () => {
-    const {page, subpage} = Router.parseFragment('#!/record');
-    expect(page).toBe('/record');
-    expect(subpage).toBe('');
-  });
-
-  test('simple route has both components defined', () => {
-    const {page, subpage} = Router.parseFragment('#!/record/memory');
-    expect(page).toBe('/record');
-    expect(subpage).toBe('/memory');
-  });
-
-  test('route broken at first slash', () => {
-    const {page, subpage} = Router.parseFragment('#!/record/memory/stuff');
-    expect(page).toBe('/record');
-    expect(subpage).toBe('/memory/stuff');
-  });
-
-  test('parameters separated from route', () => {
-    const {page, subpage, args} = Router.parseFragment(
-      '#!/record/memory?url=http://localhost:1234/aaaa',
-    );
-    expect(page).toBe('/record');
-    expect(subpage).toBe('/memory');
-    expect(args.url).toEqual('http://localhost:1234/aaaa');
-  });
-
-  test('openFromAndroidBugTool can be false', () => {
-    const {args} = Router.parseFragment('#!/?openFromAndroidBugTool=false');
-    expect(args.openFromAndroidBugTool).toEqual(false);
-  });
-
-  test('openFromAndroidBugTool can be true', () => {
-    const {args} = Router.parseFragment('#!/?openFromAndroidBugTool=true');
-    expect(args.openFromAndroidBugTool).toEqual(true);
-  });
-
-  test('bad modes are coerced to default', () => {
-    const {args} = Router.parseFragment('#!/?mode=1234');
-    expect(args.mode).toEqual(undefined);
-  });
-
-  test('bad hideSidebar is coerced to default', () => {
-    const {args} = Router.parseFragment('#!/?hideSidebar=helloworld!');
-    expect(args.hideSidebar).toEqual(undefined);
-  });
-});
diff --git a/ui/src/frontend/rpc_http_dialog.ts b/ui/src/frontend/rpc_http_dialog.ts
index 13db6b6..e031b09 100644
--- a/ui/src/frontend/rpc_http_dialog.ts
+++ b/ui/src/frontend/rpc_http_dialog.ts
@@ -13,17 +13,12 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {assertExists} from '../base/logging';
-import {Actions} from '../common/actions';
 import {VERSION} from '../gen/perfetto_version';
 import {StatusResult, TraceProcessorApiVersion} from '../protos';
 import {HttpRpcEngine} from '../trace_processor/http_rpc_engine';
 import {showModal} from '../widgets/modal';
-import {Router} from './router';
-
-import {globals} from './globals';
-import {publishHttpRpcState} from './publish';
+import {AppImpl} from '../core/app_impl';
 
 const CURRENT_API_VERSION =
   TraceProcessorApiVersion.TRACE_PROCESSOR_CURRENT_API_VERSION;
@@ -155,7 +150,7 @@
 // having to open a trace).
 export async function CheckHttpRpcConnection(): Promise<void> {
   const state = await HttpRpcEngine.checkConnection();
-  publishHttpRpcState(state);
+  AppImpl.instance.httpRpc.httpRpcAvailable = state.connected;
   if (!state.connected) {
     // No RPC = exit immediately to the WASM UI.
     return;
@@ -163,12 +158,12 @@
   const tpStatus = assertExists(state.status);
 
   function forceWasm() {
-    globals.dispatch(Actions.setNewEngineMode({mode: 'FORCE_BUILTIN_WASM'}));
+    AppImpl.instance.httpRpc.newEngineMode = 'FORCE_BUILTIN_WASM';
   }
 
   // Check short version:
   if (tpStatus.versionCode !== '' && tpStatus.versionCode !== VERSION) {
-    const url = await Router.isVersionAvailable(tpStatus.versionCode);
+    const url = await isVersionAvailable(tpStatus.versionCode);
     if (url !== undefined) {
       // If matched UI available show a dialog asking the user to
       // switch.
@@ -176,7 +171,7 @@
       switch (result) {
         case MismatchedVersionDialog.Dismissed:
         case MismatchedVersionDialog.UseMatchingUi:
-          Router.navigateToVersion(tpStatus.versionCode);
+          navigateToVersion(tpStatus.versionCode);
           return;
         case MismatchedVersionDialog.UseMismatchedRpc:
           break;
@@ -215,7 +210,7 @@
     switch (result) {
       case PreloadedDialogResult.Dismissed:
       case PreloadedDialogResult.UseRpcWithPreloadedTrace:
-        globals.dispatch(Actions.openTraceFromHttpRpc({}));
+        AppImpl.instance.openTraceFromHttpRpc();
         return;
       case PreloadedDialogResult.UseRpc:
         // Resetting state is the default.
@@ -340,3 +335,42 @@
   });
   return result;
 }
+
+function getUrlForVersion(versionCode: string): string {
+  const url = `${window.location.origin}/${versionCode}/`;
+  return url;
+}
+
+async function isVersionAvailable(
+  versionCode: string,
+): Promise<string | undefined> {
+  if (versionCode === '') {
+    return undefined;
+  }
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), 1000);
+  const url = getUrlForVersion(versionCode);
+  let r;
+  try {
+    r = await fetch(url, {signal: controller.signal});
+  } catch (e) {
+    console.error(
+      `No UI version for ${versionCode} at ${url}. This is an error if ${versionCode} is a released Perfetto version`,
+    );
+    return undefined;
+  } finally {
+    clearTimeout(timeoutId);
+  }
+  if (!r.ok) {
+    return undefined;
+  }
+  return url;
+}
+
+function navigateToVersion(versionCode: string): void {
+  const url = getUrlForVersion(versionCode);
+  if (url === undefined) {
+    throw new Error(`No URL known for UI version ${versionCode}.`);
+  }
+  window.location.replace(url);
+}
diff --git a/ui/src/frontend/scroll_helper.ts b/ui/src/frontend/scroll_helper.ts
deleted file mode 100644
index 6511744..0000000
--- a/ui/src/frontend/scroll_helper.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {time} from '../base/time';
-import {escapeCSSSelector, exists} from '../base/utils';
-import {Actions} from '../common/actions';
-import {HighPrecisionTime} from '../common/high_precision_time';
-import {HighPrecisionTimeSpan} from '../common/high_precision_time_span';
-import {getContainingGroupKey} from '../common/state';
-import {raf} from '../core/raf_scheduler';
-import {globals} from './globals';
-
-// Given a timestamp, if |ts| is not currently in view move the view to
-// center |ts|, keeping the same zoom level.
-export function horizontalScrollToTs(ts: time) {
-  const visibleWindow = globals.timeline.visibleWindow;
-  if (!visibleWindow.contains(ts)) {
-    // TODO(hjd): This is an ugly jump, we should do a smooth pan instead.
-    const halfDuration = visibleWindow.duration / 2;
-    const newStart = new HighPrecisionTime(ts).subNumber(halfDuration);
-    const newWindow = new HighPrecisionTimeSpan(
-      newStart,
-      visibleWindow.duration,
-    );
-    globals.timeline.updateVisibleTimeHP(newWindow);
-  }
-}
-
-// Given a start and end timestamp (in ns), move the viewport to center this
-// range and zoom if necessary:
-// - If [viewPercentage] is specified, the viewport will be zoomed so that
-//   the given time range takes up this percentage of the viewport.
-// The following scenarios assume [viewPercentage] is undefined.
-// - If the new range is more than 50% of the viewport, zoom out to a level
-// where
-//   the range is 1/5 of the viewport.
-// - If the new range is already centered, update the zoom level for the
-// viewport
-//   to cover 1/5 of the viewport.
-// - Otherwise, preserve the zoom range.
-export function focusHorizontalRange(
-  start: time,
-  end: time,
-  viewPercentage?: number,
-): void {
-  if (exists(viewPercentage)) {
-    focusHorizontalRangePercentage(start, end, viewPercentage);
-  } else {
-    focusHorizontalRangeImpl(start, end);
-  }
-}
-
-// Given a track id, find a track with that id and scroll it into view. If the
-// track is nested inside a track group, scroll to that track group instead.
-// If |openGroup| then open the track group and scroll to the track.
-export function verticalScrollToTrack(trackKey: string, openGroup = false) {
-  const track = document.querySelector('#track_' + escapeCSSSelector(trackKey));
-
-  if (track) {
-    // block: 'nearest' means that it will only scroll if the track is not
-    // currently in view.
-    track.scrollIntoView({behavior: 'smooth', block: 'nearest'});
-    return;
-  }
-
-  let trackGroup = null;
-  const groupKey = getContainingGroupKey(globals.state, trackKey);
-  if (groupKey) {
-    trackGroup = document.querySelector('#track_' + groupKey);
-  }
-
-  if (!groupKey || !trackGroup) {
-    console.error(`Can't scroll, track (${trackKey}) not found.`);
-    return;
-  }
-
-  // The requested track is inside a closed track group, either open the track
-  // group and scroll to the track or just scroll to the track group.
-  if (openGroup) {
-    // After the track exists in the dom, it will be scrolled to.
-    globals.scrollToTrackKey = trackKey;
-    globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
-    return;
-  } else {
-    trackGroup.scrollIntoView({behavior: 'smooth', block: 'nearest'});
-  }
-}
-
-// Scroll vertically and horizontally to reach track (|trackKey|) at |ts|.
-export function scrollToTrackAndTs(
-  trackKey: string | undefined,
-  ts: time,
-  openGroup = false,
-) {
-  if (trackKey !== undefined) {
-    verticalScrollToTrack(trackKey, openGroup);
-  }
-  horizontalScrollToTs(ts);
-}
-
-// Scroll vertically and horizontally to a track and time range
-export function reveal(
-  trackKey: string,
-  start: time,
-  end: time,
-  openGroup = false,
-) {
-  verticalScrollToTrack(trackKey, openGroup);
-  focusHorizontalRange(start, end);
-}
-
-function focusHorizontalRangePercentage(
-  start: time,
-  end: time,
-  viewPercentage: number,
-): void {
-  const aoi = HighPrecisionTimeSpan.fromTime(start, end);
-
-  if (viewPercentage <= 0.0 || viewPercentage > 1.0) {
-    console.warn(
-      'Invalid value for [viewPercentage]. ' +
-        'Value must be between 0.0 (exclusive) and 1.0 (inclusive).',
-    );
-    // Default to 50%.
-    viewPercentage = 0.5;
-  }
-  const paddingPercentage = 1.0 - viewPercentage;
-  const halfPaddingTime = (aoi.duration * paddingPercentage) / 2;
-  globals.timeline.updateVisibleTimeHP(aoi.pad(halfPaddingTime));
-
-  raf.scheduleRedraw();
-}
-
-function focusHorizontalRangeImpl(start: time, end: time): void {
-  const visible = globals.timeline.visibleWindow;
-  const aoi = HighPrecisionTimeSpan.fromTime(start, end);
-  const fillRatio = 5; // Default amount to make the AOI fill the viewport
-  const padRatio = (fillRatio - 1) / 2;
-
-  // If the area of interest already fills more than half the viewport, zoom out
-  // so that the AOI fills 20% of the viewport
-  if (aoi.duration * 2 > visible.duration) {
-    const padded = aoi.pad(aoi.duration * padRatio);
-    globals.timeline.updateVisibleTimeHP(padded);
-  } else {
-    // Center visible window on the middle of the AOI, preserving the zoom level
-    const newStart = aoi.midpoint.subNumber(visible.duration / 2);
-
-    // Adjust the new visible window if it intersects with the trace boundaries.
-    // It's needed to make the "update the zoom level if visible window doesn't
-    // change" logic reliable.
-    const newVisibleWindow = new HighPrecisionTimeSpan(
-      newStart,
-      visible.duration,
-    ).fitWithin(globals.traceContext.start, globals.traceContext.end);
-
-    // If preserving the zoom doesn't change the visible window, consider this
-    // to be the "second" hotkey press, so just make the AOI fill 20% of the
-    // viewport
-    if (newVisibleWindow.equals(visible)) {
-      const padded = aoi.pad(aoi.duration * padRatio);
-      globals.timeline.updateVisibleTimeHP(padded);
-    } else {
-      globals.timeline.updateVisibleTimeHP(newVisibleWindow);
-    }
-  }
-
-  raf.scheduleRedraw();
-}
diff --git a/ui/src/frontend/search_handler.ts b/ui/src/frontend/search_handler.ts
deleted file mode 100644
index 8a65bcb..0000000
--- a/ui/src/frontend/search_handler.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-// Copyright (C) 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
-//
-//      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 {searchSegment} from '../base/binary_search';
-import {assertUnreachable} from '../base/logging';
-import {Actions} from '../common/actions';
-import {globals} from './globals';
-import {verticalScrollToTrack} from './scroll_helper';
-
-function setToPrevious(current: number) {
-  let index = current - 1;
-  if (index < 0) {
-    index = globals.currentSearchResults.totalResults - 1;
-  }
-  globals.dispatch(Actions.setSearchIndex({index}));
-}
-
-function setToNext(current: number) {
-  const index = (current + 1) % globals.currentSearchResults.totalResults;
-  globals.dispatch(Actions.setSearchIndex({index}));
-}
-
-export function executeSearch(reverse = false) {
-  const index = globals.state.searchIndex;
-  const vizWindow = globals.timeline.visibleWindow.toTimeSpan();
-  const startNs = vizWindow.start;
-  const endNs = vizWindow.end;
-  const currentTs = globals.currentSearchResults.tses[index];
-
-  // If the value of |globals.currentSearchResults.totalResults| is 0,
-  // it means that the query is in progress or no results are found.
-  if (globals.currentSearchResults.totalResults === 0) {
-    return;
-  }
-
-  // If this is a new search or the currentTs is not in the viewport,
-  // select the first/last item in the viewport.
-  if (
-    index === -1 ||
-    (currentTs !== -1n && (currentTs < startNs || currentTs > endNs))
-  ) {
-    if (reverse) {
-      const [smaller] = searchSegment(globals.currentSearchResults.tses, endNs);
-      // If there is no item in the viewport just go to the previous.
-      if (smaller === -1) {
-        setToPrevious(index);
-      } else {
-        globals.dispatch(Actions.setSearchIndex({index: smaller}));
-      }
-    } else {
-      const [, larger] = searchSegment(
-        globals.currentSearchResults.tses,
-        startNs,
-      );
-      // If there is no item in the viewport just go to the next.
-      if (larger === -1) {
-        setToNext(index);
-      } else {
-        globals.dispatch(Actions.setSearchIndex({index: larger}));
-      }
-    }
-  } else {
-    // If the currentTs is in the viewport, increment the index.
-    if (reverse) {
-      setToPrevious(index);
-    } else {
-      setToNext(index);
-    }
-  }
-  selectCurrentSearchResult();
-}
-
-function selectCurrentSearchResult() {
-  const searchIndex = globals.state.searchIndex;
-  const source = globals.currentSearchResults.sources[searchIndex];
-  const currentId = globals.currentSearchResults.eventIds[searchIndex];
-  const trackKey = globals.currentSearchResults.trackKeys[searchIndex];
-
-  if (currentId === undefined) return;
-
-  switch (source) {
-    case 'track':
-      verticalScrollToTrack(trackKey, true);
-      break;
-    case 'cpu':
-      globals.setLegacySelection(
-        {
-          kind: 'SCHED_SLICE',
-          id: currentId,
-          trackKey,
-        },
-        {
-          clearSearch: false,
-          pendingScrollId: currentId,
-          switchToCurrentSelectionTab: true,
-        },
-      );
-      break;
-    case 'log':
-      globals.setLegacySelection(
-        {
-          kind: 'LOG',
-          id: currentId,
-          trackKey,
-        },
-        {
-          clearSearch: false,
-          pendingScrollId: currentId,
-          switchToCurrentSelectionTab: true,
-        },
-      );
-      break;
-    case 'slice':
-      // Search results only include slices from the slice table for now.
-      // When we include annotations we need to pass the correct table.
-      globals.setLegacySelection(
-        {
-          kind: 'SLICE',
-          id: currentId,
-          trackKey,
-          table: 'slice',
-        },
-        {
-          clearSearch: false,
-          pendingScrollId: currentId,
-          switchToCurrentSelectionTab: true,
-        },
-      );
-      break;
-    default:
-      assertUnreachable(source);
-  }
-}
diff --git a/ui/src/frontend/search_overview_track.ts b/ui/src/frontend/search_overview_track.ts
index 2772174..2e6bc8a 100644
--- a/ui/src/frontend/search_overview_track.ts
+++ b/ui/src/frontend/search_overview_track.ts
@@ -12,67 +12,80 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Duration, Time, TimeSpan, duration, time} from '../base/time';
-import {Size} from '../base/geom';
-import {PxSpan, TimeScale} from './time_scale';
 import {AsyncLimiter} from '../base/async_limiter';
 import {AsyncDisposableStack} from '../base/disposable_stack';
-import {createVirtualTable} from '../trace_processor/sql_utils';
-import {SearchSummary} from '../common/search_data';
-import {escapeSearchQuery} from '../trace_processor/query_utils';
+import {Size2D} from '../base/geom';
+import {Duration, Time, TimeSpan, duration, time} from '../base/time';
+import {TimeScale} from '../base/time_scale';
 import {calculateResolution} from '../common/resolution';
-import {OmniboxState} from '../common/state';
-import {Optional} from '../base/utils';
-import {AppContext} from './app_context';
-import {Engine} from '../trace_processor/engine';
+import {TraceImpl} from '../core/trace_impl';
 import {LONG, NUM} from '../trace_processor/query_result';
+import {escapeSearchQuery} from '../trace_processor/query_utils';
+import {createVirtualTable} from '../trace_processor/sql_utils';
 
-export interface SearchOverviewTrack extends AsyncDisposable {
-  render(ctx: CanvasRenderingContext2D, size: Size): void;
+interface SearchSummary {
+  tsStarts: BigInt64Array;
+  tsEnds: BigInt64Array;
+  count: Uint8Array;
 }
 
 /**
- * This function describes a pseudo-track that renders the search overview
- * blobs.
- *
- * @param engine The engine to use for loading data.
- * @returns A new search overview renderer.
+ * This component is drawn on top of the timeline and creates small yellow
+ * rectangles that highlight the time span of search results (similarly to what
+ * Chrome does on the scrollbar when you Ctrl+F and type a search term).
+ * It reacts to changes in SearchManager and queries the quantized ranges of the
+ * search results.
  */
-export async function createSearchOverviewTrack(
-  engine: Engine,
-  app: AppContext,
-): Promise<SearchOverviewTrack> {
-  const trash = new AsyncDisposableStack();
-  trash.use(
-    await createVirtualTable(engine, 'search_summary_window', 'window'),
-  );
-  trash.use(
-    await createVirtualTable(
-      engine,
-      'search_summary_sched_span',
-      'span_join(sched PARTITIONED cpu, search_summary_window)',
-    ),
-  );
-  trash.use(
-    await createVirtualTable(
-      engine,
-      'search_summary_slice_span',
-      'span_join(slice PARTITIONED track_id, search_summary_window)',
-    ),
-  );
+export class SearchOverviewTrack implements AsyncDisposable {
+  private readonly trash = new AsyncDisposableStack();
+  private readonly trace: TraceImpl;
+  private readonly limiter = new AsyncLimiter();
+  private initialized = false;
+  private previousResolution: duration | undefined;
+  private previousSpan: TimeSpan | undefined;
+  private previousSearchGeneration = 0;
+  private searchSummary: SearchSummary | undefined;
 
-  let previousResolution: duration;
-  let previousSpan: TimeSpan;
-  let previousOmniboxState: OmniboxState;
-  let searchSummary: Optional<SearchSummary>;
-  const limiter = new AsyncLimiter();
+  constructor(trace: TraceImpl) {
+    this.trace = trace;
+  }
 
-  async function update(
+  render(ctx: CanvasRenderingContext2D, size: Size2D) {
+    this.maybeUpdate(size);
+    this.renderSearchOverview(ctx, size);
+  }
+
+  private async initialize() {
+    const engine = this.trace.engine;
+    this.trash.use(
+      await createVirtualTable(engine, 'search_summary_window', 'window'),
+    );
+    this.trash.use(
+      await createVirtualTable(
+        engine,
+        'search_summary_sched_span',
+        'span_join(sched PARTITIONED cpu, search_summary_window)',
+      ),
+    );
+    this.trash.use(
+      await createVirtualTable(
+        engine,
+        'search_summary_slice_span',
+        'span_join(slice PARTITIONED track_id, search_summary_window)',
+      ),
+    );
+  }
+
+  private async update(
     search: string,
     start: time,
     end: time,
     resolution: duration,
   ): Promise<SearchSummary> {
+    if (!this.initialized) {
+      this.initialized = true;
+      await this.initialize();
+    }
     const searchLiteral = escapeSearchQuery(search);
 
     const resolutionScalingFactor = 10n;
@@ -80,6 +93,7 @@
     start = Time.quantFloor(start, quantum);
 
     const windowDur = Duration.max(Time.diff(end, start), 1n);
+    const engine = this.trace.engine;
     await engine.query(`update search_summary_window set
       window_start=${start},
       window_dur=${windowDur},
@@ -95,9 +109,6 @@
       utids.push(it.utid);
     }
 
-    const cpus = app.traceContext.cpus;
-    const maxCpu = Math.max(...cpus, -1);
-
     const res = await engine.query(`
         select
           (quantum_ts * ${quantum} + ${start}) as tsStart,
@@ -107,7 +118,7 @@
               select
               quantum_ts
               from search_summary_sched_span
-              where utid in (${utids.join(',')}) and cpu <= ${maxCpu}
+              where utid in (${utids.join(',')})
             union all
               select
               quantum_ts
@@ -133,19 +144,20 @@
     return summary;
   }
 
-  function maybeUpdate(size: Size) {
-    const omniboxState = app.state.omniboxState;
-    if (omniboxState === undefined || omniboxState.mode === 'COMMAND') {
+  private maybeUpdate(size: Size2D) {
+    const searchManager = this.trace.search;
+    const timeline = this.trace.timeline;
+    if (!searchManager.hasResults) {
       return;
     }
-    const newSpan = app.timeline.visibleWindow;
-    const newOmniboxState = omniboxState;
+    const newSpan = timeline.visibleWindow;
+    const newSearchGeneration = searchManager.searchGeneration;
     const newResolution = calculateResolution(newSpan, size.width);
     const newTimeSpan = newSpan.toTimeSpan();
     if (
-      previousSpan?.containsSpan(newTimeSpan.start, newTimeSpan.end) &&
-      previousResolution === newResolution &&
-      previousOmniboxState === newOmniboxState
+      this.previousSpan?.containsSpan(newTimeSpan.start, newTimeSpan.end) &&
+      this.previousResolution === newResolution &&
+      this.previousSearchGeneration === newSearchGeneration
     ) {
       return;
     }
@@ -154,12 +166,12 @@
     // that is not easily available here.
     // N.B. Timestamps can be negative.
     const {start, end} = newTimeSpan.pad(newTimeSpan.duration);
-    previousSpan = new TimeSpan(start, end);
-    previousResolution = newResolution;
-    previousOmniboxState = newOmniboxState;
-    const search = newOmniboxState.omnibox;
-    if (search === '' || (search.length < 4 && !newOmniboxState.force)) {
-      searchSummary = {
+    this.previousSpan = new TimeSpan(start, end);
+    this.previousResolution = newResolution;
+    this.previousSearchGeneration = newSearchGeneration;
+    const search = searchManager.searchText;
+    if (search === '') {
+      this.searchSummary = {
         tsStarts: new BigInt64Array(0),
         tsEnds: new BigInt64Array(0),
         count: new Uint8Array(0),
@@ -167,29 +179,32 @@
       return;
     }
 
-    limiter.schedule(async () => {
-      const summary = await update(
-        newOmniboxState.omnibox,
+    this.limiter.schedule(async () => {
+      const summary = await this.update(
+        searchManager.searchText,
         start,
         end,
         newResolution,
       );
-      searchSummary = summary;
+      this.searchSummary = summary;
     });
   }
 
-  function renderSearchOverview(
+  private renderSearchOverview(
     ctx: CanvasRenderingContext2D,
-    size: Size,
+    size: Size2D,
   ): void {
-    const visibleWindow = app.timeline.visibleWindow;
-    const timescale = new TimeScale(visibleWindow, new PxSpan(0, size.width));
+    const visibleWindow = this.trace.timeline.visibleWindow;
+    const timescale = new TimeScale(visibleWindow, {
+      left: 0,
+      right: size.width,
+    });
 
-    if (!searchSummary) return;
+    if (!this.searchSummary) return;
 
-    for (let i = 0; i < searchSummary.tsStarts.length; i++) {
-      const tStart = Time.fromRaw(searchSummary.tsStarts[i]);
-      const tEnd = Time.fromRaw(searchSummary.tsEnds[i]);
+    for (let i = 0; i < this.searchSummary.tsStarts.length; i++) {
+      const tStart = Time.fromRaw(this.searchSummary.tsStarts[i]);
+      const tEnd = Time.fromRaw(this.searchSummary.tsEnds[i]);
       if (!visibleWindow.overlaps(tStart, tEnd)) {
         continue;
       }
@@ -203,9 +218,13 @@
         size.height,
       );
     }
-    const index = app.state.searchIndex;
-    if (index !== -1 && index < app.currentSearchResults.tses.length) {
-      const start = app.currentSearchResults.tses[index];
+    const results = this.trace.search.searchResults;
+    if (results === undefined) {
+      return;
+    }
+    const index = this.trace.search.resultIndex;
+    if (index !== -1 && index < results.tses.length) {
+      const start = results.tses[index];
       if (start !== -1n) {
         const triangleStart = Math.max(
           timescale.timeToPx(Time.fromRaw(start)),
@@ -225,13 +244,7 @@
     ctx.restore();
   }
 
-  return {
-    render(ctx: CanvasRenderingContext2D, size: Size) {
-      maybeUpdate(size);
-      renderSearchOverview(ctx, size);
-    },
-    async [Symbol.asyncDispose](): Promise<void> {
-      return await trash.asyncDispose();
-    },
-  };
+  async [Symbol.asyncDispose](): Promise<void> {
+    return await this.trash.asyncDispose();
+  }
 }
diff --git a/ui/src/frontend/service_worker_controller.ts b/ui/src/frontend/service_worker_controller.ts
index 6bd3b81..a246a61 100644
--- a/ui/src/frontend/service_worker_controller.ts
+++ b/ui/src/frontend/service_worker_controller.ts
@@ -18,6 +18,7 @@
 // The actual service worker code is in src/service_worker.
 // Design doc: http://go/perfetto-offline.
 
+import {getServingRoot} from '../base/http_utils';
 import {reportError} from '../base/logging';
 import {raf} from '../core/raf_scheduler';
 
@@ -52,11 +53,10 @@
 }
 
 export class ServiceWorkerController {
+  private readonly servingRoot = getServingRoot();
   private _bypassed = false;
   private _installing = false;
 
-  constructor(private servingRoot: string) {}
-
   // Caller should reload().
   async setBypass(bypass: boolean) {
     if (!('serviceWorker' in navigator)) return; // Not supported.
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index 02a42c7..8ece2d3 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -13,38 +13,39 @@
 // limitations under the License.
 
 import m from 'mithril';
-
-import {assertExists, assertTrue} from '../base/logging';
-import {isString} from '../base/object_utils';
-import {Actions} from '../common/actions';
-import {getCurrentChannel} from '../common/channels';
-import {TRACE_SUFFIX} from '../common/constants';
-import {ConversionJobStatus} from '../common/conversion_jobs';
+import {getCurrentChannel} from '../core/channels';
+import {TRACE_SUFFIX} from '../public/trace';
 import {
   disableMetatracingAndGetTrace,
   enableMetatracing,
   isMetatracingEnabled,
-} from '../common/metatracing';
-import {EngineMode} from '../common/state';
+} from '../core/metatracing';
+import {Engine, EngineMode} from '../trace_processor/engine';
 import {featureFlags} from '../core/feature_flags';
 import {raf} from '../core/raf_scheduler';
 import {SCM_REVISION, VERSION} from '../gen/perfetto_version';
-import {EngineBase} from '../trace_processor/engine';
 import {showModal} from '../widgets/modal';
-
 import {Animation} from './animation';
 import {downloadData, downloadUrl} from './download_utils';
 import {globals} from './globals';
 import {toggleHelp} from './help_modal';
-import {Router} from './router';
-import {createTraceLink, isDownloadable, shareTrace} from './trace_attrs';
+import {shareTrace} from './trace_share_utils';
 import {
   convertTraceToJsonAndDownload,
   convertTraceToSystraceAndDownload,
 } from './trace_converter';
 import {openInOldUIWithSizeCheck} from './legacy_trace_viewer';
+import {SIDEBAR_SECTIONS, SidebarSections} from '../public/sidebar';
+import {AppImpl} from '../core/app_impl';
+import {Trace} from '../public/trace';
+import {OptionalTraceImplAttrs, TraceImpl} from '../core/trace_impl';
+import {Command} from '../public/command';
+import {SidebarMenuItemInternal} from '../core/sidebar_manager';
+import {exists, getOrCreate} from '../base/utils';
+import {copyToClipboard} from '../base/clipboard';
+import {classNames} from '../base/classnames';
 import {formatHotkey} from '../base/hotkeys';
-import {SidebarMenuItem} from '../public';
+import {assetSrc} from '../base/assets';
 
 const GITILES_URL =
   'https://android.googlesource.com/platform/external/perfetto';
@@ -64,382 +65,38 @@
   defaultValue: false,
 });
 
-const WIDGETS_PAGE_IN_NAV_FLAG = featureFlags.register({
-  id: 'showWidgetsPageInNav',
-  name: 'Show widgets page',
-  description: 'Show a link to the widgets page in the side bar.',
-  defaultValue: false,
-});
-
-const PLUGINS_PAGE_IN_NAV_FLAG = featureFlags.register({
-  id: 'showPluginsPageInNav',
-  name: 'Show plugins page',
-  description: 'Show a link to the plugins page in the side bar.',
-  defaultValue: false,
-});
-
-const INSIGHTS_PAGE_IN_NAV_FLAG = featureFlags.register({
-  id: 'showInsightsPageInNav',
-  name: 'Show insights page',
-  description: 'Show a link to the insights page in the side bar.',
-  defaultValue: false,
-});
-
-const VIZ_PAGE_IN_NAV_FLAG = featureFlags.register({
-  id: 'showVizPageInNav',
-  name: 'Show viz page',
-  description: 'Show a link to the viz page in the side bar.',
-  defaultValue: true,
-});
-
 function shouldShowHiringBanner(): boolean {
   return globals.isInternalUser && HIRING_BANNER_FLAG.get();
 }
 
-interface SectionItem {
-  t: string;
-  a: string | ((e: Event) => void);
-  i: string;
-  title?: string;
-  isPending?: () => boolean;
-  isVisible?: () => boolean;
-  internalUserOnly?: boolean;
-  checkDownloadDisabled?: boolean;
-  checkMetatracingEnabled?: boolean;
-  checkMetatracingDisabled?: boolean;
+async function openCurrentTraceWithOldUI(trace: Trace): Promise<void> {
+  AppImpl.instance.analytics.logEvent(
+    'Trace Actions',
+    'Open current trace in legacy UI',
+  );
+  const file = await trace.getTraceFile();
+  await openInOldUIWithSizeCheck(file);
 }
 
-interface Section {
-  title: string;
-  summary: string;
-  items: SectionItem[];
-  expanded?: boolean;
-  hideIfNoTraceLoaded?: boolean;
-  appendOpenedTraceTitle?: boolean;
+async function convertTraceToSystrace(trace: Trace): Promise<void> {
+  AppImpl.instance.analytics.logEvent('Trace Actions', 'Convert to .systrace');
+  const file = await trace.getTraceFile();
+  await convertTraceToSystraceAndDownload(file);
 }
 
-function insertSidebarMenuitems(
-  groupSelector: SidebarMenuItem['group'],
-): ReadonlyArray<SectionItem> {
-  return globals.sidebarMenuItems
-    .valuesAsArray()
-    .filter(({group}) => group === groupSelector)
-    .sort((a, b) => {
-      const prioA = a.priority ?? 0;
-      const prioB = b.priority ?? 0;
-      return prioA - prioB;
-    })
-    .map((item) => {
-      const cmd = globals.commandManager.getCommand(item.commandId);
-      const title = cmd.defaultHotkey
-        ? `${cmd.name} [${formatHotkey(cmd.defaultHotkey)}]`
-        : cmd.name;
-      return {
-        t: cmd.name,
-        a: (e: Event) => {
-          e.preventDefault();
-          cmd.callback();
-        },
-        i: item.icon,
-        title,
-      };
-    });
+async function convertTraceToJson(trace: Trace): Promise<void> {
+  AppImpl.instance.analytics.logEvent('Trace Actions', 'Convert to .json');
+  const file = await trace.getTraceFile();
+  await convertTraceToJsonAndDownload(file);
 }
 
-function getSections(): Section[] {
-  return [
-    {
-      title: 'Navigation',
-      summary: 'Open or record a new trace',
-      expanded: true,
-      items: [
-        ...insertSidebarMenuitems('navigation'),
-        {t: 'Record new trace', a: navigateRecord, i: 'fiber_smart_record'},
-        {
-          t: 'Widgets',
-          a: navigateWidgets,
-          i: 'widgets',
-          isVisible: () => WIDGETS_PAGE_IN_NAV_FLAG.get(),
-        },
-        {
-          t: 'Plugins',
-          a: navigatePlugins,
-          i: 'extension',
-          isVisible: () => PLUGINS_PAGE_IN_NAV_FLAG.get(),
-        },
-      ],
-    },
+function downloadTrace(trace: TraceImpl) {
+  if (!trace.traceInfo.downloadable) return;
+  AppImpl.instance.analytics.logEvent('Trace Actions', 'Download trace');
 
-    {
-      title: 'Current Trace',
-      summary: 'Actions on the current trace',
-      expanded: true,
-      hideIfNoTraceLoaded: true,
-      appendOpenedTraceTitle: true,
-      items: [
-        {t: 'Show timeline', a: navigateViewer, i: 'line_style'},
-        {
-          t: 'Share',
-          a: handleShareTrace,
-          i: 'share',
-          internalUserOnly: true,
-          isPending: () =>
-            globals.getConversionJobStatus('create_permalink') ===
-            ConversionJobStatus.InProgress,
-        },
-        {
-          t: 'Download',
-          a: downloadTrace,
-          i: 'file_download',
-          checkDownloadDisabled: true,
-        },
-        {t: 'Query (SQL)', a: navigateQuery, i: 'database'},
-        {
-          t: 'Insights',
-          a: navigateInsights,
-          i: 'insights',
-          isVisible: () => INSIGHTS_PAGE_IN_NAV_FLAG.get(),
-        },
-        {
-          t: 'Viz',
-          a: navigateViz,
-          i: 'area_chart',
-          isVisible: () => VIZ_PAGE_IN_NAV_FLAG.get(),
-        },
-        {t: 'Metrics', a: navigateMetrics, i: 'speed'},
-        {t: 'Info and stats', a: navigateInfo, i: 'info'},
-      ],
-    },
-
-    {
-      title: 'Convert trace',
-      summary: 'Convert to other formats',
-      expanded: true,
-      hideIfNoTraceLoaded: true,
-      items: [
-        {
-          t: 'Switch to legacy UI',
-          a: openCurrentTraceWithOldUI,
-          i: 'filter_none',
-          isPending: () =>
-            globals.getConversionJobStatus('open_in_legacy') ===
-            ConversionJobStatus.InProgress,
-        },
-        {
-          t: 'Convert to .json',
-          a: convertTraceToJson,
-          i: 'file_download',
-          isPending: () =>
-            globals.getConversionJobStatus('convert_json') ===
-            ConversionJobStatus.InProgress,
-          checkDownloadDisabled: true,
-        },
-
-        {
-          t: 'Convert to .systrace',
-          a: convertTraceToSystrace,
-          i: 'file_download',
-          isVisible: () => globals.hasFtrace,
-          isPending: () =>
-            globals.getConversionJobStatus('convert_systrace') ===
-            ConversionJobStatus.InProgress,
-          checkDownloadDisabled: true,
-        },
-      ],
-    },
-
-    {
-      title: 'Example Traces',
-      expanded: true,
-      summary: 'Open an example trace',
-      items: [...insertSidebarMenuitems('example_traces')],
-    },
-
-    {
-      title: 'Support',
-      expanded: true,
-      summary: 'Documentation & Bugs',
-      items: [
-        {t: 'Keyboard shortcuts', a: openHelp, i: 'help'},
-        {t: 'Documentation', a: 'https://perfetto.dev/docs', i: 'find_in_page'},
-        {t: 'Flags', a: navigateFlags, i: 'emoji_flags'},
-        {
-          t: 'Report a bug',
-          a: getBugReportUrl(),
-          i: 'bug_report',
-        },
-        {
-          t: 'Record metatrace',
-          a: recordMetatrace,
-          i: 'fiber_smart_record',
-          checkMetatracingDisabled: true,
-        },
-        {
-          t: 'Finalise metatrace',
-          a: finaliseMetatrace,
-          i: 'file_download',
-          checkMetatracingEnabled: true,
-        },
-      ],
-    },
-  ];
-}
-
-function openHelp(e: Event) {
-  e.preventDefault();
-  toggleHelp();
-}
-
-function downloadTraceFromUrl(url: string): Promise<File> {
-  return m.request({
-    method: 'GET',
-    url,
-    // TODO(hjd): Once mithril is updated we can use responseType here rather
-    // than using config and remove the extract below.
-    config: (xhr) => {
-      xhr.responseType = 'blob';
-      xhr.onprogress = (progress) => {
-        const percent = ((100 * progress.loaded) / progress.total).toFixed(1);
-        globals.dispatch(
-          Actions.updateStatus({
-            msg: `Downloading trace ${percent}%`,
-            timestamp: Date.now() / 1000,
-          }),
-        );
-      };
-    },
-    extract: (xhr) => {
-      return xhr.response;
-    },
-  });
-}
-
-export async function getCurrentTrace(): Promise<Blob> {
-  // Caller must check engine exists.
-  const engine = assertExists(globals.getCurrentEngine());
-  const src = engine.source;
-  if (src.type === 'ARRAY_BUFFER') {
-    return new Blob([src.buffer]);
-  } else if (src.type === 'FILE') {
-    return src.file;
-  } else if (src.type === 'URL') {
-    return downloadTraceFromUrl(src.url);
-  } else {
-    throw new Error(`Loading to catapult from source with type ${src.type}`);
-  }
-}
-
-function openCurrentTraceWithOldUI(e: Event) {
-  e.preventDefault();
-  assertTrue(isTraceLoaded());
-  globals.logging.logEvent('Trace Actions', 'Open current trace in legacy UI');
-  if (!isTraceLoaded()) return;
-  getCurrentTrace()
-    .then((file) => {
-      openInOldUIWithSizeCheck(file);
-    })
-    .catch((error) => {
-      throw new Error(`Failed to get current trace ${error}`);
-    });
-}
-
-function convertTraceToSystrace(e: Event) {
-  e.preventDefault();
-  assertTrue(isTraceLoaded());
-  globals.logging.logEvent('Trace Actions', 'Convert to .systrace');
-  if (!isTraceLoaded()) return;
-  getCurrentTrace()
-    .then((file) => {
-      convertTraceToSystraceAndDownload(file);
-    })
-    .catch((error) => {
-      throw new Error(`Failed to get current trace ${error}`);
-    });
-}
-
-function convertTraceToJson(e: Event) {
-  e.preventDefault();
-  assertTrue(isTraceLoaded());
-  globals.logging.logEvent('Trace Actions', 'Convert to .json');
-  if (!isTraceLoaded()) return;
-  getCurrentTrace()
-    .then((file) => {
-      convertTraceToJsonAndDownload(file);
-    })
-    .catch((error) => {
-      throw new Error(`Failed to get current trace ${error}`);
-    });
-}
-
-export function isTraceLoaded(): boolean {
-  return globals.getCurrentEngine() !== undefined;
-}
-
-function navigateRecord(e: Event) {
-  e.preventDefault();
-  Router.navigate('#!/record');
-}
-
-function navigateWidgets(e: Event) {
-  e.preventDefault();
-  Router.navigate('#!/widgets');
-}
-
-function navigatePlugins(e: Event) {
-  e.preventDefault();
-  Router.navigate('#!/plugins');
-}
-
-function navigateQuery(e: Event) {
-  e.preventDefault();
-  Router.navigate('#!/query');
-}
-
-function navigateInsights(e: Event) {
-  e.preventDefault();
-  Router.navigate('#!/insights');
-}
-
-function navigateViz(e: Event) {
-  e.preventDefault();
-  Router.navigate('#!/viz');
-}
-
-function navigateFlags(e: Event) {
-  e.preventDefault();
-  Router.navigate('#!/flags');
-}
-
-function navigateMetrics(e: Event) {
-  e.preventDefault();
-  Router.navigate('#!/metrics');
-}
-
-function navigateInfo(e: Event) {
-  e.preventDefault();
-  Router.navigate('#!/info');
-}
-
-function navigateViewer(e: Event) {
-  e.preventDefault();
-  Router.navigate('#!/viewer');
-}
-
-function handleShareTrace(e: Event) {
-  e.preventDefault();
-  shareTrace();
-}
-
-function downloadTrace(e: Event) {
-  e.preventDefault();
-  if (!isDownloadable() || !isTraceLoaded()) return;
-  globals.logging.logEvent('Trace Actions', 'Download trace');
-
-  const engine = globals.getCurrentEngine();
-  if (!engine) return;
   let url = '';
   let fileName = `trace${TRACE_SUFFIX}`;
-  const src = engine.source;
+  const src = trace.traceInfo.source;
   if (src.type === 'URL') {
     url = src.url;
     fileName = url.split('/').slice(-1)[0];
@@ -464,27 +121,17 @@
   downloadUrl(fileName, url);
 }
 
-function getCurrentEngine(): EngineBase | undefined {
-  const engineId = globals.getCurrentEngine()?.id;
-  if (engineId === undefined) return undefined;
-  return globals.engines.get(engineId);
-}
-
 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 ||
-    globals.getCurrentEngine()?.mode === 'HTTP_RPC'
+    AppImpl.instance.trace?.engine.mode === 'HTTP_RPC'
   );
 }
 
-function recordMetatrace(e: Event) {
-  e.preventDefault();
-  globals.logging.logEvent('Trace Actions', 'Record metatrace');
-
-  const engine = getCurrentEngine();
-  if (!engine) return;
+function recordMetatrace(engine: Engine) {
+  AppImpl.instance.analytics.logEvent('Trace Actions', 'Record metatrace');
 
   if (!highPrecisionTimersAvailable()) {
     const PROMPT = `High-precision timers are not available to WASM trace processor yet.
@@ -520,15 +167,15 @@
   }
 }
 
-async function finaliseMetatrace(e: Event) {
-  e.preventDefault();
-  globals.logging.logEvent('Trace Actions', 'Finalise metatrace');
+async function toggleMetatrace(e: Engine) {
+  return isMetatracingEnabled() ? finaliseMetatrace(e) : recordMetatrace(e);
+}
+
+async function finaliseMetatrace(engine: Engine) {
+  AppImpl.instance.analytics.logEvent('Trace Actions', 'Finalise metatrace');
 
   const jsEvents = disableMetatracingAndGetTrace();
 
-  const engine = getCurrentEngine();
-  if (!engine) return;
-
   const result = await engine.stopAndGetMetatrace();
   if (result.error.length !== 0) {
     throw new Error(`Failed to read metatrace: ${result.error}`);
@@ -537,15 +184,15 @@
   downloadData('metatrace', result.metatrace, jsEvents);
 }
 
-const EngineRPCWidget: m.Component = {
-  view() {
+class EngineRPCWidget implements m.ClassComponent<OptionalTraceImplAttrs> {
+  view({attrs}: m.CVnode<OptionalTraceImplAttrs>) {
     let cssClass = '';
     let title = 'Number of pending SQL queries';
     let label: string;
     let failed = false;
     let mode: EngineMode | undefined;
 
-    const engine = globals.state.engine;
+    const engine = attrs.trace?.engine;
     if (engine !== undefined) {
       mode = engine.mode;
       if (engine.failed !== undefined) {
@@ -562,8 +209,8 @@
     // this will eventually become  consistent once the engine is created.
     if (mode === undefined) {
       if (
-        globals.httpRpcState.connected &&
-        globals.state.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE'
+        AppImpl.instance.httpRpc.httpRpcAvailable &&
+        AppImpl.instance.httpRpc.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE'
       ) {
         mode = 'HTTP_RPC';
       } else {
@@ -580,21 +227,22 @@
       title += '\n(Query engine: built-in WASM)';
     }
 
+    const numReqs = attrs.trace?.engine.numRequestsPending ?? 0;
     return m(
       `.dbg-info-square${cssClass}`,
       {title},
       m('div', label),
-      m('div', `${failed ? 'FAIL' : globals.numQueuedQueries}`),
+      m('div', `${failed ? 'FAIL' : numReqs}`),
     );
-  },
-};
+  }
+}
 
 const ServiceWorkerWidget: m.Component = {
   view() {
     let cssClass = '';
     let title = 'Service Worker: ';
     let label = 'N/A';
-    const ctl = globals.serviceWorkerController;
+    const ctl = AppImpl.instance.serviceWorkerController;
     if (!('serviceWorker' in navigator)) {
       label = 'N/A';
       title += 'not supported by the browser (requires HTTPS)';
@@ -616,8 +264,8 @@
     }
 
     const toggle = async () => {
-      if (globals.serviceWorkerController.bypassed) {
-        globals.serviceWorkerController.setBypass(false);
+      if (ctl.bypassed) {
+        ctl.setBypass(false);
         return;
       }
       showModal({
@@ -649,11 +297,7 @@
           {
             text: 'Disable and reload',
             primary: true,
-            action: () => {
-              globals.serviceWorkerController
-                .setBypass(true)
-                .then(() => location.reload());
-            },
+            action: () => ctl.setBypass(true).then(() => location.reload()),
           },
           {text: 'Cancel'},
         ],
@@ -669,11 +313,11 @@
   },
 };
 
-const SidebarFooter: m.Component = {
-  view() {
+class SidebarFooter implements m.ClassComponent<OptionalTraceImplAttrs> {
+  view({attrs}: m.CVnode<OptionalTraceImplAttrs>) {
     return m(
       '.sidebar-footer',
-      m(EngineRPCWidget),
+      m(EngineRPCWidget, attrs),
       m(ServiceWorkerWidget),
       m(
         '.version',
@@ -688,8 +332,8 @@
         ),
       ),
     );
-  },
-};
+  }
+}
 
 class HiringBanner implements m.ClassComponent {
   view() {
@@ -707,105 +351,22 @@
   }
 }
 
-export class Sidebar implements m.ClassComponent {
+export class Sidebar implements m.ClassComponent<OptionalTraceImplAttrs> {
   private _redrawWhileAnimating = new Animation(() => raf.scheduleFullRedraw());
-  view() {
-    if (globals.hideSidebar) return null;
-    const vdomSections = [];
-    for (const section of getSections()) {
-      if (section.hideIfNoTraceLoaded && !isTraceLoaded()) continue;
-      const vdomItems = [];
-      for (const item of section.items) {
-        if (item.isVisible !== undefined && !item.isVisible()) {
-          continue;
-        }
-        let css = '';
-        let attrs = {
-          onclick: typeof item.a === 'function' ? item.a : null,
-          href: isString(item.a) ? item.a : '#',
-          target: isString(item.a) ? '_blank' : null,
-          disabled: false,
-          id: item.t.toLowerCase().replace(/[^\w]/g, '_'),
-        };
-        if (item.isPending && item.isPending()) {
-          attrs.onclick = (e) => e.preventDefault();
-          css = '.pending';
-        }
-        if (item.internalUserOnly && !globals.isInternalUser) {
-          continue;
-        }
-        if (item.checkMetatracingEnabled || item.checkMetatracingDisabled) {
-          if (
-            item.checkMetatracingEnabled === true &&
-            !isMetatracingEnabled()
-          ) {
-            continue;
-          }
-          if (
-            item.checkMetatracingDisabled === true &&
-            isMetatracingEnabled()
-          ) {
-            continue;
-          }
-          if (
-            item.checkMetatracingDisabled &&
-            !highPrecisionTimersAvailable()
-          ) {
-            attrs.disabled = true;
-          }
-        }
-        if (item.checkDownloadDisabled && !isDownloadable()) {
-          attrs = {
-            onclick: (e) => {
-              e.preventDefault();
-              alert('Can not download external trace.');
-            },
-            href: '#',
-            target: null,
-            disabled: true,
-            id: '',
-          };
-        }
-        vdomItems.push(
-          m(
-            'li',
-            m(
-              `a${css}`,
-              {...attrs, title: item.title},
-              m('i.material-icons', item.i),
-              item.t,
-            ),
-          ),
-        );
-      }
-      if (section.appendOpenedTraceTitle) {
-        if (globals.traceContext.traceTitle) {
-          const {traceTitle, traceUrl} = globals.traceContext;
-          vdomItems.unshift(m('li', createTraceLink(traceTitle, traceUrl)));
-        }
-      }
-      vdomSections.push(
-        m(
-          `section${section.expanded ? '.expanded' : ''}`,
-          m(
-            '.section-header',
-            {
-              onclick: () => {
-                section.expanded = !section.expanded;
-                raf.scheduleFullRedraw();
-              },
-            },
-            m('h1', {title: section.summary}, section.title),
-            m('h2', section.summary),
-          ),
-          m('.section-content', m('ul', vdomItems)),
-        ),
-      );
-    }
+  private _asyncJobPending = new Set<string>();
+  private _sectionExpanded = new Map<string, boolean>();
+
+  constructor() {
+    registerMenuItems();
+  }
+
+  view({attrs}: m.CVnode<OptionalTraceImplAttrs>) {
+    const sidebar = AppImpl.instance.sidebar;
+    if (!sidebar.enabled) return null;
     return m(
       'nav.sidebar',
       {
-        class: globals.state.sidebarVisible ? 'show-sidebar' : 'hide-sidebar',
+        class: sidebar.visible ? 'show-sidebar' : 'hide-sidebar',
         // 150 here matches --sidebar-timing in the css.
         // TODO(hjd): Should link to the CSS variable.
         ontransitionstart: (e: TransitionEvent) => {
@@ -820,20 +381,16 @@
       shouldShowHiringBanner() ? m(HiringBanner) : null,
       m(
         `header.${getCurrentChannel()}`,
-        m(`img[src=${globals.root}assets/brand.png].brand`),
+        m(`img[src=${assetSrc('assets/brand.png')}].brand`),
         m(
           'button.sidebar-button',
           {
-            onclick: () => {
-              globals.commandManager.runCommand(
-                'perfetto.CoreCommands#ToggleLeftSidebar',
-              );
-            },
+            onclick: () => sidebar.toggleVisibility(),
           },
           m(
             'i.material-icons',
             {
-              title: globals.state.sidebarVisible ? 'Hide menu' : 'Show menu',
+              title: sidebar.visible ? 'Hide menu' : 'Show menu',
             },
             'menu',
           ),
@@ -841,8 +398,254 @@
       ),
       m(
         '.sidebar-scroll',
-        m('.sidebar-scroll-container', ...vdomSections, m(SidebarFooter)),
+        m(
+          '.sidebar-scroll-container',
+          ...(Object.keys(SIDEBAR_SECTIONS) as SidebarSections[]).map((s) =>
+            this.renderSection(s),
+          ),
+          m(SidebarFooter, attrs),
+        ),
       ),
     );
   }
+
+  private renderSection(sectionId: SidebarSections) {
+    const section = SIDEBAR_SECTIONS[sectionId];
+    const menuItems = AppImpl.instance.sidebar.menuItems
+      .valuesAsArray()
+      .filter((item) => item.section === sectionId)
+      .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
+      .map((item) => this.renderItem(item));
+
+    // Don't render empty sections.
+    if (menuItems.length === 0) return undefined;
+
+    const expanded = getOrCreate(this._sectionExpanded, sectionId, () => true);
+    return m(
+      `section${expanded ? '.expanded' : ''}`,
+      m(
+        '.section-header',
+        {
+          onclick: () => {
+            this._sectionExpanded.set(sectionId, !expanded);
+            raf.scheduleFullRedraw();
+          },
+        },
+        m('h1', {title: section.title}, section.title),
+        m('h2', section.summary),
+      ),
+      m('.section-content', m('ul', menuItems)),
+    );
+  }
+
+  private renderItem(item: SidebarMenuItemInternal): m.Child {
+    let href = '#';
+    let disabled = false;
+    let target = null;
+    let command: Command | undefined = undefined;
+    let tooltip = valueOrCallback(item.tooltip);
+    let onclick: (() => unknown | Promise<unknown>) | undefined = undefined;
+    const commandId = 'commandId' in item ? item.commandId : undefined;
+    const action = 'action' in item ? item.action : undefined;
+    let text = valueOrCallback(item.text);
+    const disabReason: boolean | string | undefined = valueOrCallback(
+      item.disabled,
+    );
+
+    if (disabReason === true || typeof disabReason === 'string') {
+      disabled = true;
+      onclick = () => typeof disabReason === 'string' && alert(disabReason);
+    } else if (action !== undefined) {
+      onclick = action;
+    } else if (commandId !== undefined) {
+      const cmdMgr = AppImpl.instance.commands;
+      command = cmdMgr.hasCommand(commandId ?? '')
+        ? cmdMgr.getCommand(commandId)
+        : undefined;
+      if (command === undefined) {
+        disabled = true;
+      } else {
+        text = text !== undefined ? text : command.name;
+        if (command.defaultHotkey !== undefined) {
+          tooltip =
+            `${tooltip ?? command.name}` +
+            ` [${formatHotkey(command.defaultHotkey)}]`;
+        }
+        onclick = () => cmdMgr.runCommand(commandId);
+      }
+    }
+
+    // This is not an else if because in some rare cases the user might want
+    // to have both an href and onclick, with different behaviors. The only case
+    // today is the trace name / URL, where we want the URL in the href to
+    // support right-click -> copy URL, but the onclick does copyToClipboard().
+    if ('href' in item && item.href !== undefined) {
+      href = item.href;
+      target = href.startsWith('#') ? null : '_blank';
+    }
+    return m(
+      'li',
+      m(
+        'a',
+        {
+          className: classNames(
+            valueOrCallback(item.cssClass),
+            this._asyncJobPending.has(item.id) && 'pending',
+          ),
+          onclick: onclick && this.wrapClickHandler(item.id, onclick),
+          href,
+          target,
+          disabled,
+          title: tooltip,
+        },
+        exists(item.icon) && m('i.material-icons', valueOrCallback(item.icon)),
+        text,
+      ),
+    );
+  }
+
+  // Creates the onClick handlers for the items which provided a function in the
+  // `action` member. The function can be either sync or async.
+  // What we want to achieve here is the following:
+  // - If the action is async (returns a Promise), we want to render a spinner,
+  //   next to the menu item, until the promise is resolved.
+  // - [Minor] we want to call e.preventDefault() to override the behaviour of
+  //   the <a href='#'> which gets rendered for accessibility reasons.
+  private wrapClickHandler(itemId: string, itemAction: Function) {
+    return (e: Event) => {
+      e.preventDefault(); // Make the <a href="#"> a no-op.
+      const res = itemAction();
+      if (!(res instanceof Promise)) return;
+      if (this._asyncJobPending.has(itemId)) {
+        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();
+      });
+    };
+  }
+}
+
+// TODO(primiano): The registrations below should be moved to dedicated
+// plugins (most of this really belongs to core_plugins/commads/index.ts).
+// For now i'm keeping everything here as splitting these require moving some
+// functions like share_trace() out of core, splitting out permalink, etc.
+
+let globalItemsRegistered = false;
+const traceItemsRegistered = new WeakSet<TraceImpl>();
+
+function registerMenuItems() {
+  if (!globalItemsRegistered) {
+    globalItemsRegistered = true;
+    registerGlobalSidebarEntries();
+  }
+  const trace = AppImpl.instance.trace;
+  if (trace !== undefined && !traceItemsRegistered.has(trace)) {
+    traceItemsRegistered.add(trace);
+    registerTraceMenuItems(trace);
+  }
+}
+
+function registerGlobalSidebarEntries() {
+  const app = AppImpl.instance;
+  // TODO(primiano): The Open file / Open with legacy entries are registered by
+  // the 'perfetto.CoreCommands' plugins. Make things consistent.
+  app.sidebar.addMenuItem({
+    section: 'support',
+    text: 'Keyboard shortcuts',
+    action: toggleHelp,
+    icon: 'help',
+  });
+  app.sidebar.addMenuItem({
+    section: 'support',
+    text: 'Documentation',
+    href: 'https://perfetto.dev/docs',
+    icon: 'find_in_page',
+  });
+  app.sidebar.addMenuItem({
+    section: 'support',
+    sortOrder: 4,
+    text: 'Report a bug',
+    href: getBugReportUrl(),
+    icon: 'bug_report',
+  });
+}
+
+function registerTraceMenuItems(trace: TraceImpl) {
+  const downloadDisabled = trace.traceInfo.downloadable
+    ? false
+    : 'Cannot download external trace';
+
+  const traceTitle = trace?.traceInfo.traceTitle;
+  traceTitle &&
+    trace.sidebar.addMenuItem({
+      section: 'current_trace',
+      text: traceTitle,
+      href: trace.traceInfo.traceUrl,
+      action: () => copyToClipboard(trace.traceInfo.traceUrl),
+      tooltip: 'Click to copy the URL',
+      cssClass: 'trace-file-name',
+    });
+  trace.sidebar.addMenuItem({
+    section: 'current_trace',
+    text: 'Show timeline',
+    href: '#!/viewer',
+    icon: 'line_style',
+  });
+  globals.isInternalUser &&
+    trace.sidebar.addMenuItem({
+      section: 'current_trace',
+      text: 'Share',
+      action: async () => await shareTrace(trace),
+      icon: 'share',
+    });
+  trace.sidebar.addMenuItem({
+    section: 'current_trace',
+    text: 'Download',
+    action: () => downloadTrace(trace),
+    icon: 'file_download',
+    disabled: downloadDisabled,
+  });
+  trace.sidebar.addMenuItem({
+    section: 'convert_trace',
+    text: 'Switch to legacy UI',
+    action: async () => await openCurrentTraceWithOldUI(trace),
+    icon: 'filter_none',
+    disabled: downloadDisabled,
+  });
+  trace.sidebar.addMenuItem({
+    section: 'convert_trace',
+    text: 'Convert to .json',
+    action: async () => await convertTraceToJson(trace),
+    icon: 'file_download',
+    disabled: downloadDisabled,
+  });
+  trace.traceInfo.hasFtrace &&
+    trace.sidebar.addMenuItem({
+      section: 'convert_trace',
+      text: 'Convert to .systrace',
+      action: async () => await convertTraceToSystrace(trace),
+      icon: 'file_download',
+      disabled: downloadDisabled,
+    });
+  trace.sidebar.addMenuItem({
+    section: 'support',
+    sortOrder: 5,
+    text: () =>
+      isMetatracingEnabled() ? 'Finalize metatrace' : 'Record metatrace',
+    action: () => toggleMetatrace(trace.engine),
+    icon: () => (isMetatracingEnabled() ? 'download' : 'fiber_smart_record'),
+  });
+}
+
+// Used to deal with fields like the entry name, which can be either a direct
+// string or a callback that returns the string.
+function valueOrCallback<T>(value: T | (() => T)): T;
+function valueOrCallback<T>(value: T | (() => T) | undefined): T | undefined;
+function valueOrCallback<T>(value: T | (() => T) | undefined): T | undefined {
+  if (value === undefined) return undefined;
+  return value instanceof Function ? value() : value;
 }
diff --git a/ui/src/frontend/simple_counter_track.ts b/ui/src/frontend/simple_counter_track.ts
deleted file mode 100644
index 2e784b5..0000000
--- a/ui/src/frontend/simple_counter_track.ts
+++ /dev/null
@@ -1,65 +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 {Engine, TrackContext} from '../public';
-import {BaseCounterTrack, CounterOptions} from './base_counter_track';
-import {CounterColumns, SqlDataSource} from './debug_tracks/debug_tracks';
-import {uuidv4Sql} from '../base/uuid';
-import {createPerfettoTable} from '../trace_processor/sql_utils';
-
-export type SimpleCounterTrackConfig = {
-  data: SqlDataSource;
-  columns: CounterColumns;
-  options?: Partial<CounterOptions>;
-};
-
-export class SimpleCounterTrack extends BaseCounterTrack {
-  private config: SimpleCounterTrackConfig;
-  private sqlTableName: string;
-
-  constructor(
-    engine: Engine,
-    ctx: TrackContext,
-    config: SimpleCounterTrackConfig,
-  ) {
-    super({
-      engine,
-      trackKey: ctx.trackKey,
-      options: config.options,
-    });
-    this.config = config;
-    this.sqlTableName = `__simple_counter_${uuidv4Sql()}`;
-  }
-
-  async onInit() {
-    return await createPerfettoTable(
-      this.engine,
-      this.sqlTableName,
-      `
-        with data as (
-          ${this.config.data.sqlSource}
-        )
-        select
-          ${this.config.columns.ts} as ts,
-          ${this.config.columns.value} as value
-        from data
-        order by ts
-      `,
-    );
-  }
-
-  getSqlSource(): string {
-    return `select * from ${this.sqlTableName}`;
-  }
-}
diff --git a/ui/src/frontend/simple_slice_track.ts b/ui/src/frontend/simple_slice_track.ts
deleted file mode 100644
index 1a49da7..0000000
--- a/ui/src/frontend/simple_slice_track.ts
+++ /dev/null
@@ -1,112 +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 {Engine, TrackContext} from '../public';
-import {
-  CustomSqlDetailsPanelConfig,
-  CustomSqlTableDefConfig,
-  CustomSqlTableSliceTrack,
-} from './tracks/custom_sql_table_slice_track';
-import {SliceColumns, SqlDataSource} from './debug_tracks/debug_tracks';
-import {uuidv4Sql} from '../base/uuid';
-import {ARG_PREFIX, DebugSliceDetailsTab} from './debug_tracks/details_tab';
-import {createPerfettoTable} from '../trace_processor/sql_utils';
-
-export interface SimpleSliceTrackConfig {
-  data: SqlDataSource;
-  columns: SliceColumns;
-  argColumns: string[];
-}
-
-export class SimpleSliceTrack extends CustomSqlTableSliceTrack {
-  private config: SimpleSliceTrackConfig;
-  private sqlTableName: string;
-
-  constructor(
-    engine: Engine,
-    ctx: TrackContext,
-    config: SimpleSliceTrackConfig,
-  ) {
-    super({
-      engine,
-      trackKey: ctx.trackKey,
-    });
-
-    this.config = config;
-    this.sqlTableName = `__simple_slice_${uuidv4Sql(ctx.trackKey)}`;
-  }
-
-  async getSqlDataSource(): Promise<CustomSqlTableDefConfig> {
-    const table = await createPerfettoTable(
-      this.engine,
-      this.sqlTableName,
-      this.createTableQuery(
-        this.config.data,
-        this.config.columns,
-        this.config.argColumns,
-      ),
-    );
-    return {
-      sqlTableName: this.sqlTableName,
-      disposable: table,
-    };
-  }
-
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    // We currently borrow the debug slice details tab.
-    // TODO: Don't do this!
-    return {
-      kind: DebugSliceDetailsTab.kind,
-      config: {
-        sqlTableName: this.sqlTableName,
-        title: 'Debug Slice',
-      },
-    };
-  }
-
-  private createTableQuery(
-    data: SqlDataSource,
-    sliceColumns: SliceColumns,
-    argColumns: string[],
-  ): string {
-    // If the view has clashing names (e.g. "name" coming from joining two
-    // different tables, we will see names like "name_1", "name_2", but they
-    // won't be addressable from the SQL. So we explicitly name them through a
-    // list of columns passed to CTE.
-    const dataColumns =
-      data.columns !== undefined ? `(${data.columns.join(', ')})` : '';
-
-    // TODO(altimin): Support removing this table when the track is closed.
-    const dur = sliceColumns.dur === '0' ? 0 : sliceColumns.dur;
-    return `
-      with data${dataColumns} as (
-        ${data.sqlSource}
-      ),
-      prepared_data as (
-        select
-          ${sliceColumns.ts} as ts,
-          ifnull(cast(${dur} as int), -1) as dur,
-          printf('%s', ${sliceColumns.name}) as name
-          ${argColumns.length > 0 ? ',' : ''}
-          ${argColumns.map((c) => `${c} as ${ARG_PREFIX}${c}`).join(',\n')}
-        from data
-      )
-      select
-        row_number() over (order by ts) as id,
-        *
-      from prepared_data
-      order by ts
-    `;
-  }
-}
diff --git a/ui/src/frontend/slice_args.ts b/ui/src/frontend/slice_args.ts
index 86f6999..88ecc65 100644
--- a/ui/src/frontend/slice_args.ts
+++ b/ui/src/frontend/slice_args.ts
@@ -13,28 +13,25 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {isString} from '../base/object_utils';
 import {Icons} from '../base/semantic_icons';
 import {sqliteString} from '../base/string_utils';
 import {exists} from '../base/utils';
-import {ArgNode, convertArgsToTree, Key} from '../controller/args_parser';
-import {Engine} from '../trace_processor/engine';
-import {addVisualisedArgTracks} from './visualized_args_tracks';
+import {ArgNode, convertArgsToTree, Key} from './slice_args_parser';
 import {Anchor} from '../widgets/anchor';
 import {MenuItem, PopupMenu2} from '../widgets/menu';
 import {TreeNode} from '../widgets/tree';
-
 import {Arg} from '../trace_processor/sql_utils/args';
-import {globals} from './globals';
-import {addSqlTableTab} from './sql_table_tab';
-import {SqlTables} from './widgets/sql/table/well_known_sql_tables';
+import {assertExists} from '../base/logging';
+import {getSqlTableDescription} from './widgets/sql/table/sql_table_registry';
+import {Trace} from '../public/trace';
+import {extensions} from '../public/lib/extensions';
 
 // Renders slice arguments (key/value pairs) as a subtree.
-export function renderArguments(engine: Engine, args: Arg[]): m.Children {
+export function renderArguments(trace: Trace, args: Arg[]): m.Children {
   if (args.length > 0) {
     const tree = convertArgsToTree(args);
-    return renderArgTreeNodes(engine, tree);
+    return renderArgTreeNodes(trace, tree);
   } else {
     return undefined;
   }
@@ -44,7 +41,7 @@
   return exists(args) && args.length > 0;
 }
 
-function renderArgTreeNodes(engine: Engine, args: ArgNode<Arg>[]): m.Children {
+function renderArgTreeNodes(trace: Trace, args: ArgNode<Arg>[]): m.Children {
   return args.map((arg) => {
     const {key, value, children} = arg;
     if (children && children.length === 1) {
@@ -54,22 +51,22 @@
         ...child,
         key: stringifyKey(key, child.key),
       };
-      return renderArgTreeNodes(engine, [compositeArg]);
+      return renderArgTreeNodes(trace, [compositeArg]);
     } else {
       return m(
         TreeNode,
         {
-          left: renderArgKey(engine, stringifyKey(key), value),
+          left: renderArgKey(trace, stringifyKey(key), value),
           right: exists(value) && renderArgValue(value),
           summary: children && renderSummary(children),
         },
-        children && renderArgTreeNodes(engine, children),
+        children && renderArgTreeNodes(trace, children),
       );
     }
   });
 }
 
-function renderArgKey(engine: Engine, key: string, value?: Arg): m.Children {
+function renderArgKey(trace: Trace, key: string, value?: Arg): m.Children {
   if (value === undefined) {
     return key;
   } else {
@@ -86,30 +83,33 @@
         label: 'Find slices with same arg value',
         icon: 'search',
         onclick: () => {
-          addSqlTableTab({
-            table: SqlTables.slice,
+          extensions.addSqlTableTab(trace, {
+            table: assertExists(getSqlTableDescription('slice')),
             filters: [
               {
-                type: 'arg_filter',
-                argSetIdColumn: 'arg_set_id',
-                argName: fullKey,
-                op: `= ${sqliteString(displayValue)}`,
+                op: (cols) => `${cols[0]} = ${sqliteString(displayValue)}`,
+                columns: [
+                  {
+                    column: 'display_value',
+                    source: {
+                      table: 'args',
+                      joinOn: {
+                        arg_set_id: 'arg_set_id',
+                        key: sqliteString(fullKey),
+                      },
+                    },
+                  },
+                ],
               },
             ],
           });
         },
       }),
       m(MenuItem, {
-        label: 'Visualise argument values',
+        label: 'Visualize argument values',
         icon: 'query_stats',
         onclick: () => {
-          addVisualisedArgTracks(
-            {
-              engine,
-              registerTrack: (t) => globals.trackManager.registerTrack(t),
-            },
-            fullKey,
-          );
+          extensions.addVisualizedArgTracks(trace, fullKey);
         },
       }),
     );
diff --git a/ui/src/controller/args_parser.ts b/ui/src/frontend/slice_args_parser.ts
similarity index 100%
rename from ui/src/controller/args_parser.ts
rename to ui/src/frontend/slice_args_parser.ts
diff --git a/ui/src/frontend/slice_args_parser_unittest.ts b/ui/src/frontend/slice_args_parser_unittest.ts
new file mode 100644
index 0000000..ba6a6f0
--- /dev/null
+++ b/ui/src/frontend/slice_args_parser_unittest.ts
@@ -0,0 +1,191 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {convertArgsToObject, convertArgsToTree} from './slice_args_parser';
+
+const args = [
+  {key: 'simple_key', value: 'simple_value'},
+  {key: 'thing.key', value: 'value'},
+  {key: 'thing.point[0].x', value: 10},
+  {key: 'thing.point[0].y', value: 20},
+  {key: 'thing.point[1].x', value: 0},
+  {key: 'thing.point[1].y', value: -10},
+  {key: 'foo.bar.foo.bar', value: 'baz'},
+];
+
+describe('convertArgsToTree', () => {
+  test('converts example arg set', () => {
+    expect(convertArgsToTree(args)).toEqual([
+      {
+        key: 'simple_key',
+        value: {key: 'simple_key', value: 'simple_value'},
+      },
+      {
+        key: 'thing',
+        children: [
+          {key: 'key', value: {key: 'thing.key', value: 'value'}},
+          {
+            key: 'point',
+            children: [
+              {
+                key: 0,
+                children: [
+                  {
+                    key: 'x',
+                    value: {key: 'thing.point[0].x', value: 10},
+                  },
+                  {
+                    key: 'y',
+                    value: {key: 'thing.point[0].y', value: 20},
+                  },
+                ],
+              },
+              {
+                key: 1,
+                children: [
+                  {
+                    key: 'x',
+                    value: {key: 'thing.point[1].x', value: 0},
+                  },
+                  {
+                    key: 'y',
+                    value: {key: 'thing.point[1].y', value: -10},
+                  },
+                ],
+              },
+            ],
+          },
+        ],
+      },
+      {
+        key: 'foo',
+        children: [
+          {
+            key: 'bar',
+            children: [
+              {
+                key: 'foo',
+                children: [
+                  {
+                    key: 'bar',
+                    value: {key: 'foo.bar.foo.bar', value: 'baz'},
+                  },
+                ],
+              },
+            ],
+          },
+        ],
+      },
+    ]);
+  });
+
+  test('handles value and children in same node', () => {
+    const args = [
+      {key: 'foo', value: 'foo'},
+      {key: 'foo.bar', value: 'bar'},
+    ];
+    expect(convertArgsToTree(args)).toEqual([
+      {
+        key: 'foo',
+        value: {key: 'foo', value: 'foo'},
+        children: [{key: 'bar', value: {key: 'foo.bar', value: 'bar'}}],
+      },
+    ]);
+  });
+
+  test('handles mixed key types', () => {
+    const args = [
+      {key: 'foo[0]', value: 'foo'},
+      {key: 'foo.bar', value: 'bar'},
+    ];
+    expect(convertArgsToTree(args)).toEqual([
+      {
+        key: 'foo',
+        children: [
+          {key: 0, value: {key: 'foo[0]', value: 'foo'}},
+          {key: 'bar', value: {key: 'foo.bar', value: 'bar'}},
+        ],
+      },
+    ]);
+  });
+
+  test('picks latest where duplicate keys exist', () => {
+    const args = [
+      {key: 'foo', value: 'foo'},
+      {key: 'foo', value: 'bar'},
+    ];
+    expect(convertArgsToTree(args)).toEqual([
+      {key: 'foo', value: {key: 'foo', value: 'bar'}},
+    ]);
+  });
+
+  test('handles sparse arrays', () => {
+    const args = [{key: 'foo[12]', value: 'foo'}];
+    expect(convertArgsToTree(args)).toEqual([
+      {
+        key: 'foo',
+        children: [{key: 12, value: {key: 'foo[12]', value: 'foo'}}],
+      },
+    ]);
+  });
+});
+
+describe('convertArgsToObject', () => {
+  it('converts example arg set', () => {
+    expect(convertArgsToObject(args)).toEqual({
+      simple_key: 'simple_value',
+      thing: {
+        key: 'value',
+        point: [
+          {x: 10, y: 20},
+          {x: 0, y: -10},
+        ],
+      },
+      foo: {bar: {foo: {bar: 'baz'}}},
+    });
+  });
+
+  test('throws on args containing a node with both value and children', () => {
+    expect(() => {
+      convertArgsToObject([
+        {key: 'foo', value: 'foo'},
+        {key: 'foo.bar', value: 'bar'},
+      ]);
+    }).toThrow();
+  });
+
+  test('throws on args containing mixed key types', () => {
+    expect(() => {
+      convertArgsToObject([
+        {key: 'foo[0]', value: 'foo'},
+        {key: 'foo.bar', value: 'bar'},
+      ]);
+    }).toThrow();
+  });
+
+  test('picks last one where duplicate keys exist', () => {
+    const args = [
+      {key: 'foo', value: 'foo'},
+      {key: 'foo', value: 'bar'},
+    ];
+    expect(convertArgsToObject(args)).toEqual({foo: 'bar'});
+  });
+
+  test('handles sparse arrays', () => {
+    const args = [{key: 'foo[3]', value: 'foo'}];
+    expect(convertArgsToObject(args)).toEqual({
+      foo: [undefined, undefined, undefined, 'foo'],
+    });
+  });
+});
diff --git a/ui/src/frontend/slice_details.ts b/ui/src/frontend/slice_details.ts
index 89dccc8..784e5b1 100644
--- a/ui/src/frontend/slice_details.ts
+++ b/ui/src/frontend/slice_details.ts
@@ -13,31 +13,32 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {BigintMath} from '../base/bigint_math';
 import {sqliteString} from '../base/string_utils';
 import {exists} from '../base/utils';
+import {SliceDetails} from '../trace_processor/sql_utils/slice';
 import {Anchor} from '../widgets/anchor';
 import {MenuItem, PopupMenu2} from '../widgets/menu';
 import {Section} from '../widgets/section';
 import {SqlRef} from '../widgets/sql_ref';
 import {Tree, TreeNode} from '../widgets/tree';
-
-import {SliceDetails} from '../trace_processor/sql_utils/slice';
 import {
   BreakdownByThreadState,
   BreakdownByThreadStateTreeNode,
 } from './sql/thread_state';
 import {DurationWidget} from './widgets/duration';
+import {renderProcessRef} from './widgets/process';
+import {renderThreadRef} from './widgets/thread';
 import {Timestamp} from './widgets/timestamp';
-import {addSqlTableTab} from './sql_table_tab';
-import {SqlTables} from './widgets/sql/table/well_known_sql_tables';
-import {getThreadName} from '../trace_processor/sql_utils/thread';
-import {getProcessName} from '../trace_processor/sql_utils/process';
+import {getSqlTableDescription} from './widgets/sql/table/sql_table_registry';
+import {assertExists} from '../base/logging';
+import {Trace} from '../public/trace';
+import {extensions} from '../public/lib/extensions';
 
 // Renders a widget storing all of the generic details for a slice from the
 // slice table.
 export function renderDetails(
+  trace: Trace,
   slice: SliceDetails,
   durationBreakdown?: BreakdownByThreadState,
 ) {
@@ -56,10 +57,14 @@
           m(MenuItem, {
             label: 'Slices with the same name',
             onclick: () => {
-              addSqlTableTab({
-                table: SqlTables.slice,
-                displayName: 'slice',
-                filters: [`name = ${sqliteString(slice.name)}`],
+              extensions.addSqlTableTab(trace, {
+                table: assertExists(getSqlTableDescription('slice')),
+                filters: [
+                  {
+                    op: (cols) => `${cols[0]} = ${sqliteString(slice.name)}`,
+                    columns: ['name'],
+                  },
+                ],
               });
             },
           }),
@@ -95,12 +100,12 @@
       slice.thread &&
         m(TreeNode, {
           left: 'Thread',
-          right: getThreadName(slice.thread),
+          right: renderThreadRef(slice.thread),
         }),
       slice.process &&
         m(TreeNode, {
           left: 'Process',
-          right: getProcessName(slice.process),
+          right: renderProcessRef(slice.process),
         }),
       slice.process &&
         exists(slice.process.uid) &&
diff --git a/ui/src/frontend/slice_details_panel.ts b/ui/src/frontend/slice_details_panel.ts
deleted file mode 100644
index bdf83d8..0000000
--- a/ui/src/frontend/slice_details_panel.ts
+++ /dev/null
@@ -1,256 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size 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 {Actions} from '../common/actions';
-import {translateState} from '../common/thread_state';
-import {Anchor} from '../widgets/anchor';
-import {DetailsShell} from '../widgets/details_shell';
-import {GridLayout} from '../widgets/grid_layout';
-import {Section} from '../widgets/section';
-import {SqlRef} from '../widgets/sql_ref';
-import {Tree, TreeNode} from '../widgets/tree';
-
-import {globals, SliceDetails, ThreadDesc} from './globals';
-import {scrollToTrackAndTs} from './scroll_helper';
-import {SlicePanel} from './slice_panel';
-import {DurationWidget} from './widgets/duration';
-import {Timestamp} from './widgets/timestamp';
-import {THREAD_STATE_TRACK_KIND} from '../core/track_kinds';
-
-const MIN_NORMAL_SCHED_PRIORITY = 100;
-
-export class SliceDetailsPanel extends SlicePanel {
-  view() {
-    const sliceInfo = globals.sliceDetails;
-    if (sliceInfo.utid === undefined) return;
-    const threadInfo = globals.threads.get(sliceInfo.utid);
-
-    return m(
-      DetailsShell,
-      {
-        title: 'CPU Sched Slice',
-        description: this.renderDescription(sliceInfo),
-      },
-      m(
-        GridLayout,
-        this.renderDetails(sliceInfo, threadInfo),
-        this.renderSchedLatencyInfo(sliceInfo),
-      ),
-    );
-  }
-
-  private renderDescription(sliceInfo: SliceDetails) {
-    const threadInfo = globals.threads.get(sliceInfo.wakerUtid!);
-    if (!threadInfo) {
-      return null;
-    }
-    return `${threadInfo.procName} [${threadInfo.pid}]`;
-  }
-
-  private renderSchedLatencyInfo(sliceInfo: SliceDetails): m.Children {
-    if (!this.hasSchedLatencyInfo(sliceInfo)) {
-      return null;
-    }
-    return m(
-      Section,
-      {title: 'Scheduling Latency'},
-      m(
-        '.slice-details-latency-panel',
-        m('img.slice-details-image', {
-          src: `${globals.root}assets/scheduling_latency.png`,
-        }),
-        this.renderWakeupText(sliceInfo),
-        this.renderDisplayLatencyText(sliceInfo),
-      ),
-    );
-  }
-
-  private renderWakeupText(sliceInfo: SliceDetails): m.Children {
-    if (sliceInfo.wakerUtid === undefined) {
-      return null;
-    }
-    const threadInfo = globals.threads.get(sliceInfo.wakerUtid!);
-    if (!threadInfo) {
-      return null;
-    }
-    return m(
-      '.slice-details-wakeup-text',
-      m(
-        '',
-        `Wakeup @ `,
-        m(Timestamp, {ts: sliceInfo.wakeupTs!}),
-        ` on CPU ${sliceInfo.wakerCpu} by`,
-      ),
-      m('', `P: ${threadInfo.procName} [${threadInfo.pid}]`),
-      m('', `T: ${threadInfo.threadName} [${threadInfo.tid}]`),
-    );
-  }
-
-  private renderDisplayLatencyText(sliceInfo: SliceDetails): m.Children {
-    if (sliceInfo.ts === undefined || sliceInfo.wakeupTs === undefined) {
-      return null;
-    }
-
-    const latency = sliceInfo.ts - sliceInfo.wakeupTs;
-    return m(
-      '.slice-details-latency-text',
-      m('', `Scheduling latency: `, m(DurationWidget, {dur: latency})),
-      m(
-        '.text-detail',
-        `This is the interval from when the task became eligible to run
-        (e.g. because of notifying a wait queue it was suspended on) to
-        when it started running.`,
-      ),
-    );
-  }
-
-  private hasSchedLatencyInfo({wakeupTs, wakerUtid}: SliceDetails): boolean {
-    return wakeupTs !== undefined && wakerUtid !== undefined;
-  }
-
-  private renderThreadDuration(sliceInfo: SliceDetails) {
-    if (sliceInfo.threadDur !== undefined && sliceInfo.threadTs !== undefined) {
-      return m(TreeNode, {
-        icon: 'timer',
-        left: 'Thread Duration',
-        right: m(DurationWidget, {dur: sliceInfo.threadDur}),
-      });
-    } else {
-      return null;
-    }
-  }
-
-  private renderPriorityText(priority?: number) {
-    if (priority === undefined) {
-      return undefined;
-    }
-    return priority < MIN_NORMAL_SCHED_PRIORITY
-      ? `${priority} (real-time)`
-      : `${priority}`;
-  }
-
-  private renderDetails(
-    sliceInfo: SliceDetails,
-    threadInfo?: ThreadDesc,
-  ): m.Children {
-    if (
-      !threadInfo ||
-      sliceInfo.ts === undefined ||
-      sliceInfo.dur === undefined
-    ) {
-      return null;
-    } else {
-      const extras: m.Children = [];
-
-      for (const [key, value] of this.getProcessThreadDetails(sliceInfo)) {
-        if (value !== undefined) {
-          extras.push(m(TreeNode, {left: key, right: value}));
-        }
-      }
-
-      const treeNodes = [
-        m(TreeNode, {
-          left: 'Process',
-          right: `${threadInfo.procName} [${threadInfo.pid}]`,
-        }),
-        m(TreeNode, {
-          left: 'Thread',
-          right: m(
-            Anchor,
-            {
-              icon: 'call_made',
-              onclick: () => {
-                this.goToThread();
-              },
-            },
-            `${threadInfo.threadName} [${threadInfo.tid}]`,
-          ),
-        }),
-        m(TreeNode, {
-          left: 'Cmdline',
-          right: threadInfo.cmdline,
-        }),
-        m(TreeNode, {
-          left: 'Start time',
-          right: m(Timestamp, {ts: sliceInfo.ts}),
-        }),
-        m(TreeNode, {
-          left: 'Duration',
-          right: m(DurationWidget, {dur: sliceInfo.dur}),
-        }),
-        this.renderThreadDuration(sliceInfo),
-        m(TreeNode, {
-          left: 'Priority',
-          right: this.renderPriorityText(sliceInfo.priority),
-        }),
-        m(TreeNode, {
-          left: 'End State',
-          right: translateState(sliceInfo.endState),
-        }),
-        m(TreeNode, {
-          left: 'SQL ID',
-          right: m(SqlRef, {table: 'sched', id: sliceInfo.id}),
-        }),
-        ...extras,
-      ];
-
-      return m(Section, {title: 'Details'}, m(Tree, treeNodes));
-    }
-  }
-
-  goToThread() {
-    const sliceInfo = globals.sliceDetails;
-    if (sliceInfo.utid === undefined) return;
-    const threadInfo = globals.threads.get(sliceInfo.utid);
-
-    if (
-      sliceInfo.id === undefined ||
-      sliceInfo.ts === undefined ||
-      sliceInfo.dur === undefined ||
-      sliceInfo.cpu === undefined ||
-      threadInfo === undefined
-    ) {
-      return;
-    }
-
-    let trackKey: string | undefined;
-    for (const track of Object.values(globals.state.tracks)) {
-      const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
-      // TODO(stevegolton): Handle v2.
-      if (
-        trackDesc &&
-        trackDesc.tags?.kind === THREAD_STATE_TRACK_KIND &&
-        trackDesc.tags?.utid === threadInfo.utid
-      ) {
-        trackKey = track.key;
-      }
-    }
-
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    if (trackKey && sliceInfo.threadStateId) {
-      globals.makeSelection(
-        Actions.selectThreadState({
-          id: sliceInfo.threadStateId,
-          trackKey: trackKey.toString(),
-        }),
-      );
-
-      scrollToTrackAndTs(trackKey, sliceInfo.ts, true);
-    }
-  }
-
-  renderCanvas() {}
-}
diff --git a/ui/src/frontend/slice_panel.ts b/ui/src/frontend/slice_panel.ts
deleted file mode 100644
index aef97ca..0000000
--- a/ui/src/frontend/slice_panel.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size 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 {exists} from '../base/utils';
-
-import {SliceDetails} from './globals';
-
-// To display process or thread, we want to concatenate their name with ID, but
-// either can be undefined and all the cases need to be considered carefully to
-// avoid `undefined undefined` showing up in the UI. This function does such
-// concatenation.
-//
-// Result can be undefined if both name and process are, in this case result is
-// not going to be displayed in the UI.
-function getDisplayName(
-  name: string | undefined,
-  id: number | undefined,
-): string | undefined {
-  if (name === undefined) {
-    return id === undefined ? undefined : `${id}`;
-  } else {
-    return id === undefined ? name : `${name} ${id}`;
-  }
-}
-
-export abstract class SlicePanel implements m.ClassComponent {
-  protected getProcessThreadDetails(sliceInfo: SliceDetails) {
-    return new Map<string, string | undefined>([
-      ['Thread', getDisplayName(sliceInfo.threadName, sliceInfo.tid)],
-      ['Process', getDisplayName(sliceInfo.processName, sliceInfo.pid)],
-      ['User ID', exists(sliceInfo.uid) ? String(sliceInfo.uid) : undefined],
-      ['Package name', sliceInfo.packageName],
-      /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-      [
-        'Version code',
-        sliceInfo.versionCode ? String(sliceInfo.versionCode) : undefined,
-      ],
-      /* eslint-enable */
-    ]);
-  }
-
-  abstract view(vnode: m.Vnode): void | m.Children;
-}
diff --git a/ui/src/frontend/sql/thread_state.ts b/ui/src/frontend/sql/thread_state.ts
index d078832..faa464e 100644
--- a/ui/src/frontend/sql/thread_state.ts
+++ b/ui/src/frontend/sql/thread_state.ts
@@ -13,9 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {duration, TimeSpan} from '../../base/time';
-import {Engine} from '../../public';
+import {Engine} from '../../trace_processor/engine';
 import {
   LONG,
   NUM_NULL,
diff --git a/ui/src/frontend/sql_table_tab.ts b/ui/src/frontend/sql_table_tab.ts
index 39a94c6..40f2012 100644
--- a/ui/src/frontend/sql_table_tab.ts
+++ b/ui/src/frontend/sql_table_tab.ts
@@ -13,73 +13,48 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {copyToClipboard} from '../base/clipboard';
 import {Icons} from '../base/semantic_icons';
 import {exists} from '../base/utils';
 import {Button} from '../widgets/button';
 import {DetailsShell} from '../widgets/details_shell';
 import {Popup, PopupPosition} from '../widgets/popup';
-
-import {Filter, SqlTableState} from './widgets/sql/table/state';
+import {AddDebugTrackMenu} from '../public/lib/tracks/add_debug_track_menu';
+import {Filter} from './widgets/sql/table/column';
+import {SqlTableState} from './widgets/sql/table/state';
 import {SqlTable} from './widgets/sql/table/table';
-import {
-  SqlTableDescription,
-  tableDisplayName,
-} from './widgets/sql/table/table_description';
-import {Engine} from '../public';
-import {assertExists} from '../base/logging';
-import {uuidv4} from '../base/uuid';
-import {addEphemeralTab} from '../common/addEphemeralTab';
-import {BottomTab, NewBottomTabArgs} from './bottom_tab';
-import {globals} from './globals';
-import {AddDebugTrackMenu} from './debug_tracks/add_debug_track_menu';
+import {SqlTableDescription} from './widgets/sql/table/table_description';
+import {Trace} from '../public/trace';
+import {MenuItem, PopupMenu2} from '../widgets/menu';
+import {addEphemeralTab} from '../common/add_ephemeral_tab';
+import {Tab} from '../public/tab';
 
-interface SqlTableTabConfig {
+export interface AddSqlTableTabParams {
   table: SqlTableDescription;
-  displayName?: string;
   filters?: Filter[];
   imports?: string[];
 }
 
-export function addSqlTableTab(config: SqlTableTabConfig): void {
-  const queryResultsTab = new SqlTableTab({
-    config,
-    engine: getEngine(),
-    uuid: uuidv4(),
-  });
-
-  addEphemeralTab(queryResultsTab, 'sqlTable');
+export function addSqlTableTab(
+  trace: Trace,
+  config: AddSqlTableTabParams,
+): void {
+  addSqlTableTabWithState(
+    new SqlTableState(trace, config.table, {
+      filters: config.filters,
+      imports: config.imports,
+    }),
+  );
 }
 
-// TODO(stevegolton): Find a way to make this more elegant.
-function getEngine(): Engine {
-  const engConfig = globals.getCurrentEngine();
-  const engineId = assertExists(engConfig).id;
-  return assertExists(globals.engines.get(engineId)).getProxy('QueryResult');
+function addSqlTableTabWithState(state: SqlTableState) {
+  addEphemeralTab('sqlTable', new SqlTableTab(state));
 }
 
-export class SqlTableTab extends BottomTab<SqlTableTabConfig> {
-  static readonly kind = 'dev.perfetto.SqlTableTab';
+class SqlTableTab implements Tab {
+  constructor(private readonly state: SqlTableState) {}
 
-  private state: SqlTableState;
-
-  constructor(args: NewBottomTabArgs<SqlTableTabConfig>) {
-    super(args);
-
-    this.state = new SqlTableState(
-      this.engine,
-      this.config.table,
-      this.config.filters,
-      this.config.imports,
-    );
-  }
-
-  static create(args: NewBottomTabArgs<SqlTableTabConfig>): SqlTableTab {
-    return new SqlTableTab(args);
-  }
-
-  viewTab() {
+  render() {
     const range = this.state.getDisplayedRange();
     const rowCount = this.state.getTotalRowCount();
     const navigation = [
@@ -97,7 +72,10 @@
         onclick: () => this.state.goForward(),
       }),
     ];
-    const {selectStatement, columns} = this.state.buildSqlSelectStatement();
+    const {selectStatement, columns} = this.state.getCurrentRequest();
+    const debugTrackColumns = Object.values(columns).filter(
+      (c) => !c.startsWith('__'),
+    );
     const addDebugTrack = m(
       Popup,
       {
@@ -105,11 +83,11 @@
         position: PopupPosition.Top,
       },
       m(AddDebugTrackMenu, {
+        trace: this.state.trace,
         dataSource: {
-          sqlSource: selectStatement,
-          columns: columns,
+          sqlSource: `SELECT ${debugTrackColumns.join(', ')} FROM (${selectStatement})`,
+          columns: debugTrackColumns,
         },
-        engine: this.engine,
       }),
     );
 
@@ -121,11 +99,25 @@
         buttons: [
           ...navigation,
           addDebugTrack,
-          m(Button, {
-            label: 'Copy SQL query',
-            onclick: () =>
-              copyToClipboard(this.state.getNonPaginatedSQLQuery()),
-          }),
+          m(
+            PopupMenu2,
+            {
+              trigger: m(Button, {
+                icon: Icons.Menu,
+              }),
+            },
+            m(MenuItem, {
+              label: 'Duplicate',
+              icon: 'tab_duplicate',
+              onclick: () => addSqlTableTabWithState(this.state.clone()),
+            }),
+            m(MenuItem, {
+              label: 'Copy SQL query',
+              icon: Icons.Copy,
+              onclick: () =>
+                copyToClipboard(this.state.getNonPaginatedSQLQuery()),
+            }),
+          ),
         ],
       },
       m(SqlTable, {
@@ -141,7 +133,7 @@
   }
 
   private getDisplayName(): string {
-    return this.config.displayName ?? tableDisplayName(this.config.table);
+    return this.state.config.displayName ?? this.state.config.name;
   }
 
   isLoading(): boolean {
diff --git a/ui/src/frontend/tab_panel.ts b/ui/src/frontend/tab_panel.ts
index 05f443e..0662ef8 100644
--- a/ui/src/frontend/tab_panel.ts
+++ b/ui/src/frontend/tab_panel.ts
@@ -13,36 +13,99 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {Gate} from '../base/mithril_utils';
-import {Actions} from '../common/actions';
-import {getLegacySelection} from '../common/state';
 import {EmptyState} from '../widgets/empty_state';
-
-import {
-  DragHandle,
-  Tab,
-  TabDropdownEntry,
-  getDefaultDetailsHeight,
-} from './drag_handle';
-import {globals} from './globals';
 import {raf} from '../core/raf_scheduler';
+import {DetailsShell} from '../widgets/details_shell';
+import {GridLayout, GridLayoutColumn} from '../widgets/grid_layout';
+import {Section} from '../widgets/section';
+import {Tree, TreeNode} from '../widgets/tree';
+import {TraceImpl, TraceImplAttrs} from '../core/trace_impl';
+import {MenuItem, PopupMenu2} from '../widgets/menu';
+import {Button} from '../widgets/button';
+import {DEFAULT_DETAILS_CONTENT_HEIGHT} from './css_constants';
+import {DisposableStack} from '../base/disposable_stack';
+import {DragGestureHandler} from '../base/drag_gesture_handler';
+import {assertExists} from '../base/logging';
+
+export type TabPanelAttrs = TraceImplAttrs;
+
+export interface Tab {
+  // Unique key for this tab, passed to callbacks.
+  key: string;
+
+  // Tab title to show on the tab handle.
+  title: m.Children;
+
+  // Whether to show a close button on the tab handle or not.
+  // Default = false.
+  hasCloseButton?: boolean;
+}
 
 interface TabWithContent extends Tab {
   content: m.Children;
 }
 
-export class TabPanel implements m.ClassComponent {
+export interface TabDropdownEntry {
+  // Unique key for this tab dropdown entry.
+  key: string;
+
+  // Title to show on this entry.
+  title: string;
+
+  // Called when tab dropdown entry is clicked.
+  onClick: () => void;
+
+  // Whether this tab is checked or not
+  checked: boolean;
+}
+
+export class TabPanel implements m.ClassComponent<TabPanelAttrs> {
+  private readonly trace: TraceImpl;
   // Tabs panel starts collapsed.
-  private detailsHeight = 0;
+
+  // NOTE: the visibility state of the tab panel (COLLAPSED, VISIBLE,
+  // FULLSCREEN) is stored in TabManagerImpl because it can be toggled via
+  // commands. Here we store only the heights for the various states, because
+  // nobody else needs to know about them and are an impl. detail of the VDOM.
+
+  // The actual height of the vdom node. It matches resizableHeight if VISIBLE,
+  // 0 if COLLAPSED, fullscreenHeight if FULLSCREEN.
+  private height = 0;
+
+  // The height when the panel is 'VISIBLE'.
+  private resizableHeight = getDefaultDetailsHeight();
+
+  // The height when the panel is 'FULLSCREEN'.
+  private fullscreenHeight = 0;
+
   private fadeContext = new FadeContext();
-  private hasBeenDragged = false;
+  private trash = new DisposableStack();
+
+  constructor({attrs}: m.CVnode<TabPanelAttrs>) {
+    this.trace = attrs.trace;
+  }
 
   view() {
-    const tabMan = globals.tabManager;
-    const tabList = globals.store.state.tabs.openTabs;
-
+    const tabMan = this.trace.tabs;
+    const tabList = this.trace.tabs.openTabsUri;
     const resolvedTabs = tabMan.resolveTabs(tabList);
+
+    switch (this.trace.tabs.tabPanelVisibility) {
+      case 'VISIBLE':
+        this.height = Math.min(
+          Math.max(this.resizableHeight, 0),
+          this.fullscreenHeight,
+        );
+        break;
+      case 'FULLSCREEN':
+        this.height = this.fullscreenHeight;
+        break;
+      case 'COLLAPSED':
+        this.height = 0;
+        break;
+    }
+
     const tabs = resolvedTabs.map(({uri, tab: tabDesc}): TabWithContent => {
       if (tabDesc) {
         return {
@@ -61,13 +124,6 @@
       }
     });
 
-    if (
-      !this.hasBeenDragged &&
-      (tabs.length > 0 || globals.state.selection.kind !== 'empty')
-    ) {
-      this.detailsHeight = getDefaultDetailsHeight();
-    }
-
     // Add the permanent current selection tab to the front of the list of tabs
     tabs.unshift({
       key: 'current_selection',
@@ -75,51 +131,158 @@
       content: this.renderCSTabContentWithFading(),
     });
 
-    const tabDropdownEntries = globals.tabManager.tabs
-      .filter((tab) => tab.isEphemeral === false)
-      .map(({content, uri}): TabDropdownEntry => {
-        // Check if the tab is already open
-        const isOpen =
-          globals.state.tabs.openTabs.find((openTabUri) => {
-            return openTabUri === uri;
-          }) !== undefined;
-        const clickAction = isOpen
-          ? Actions.hideTab({uri})
-          : Actions.showTab({uri});
-        return {
-          key: uri,
-          title: content.getTitle(),
-          onClick: () => globals.dispatch(clickAction),
-          checked: isOpen,
-        };
-      });
-
     return [
-      m(DragHandle, {
-        resize: (height: number) => {
-          this.detailsHeight = Math.max(height, 0);
-          this.hasBeenDragged = true;
-        },
-        height: this.detailsHeight,
-        tabs,
-        currentTabKey: globals.state.tabs.currentTab,
-        tabDropdownEntries,
-        onTabClick: (key) => globals.dispatch(Actions.showTab({uri: key})),
-        onTabClose: (key) => globals.dispatch(Actions.hideTab({uri: key})),
-      }),
+      // Render the header with the ... menu, tab strip and resize buttons.
+      m(
+        '.handle',
+        this.renderTripleDotDropdownMenu(),
+        this.renderTabStrip(tabs),
+        this.renderTabResizeButtons(),
+      ),
+      // Render the tab contents.
       m(
         '.details-panel-container',
         {
-          style: {height: `${this.detailsHeight}px`},
+          style: {height: `${this.height}px`},
         },
         tabs.map(({key, content}) => {
-          const active = key === globals.state.tabs.currentTab;
+          const active = key === this.trace.tabs.currentTabUri;
           return m(Gate, {open: active}, content);
         }),
       ),
     ];
   }
 
+  oncreate(vnode: m.VnodeDOM<TraceImplAttrs, this>) {
+    let dragStartY = 0;
+    let heightWhenDragStarted = 0;
+
+    this.trash.use(
+      new DragGestureHandler(
+        vnode.dom as HTMLElement,
+        /* onDrag */ (_x, y) => {
+          const deltaYSinceDragStart = dragStartY - y;
+          this.resizableHeight = heightWhenDragStarted + deltaYSinceDragStart;
+          raf.scheduleFullRedraw();
+        },
+        /* onDragStarted */ (_x, y) => {
+          this.resizableHeight = this.height;
+          heightWhenDragStarted = this.height;
+          dragStartY = y;
+          this.trace.tabs.setTabPanelVisibility('VISIBLE');
+        },
+        /* onDragFinished */ () => {},
+      ),
+    );
+
+    const page = assertExists(vnode.dom.parentElement);
+    this.fullscreenHeight = page.clientHeight;
+    const resizeObs = new ResizeObserver(() => {
+      this.fullscreenHeight = page.clientHeight;
+      raf.scheduleFullRedraw();
+    });
+    resizeObs.observe(page);
+    this.trash.defer(() => resizeObs.disconnect());
+  }
+
+  onremove() {
+    this.trash.dispose();
+  }
+
+  private renderTripleDotDropdownMenu(): m.Child {
+    const entries = this.trace.tabs.tabs
+      .filter((tab) => tab.isEphemeral === false)
+      .map(({content, uri}): TabDropdownEntry => {
+        return {
+          key: uri,
+          title: content.getTitle(),
+          onClick: () => this.trace.tabs.toggleTab(uri),
+          checked: this.trace.tabs.isOpen(uri),
+        };
+      });
+
+    return m(
+      '.buttons',
+      m(
+        PopupMenu2,
+        {
+          trigger: m(Button, {
+            compact: true,
+            icon: 'more_vert',
+            disabled: entries.length === 0,
+            title: 'More Tabs',
+          }),
+        },
+        entries.map((entry) => {
+          return m(MenuItem, {
+            key: entry.key,
+            label: entry.title,
+            onclick: () => entry.onClick(),
+            icon: entry.checked ? 'check_box' : 'check_box_outline_blank',
+          });
+        }),
+      ),
+    );
+  }
+
+  private renderTabStrip(tabs: Tab[]): m.Child {
+    const currentTabKey = this.trace.tabs.currentTabUri;
+    return m(
+      '.tabs',
+      tabs.map((tab) => {
+        const {key, hasCloseButton = false} = tab;
+        const tag = currentTabKey === key ? '.tab[active]' : '.tab';
+        return m(
+          tag,
+          {
+            key,
+            onclick: (event: Event) => {
+              if (!event.defaultPrevented) {
+                this.trace.tabs.showTab(key);
+              }
+            },
+            // Middle click to close
+            onauxclick: (event: MouseEvent) => {
+              if (!event.defaultPrevented) {
+                this.trace.tabs.hideTab(key);
+              }
+            },
+          },
+          m('span.pf-tab-title', tab.title),
+          hasCloseButton &&
+            m(Button, {
+              onclick: (event: Event) => {
+                this.trace.tabs.hideTab(key);
+                event.preventDefault();
+              },
+              compact: true,
+              icon: 'close',
+            }),
+        );
+      }),
+    );
+  }
+
+  private renderTabResizeButtons(): m.Child {
+    const isClosed = this.trace.tabs.tabPanelVisibility === 'COLLAPSED';
+    return m(
+      '.buttons',
+      m(Button, {
+        title: 'Open fullscreen',
+        disabled: this.trace.tabs.tabPanelVisibility === 'FULLSCREEN',
+        icon: 'vertical_align_top',
+        compact: true,
+        onclick: () => this.trace.tabs.setTabPanelVisibility('FULLSCREEN'),
+      }),
+      m(Button, {
+        onclick: () => this.trace.tabs.toggleTabPanelVisibility(),
+        title: isClosed ? 'Show panel' : 'Hide panel',
+        icon: isClosed ? 'keyboard_arrow_up' : 'keyboard_arrow_down',
+        compact: true,
+      }),
+    );
+  }
+
   private renderCSTabContentWithFading(): m.Children {
     const section = this.renderCSTabContent();
     if (section.isLoading) {
@@ -130,8 +293,7 @@
   }
 
   private renderCSTabContent(): {isLoading: boolean; content: m.Children} {
-    const currentSelection = globals.state.selection;
-    const legacySelection = getLegacySelection(globals.state);
+    const currentSelection = this.trace.selection.selection;
     if (currentSelection.kind === 'empty') {
       return {
         isLoading: false,
@@ -146,44 +308,29 @@
       };
     }
 
-    // Show single selection panels if they are registered
-    if (currentSelection.kind === 'single') {
-      const trackKey = currentSelection.trackKey;
-      const uri = globals.state.tracks[trackKey]?.uri;
+    if (currentSelection.kind === 'track') {
+      return {
+        isLoading: false,
+        content: this.renderTrackDetailsPanel(currentSelection.trackUri),
+      };
+    }
 
-      if (uri) {
-        const trackDesc = globals.trackManager.resolveTrackInfo(uri);
-        const panel = trackDesc?.detailsPanel;
-        if (panel) {
-          return {
-            content: panel.render(currentSelection.eventId),
-            isLoading: panel.isLoading?.() ?? false,
-          };
-        }
-      }
+    const detailsPanel = this.trace.selection.getDetailsPanelForSelection();
+    if (currentSelection.kind === 'track_event' && detailsPanel !== undefined) {
+      return {
+        isLoading: detailsPanel.isLoading,
+        content: detailsPanel.render(),
+      };
     }
 
     // Get the first "truthy" details panel
-    let detailsPanels = globals.tabManager.detailsPanels.map((dp) => {
+    const detailsPanels = this.trace.tabs.detailsPanels.map((dp) => {
       return {
         content: dp.render(currentSelection),
         isLoading: dp.isLoading?.() ?? false,
       };
     });
 
-    if (legacySelection !== null) {
-      const legacyDetailsPanels = globals.tabManager.legacyDetailsPanels.map(
-        (dp) => {
-          return {
-            content: dp.render(legacySelection),
-            isLoading: dp.isLoading?.() ?? false,
-          };
-        },
-      );
-
-      detailsPanels = detailsPanels.concat(legacyDetailsPanels);
-    }
-
     const panel = detailsPanels.find(({content}) => content);
 
     if (panel) {
@@ -203,6 +350,42 @@
       };
     }
   }
+
+  private renderTrackDetailsPanel(trackUri: string) {
+    const track = this.trace.tracks.getTrack(trackUri);
+    if (track) {
+      return m(
+        DetailsShell,
+        {title: 'Track', description: track.title},
+        m(
+          GridLayout,
+          m(
+            GridLayoutColumn,
+            m(
+              Section,
+              {title: 'Details'},
+              m(
+                Tree,
+                m(TreeNode, {left: 'Name', right: track.title}),
+                m(TreeNode, {left: 'URI', right: track.uri}),
+                m(TreeNode, {left: 'Plugin ID', right: track.pluginId}),
+                m(
+                  TreeNode,
+                  {left: 'Tags'},
+                  track.tags &&
+                    Object.entries(track.tags).map(([key, value]) => {
+                      return m(TreeNode, {left: key, right: value?.toString()});
+                    }),
+                ),
+              ),
+            ),
+          ),
+        ),
+      );
+    } else {
+      return undefined; // TODO show something sensible here
+    }
+  }
 }
 
 const FADE_TIME_MS = 50;
@@ -255,3 +438,10 @@
     return this.show ? vnode.children : undefined;
   }
 }
+
+function getDefaultDetailsHeight() {
+  const DRAG_HANDLE_HEIGHT_PX = 28;
+  // This needs to be a function instead of a const to ensure the CSS constants
+  // have been initialized by the time we perform this calculation;
+  return DRAG_HANDLE_HEIGHT_PX + DEFAULT_DETAILS_CONTENT_HEIGHT;
+}
diff --git a/ui/src/frontend/tables/attribute_modal_holder.ts b/ui/src/frontend/tables/attribute_modal_holder.ts
index d154ab1..8f94f5c 100644
--- a/ui/src/frontend/tables/attribute_modal_holder.ts
+++ b/ui/src/frontend/tables/attribute_modal_holder.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {showModal} from '../../widgets/modal';
 import {ArgumentPopup} from '../pivot_table_argument_popup';
 
diff --git a/ui/src/frontend/tables/table.ts b/ui/src/frontend/tables/table.ts
deleted file mode 100644
index c366c19..0000000
--- a/ui/src/frontend/tables/table.ts
+++ /dev/null
@@ -1,278 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size 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 {allUnique, range} from '../../base/array_utils';
-import {
-  compareUniversal,
-  comparingBy,
-  ComparisonFn,
-  SortableValue,
-  SortDirection,
-  withDirection,
-} from '../../base/comparison_utils';
-import {raf} from '../../core/raf_scheduler';
-import {
-  menuItem,
-  PopupMenuButton,
-  popupMenuIcon,
-  PopupMenuItem,
-} from '../popup_menu';
-
-export interface ColumnDescriptorAttrs<T> {
-  // Context menu items displayed on the column header.
-  contextMenu?: PopupMenuItem[];
-
-  // Unique column ID, used to identify which column is currently sorted.
-  columnId?: string;
-
-  // Sorting predicate: if provided, column would be sortable.
-  ordering?: ComparisonFn<T>;
-
-  // Simpler way to provide a sorting: instead of full predicate, the function
-  // can map the row for "sorting key" associated with the column.
-  sortKey?: (value: T) => SortableValue;
-}
-
-export class ColumnDescriptor<T> {
-  name: string;
-  render: (row: T) => m.Child;
-  id: string;
-  contextMenu?: PopupMenuItem[];
-  ordering?: ComparisonFn<T>;
-
-  constructor(
-    name: string,
-    render: (row: T) => m.Child,
-    attrs?: ColumnDescriptorAttrs<T>,
-  ) {
-    this.name = name;
-    this.render = render;
-    this.id = attrs?.columnId === undefined ? name : attrs.columnId;
-
-    if (attrs === undefined) {
-      return;
-    }
-
-    if (attrs.sortKey !== undefined && attrs.ordering !== undefined) {
-      throw new Error('only one way to order a column should be specified');
-    }
-
-    if (attrs.sortKey !== undefined) {
-      this.ordering = comparingBy(attrs.sortKey, compareUniversal);
-    }
-    if (attrs.ordering !== undefined) {
-      this.ordering = attrs.ordering;
-    }
-  }
-}
-
-export function numberColumn<T>(
-  name: string,
-  getter: (t: T) => number,
-  contextMenu?: PopupMenuItem[],
-): ColumnDescriptor<T> {
-  return new ColumnDescriptor<T>(name, getter, {contextMenu, sortKey: getter});
-}
-
-export function stringColumn<T>(
-  name: string,
-  getter: (t: T) => string,
-  contextMenu?: PopupMenuItem[],
-): ColumnDescriptor<T> {
-  return new ColumnDescriptor<T>(name, getter, {contextMenu, sortKey: getter});
-}
-
-export function widgetColumn<T>(
-  name: string,
-  getter: (t: T) => m.Child,
-): ColumnDescriptor<T> {
-  return new ColumnDescriptor<T>(name, getter);
-}
-
-interface SortingInfo<T> {
-  columnId: string;
-  direction: SortDirection;
-  // TODO(ddrone): figure out if storing this can be avoided.
-  ordering: ComparisonFn<T>;
-}
-
-// Encapsulated table data, that contains the input to be displayed, as well as
-// some helper information to allow sorting.
-export class TableData<T> {
-  data: T[];
-  private _sortingInfo?: SortingInfo<T>;
-  private permutation: number[];
-
-  constructor(data: T[]) {
-    this.data = data;
-    this.permutation = range(data.length);
-  }
-
-  *iterateItems(): Generator<T> {
-    for (const index of this.permutation) {
-      yield this.data[index];
-    }
-  }
-
-  items(): T[] {
-    return Array.from(this.iterateItems());
-  }
-
-  setItems(newItems: T[]) {
-    this.data = newItems;
-    this.permutation = range(newItems.length);
-    if (this._sortingInfo !== undefined) {
-      this.reorder(this._sortingInfo);
-    }
-    raf.scheduleFullRedraw();
-  }
-
-  resetOrder() {
-    this.permutation = range(this.data.length);
-    this._sortingInfo = undefined;
-    raf.scheduleFullRedraw();
-  }
-
-  get sortingInfo(): SortingInfo<T> | undefined {
-    return this._sortingInfo;
-  }
-
-  reorder(info: SortingInfo<T>) {
-    this._sortingInfo = info;
-    this.permutation.sort(
-      withDirection(
-        comparingBy((index: number) => this.data[index], info.ordering),
-        info.direction,
-      ),
-    );
-    raf.scheduleFullRedraw();
-  }
-}
-
-export interface TableAttrs<T> {
-  data: TableData<T>;
-  columns: ColumnDescriptor<T>[];
-}
-
-function directionOnIndex(
-  columnId: string,
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  info?: SortingInfo<any>,
-): SortDirection | undefined {
-  if (info === undefined) {
-    return undefined;
-  }
-  return info.columnId === columnId ? info.direction : undefined;
-}
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export class Table implements m.ClassComponent<TableAttrs<any>> {
-  renderColumnHeader(
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    vnode: m.Vnode<TableAttrs<any>>,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    column: ColumnDescriptor<any>,
-  ): m.Child {
-    let currDirection: SortDirection | undefined = undefined;
-
-    let items = column.contextMenu;
-    if (column.ordering !== undefined) {
-      const ordering = column.ordering;
-      currDirection = directionOnIndex(column.id, vnode.attrs.data.sortingInfo);
-      const newItems: PopupMenuItem[] = [];
-      if (currDirection !== 'ASC') {
-        newItems.push(
-          menuItem('Sort ascending', () => {
-            vnode.attrs.data.reorder({
-              columnId: column.id,
-              direction: 'ASC',
-              ordering,
-            });
-          }),
-        );
-      }
-      if (currDirection !== 'DESC') {
-        newItems.push(
-          menuItem('Sort descending', () => {
-            vnode.attrs.data.reorder({
-              columnId: column.id,
-              direction: 'DESC',
-              ordering,
-            });
-          }),
-        );
-      }
-      if (currDirection !== undefined) {
-        newItems.push(
-          menuItem('Restore original order', () => {
-            vnode.attrs.data.resetOrder();
-          }),
-        );
-      }
-      items = [...newItems, ...(items ?? [])];
-    }
-
-    return m(
-      'td',
-      column.name,
-      items === undefined
-        ? null
-        : m(PopupMenuButton, {
-            icon: popupMenuIcon(currDirection),
-            items,
-          }),
-    );
-  }
-
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  checkValid(attrs: TableAttrs<any>) {
-    if (!allUnique(attrs.columns.map((c) => c.id))) {
-      throw new Error('column IDs should be unique');
-    }
-  }
-
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  oncreate(vnode: m.VnodeDOM<TableAttrs<any>, this>) {
-    this.checkValid(vnode.attrs);
-  }
-
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  onupdate(vnode: m.VnodeDOM<TableAttrs<any>, this>) {
-    this.checkValid(vnode.attrs);
-  }
-
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  view(vnode: m.Vnode<TableAttrs<any>>): m.Child {
-    const attrs = vnode.attrs;
-
-    return m(
-      'table.generic-table',
-      m(
-        'thead',
-        m(
-          'tr.header',
-          attrs.columns.map((column) => this.renderColumnHeader(vnode, column)),
-        ),
-      ),
-      attrs.data.items().map((row) =>
-        m(
-          'tr',
-          attrs.columns.map((column) => m('td', column.render(row))),
-        ),
-      ),
-    );
-  }
-}
diff --git a/ui/src/frontend/tables/table_showcase.ts b/ui/src/frontend/tables/table_showcase.ts
deleted file mode 100644
index c45cde6..0000000
--- a/ui/src/frontend/tables/table_showcase.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {
-  ColumnDescriptor,
-  numberColumn,
-  stringColumn,
-  Table,
-  TableData,
-} from './table';
-
-// This file serves as an example of a table component present in the widgets
-// showcase. Since table is somewhat complicated component that requires some
-// setup spread across several declarations, all the necessary code resides in a
-// separate file (this one) and provides a no-argument wrapper component that
-// can be used in the widgets showcase directly.
-
-interface ProgrammingLanguage {
-  id: number;
-  name: string;
-  year: number;
-}
-
-const languagesList: ProgrammingLanguage[] = [
-  {
-    id: 1,
-    name: 'TypeScript',
-    year: 2012,
-  },
-  {
-    id: 2,
-    name: 'JavaScript',
-    year: 1995,
-  },
-  {
-    id: 3,
-    name: 'Lean',
-    year: 2013,
-  },
-];
-
-const columns: ColumnDescriptor<ProgrammingLanguage>[] = [
-  numberColumn('ID', (x) => x.id),
-  stringColumn('Name', (x) => x.name),
-  numberColumn('Year', (x) => x.year),
-];
-
-export class TableShowcase implements m.ClassComponent {
-  data = new TableData(languagesList);
-
-  view(): m.Child {
-    return m(Table, {
-      data: this.data,
-      columns,
-    });
-  }
-}
diff --git a/ui/src/frontend/thread_details_tab.ts b/ui/src/frontend/thread_details_tab.ts
new file mode 100644
index 0000000..f790a68
--- /dev/null
+++ b/ui/src/frontend/thread_details_tab.ts
@@ -0,0 +1,62 @@
+// 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 {Tab} from '../public/tab';
+import {Utid} from '../trace_processor/sql_utils/core_types';
+import {DetailsShell} from '../widgets/details_shell';
+import {GridLayout, GridLayoutColumn} from '../widgets/grid_layout';
+import {Section} from '../widgets/section';
+import {Details, DetailsSchema} from './widgets/sql/details/details';
+import d = DetailsSchema;
+import {Trace} from '../public/trace';
+
+export class ThreadDetailsTab implements Tab {
+  private data: Details;
+
+  // TODO(altimin): Ideally, we would not require the tid to be passed in, but
+  // fetch it from the underlying data instead. See comment in ProcessDetailsTab
+  // for more details.
+  constructor(private args: {trace: Trace; utid: Utid; tid?: number}) {
+    this.data = new Details(args.trace, 'thread', args.utid, {
+      'tid': d.Value('tid'),
+      'Name': d.Value('name'),
+      'Process': d.SqlIdRef('process', 'upid'),
+      'Is main thread': d.Boolean('is_main_thread'),
+      'Start time': d.Timestamp('start_ts', {skipIfNull: true}),
+      'End time': d.Timestamp('end_ts', {skipIfNull: true}),
+      'Machine id': d.Value('machine_id', {skipIfNull: true}),
+    });
+  }
+
+  render() {
+    return m(
+      DetailsShell,
+      {
+        title: this.getTitle(),
+      },
+      m(
+        GridLayout,
+        m(GridLayoutColumn, m(Section, {title: 'Details'}, this.data.render())),
+      ),
+    );
+  }
+
+  getTitle(): string {
+    if (this.args.tid !== undefined) {
+      return `Thread ${this.args.tid}`;
+    }
+    return `Thread utid:${this.args.utid}`;
+  }
+}
diff --git a/ui/src/frontend/thread_slice_details_tab.ts b/ui/src/frontend/thread_slice_details_tab.ts
index bf2f800..b549971 100644
--- a/ui/src/frontend/thread_slice_details_tab.ts
+++ b/ui/src/frontend/thread_slice_details_tab.ts
@@ -13,24 +13,17 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {Icons} from '../base/semantic_icons';
-import {Time, TimeSpan} from '../base/time';
+import {TimeSpan} from '../base/time';
 import {exists} from '../base/utils';
-import {raf} from '../core/raf_scheduler';
 import {Engine} from '../trace_processor/engine';
-import {LONG, LONG_NULL, NUM, STR_NULL} from '../trace_processor/query_result';
 import {Button} from '../widgets/button';
 import {DetailsShell} from '../widgets/details_shell';
 import {GridLayout, GridLayoutColumn} from '../widgets/grid_layout';
 import {MenuItem, PopupMenu2} from '../widgets/menu';
 import {Section} from '../widgets/section';
 import {Tree} from '../widgets/tree';
-
-import {BottomTab, NewBottomTabArgs} from './bottom_tab';
-import {addDebugSliceTrack} from './debug_tracks/debug_tracks';
-import {Flow, FlowPoint, globals} from './globals';
-import {addQueryResultsTab} from './query_result_tab';
+import {Flow, FlowPoint} from '../core/flow_types';
 import {hasArgs, renderArguments} from './slice_args';
 import {renderDetails} from './slice_details';
 import {getSlice, SliceDetails} from '../trace_processor/sql_utils/slice';
@@ -40,15 +33,20 @@
 } from './sql/thread_state';
 import {asSliceSqlId} from '../trace_processor/sql_utils/core_types';
 import {DurationWidget} from './widgets/duration';
-import {addSqlTableTab} from './sql_table_tab';
-import {SqlTables} from './widgets/sql/table/well_known_sql_tables';
 import {SliceRef} from './widgets/slice';
 import {BasicTable} from '../widgets/basic_table';
+import {getSqlTableDescription} from './widgets/sql/table/sql_table_registry';
+import {assertExists} from '../base/logging';
+import {Trace} from '../public/trace';
+import {TrackEventDetailsPanel} from '../public/details_panel';
+import {TrackEventSelection} from '../public/selection';
+import {extensions} from '../public/lib/extensions';
+import {TraceImpl} from '../core/trace_impl';
 
 interface ContextMenuItem {
   name: string;
   shouldDisplay(slice: SliceDetails): boolean;
-  run(slice: SliceDetails): void;
+  run(slice: SliceDetails, trace: Trace): void;
 }
 
 function getTidFromSlice(slice: SliceDetails): number | undefined {
@@ -91,11 +89,15 @@
   {
     name: 'Ancestor slices',
     shouldDisplay: (slice: SliceDetails) => slice.parentId !== undefined,
-    run: (slice: SliceDetails) =>
-      addSqlTableTab({
-        table: SqlTables.slice,
+    run: (slice: SliceDetails, trace: Trace) =>
+      extensions.addSqlTableTab(trace, {
+        table: assertExists(getSqlTableDescription('slice')),
         filters: [
-          `id IN (SELECT id FROM _slice_ancestor_and_self(${slice.id}))`,
+          {
+            op: (cols) =>
+              `${cols[0]} IN (SELECT id FROM _slice_ancestor_and_self(${slice.id}))`,
+            columns: ['id'],
+          },
         ],
         imports: ['slices.hierarchy'],
       }),
@@ -103,11 +105,15 @@
   {
     name: 'Descendant slices',
     shouldDisplay: () => true,
-    run: (slice: SliceDetails) =>
-      addSqlTableTab({
-        table: SqlTables.slice,
+    run: (slice: SliceDetails, trace: Trace) =>
+      extensions.addSqlTableTab(trace, {
+        table: assertExists(getSqlTableDescription('slice')),
         filters: [
-          `id IN (SELECT id FROM _slice_descendant_and_self(${slice.id}))`,
+          {
+            op: (cols) =>
+              `${cols[0]} IN (SELECT id FROM _slice_descendant_and_self(${slice.id}))`,
+            columns: ['id'],
+          },
         ],
         imports: ['slices.hierarchy'],
       }),
@@ -115,8 +121,8 @@
   {
     name: 'Average duration of slice name',
     shouldDisplay: (slice: SliceDetails) => hasName(slice),
-    run: (slice: SliceDetails) =>
-      addQueryResultsTab({
+    run: (slice: SliceDetails, trace: Trace) =>
+      extensions.addQueryResultsTab(trace, {
         query: `SELECT AVG(dur) / 1e9 FROM slice WHERE name = '${slice.name!}'`,
         title: `${slice.name} average dur`,
       }),
@@ -128,28 +134,16 @@
       hasThreadName(slice) &&
       hasTid(slice) &&
       hasPid(slice),
-    run: (slice: SliceDetails) => {
-      const engine = getEngine();
-      if (engine === undefined) {
-        return;
-      }
-      engine
+    run: (slice: SliceDetails, trace: Trace) => {
+      trace.engine
         .query(
-          `
-        INCLUDE PERFETTO MODULE android.binder;
-        INCLUDE PERFETTO MODULE android.monitor_contention;
-      `,
+          `INCLUDE PERFETTO MODULE android.binder;
+           INCLUDE PERFETTO MODULE android.monitor_contention;`,
         )
         .then(() =>
-          addDebugSliceTrack(
-            // NOTE(stevegolton): This is a temporary patch, this menu should
-            // become part of another plugin, at which point we can just use the
-            // plugin's context object.
-            {
-              engine,
-              registerTrack: (x) => globals.trackManager.registerTrack(x),
-            },
-            {
+          extensions.addDebugSliceTrack({
+            trace,
+            data: {
               sqlSource: `
                                 WITH merged AS (
                                   SELECT s.ts, s.dur, tx.aidl_name AS name, 0 AS depth
@@ -188,12 +182,10 @@
                                   ORDER BY depth
                                 ) SELECT ts, dur, name FROM merged`,
             },
-            `Binder names (${getProcessNameFromSlice(
+            title: `Binder names (${getProcessNameFromSlice(
               slice,
             )}:${getThreadNameFromSlice(slice)})`,
-            {ts: 'ts', dur: 'dur', name: 'name'},
-            [],
-          ),
+          }),
         );
     },
   },
@@ -203,92 +195,22 @@
   return ITEMS.filter((item) => item.shouldDisplay(slice));
 }
 
-function getEngine(): Engine | undefined {
-  const engineId = globals.getCurrentEngine()?.id;
-  if (engineId === undefined) {
-    return undefined;
-  }
-  const engine = globals.engines.get(engineId)?.getProxy('SlicePanel');
-  return engine;
-}
-
-async function getAnnotationSlice(
-  engine: Engine,
-  id: number,
-): Promise<SliceDetails | undefined> {
-  const query = await engine.query(`
-    SELECT
-      id,
-      name,
-      ts,
-      dur,
-      track_id as trackId,
-      thread_dur as threadDur,
-      cat,
-      ABS_TIME_STR(ts) as absTime
-    FROM annotation_slice
-    where id = ${id}`);
-
-  const it = query.firstRow({
-    id: NUM,
-    name: STR_NULL,
-    ts: LONG,
-    dur: LONG,
-    trackId: NUM,
-    threadDur: LONG_NULL,
-    cat: STR_NULL,
-    absTime: STR_NULL,
-  });
-
-  return {
-    id: asSliceSqlId(it.id),
-    name: it.name ?? 'null',
-    ts: Time.fromRaw(it.ts),
-    dur: it.dur,
-    depth: 0,
-    trackId: it.trackId,
-    threadDur: it.threadDur ?? undefined,
-    category: it.cat ?? undefined,
-    absTime: it.absTime ?? undefined,
-  };
-}
-
 async function getSliceDetails(
   engine: Engine,
   id: number,
-  table: string,
 ): Promise<SliceDetails | undefined> {
-  if (table === 'annotation_slice') {
-    return getAnnotationSlice(engine, id);
-  } else {
-    return getSlice(engine, asSliceSqlId(id));
-  }
+  return getSlice(engine, asSliceSqlId(id));
 }
 
-interface ThreadSliceDetailsTabConfig {
-  id: number;
-  table: string;
-}
-
-export class ThreadSliceDetailsTab extends BottomTab<ThreadSliceDetailsTabConfig> {
+export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel {
   private sliceDetails?: SliceDetails;
   private breakdownByThreadState?: BreakdownByThreadState;
 
-  static create(
-    args: NewBottomTabArgs<ThreadSliceDetailsTabConfig>,
-  ): ThreadSliceDetailsTab {
-    return new ThreadSliceDetailsTab(args);
-  }
+  constructor(private readonly trace: TraceImpl) {}
 
-  constructor(args: NewBottomTabArgs<ThreadSliceDetailsTabConfig>) {
-    super(args);
-    this.load();
-  }
-
-  async load() {
-    // Start loading the slice details
-    const {id, table} = this.config;
-    const details = await getSliceDetails(this.engine, id, table);
+  async load({eventId}: TrackEventSelection) {
+    const {trace} = this;
+    const details = await getSliceDetails(trace.engine, eventId);
 
     if (
       details !== undefined &&
@@ -296,21 +218,16 @@
       details.dur > 0
     ) {
       this.breakdownByThreadState = await breakDownIntervalByThreadState(
-        this.engine,
+        trace.engine,
         TimeSpan.fromTimeAndDuration(details.ts, details.dur),
         details.thread.utid,
       );
     }
 
     this.sliceDetails = details;
-    raf.scheduleFullRedraw();
   }
 
-  getTitle(): string {
-    return `Current Selection`;
-  }
-
-  viewTab() {
+  render() {
     if (!exists(this.sliceDetails)) {
       return m(DetailsShell, {title: 'Slice', description: 'Loading...'});
     }
@@ -324,17 +241,13 @@
       },
       m(
         GridLayout,
-        renderDetails(slice, this.breakdownByThreadState),
-        this.renderRhs(this.engine, slice),
+        renderDetails(this.trace, slice, this.breakdownByThreadState),
+        this.renderRhs(this.trace, slice),
       ),
     );
   }
 
-  isLoading() {
-    return !exists(this.sliceDetails);
-  }
-
-  private renderRhs(engine: Engine, slice: SliceDetails): m.Children {
+  private renderRhs(trace: Trace, slice: SliceDetails): m.Children {
     const precFlows = this.renderPrecedingFlows(slice);
     const followingFlows = this.renderFollowingFlows(slice);
     const args =
@@ -342,7 +255,7 @@
       m(
         Section,
         {title: 'Arguments'},
-        m(Tree, renderArguments(engine, slice.args)),
+        m(Tree, renderArguments(trace, slice.args)),
       );
     // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
     if (precFlows ?? followingFlows ?? args) {
@@ -353,7 +266,7 @@
   }
 
   private renderPrecedingFlows(slice: SliceDetails): m.Children {
-    const flows = globals.connectedFlows;
+    const flows = this.trace.flows.connectedFlows;
     const inFlows = flows.filter(({end}) => end.sliceId === slice.id);
 
     if (inFlows.length > 0) {
@@ -373,9 +286,6 @@
                   id: asSliceSqlId(flow.begin.sliceId),
                   name:
                     flow.begin.sliceChromeCustomName ?? flow.begin.sliceName,
-                  ts: flow.begin.sliceStartTs,
-                  dur: flow.begin.sliceEndTs - flow.begin.sliceStartTs,
-                  sqlTrackId: flow.begin.trackId,
                 }),
             },
             {
@@ -400,7 +310,7 @@
   }
 
   private renderFollowingFlows(slice: SliceDetails): m.Children {
-    const flows = globals.connectedFlows;
+    const flows = this.trace.flows.connectedFlows;
     const outFlows = flows.filter(({begin}) => begin.sliceId === slice.id);
 
     if (outFlows.length > 0) {
@@ -419,16 +329,13 @@
                 m(SliceRef, {
                   id: asSliceSqlId(flow.end.sliceId),
                   name: flow.end.sliceChromeCustomName ?? flow.end.sliceName,
-                  ts: flow.end.sliceStartTs,
-                  dur: flow.end.sliceEndTs - flow.end.sliceStartTs,
-                  sqlTrackId: flow.end.trackId,
                 }),
             },
             {
               title: 'Delay',
               render: (flow: Flow) =>
                 m(DurationWidget, {
-                  dur: flow.end.sliceEndTs - flow.end.sliceStartTs,
+                  dur: flow.end.sliceStartTs - flow.begin.sliceEndTs,
                 }),
             },
             {
@@ -466,7 +373,7 @@
         PopupMenu2,
         {trigger},
         contextMenuItems.map(({name, run}) =>
-          m(MenuItem, {label: name, onclick: () => run(sliceInfo)}),
+          m(MenuItem, {label: name, onclick: () => run(sliceInfo, this.trace)}),
         ),
       );
     } else {
diff --git a/ui/src/frontend/thread_slice_track.ts b/ui/src/frontend/thread_slice_track.ts
index 11a1b78..6e47c8a 100644
--- a/ui/src/frontend/thread_slice_track.ts
+++ b/ui/src/frontend/thread_slice_track.ts
@@ -14,13 +14,15 @@
 
 import {BigintMath as BIMath} from '../base/bigint_math';
 import {clamp} from '../base/math_utils';
-import {OnSliceClickArgs} from './base_slice_track';
-import {globals} from './globals';
 import {NAMED_ROW, NamedSliceTrack} from './named_slice_track';
 import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from './slice_layout';
 import {NewTrackArgs} from './track';
 import {LONG_NULL} from '../trace_processor/query_result';
-import {Slice} from 'src/public';
+import {Slice} from '../public/track';
+import {TrackEventDetails} from '../public/selection';
+import {ThreadSliceDetailsPanel} from './thread_slice_details_tab';
+import {TraceImpl} from '../core/trace_impl';
+import {assertIsInstance} from '../base/logging';
 
 export const THREAD_SLICE_ROW = {
   // Base columns (tsq, ts, dur, id, depth).
@@ -82,19 +84,21 @@
     }
   }
 
-  onSliceClick(args: OnSliceClickArgs<Slice>) {
-    globals.setLegacySelection(
-      {
-        kind: 'SLICE',
-        id: args.slice.id,
-        trackKey: this.trackKey,
-        table: this.tableName,
-      },
-      {
-        clearSearch: true,
-        pendingScrollId: undefined,
-        switchToCurrentSelectionTab: true,
-      },
-    );
+  async getSelectionDetails(
+    id: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const baseDetails = await super.getSelectionDetails(id);
+    if (!baseDetails) return undefined;
+    return {
+      ...baseDetails,
+      tableName: this.tableName,
+    };
+  }
+
+  override detailsPanel() {
+    // Rationale for the assertIsInstance: ThreadSliceDetailsPanel requires a
+    // TraceImpl (because of flows) but here we must take a Trace interface,
+    // because this class is exposed to plugins (which see only Trace).
+    return new ThreadSliceDetailsPanel(assertIsInstance(this.trace, TraceImpl));
   }
 }
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
deleted file mode 100644
index ed20d99..0000000
--- a/ui/src/frontend/thread_state_tab.ts
+++ /dev/null
@@ -1,407 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size 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 {Time, time} from '../base/time';
-import {raf} from '../core/raf_scheduler';
-import {Anchor} from '../widgets/anchor';
-import {Button} from '../widgets/button';
-import {DetailsShell} from '../widgets/details_shell';
-import {GridLayout} from '../widgets/grid_layout';
-import {Section} from '../widgets/section';
-import {SqlRef} from '../widgets/sql_ref';
-import {Tree, TreeNode} from '../widgets/tree';
-import {Intent} from '../widgets/common';
-
-import {BottomTab, NewBottomTabArgs} from './bottom_tab';
-import {
-  SchedSqlId,
-  ThreadStateSqlId,
-} from '../trace_processor/sql_utils/core_types';
-import {
-  getThreadState,
-  getThreadStateFromConstraints,
-  goToSchedSlice,
-  ThreadState,
-} from '../trace_processor/sql_utils/thread_state';
-import {DurationWidget, renderDuration} from './widgets/duration';
-import {Timestamp} from './widgets/timestamp';
-import {addDebugSliceTrack} from './debug_tracks/debug_tracks';
-import {globals} from './globals';
-import {getProcessName} from '../trace_processor/sql_utils/process';
-import {
-  ThreadInfo,
-  getFullThreadName,
-  getThreadName,
-} from '../trace_processor/sql_utils/thread';
-import {ThreadStateRef} from './widgets/thread_state';
-
-interface ThreadStateTabConfig {
-  // Id into |thread_state| sql table.
-  readonly id: ThreadStateSqlId;
-}
-
-interface RelatedThreadStates {
-  prev?: ThreadState;
-  next?: ThreadState;
-  waker?: ThreadState;
-  wakee?: ThreadState[];
-}
-
-export class ThreadStateTab extends BottomTab<ThreadStateTabConfig> {
-  static readonly kind = 'dev.perfetto.ThreadStateTab';
-
-  state?: ThreadState;
-  relatedStates?: RelatedThreadStates;
-  loaded: boolean = false;
-
-  static create(args: NewBottomTabArgs<ThreadStateTabConfig>): ThreadStateTab {
-    return new ThreadStateTab(args);
-  }
-
-  constructor(args: NewBottomTabArgs<ThreadStateTabConfig>) {
-    super(args);
-
-    this.load().then(() => {
-      this.loaded = true;
-      raf.scheduleFullRedraw();
-    });
-  }
-
-  async load() {
-    this.state = await getThreadState(this.engine, this.config.id);
-
-    if (!this.state) {
-      return;
-    }
-
-    const relatedStates: RelatedThreadStates = {};
-    relatedStates.prev = (
-      await getThreadStateFromConstraints(this.engine, {
-        filters: [
-          `ts + dur = ${this.state.ts}`,
-          `utid = ${this.state.thread?.utid}`,
-        ],
-        limit: 1,
-      })
-    )[0];
-    relatedStates.next = (
-      await getThreadStateFromConstraints(this.engine, {
-        filters: [
-          `ts = ${this.state.ts + this.state.dur}`,
-          `utid = ${this.state.thread?.utid}`,
-        ],
-        limit: 1,
-      })
-    )[0];
-    if (this.state.wakerThread?.utid !== undefined) {
-      relatedStates.waker = (
-        await getThreadStateFromConstraints(this.engine, {
-          filters: [
-            `utid = ${this.state.wakerThread?.utid}`,
-            `ts <= ${this.state.ts}`,
-            `ts + dur >= ${this.state.ts}`,
-          ],
-        })
-      )[0];
-    }
-    relatedStates.wakee = await getThreadStateFromConstraints(this.engine, {
-      filters: [
-        `waker_utid = ${this.state.thread?.utid}`,
-        `state = 'R'`,
-        `ts >= ${this.state.ts}`,
-        `ts <= ${this.state.ts + this.state.dur}`,
-      ],
-    });
-
-    this.relatedStates = relatedStates;
-  }
-
-  getTitle() {
-    // TODO(altimin): Support dynamic titles here.
-    return 'Current Selection';
-  }
-
-  viewTab() {
-    // TODO(altimin/stevegolton): Differentiate between "Current Selection" and
-    // "Pinned" views in DetailsShell.
-    return m(
-      DetailsShell,
-      {title: 'Thread State', description: this.renderLoadingText()},
-      m(
-        GridLayout,
-        m(
-          Section,
-          {title: 'Details'},
-          this.state && this.renderTree(this.state),
-        ),
-        m(
-          Section,
-          {title: 'Related thread states'},
-          this.renderRelatedThreadStates(),
-        ),
-      ),
-    );
-  }
-
-  private renderLoadingText() {
-    if (!this.loaded) {
-      return 'Loading';
-    }
-    if (!this.state) {
-      return `Thread state ${this.config.id} does not exist`;
-    }
-    // TODO(stevegolton): Return something intelligent here.
-    return this.config.id;
-  }
-
-  private renderTree(state: ThreadState) {
-    const thread = state.thread;
-    const process = state.thread?.process;
-    return m(
-      Tree,
-      m(TreeNode, {
-        left: 'Start time',
-        right: m(Timestamp, {ts: state.ts}),
-      }),
-      m(TreeNode, {
-        left: 'Duration',
-        right: m(DurationWidget, {dur: state.dur}),
-      }),
-      m(TreeNode, {
-        left: 'State',
-        right: this.renderState(
-          state.state,
-          state.cpu,
-          state.schedSqlId,
-          state.ts,
-        ),
-      }),
-      state.blockedFunction &&
-        m(TreeNode, {
-          left: 'Blocked function',
-          right: state.blockedFunction,
-        }),
-      process &&
-        m(TreeNode, {
-          left: 'Process',
-          right: getProcessName(process),
-        }),
-      thread && m(TreeNode, {left: 'Thread', right: getThreadName(thread)}),
-      state.wakerThread && this.renderWakerThread(state.wakerThread),
-      m(TreeNode, {
-        left: 'SQL ID',
-        right: m(SqlRef, {table: 'thread_state', id: state.threadStateSqlId}),
-      }),
-    );
-  }
-
-  private renderState(
-    state: string,
-    cpu: number | undefined,
-    id: SchedSqlId | undefined,
-    ts: time,
-  ): m.Children {
-    if (!state) {
-      return null;
-    }
-    if (id === undefined || cpu === undefined) {
-      return state;
-    }
-    return m(
-      Anchor,
-      {
-        title: 'Go to CPU slice',
-        icon: 'call_made',
-        onclick: () => goToSchedSlice(cpu, id, ts),
-      },
-      `${state} on CPU ${cpu}`,
-    );
-  }
-
-  private renderWakerThread(wakerThread: ThreadInfo) {
-    return m(
-      TreeNode,
-      {left: 'Waker'},
-      m(TreeNode, {
-        left: 'Process',
-        right: getProcessName(wakerThread.process),
-      }),
-      m(TreeNode, {left: 'Thread', right: getThreadName(wakerThread)}),
-    );
-  }
-
-  private renderRelatedThreadStates(): m.Children {
-    if (this.state === undefined || this.relatedStates === undefined) {
-      return 'Loading';
-    }
-    const startTs = this.state.ts;
-    const renderRef = (state: ThreadState, name?: string) =>
-      m(ThreadStateRef, {
-        id: state.threadStateSqlId,
-        ts: state.ts,
-        dur: state.dur,
-        utid: state.thread!.utid,
-        name,
-      });
-
-    const sliceColumns = {ts: 'ts', dur: 'dur', name: 'name'};
-    const sliceColumnNames = ['id', 'utid', 'ts', 'dur', 'name', 'table_name'];
-
-    const sliceLiteColumns = {ts: 'ts', dur: 'dur', name: 'thread_name'};
-    const sliceLiteColumnNames = [
-      'id',
-      'utid',
-      'ts',
-      'dur',
-      'thread_name',
-      'process_name',
-      'table_name',
-    ];
-
-    const nameForNextOrPrev = (state: ThreadState) =>
-      `${state.state} for ${renderDuration(state.dur)}`;
-    return [
-      m(
-        Tree,
-        this.relatedStates.waker &&
-          m(TreeNode, {
-            left: 'Waker',
-            right: renderRef(
-              this.relatedStates.waker,
-              getFullThreadName(this.relatedStates.waker.thread),
-            ),
-          }),
-        this.relatedStates.prev &&
-          m(TreeNode, {
-            left: 'Previous state',
-            right: renderRef(
-              this.relatedStates.prev,
-              nameForNextOrPrev(this.relatedStates.prev),
-            ),
-          }),
-        this.relatedStates.next &&
-          m(TreeNode, {
-            left: 'Next state',
-            right: renderRef(
-              this.relatedStates.next,
-              nameForNextOrPrev(this.relatedStates.next),
-            ),
-          }),
-        this.relatedStates.wakee &&
-          this.relatedStates.wakee.length > 0 &&
-          m(
-            TreeNode,
-            {
-              left: 'Woken threads',
-            },
-            this.relatedStates.wakee.map((state) =>
-              m(TreeNode, {
-                left: m(Timestamp, {
-                  ts: state.ts,
-                  display: [
-                    'Start+',
-                    m(DurationWidget, {dur: Time.sub(state.ts, startTs)}),
-                  ],
-                }),
-                right: renderRef(state, getFullThreadName(state.thread)),
-              }),
-            ),
-          ),
-      ),
-      m(Button, {
-        label: 'Critical path lite',
-        intent: Intent.Primary,
-        onclick: () =>
-          this.engine
-            .query(`INCLUDE PERFETTO MODULE sched.thread_executing_span;`)
-            .then(() =>
-              addDebugSliceTrack(
-                // NOTE(stevegolton): This is a temporary patch, this menu
-                // should become part of a critical path plugin, at which point
-                // we can just use the plugin's context object.
-                {
-                  engine: this.engine,
-                  registerTrack: (x) => globals.trackManager.registerTrack(x),
-                },
-                {
-                  sqlSource: `
-                    SELECT
-                      cr.id,
-                      cr.utid,
-                      cr.ts,
-                      cr.dur,
-                      thread.name AS thread_name,
-                      process.name AS process_name,
-                      'thread_state' AS table_name
-                    FROM
-                      _thread_executing_span_critical_path(
-                        ${this.state?.thread?.utid},
-                        trace_bounds.start_ts,
-                        trace_bounds.end_ts - trace_bounds.start_ts) cr,
-                      trace_bounds
-                    JOIN thread USING(utid)
-                    JOIN process USING(upid)
-                  `,
-                  columns: sliceLiteColumnNames,
-                },
-                `${this.state?.thread?.name}`,
-                sliceLiteColumns,
-                sliceLiteColumnNames,
-              ),
-            ),
-      }),
-      m(Button, {
-        label: 'Critical path',
-        intent: Intent.Primary,
-        onclick: () =>
-          this.engine
-            .query(
-              `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
-            )
-            .then(() =>
-              addDebugSliceTrack(
-                // NOTE(stevegolton): This is a temporary patch, this menu
-                // should become part of a critical path plugin, at which point
-                // we can just use the plugin's context object.
-                {
-                  engine: this.engine,
-                  registerTrack: (x) => globals.trackManager.registerTrack(x),
-                },
-                {
-                  sqlSource: `
-                    SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
-                      FROM
-                        _thread_executing_span_critical_path_stack(
-                          ${this.state?.thread?.utid},
-                          trace_bounds.start_ts,
-                          trace_bounds.end_ts - trace_bounds.start_ts) cr,
-                        trace_bounds WHERE name IS NOT NULL
-                  `,
-                  columns: sliceColumnNames,
-                },
-                `${this.state?.thread?.name}`,
-                sliceColumns,
-                sliceColumnNames,
-              ),
-            ),
-      }),
-    ];
-  }
-
-  isLoading() {
-    return this.state === undefined || this.relatedStates === undefined;
-  }
-}
diff --git a/ui/src/frontend/tickmark_panel.ts b/ui/src/frontend/tickmark_panel.ts
index dd406bc..8ce8336 100644
--- a/ui/src/frontend/tickmark_panel.ts
+++ b/ui/src/frontend/tickmark_panel.ts
@@ -13,25 +13,41 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {TRACK_SHELL_WIDTH} from './css_constants';
-import {globals} from './globals';
 import {getMaxMajorTicks, generateTicks, TickType} from './gridline_helper';
-import {Size} from '../base/geom';
+import {Size2D} from '../base/geom';
 import {Panel} from './panel_container';
-import {PxSpan, TimeScale} from './time_scale';
-import {canvasClip} from '../common/canvas_utils';
+import {TimeScale} from '../base/time_scale';
+import {canvasClip} from '../base/canvas_utils';
+import {SearchOverviewTrack} from './search_overview_track';
+import {TraceImpl} from '../core/trace_impl';
+import {getOrCreate} from '../base/utils';
+
+// We want to create the overview track only once per trace, but this
+// class can be delete and re-instantiated when switching between pages via
+// the sidebar. So we cache the overview track and bind it to the lifetime of
+// the TraceImpl object.
+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;
+  private searchOverviewTrack: SearchOverviewTrack;
+
+  constructor(private readonly trace: TraceImpl) {
+    this.searchOverviewTrack = getOrCreate(
+      trackTraceMap,
+      trace,
+      () => new SearchOverviewTrack(trace),
+    );
+  }
 
   render(): m.Children {
     return m('.tickbar');
   }
 
-  renderCanvas(ctx: CanvasRenderingContext2D, size: Size): void {
+  renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D): void {
     ctx.fillStyle = '#999';
     ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
 
@@ -43,15 +59,18 @@
     ctx.restore();
   }
 
-  private renderTrack(ctx: CanvasRenderingContext2D, size: Size): void {
-    const visibleWindow = globals.timeline.visibleWindow;
-    const timescale = new TimeScale(visibleWindow, new PxSpan(0, size.width));
+  private renderTrack(ctx: CanvasRenderingContext2D, size: Size2D): void {
+    const visibleWindow = this.trace.timeline.visibleWindow;
+    const timescale = new TimeScale(visibleWindow, {
+      left: 0,
+      right: size.width,
+    });
     const timespan = visibleWindow.toTimeSpan();
 
     if (size.width > 0 && timespan.duration > 0n) {
       const maxMajorTicks = getMaxMajorTicks(size.width);
 
-      const offset = globals.timestampOffset();
+      const offset = this.trace.timeline.timestampOffset();
       const tickGen = generateTicks(timespan, maxMajorTicks, offset);
       for (const {type, time} of tickGen) {
         const px = Math.floor(timescale.timeToPx(time));
@@ -61,9 +80,6 @@
       }
     }
 
-    const searchOverviewRenderer = globals.searchOverviewTrack;
-    if (searchOverviewRenderer) {
-      searchOverviewRenderer.render(ctx, size);
-    }
+    this.searchOverviewTrack.render(ctx, size);
   }
 }
diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts
index c980970..143e402 100644
--- a/ui/src/frontend/time_axis_panel.ts
+++ b/ui/src/frontend/time_axis_panel.ts
@@ -13,32 +13,33 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {Time, time, toISODateOnly} from '../base/time';
 import {TimestampFormat, timestampFormat} from '../core/timestamp_format';
-
 import {TRACK_SHELL_WIDTH} from './css_constants';
-import {globals} from './globals';
 import {
   getMaxMajorTicks,
   MIN_PX_PER_STEP,
   generateTicks,
   TickType,
 } from './gridline_helper';
-import {Size} from '../base/geom';
+import {Size2D} from '../base/geom';
 import {Panel} from './panel_container';
-import {PxSpan, TimeScale} from './time_scale';
-import {canvasClip} from '../common/canvas_utils';
+import {TimeScale} from '../base/time_scale';
+import {canvasClip} from '../base/canvas_utils';
+import {Trace} from '../public/trace';
 
 export class TimeAxisPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = false;
+  readonly id = 'time-axis-panel';
+
+  constructor(private readonly trace: Trace) {}
 
   render(): m.Children {
     return m('.time-axis-panel');
   }
 
-  renderCanvas(ctx: CanvasRenderingContext2D, size: Size) {
+  renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
     ctx.fillStyle = '#999';
     ctx.textAlign = 'left';
     ctx.font = '11px Roboto Condensed';
@@ -56,10 +57,10 @@
   }
 
   private renderOffsetTimestamp(ctx: CanvasRenderingContext2D): void {
-    const offset = globals.timestampOffset();
+    const offset = this.trace.timeline.timestampOffset();
     switch (timestampFormat()) {
-      case TimestampFormat.Raw:
-      case TimestampFormat.RawLocale:
+      case TimestampFormat.TraceNs:
+      case TimestampFormat.TraceNsLocale:
         break;
       case TimestampFormat.Seconds:
       case TimestampFormat.Timecode:
@@ -68,16 +69,16 @@
         break;
       case TimestampFormat.UTC:
         const offsetDate = Time.toDate(
-          globals.traceContext.utcOffset,
-          globals.traceContext.realtimeOffset,
+          this.trace.traceInfo.utcOffset,
+          this.trace.traceInfo.realtimeOffset,
         );
         const dateStr = toISODateOnly(offsetDate);
         ctx.fillText(`UTC ${dateStr}`, 6, 10);
         break;
       case TimestampFormat.TraceTz:
         const offsetTzDate = Time.toDate(
-          globals.traceContext.traceTzOffset,
-          globals.traceContext.realtimeOffset,
+          this.trace.traceInfo.traceTzOffset,
+          this.trace.traceInfo.realtimeOffset,
         );
         const dateTzStr = toISODateOnly(offsetTzDate);
         ctx.fillText(dateTzStr, 6, 10);
@@ -85,11 +86,14 @@
     }
   }
 
-  private renderPanel(ctx: CanvasRenderingContext2D, size: Size): void {
-    const visibleWindow = globals.timeline.visibleWindow;
-    const timescale = new TimeScale(visibleWindow, new PxSpan(0, size.width));
+  private renderPanel(ctx: CanvasRenderingContext2D, size: Size2D): void {
+    const visibleWindow = this.trace.timeline.visibleWindow;
+    const timescale = new TimeScale(visibleWindow, {
+      left: 0,
+      right: size.width,
+    });
     const timespan = visibleWindow.toTimeSpan();
-    const offset = globals.timestampOffset();
+    const offset = this.trace.timeline.timestampOffset();
 
     // Draw time axis.
     if (size.width > 0 && timespan.duration > 0n) {
@@ -99,7 +103,7 @@
         if (type === TickType.MAJOR) {
           const position = Math.floor(timescale.timeToPx(time));
           ctx.fillRect(position, 0, 1, size.height);
-          const domainTime = globals.toDomainTime(time);
+          const domainTime = this.trace.timeline.toDomainTime(time);
           renderTimestamp(ctx, domainTime, position + 5, 10, MIN_PX_PER_STEP);
         }
       }
@@ -120,12 +124,28 @@
     case TimestampFormat.TraceTz:
     case TimestampFormat.Timecode:
       return renderTimecode(ctx, time, x, y, minWidth);
-    case TimestampFormat.Raw:
+    case TimestampFormat.TraceNs:
       return renderRawTimestamp(ctx, time.toString(), x, y, minWidth);
-    case TimestampFormat.RawLocale:
+    case TimestampFormat.TraceNsLocale:
       return renderRawTimestamp(ctx, time.toLocaleString(), x, y, minWidth);
     case TimestampFormat.Seconds:
       return renderRawTimestamp(ctx, Time.formatSeconds(time), x, y, minWidth);
+    case TimestampFormat.Milliseoncds:
+      return renderRawTimestamp(
+        ctx,
+        Time.formatMilliseconds(time),
+        x,
+        y,
+        minWidth,
+      );
+    case TimestampFormat.Microseconds:
+      return renderRawTimestamp(
+        ctx,
+        Time.formatMicroseconds(time),
+        x,
+        y,
+        minWidth,
+      );
     default:
       const z: never = fmt;
       throw new Error(`Invalid timestamp ${z}`);
diff --git a/ui/src/frontend/time_scale.ts b/ui/src/frontend/time_scale.ts
deleted file mode 100644
index 0fe19ee..0000000
--- a/ui/src/frontend/time_scale.ts
+++ /dev/null
@@ -1,76 +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 {duration, time} from '../base/time';
-import {HighPrecisionTime} from '../common/high_precision_time';
-import {HighPrecisionTimeSpan} from '../common/high_precision_time_span';
-
-export class TimeScale {
-  readonly timeSpan: HighPrecisionTimeSpan;
-  readonly pxSpan: PxSpan;
-  private readonly timePerPx: number;
-
-  constructor(timespan: HighPrecisionTimeSpan, pxSpan: PxSpan) {
-    this.pxSpan = pxSpan;
-    this.timeSpan = timespan;
-    if (timespan.duration <= 0 || pxSpan.delta <= 0) {
-      this.timePerPx = 1;
-    } else {
-      this.timePerPx = timespan.duration / pxSpan.delta;
-    }
-  }
-
-  timeToPx(ts: time): number {
-    const timeOffset =
-      Number(ts - this.timeSpan.start.integral) -
-      this.timeSpan.start.fractional;
-    return this.pxSpan.start + timeOffset / this.timePerPx;
-  }
-
-  hpTimeToPx(time: HighPrecisionTime): number {
-    const timeOffset = time.sub(this.timeSpan.start).toNumber();
-    return this.pxSpan.start + timeOffset / this.timePerPx;
-  }
-
-  // Convert pixels to a high precision time object, which can be further
-  // converted to other time formats.
-  pxToHpTime(px: number): HighPrecisionTime {
-    const timeOffset = (px - this.pxSpan.start) * this.timePerPx;
-    return this.timeSpan.start.addNumber(timeOffset);
-  }
-
-  durationToPx(dur: duration): number {
-    return Number(dur) / this.timePerPx;
-  }
-
-  pxToDuration(pxDelta: number): number {
-    return pxDelta * this.timePerPx;
-  }
-}
-
-export class PxSpan {
-  static readonly ZERO = new PxSpan(0, 0);
-
-  readonly start: number;
-  readonly end: number;
-
-  constructor(start: number, end: number) {
-    this.start = start;
-    this.end = end;
-  }
-
-  get delta(): number {
-    return this.end - this.start;
-  }
-}
diff --git a/ui/src/frontend/time_scale_unittest.ts b/ui/src/frontend/time_scale_unittest.ts
deleted file mode 100644
index 16b4548..0000000
--- a/ui/src/frontend/time_scale_unittest.ts
+++ /dev/null
@@ -1,75 +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 {Time} from '../base/time';
-import {HighPrecisionTime} from '../common/high_precision_time';
-import {HighPrecisionTimeSpan} from '../common/high_precision_time_span';
-import {PxSpan, TimeScale} from './time_scale';
-
-const t = Time.fromRaw;
-
-describe('TimeScale', () => {
-  const ts = new TimeScale(
-    new HighPrecisionTimeSpan(new HighPrecisionTime(t(40n)), 100),
-    new PxSpan(200, 1000),
-  );
-
-  it('converts timescales to pixels', () => {
-    expect(ts.timeToPx(Time.fromRaw(40n))).toEqual(200);
-    expect(ts.timeToPx(Time.fromRaw(140n))).toEqual(1000);
-    expect(ts.timeToPx(Time.fromRaw(90n))).toEqual(600);
-
-    expect(ts.timeToPx(Time.fromRaw(240n))).toEqual(1800);
-    expect(ts.timeToPx(Time.fromRaw(-60n))).toEqual(-600);
-  });
-
-  it('converts pixels to HPTime objects', () => {
-    let result = ts.pxToHpTime(200);
-    expect(result.integral).toEqual(40n);
-    expect(result.fractional).toBeCloseTo(0);
-
-    result = ts.pxToHpTime(1000);
-    expect(result.integral).toEqual(140n);
-    expect(result.fractional).toBeCloseTo(0);
-
-    result = ts.pxToHpTime(600);
-    expect(result.integral).toEqual(90n);
-    expect(result.fractional).toBeCloseTo(0);
-
-    result = ts.pxToHpTime(1800);
-    expect(result.integral).toEqual(240n);
-    expect(result.fractional).toBeCloseTo(0);
-
-    result = ts.pxToHpTime(-600);
-    expect(result.integral).toEqual(-60n);
-    expect(result.fractional).toBeCloseTo(0);
-  });
-
-  it('converts durations to pixels', () => {
-    expect(ts.durationToPx(0n)).toEqual(0);
-    expect(ts.durationToPx(1n)).toEqual(8);
-    expect(ts.durationToPx(1000n)).toEqual(8000);
-  });
-
-  it('converts pxDeltaToDurations to HPTime durations', () => {
-    let result = ts.pxToDuration(0);
-    expect(result).toBeCloseTo(0);
-
-    result = ts.pxToDuration(1);
-    expect(result).toBeCloseTo(0.125);
-
-    result = ts.pxToDuration(100);
-    expect(result).toBeCloseTo(12.5);
-  });
-});
diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts
index afeb1d9..5621ff4 100644
--- a/ui/src/frontend/time_selection_panel.ts
+++ b/ui/src/frontend/time_selection_panel.ts
@@ -13,22 +13,20 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {time, Time} from '../base/time';
 import {timestampFormat, TimestampFormat} from '../core/timestamp_format';
-
 import {
   BACKGROUND_COLOR,
   FOREGROUND_COLOR,
   TRACK_SHELL_WIDTH,
 } from './css_constants';
-import {globals} from './globals';
 import {getMaxMajorTicks, generateTicks, TickType} from './gridline_helper';
-import {Size} from '../base/geom';
+import {Size2D} from '../base/geom';
 import {Panel} from './panel_container';
 import {renderDuration} from './widgets/duration';
-import {canvasClip} from '../common/canvas_utils';
-import {PxSpan, TimeScale} from './time_scale';
+import {canvasClip} from '../base/canvas_utils';
+import {TimeScale} from '../base/time_scale';
+import {TraceImpl} from '../core/trace_impl';
 
 export interface BBox {
   x: number;
@@ -137,11 +135,13 @@
   readonly kind = 'panel';
   readonly selectable = false;
 
+  constructor(private readonly trace: TraceImpl) {}
+
   render(): m.Children {
     return m('.time-selection-panel');
   }
 
-  renderCanvas(ctx: CanvasRenderingContext2D, size: Size) {
+  renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
     ctx.fillStyle = '#999';
     ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
 
@@ -154,14 +154,17 @@
     ctx.restore();
   }
 
-  private renderPanel(ctx: CanvasRenderingContext2D, size: Size): void {
-    const visibleWindow = globals.timeline.visibleWindow;
-    const timescale = new TimeScale(visibleWindow, new PxSpan(0, size.width));
+  private renderPanel(ctx: CanvasRenderingContext2D, size: Size2D): void {
+    const visibleWindow = this.trace.timeline.visibleWindow;
+    const timescale = new TimeScale(visibleWindow, {
+      left: 0,
+      right: size.width,
+    });
     const timespan = visibleWindow.toTimeSpan();
 
     if (size.width > 0 && timespan.duration > 0n) {
       const maxMajorTicks = getMaxMajorTicks(size.width);
-      const offset = globals.timestampOffset();
+      const offset = this.trace.timeline.timestampOffset();
       const tickGen = generateTicks(timespan, maxMajorTicks, offset);
       for (const {type, time} of tickGen) {
         const px = Math.floor(timescale.timeToPx(time));
@@ -171,31 +174,39 @@
       }
     }
 
-    const localArea = globals.timeline.selectedArea;
-    const selection = globals.state.selection;
+    const localArea = this.trace.timeline.selectedArea;
+    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);
       this.renderSpan(ctx, timescale, size, start, end);
-    } else if (selection.kind === 'area') {
-      const start = Time.min(selection.start, selection.end);
-      const end = Time.max(selection.start, selection.end);
-      this.renderSpan(ctx, timescale, size, start, end);
+    } else {
+      if (selection.kind === 'area') {
+        const start = Time.min(selection.start, selection.end);
+        const end = Time.max(selection.start, selection.end);
+        this.renderSpan(ctx, timescale, size, start, end);
+      } else if (selection.kind === 'track_event') {
+        const start = selection.ts;
+        const end = Time.add(selection.ts, selection.dur);
+        if (end > start) {
+          this.renderSpan(ctx, timescale, size, start, end);
+        }
+      }
     }
 
-    if (globals.state.hoverCursorTimestamp !== -1n) {
+    if (this.trace.timeline.hoverCursorTimestamp !== undefined) {
       this.renderHover(
         ctx,
         timescale,
         size,
-        globals.state.hoverCursorTimestamp,
+        this.trace.timeline.hoverCursorTimestamp,
       );
     }
 
-    for (const note of Object.values(globals.state.notes)) {
+    for (const note of this.trace.notes.notes.values()) {
       const noteIsSelected =
         selection.kind === 'note' && selection.id === note.id;
-      if (note.noteType === 'SPAN' && !noteIsSelected) {
+      if (note.noteType === 'SPAN' && noteIsSelected) {
         this.renderSpan(ctx, timescale, size, note.start, note.end);
       }
     }
@@ -206,11 +217,11 @@
   renderHover(
     ctx: CanvasRenderingContext2D,
     timescale: TimeScale,
-    size: Size,
+    size: Size2D,
     ts: time,
   ) {
     const xPos = Math.floor(timescale.timeToPx(ts));
-    const domainTime = globals.toDomainTime(ts);
+    const domainTime = this.trace.timeline.toDomainTime(ts);
     const label = stringifyTimestamp(domainTime);
     drawIBar(ctx, xPos, this.getBBoxFromSize(size), label);
   }
@@ -218,7 +229,7 @@
   renderSpan(
     ctx: CanvasRenderingContext2D,
     timescale: TimeScale,
-    trackSize: Size,
+    trackSize: Size2D,
     start: time,
     end: time,
   ) {
@@ -238,7 +249,7 @@
     );
   }
 
-  private getBBoxFromSize(size: Size): BBox {
+  private getBBoxFromSize(size: Size2D): BBox {
     return {
       x: 0,
       y: 0,
@@ -256,12 +267,16 @@
     case TimestampFormat.Timecode:
       const THIN_SPACE = '\u2009';
       return Time.toTimecode(time).toString(THIN_SPACE);
-    case TimestampFormat.Raw:
+    case TimestampFormat.TraceNs:
       return time.toString();
-    case TimestampFormat.RawLocale:
+    case TimestampFormat.TraceNsLocale:
       return time.toLocaleString();
     case TimestampFormat.Seconds:
       return Time.formatSeconds(time);
+    case TimestampFormat.Milliseoncds:
+      return Time.formatMilliseconds(time);
+    case TimestampFormat.Microseconds:
+      return Time.formatMicroseconds(time);
     default:
       const z: never = fmt;
       throw new Error(`Invalid timestamp ${z}`);
diff --git a/ui/src/frontend/timeline.ts b/ui/src/frontend/timeline.ts
deleted file mode 100644
index bdd244e..0000000
--- a/ui/src/frontend/timeline.ts
+++ /dev/null
@@ -1,123 +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 {assertTrue} from '../base/logging';
-import {time, TimeSpan} from '../base/time';
-import {HighPrecisionTimeSpan} from '../common/high_precision_time_span';
-import {Area, State} from '../common/state';
-import {raf} from '../core/raf_scheduler';
-import {Store} from '../public';
-import {ratelimit} from './rate_limiters';
-
-interface Range {
-  start?: number;
-  end?: number;
-}
-
-const MIN_DURATION = 10;
-
-/**
- * State that is shared between several frontend components, but not the
- * controller. This state is updated at 60fps.
- */
-export class Timeline {
-  private _visibleWindow: HighPrecisionTimeSpan;
-  private readonly traceSpan: TimeSpan;
-  private readonly store: Store<State>;
-
-  // This is used to calculate the tracks within a Y range for area selection.
-  areaY: Range = {};
-  private _selectedArea?: Area;
-
-  constructor(store: Store<State>, traceSpan: TimeSpan) {
-    this.store = store;
-    this.traceSpan = traceSpan;
-    this._visibleWindow = HighPrecisionTimeSpan.fromTime(
-      traceSpan.start,
-      traceSpan.end,
-    );
-  }
-
-  // This is a giant hack. Basically, removing visible window from the state
-  // means that we no longer update the state periodically while navigating
-  // the timeline, which means that controllers are not running. This keeps
-  // making null edits to the store which triggers the controller to run.
-  //
-  // TODO(stevegolton): When we remove controllers, we can remove this!
-  private readonly rateLimitedPoker = ratelimit(
-    () => this.store.edit(() => {}),
-    50,
-  );
-
-  // TODO: there is some redundancy in the fact that both |visibleWindowTime|
-  // and a |timeScale| have a notion of time range. That should live in one
-  // place only.
-
-  zoomVisibleWindow(ratio: number, centerPoint: number) {
-    this._visibleWindow = this._visibleWindow
-      .scale(ratio, centerPoint, MIN_DURATION)
-      .fitWithin(this.traceSpan.start, this.traceSpan.end);
-
-    this.rateLimitedPoker();
-  }
-
-  panVisibleWindow(delta: number) {
-    this._visibleWindow = this._visibleWindow
-      .translate(delta)
-      .fitWithin(this.traceSpan.start, this.traceSpan.end);
-    this.rateLimitedPoker();
-  }
-
-  // Set the highlight box to draw
-  selectArea(
-    start: time,
-    end: time,
-    tracks = this._selectedArea ? this._selectedArea.tracks : [],
-  ) {
-    assertTrue(
-      end >= start,
-      `Impossible select area: start [${start}] >= end [${end}]`,
-    );
-    this._selectedArea = {start, end, tracks};
-    raf.scheduleFullRedraw();
-  }
-
-  deselectArea() {
-    this._selectedArea = undefined;
-    raf.scheduleRedraw();
-  }
-
-  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));
-  }
-
-  // Set visible window using a high precision time span
-  updateVisibleTimeHP(ts: HighPrecisionTimeSpan) {
-    this._visibleWindow = ts
-      .clampDuration(MIN_DURATION)
-      .fitWithin(this.traceSpan.start, this.traceSpan.end);
-
-    this.rateLimitedPoker();
-  }
-
-  // Get the bounds of the visible window as a high-precision time span
-  get visibleWindow(): HighPrecisionTimeSpan {
-    return this._visibleWindow;
-  }
-}
diff --git a/ui/src/frontend/topbar.ts b/ui/src/frontend/topbar.ts
index 485ff76..da440d3 100644
--- a/ui/src/frontend/topbar.ts
+++ b/ui/src/frontend/topbar.ts
@@ -13,79 +13,40 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {classNames} from '../base/classnames';
-import {raf} from '../core/raf_scheduler';
-
-import {globals} from './globals';
 import {taskTracker} from './task_tracker';
 import {Popup, PopupPosition} from '../widgets/popup';
 import {assertFalse} from '../base/logging';
+import {OmniboxMode} from '../core/omnibox_manager';
+import {AppImpl} from '../core/app_impl';
+import {TraceImpl, TraceImplAttrs} from '../core/trace_impl';
 
-export const DISMISSED_PANNING_HINT_KEY = 'dismissedPanningHint';
-
-class Progress implements m.ClassComponent {
-  view(_vnode: m.Vnode): m.Children {
-    const classes = classNames(this.isLoading() && 'progress-anim');
+class Progress implements m.ClassComponent<TraceImplAttrs> {
+  view({attrs}: m.CVnode<TraceImplAttrs>): m.Children {
+    const engine = attrs.trace.engine;
+    const isLoading =
+      AppImpl.instance.isLoadingTrace ||
+      engine.numRequestsPending > 0 ||
+      taskTracker.hasPendingTasks();
+    const classes = classNames(isLoading && 'progress-anim');
     return m('.progress', {class: classes});
   }
-
-  private isLoading(): boolean {
-    const engine = globals.getCurrentEngine();
-    return (
-      (engine && !engine.ready) ||
-      globals.numQueuedQueries > 0 ||
-      taskTracker.hasPendingTasks()
-    );
-  }
 }
 
-class HelpPanningNotification implements m.ClassComponent {
-  view() {
-    const dismissed = localStorage.getItem(DISMISSED_PANNING_HINT_KEY);
-    // 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 (
-      globals.embeddedMode ||
-      dismissed === 'true' ||
-      !globals.showPanningHint
-    ) {
+class TraceErrorIcon implements m.ClassComponent<TraceImplAttrs> {
+  private tracePopupErrorDismissed = false;
+
+  view({attrs}: m.CVnode<TraceImplAttrs>) {
+    const trace = attrs.trace;
+    if (AppImpl.instance.embeddedMode) return;
+
+    const mode = AppImpl.instance.omnibox.mode;
+    const totErrors = trace.traceInfo.importErrors + trace.loadingErrors.length;
+    if (totErrors === 0 || mode === OmniboxMode.Command) {
       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: () => {
-            globals.showPanningHint = false;
-            localStorage.setItem(DISMISSED_PANNING_HINT_KEY, 'true');
-            raf.scheduleFullRedraw();
-          },
-        },
-        'Dismiss',
-      ),
-    );
-  }
-}
-
-class TraceErrorIcon implements m.ClassComponent {
-  view() {
-    if (globals.embeddedMode) return;
-
-    const mode = globals.state.omniboxState.mode;
-    const errors = globals.traceErrors;
-    if ((!Boolean(errors) && !globals.metricError) || mode === 'COMMAND') {
-      return;
-    }
-    const message = Boolean(errors)
-      ? `${errors} import or data loss errors detected.`
+    const message = Boolean(totErrors)
+      ? `${totErrors} import or data loss errors detected.`
       : `Metric error detected.`;
     return m(
       '.error-box',
@@ -93,11 +54,11 @@
         Popup,
         {
           trigger: m('.popup-trigger'),
-          isOpen: globals.showTraceErrorPopup,
+          isOpen: !this.tracePopupErrorDismissed,
           position: PopupPosition.Left,
           onChange: (shouldOpen: boolean) => {
             assertFalse(shouldOpen);
-            globals.showTraceErrorPopup = false;
+            this.tracePopupErrorDismissed = true;
           },
         },
         m('.error-popup', 'Data-loss/import error. Click for more info.'),
@@ -119,6 +80,7 @@
 
 export interface TopbarAttrs {
   omnibox: m.Children;
+  trace?: TraceImpl;
 }
 
 export class Topbar implements m.ClassComponent<TopbarAttrs> {
@@ -126,11 +88,12 @@
     const {omnibox} = attrs;
     return m(
       '.topbar',
-      {class: globals.state.sidebarVisible ? '' : 'hide-sidebar'},
+      {
+        class: AppImpl.instance.sidebar.visible ? '' : 'hide-sidebar',
+      },
       omnibox,
-      m(Progress),
-      m(HelpPanningNotification),
-      m(TraceErrorIcon),
+      attrs.trace && m(Progress, {trace: attrs.trace}),
+      attrs.trace && m(TraceErrorIcon, {trace: attrs.trace}),
     );
   }
 }
diff --git a/ui/src/frontend/trace_attrs.ts b/ui/src/frontend/trace_attrs.ts
deleted file mode 100644
index f0f853d..0000000
--- a/ui/src/frontend/trace_attrs.ts
+++ /dev/null
@@ -1,94 +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.
-
-import m from 'mithril';
-
-import {assertExists} from '../base/logging';
-import {TraceArrayBufferSource} from '../common/state';
-import {createPermalink} from './permalink';
-import {showModal} from '../widgets/modal';
-
-import {onClickCopy} from './clipboard';
-import {globals} from './globals';
-import {isTraceLoaded} from './sidebar';
-
-export function isShareable() {
-  return globals.isInternalUser && isDownloadable();
-}
-
-export function isDownloadable() {
-  const engine = globals.getCurrentEngine();
-  if (!engine) {
-    return false;
-  }
-  if (engine.source.type === 'ARRAY_BUFFER' && engine.source.localOnly) {
-    return false;
-  }
-  if (engine.source.type === 'HTTP_RPC') {
-    return false;
-  }
-  return true;
-}
-
-export function shareTrace() {
-  const engine = assertExists(globals.getCurrentEngine());
-  const traceUrl = (engine.source as TraceArrayBufferSource).url ?? '';
-
-  // If the trace is not shareable (has been pushed via postMessage()) but has
-  // a url, create a pseudo-permalink by echoing back the URL.
-  if (!isShareable()) {
-    const msg = [
-      m(
-        'p',
-        'This trace was opened by an external site and as such cannot ' +
-          'be re-shared preserving the UI state.',
-      ),
-    ];
-    if (traceUrl) {
-      msg.push(m('p', 'By using the URL below you can open this trace again.'));
-      msg.push(m('p', 'Clicking will copy the URL into the clipboard.'));
-      msg.push(createTraceLink(traceUrl, traceUrl));
-    }
-
-    showModal({
-      title: 'Cannot create permalink from external trace',
-      content: m('div', msg),
-    });
-    return;
-  }
-
-  if (!isShareable() || !isTraceLoaded()) return;
-
-  const result = confirm(
-    `Upload UI state and generate a permalink. ` +
-      `The trace will be accessible by anybody with the permalink.`,
-  );
-  if (result) {
-    globals.logging.logEvent('Trace Actions', 'Create permalink');
-    createPermalink({mode: 'APP_STATE'});
-  }
-}
-
-export function createTraceLink(title: string, url: string) {
-  if (url === '') {
-    return m('a.trace-file-name', title);
-  }
-  const linkProps = {
-    href: url,
-    title: 'Click to copy the URL',
-    target: '_blank',
-    onclick: onClickCopy(url),
-  };
-  return m('a.trace-file-name', linkProps, title);
-}
diff --git a/ui/src/frontend/trace_context.ts b/ui/src/frontend/trace_context.ts
deleted file mode 100644
index fbd84d6..0000000
--- a/ui/src/frontend/trace_context.ts
+++ /dev/null
@@ -1,41 +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 {time} from '../base/time';
-
-export interface TraceContext {
-  traceTitle: string; // File name and size of the current trace.
-  traceUrl: string; // URL of the Trace.
-  readonly start: time;
-  readonly end: time;
-
-  // This is the ts value at the time of the Unix epoch.
-  // Normally some large negative value, because the unix epoch is normally in
-  // the past compared to ts=0.
-  readonly realtimeOffset: time;
-
-  // This is the timestamp that we should use for our offset when in UTC mode.
-  // Usually the most recent UTC midnight compared to the trace start time.
-  readonly utcOffset: time;
-
-  // Trace TZ is like UTC but keeps into account also the timezone_off_mins
-  // recorded into the trace, to show timestamps in the device local time.
-  readonly traceTzOffset: time;
-
-  // The list of CPUs in the trace
-  readonly cpus: number[];
-
-  // The number of gpus in the trace
-  readonly gpuCount: number;
-}
diff --git a/ui/src/frontend/trace_converter.ts b/ui/src/frontend/trace_converter.ts
index 6c4a688..7960cc5 100644
--- a/ui/src/frontend/trace_converter.ts
+++ b/ui/src/frontend/trace_converter.ts
@@ -12,23 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {assetSrc} from '../base/assets';
 import {download} from '../base/clipboard';
+import {defer} from '../base/deferred';
 import {ErrorDetails} from '../base/logging';
 import {utf8Decode} from '../base/string_utils';
 import {time} from '../base/time';
-import {Actions} from '../common/actions';
-import {
-  ConversionJobName,
-  ConversionJobStatus,
-} from '../common/conversion_jobs';
-
+import {AppImpl} from '../core/app_impl';
 import {maybeShowErrorDialog} from './error_dialog';
-import {globals} from './globals';
-import {openBufferWithLegacyTraceViewer} from './legacy_trace_viewer';
 
 type Args =
   | UpdateStatusArgs
-  | UpdateJobStatusArgs
+  | JobCompletedArgs
   | DownloadFileArgs
   | OpenTraceInLegacyArgs
   | ErrorArgs;
@@ -38,10 +33,8 @@
   status: string;
 }
 
-interface UpdateJobStatusArgs {
-  kind: 'updateJobStatus';
-  name: ConversionJobName;
-  status: ConversionJobStatus;
+interface JobCompletedArgs {
+  kind: 'jobCompleted';
 }
 
 interface DownloadFileArgs {
@@ -60,65 +53,79 @@
   error: ErrorDetails;
 }
 
-function handleOnMessage(msg: MessageEvent): void {
-  const args: Args = msg.data;
-  if (args.kind === 'updateStatus') {
-    globals.dispatch(
-      Actions.updateStatus({
-        msg: args.status,
-        timestamp: Date.now() / 1000,
-      }),
-    );
-  } else if (args.kind === 'updateJobStatus') {
-    globals.setConversionJobStatus(args.name, args.status);
-  } else if (args.kind === 'downloadFile') {
-    download(new File([new Blob([args.buffer])], args.name));
-  } else if (args.kind === 'openTraceInLegacy') {
-    const str = utf8Decode(args.buffer);
-    openBufferWithLegacyTraceViewer('trace.json', str, 0);
-  } else if (args.kind === 'error') {
-    maybeShowErrorDialog(args.error);
-  } else {
-    throw new Error(`Unhandled message ${JSON.stringify(args)}`);
-  }
-}
+type OpenTraceInLegacyCallback = (
+  name: string,
+  data: ArrayBuffer | string,
+  size: number,
+) => void;
 
-function makeWorkerAndPost(msg: unknown) {
-  const worker = new Worker(globals.root + 'traceconv_bundle.js');
+async function makeWorkerAndPost(
+  msg: unknown,
+  openTraceInLegacy?: OpenTraceInLegacyCallback,
+) {
+  const promise = defer<void>();
+
+  function handleOnMessage(msg: MessageEvent): void {
+    const args: Args = msg.data;
+    if (args.kind === 'updateStatus') {
+      AppImpl.instance.omnibox.showStatusMessage(args.status);
+    } else if (args.kind === 'jobCompleted') {
+      promise.resolve();
+    } else if (args.kind === 'downloadFile') {
+      download(new File([new Blob([args.buffer])], args.name));
+    } else if (args.kind === 'openTraceInLegacy') {
+      const str = utf8Decode(args.buffer);
+      openTraceInLegacy?.('trace.json', str, 0);
+    } else if (args.kind === 'error') {
+      maybeShowErrorDialog(args.error);
+    } else {
+      throw new Error(`Unhandled message ${JSON.stringify(args)}`);
+    }
+  }
+
+  const worker = new Worker(assetSrc('traceconv_bundle.js'));
   worker.onmessage = handleOnMessage;
   worker.postMessage(msg);
+  return promise;
 }
 
-export function convertTraceToJsonAndDownload(trace: Blob) {
-  makeWorkerAndPost({
+export function convertTraceToJsonAndDownload(trace: Blob): Promise<void> {
+  return makeWorkerAndPost({
     kind: 'ConvertTraceAndDownload',
     trace,
     format: 'json',
   });
 }
 
-export function convertTraceToSystraceAndDownload(trace: Blob) {
-  makeWorkerAndPost({
+export function convertTraceToSystraceAndDownload(trace: Blob): Promise<void> {
+  return makeWorkerAndPost({
     kind: 'ConvertTraceAndDownload',
     trace,
     format: 'systrace',
   });
 }
 
-export function convertToJson(trace: Blob, truncate?: 'start' | 'end') {
-  makeWorkerAndPost({
-    kind: 'ConvertTraceAndOpenInLegacy',
-    trace,
-    truncate,
-  });
+export function convertToJson(
+  trace: Blob,
+  openTraceInLegacy: OpenTraceInLegacyCallback,
+  truncate?: 'start' | 'end',
+): Promise<void> {
+  return makeWorkerAndPost(
+    {
+      kind: 'ConvertTraceAndOpenInLegacy',
+      trace,
+      truncate,
+    },
+    openTraceInLegacy,
+  );
 }
 
 export function convertTraceToPprofAndDownload(
   trace: Blob,
   pid: number,
   ts: time,
-) {
-  makeWorkerAndPost({
+): Promise<void> {
+  return makeWorkerAndPost({
     kind: 'ConvertTraceToPprof',
     trace,
     pid,
diff --git a/ui/src/frontend/trace_info_page.ts b/ui/src/frontend/trace_info_page.ts
deleted file mode 100644
index 87f270a..0000000
--- a/ui/src/frontend/trace_info_page.ts
+++ /dev/null
@@ -1,480 +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.
-
-import m from 'mithril';
-
-import {raf} from '../core/raf_scheduler';
-import {Engine} from '../trace_processor/engine';
-
-import {globals} from './globals';
-import {createPage} from './pages';
-import {QueryResult, UNKNOWN} from '../trace_processor/query_result';
-
-function getEngine(name: string): Engine | undefined {
-  const currentEngine = globals.getCurrentEngine();
-  if (currentEngine === undefined) return undefined;
-  const engineId = currentEngine.id;
-  return globals.engines.get(engineId)?.getProxy(name);
-}
-
-/**
- * Extracts and copies fields from a source object based on the keys present in
- * a spec object, effectively creating a new object that includes only the
- * fields that are present in the spec object.
- *
- * @template S - A type representing the spec object, a subset of T.
- * @template T - A type representing the source object, a superset of S.
- *
- * @param {T} source - The source object containing the full set of properties.
- * @param {S} spec - The specification object whose keys determine which fields
- * should be extracted from the source object.
- *
- * @returns {S} A new object containing only the fields from the source object
- * that are also present in the specification object.
- *
- * @example
- * const fullObject = { foo: 123, bar: '123', baz: true };
- * const spec = { foo: 0, bar: '' };
- * const result = pickFields(fullObject, spec);
- * console.log(result); // Output: { foo: 123, bar: '123' }
- */
-function pickFields<S extends Record<string, unknown>, T extends S>(
-  source: T,
-  spec: S,
-): S {
-  const result: Record<string, unknown> = {};
-  for (const key of Object.keys(spec)) {
-    result[key] = source[key];
-  }
-  return result as S;
-}
-
-interface StatsSectionAttrs {
-  title: string;
-  subTitle: string;
-  sqlConstraints: string;
-  cssClass: string;
-  queryId: string;
-}
-
-const statsSpec = {
-  name: UNKNOWN,
-  value: UNKNOWN,
-  description: UNKNOWN,
-  idx: UNKNOWN,
-  severity: UNKNOWN,
-  source: UNKNOWN,
-};
-
-type StatsSectionRow = typeof statsSpec;
-
-// Generic class that generate a <section> + <table> from the stats table.
-// The caller defines the query constraint, title and styling.
-// Used for errors, data losses and debugging sections.
-class StatsSection implements m.ClassComponent<StatsSectionAttrs> {
-  private data?: StatsSectionRow[];
-
-  constructor({attrs}: m.CVnode<StatsSectionAttrs>) {
-    const engine = getEngine('StatsSection');
-    if (engine === undefined) {
-      return;
-    }
-    const query = `
-      select
-        name,
-        value,
-        cast(ifnull(idx, '') as text) as idx,
-        description,
-        severity,
-        source from stats
-      where ${attrs.sqlConstraints || '1=1'}
-      order by name, idx
-    `;
-
-    engine.query(query).then((resp) => {
-      const data: StatsSectionRow[] = [];
-      const it = resp.iter(statsSpec);
-      for (; it.valid(); it.next()) {
-        data.push(pickFields(it, statsSpec));
-      }
-      this.data = data;
-
-      raf.scheduleFullRedraw();
-    });
-  }
-
-  view({attrs}: m.CVnode<StatsSectionAttrs>) {
-    const data = this.data;
-    if (data === undefined || data.length === 0) {
-      return m('');
-    }
-
-    const tableRows = data.map((row) => {
-      const help = [];
-      if (Boolean(row.description)) {
-        help.push(m('i.material-icons.contextual-help', 'help_outline'));
-      }
-      const idx = row.idx !== '' ? `[${row.idx}]` : '';
-      return m(
-        'tr',
-        m('td.name', {title: row.description}, `${row.name}${idx}`, help),
-        m('td', `${row.value}`),
-        m('td', `${row.severity} (${row.source})`),
-      );
-    });
-
-    return m(
-      `section${attrs.cssClass}`,
-      m('h2', attrs.title),
-      m('h3', attrs.subTitle),
-      m(
-        'table',
-        m('thead', m('tr', m('td', 'Name'), m('td', 'Value'), m('td', 'Type'))),
-        m('tbody', tableRows),
-      ),
-    );
-  }
-}
-
-class MetricErrors implements m.ClassComponent {
-  view() {
-    if (!globals.metricError) return;
-    return m(
-      `section.errors`,
-      m('h2', `Metric Errors`),
-      m('h3', `One or more metrics were not computed successfully:`),
-      m('div.metric-error', globals.metricError),
-    );
-  }
-}
-
-const traceMetadataRowSpec = {name: UNKNOWN, value: UNKNOWN};
-
-type TraceMetadataRow = typeof traceMetadataRowSpec;
-
-class TraceMetadata implements m.ClassComponent {
-  private data?: TraceMetadataRow[];
-
-  constructor() {
-    const engine = getEngine('StatsSection');
-    if (engine === undefined) {
-      return;
-    }
-    const query = `
-      with metadata_with_priorities as (
-        select
-          name,
-          ifnull(str_value, cast(int_value as text)) as value,
-          name in (
-            "trace_size_bytes", 
-            "cr-os-arch",
-            "cr-os-name",
-            "cr-os-version",
-            "cr-physical-memory",
-            "cr-product-version",
-            "cr-hardware-class"
-          ) as priority 
-        from metadata
-      )
-      select
-        name,
-        value
-      from metadata_with_priorities 
-      order by
-        priority desc,
-        name
-    `;
-
-    engine.query(query).then((resp: QueryResult) => {
-      const tableRows: TraceMetadataRow[] = [];
-      const it = resp.iter(traceMetadataRowSpec);
-      for (; it.valid(); it.next()) {
-        tableRows.push(pickFields(it, traceMetadataRowSpec));
-      }
-      this.data = tableRows;
-      raf.scheduleFullRedraw();
-    });
-  }
-
-  view() {
-    const data = this.data;
-    if (data === undefined || data.length === 0) {
-      return m('');
-    }
-
-    const tableRows = data.map((row) => {
-      return m('tr', m('td.name', `${row.name}`), m('td', `${row.value}`));
-    });
-
-    return m(
-      'section',
-      m('h2', 'System info and metadata'),
-      m(
-        'table',
-        m('thead', m('tr', m('td', 'Name'), m('td', 'Value'))),
-        m('tbody', tableRows),
-      ),
-    );
-  }
-}
-
-const androidGameInterventionRowSpec = {
-  package_name: UNKNOWN,
-  uid: UNKNOWN,
-  current_mode: UNKNOWN,
-  standard_mode_supported: UNKNOWN,
-  standard_mode_downscale: UNKNOWN,
-  standard_mode_use_angle: UNKNOWN,
-  standard_mode_fps: UNKNOWN,
-  perf_mode_supported: UNKNOWN,
-  perf_mode_downscale: UNKNOWN,
-  perf_mode_use_angle: UNKNOWN,
-  perf_mode_fps: UNKNOWN,
-  battery_mode_supported: UNKNOWN,
-  battery_mode_downscale: UNKNOWN,
-  battery_mode_use_angle: UNKNOWN,
-  battery_mode_fps: UNKNOWN,
-};
-
-type AndroidGameInterventionRow = typeof androidGameInterventionRowSpec;
-
-class AndroidGameInterventionList implements m.ClassComponent {
-  private data?: AndroidGameInterventionRow[];
-
-  constructor() {
-    const engine = getEngine('StatsSection');
-    if (engine === undefined) {
-      return;
-    }
-    const query = `
-      select
-        package_name,
-        uid,
-        current_mode,
-        standard_mode_supported,
-        standard_mode_downscale,
-        standard_mode_use_angle,
-        standard_mode_fps,
-        perf_mode_supported,
-        perf_mode_downscale,
-        perf_mode_use_angle,
-        perf_mode_fps,
-        battery_mode_supported,
-        battery_mode_downscale,
-        battery_mode_use_angle,
-        battery_mode_fps
-      from android_game_intervention_list
-    `;
-
-    engine.query(query).then((resp) => {
-      const data: AndroidGameInterventionRow[] = [];
-      const it = resp.iter(androidGameInterventionRowSpec);
-      for (; it.valid(); it.next()) {
-        data.push(pickFields(it, androidGameInterventionRowSpec));
-      }
-      this.data = data;
-      raf.scheduleFullRedraw();
-    });
-  }
-
-  view() {
-    const data = this.data;
-    if (data === undefined || data.length === 0) {
-      return m('');
-    }
-
-    const tableRows = [];
-    let standardInterventions = '';
-    let perfInterventions = '';
-    let batteryInterventions = '';
-
-    for (const row of data) {
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      if (row.standard_mode_supported) {
-        standardInterventions = `angle=${row.standard_mode_use_angle},downscale=${row.standard_mode_downscale},fps=${row.standard_mode_fps}`;
-      } else {
-        standardInterventions = 'Not supported';
-      }
-
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      if (row.perf_mode_supported) {
-        perfInterventions = `angle=${row.perf_mode_use_angle},downscale=${row.perf_mode_downscale},fps=${row.perf_mode_fps}`;
-      } else {
-        perfInterventions = 'Not supported';
-      }
-
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      if (row.battery_mode_supported) {
-        batteryInterventions = `angle=${row.battery_mode_use_angle},downscale=${row.battery_mode_downscale},fps=${row.battery_mode_fps}`;
-      } else {
-        batteryInterventions = 'Not supported';
-      }
-      // Game mode numbers are defined in
-      // https://cs.android.com/android/platform/superproject/+/main:frameworks/base/core/java/android/app/GameManager.java;l=68
-      if (row.current_mode === 1) {
-        row.current_mode = 'Standard';
-      } else if (row.current_mode === 2) {
-        row.current_mode = 'Performance';
-      } else if (row.current_mode === 3) {
-        row.current_mode = 'Battery';
-      }
-      tableRows.push(
-        m(
-          'tr',
-          m('td.name', `${row.package_name}`),
-          m('td', `${row.current_mode}`),
-          m('td', standardInterventions),
-          m('td', perfInterventions),
-          m('td', batteryInterventions),
-        ),
-      );
-    }
-
-    return m(
-      'section',
-      m('h2', 'Game Intervention List'),
-      m(
-        'table',
-        m(
-          'thead',
-          m(
-            'tr',
-            m('td', 'Name'),
-            m('td', 'Current mode'),
-            m('td', 'Standard mode interventions'),
-            m('td', 'Performance mode interventions'),
-            m('td', 'Battery mode interventions'),
-          ),
-        ),
-        m('tbody', tableRows),
-      ),
-    );
-  }
-}
-
-const packageDataSpec = {
-  packageName: UNKNOWN,
-  versionCode: UNKNOWN,
-  debuggable: UNKNOWN,
-  profileableFromShell: UNKNOWN,
-};
-
-type PackageData = typeof packageDataSpec;
-
-class PackageListSection implements m.ClassComponent {
-  private packageList?: PackageData[];
-
-  constructor() {
-    const engine = getEngine('StatsSection');
-    if (engine === undefined) {
-      return;
-    }
-    this.loadData(engine);
-  }
-
-  private async loadData(engine: Engine): Promise<void> {
-    const query = `
-      select
-        package_name as packageName,
-        version_code as versionCode,
-        debuggable,
-        profileable_from_shell as profileableFromShell
-      from package_list
-    `;
-
-    const packageList: PackageData[] = [];
-    const result = await engine.query(query);
-    const it = result.iter(packageDataSpec);
-    for (; it.valid(); it.next()) {
-      packageList.push(pickFields(it, packageDataSpec));
-    }
-
-    this.packageList = packageList;
-    raf.scheduleFullRedraw();
-  }
-
-  view() {
-    const packageList = this.packageList;
-    if (packageList === undefined || packageList.length === 0) {
-      return undefined;
-    }
-
-    const tableRows = packageList.map((it) => {
-      return m(
-        'tr',
-        m('td.name', `${it.packageName}`),
-        m('td', `${it.versionCode}`),
-        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-        m(
-          'td',
-          `${it.debuggable ? 'debuggable' : ''} ${
-            it.profileableFromShell ? 'profileable' : ''
-          }`,
-        ),
-        /* eslint-enable */
-      );
-    });
-
-    return m(
-      'section',
-      m('h2', 'Package list'),
-      m(
-        'table',
-        m(
-          'thead',
-          m('tr', m('td', 'Name'), m('td', 'Version code'), m('td', 'Flags')),
-        ),
-        m('tbody', tableRows),
-      ),
-    );
-  }
-}
-
-export const TraceInfoPage = createPage({
-  view() {
-    return m(
-      '.trace-info-page',
-      m(MetricErrors),
-      m(StatsSection, {
-        queryId: 'info_errors',
-        title: 'Import errors',
-        cssClass: '.errors',
-        subTitle: `The following errors have been encountered while importing the
-               trace. These errors are usually non-fatal but indicate that one
-               or more tracks might be missing or showing erroneous data.`,
-        sqlConstraints: `severity = 'error' and value > 0`,
-      }),
-      m(StatsSection, {
-        queryId: 'info_data_losses',
-        title: 'Data losses',
-        cssClass: '.errors',
-        subTitle: `These counters are collected at trace recording time. The trace
-               data for one or more data sources was dropped and hence some
-               track contents will be incomplete.`,
-        sqlConstraints: `severity = 'data_loss' and value > 0`,
-      }),
-      m(TraceMetadata),
-      m(PackageListSection),
-      m(AndroidGameInterventionList),
-      m(StatsSection, {
-        queryId: 'info_all',
-        title: 'Debugging stats',
-        cssClass: '',
-        subTitle: `Debugging statistics such as trace buffer usage and metrics
-                     coming from the TraceProcessor importer stages.`,
-        sqlConstraints: '',
-      }),
-    );
-  },
-});
diff --git a/ui/src/frontend/trace_share_utils.ts b/ui/src/frontend/trace_share_utils.ts
new file mode 100644
index 0000000..cf8f185
--- /dev/null
+++ b/ui/src/frontend/trace_share_utils.ts
@@ -0,0 +1,66 @@
+// 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.
+
+import m from 'mithril';
+import {TraceUrlSource} from '../core/trace_source';
+import {createPermalink} from './permalink';
+import {showModal} from '../widgets/modal';
+import {globals} from './globals';
+import {AppImpl} from '../core/app_impl';
+import {Trace} from '../public/trace';
+import {TraceImpl} from '../core/trace_impl';
+import {CopyableLink} from '../widgets/copyable_link';
+
+export function isShareable(trace: Trace) {
+  return globals.isInternalUser && trace.traceInfo.downloadable;
+}
+
+export async function shareTrace(trace: TraceImpl) {
+  const traceSource = trace.traceInfo.source;
+  const traceUrl = (traceSource as TraceUrlSource).url ?? '';
+
+  // If the trace is not shareable (has been pushed via postMessage()) but has
+  // a url, create a pseudo-permalink by echoing back the URL.
+  if (!isShareable(trace)) {
+    const msg = [
+      m(
+        'p',
+        'This trace was opened by an external site and as such cannot ' +
+          'be re-shared preserving the UI state.',
+      ),
+    ];
+    if (traceUrl) {
+      msg.push(m('p', 'By using the URL below you can open this trace again.'));
+      msg.push(m('p', 'Clicking will copy the URL into the clipboard.'));
+      msg.push(m(CopyableLink, {url: traceUrl}));
+    }
+
+    showModal({
+      title: 'Cannot create permalink from external trace',
+      content: m('div', msg),
+    });
+    return;
+  }
+
+  if (!isShareable(trace)) return;
+
+  const result = confirm(
+    `Upload UI state and generate a permalink. ` +
+      `The trace will be accessible by anybody with the permalink.`,
+  );
+  if (result) {
+    AppImpl.instance.analytics.logEvent('Trace Actions', 'Create permalink');
+    return await createPermalink();
+  }
+}
diff --git a/ui/src/frontend/trace_url_handler.ts b/ui/src/frontend/trace_url_handler.ts
index e080027..8808818 100644
--- a/ui/src/frontend/trace_url_handler.ts
+++ b/ui/src/frontend/trace_url_handler.ts
@@ -13,19 +13,16 @@
 // limitations under the License.
 
 import m from 'mithril';
-
-import {Actions} from '../common/actions';
-import {tryGetTrace} from '../common/cache_manager';
+import {tryGetTrace} from '../core/cache_manager';
 import {showModal} from '../widgets/modal';
-
 import {loadPermalink} from './permalink';
 import {loadAndroidBugToolInfo} from './android_bug_tool';
-import {globals} from './globals';
-import {Route, Router} from './router';
+import {Route, Router} from '../core/router';
 import {taskTracker} from './task_tracker';
+import {AppImpl} from '../core/app_impl';
 
 function getCurrentTraceUrl(): undefined | string {
-  const source = globals.getCurrentEngine()?.source;
+  const source = AppImpl.instance.trace?.traceInfo.source;
   if (source && source.type === 'URL') {
     return source.url;
   }
@@ -84,13 +81,13 @@
  * 3. '' -> URL with a ?local_cache_key=xxx arg:
  *  - Same as case 2.
  * 4. URL with local_cache_key=1 -> URL with local_cache_key=2:
- *  a) If 2 != uuid of the trace currently loaded (globals.state.traceUuid):
+ *  a) If 2 != uuid of the trace currently loaded (TraceImpl.traceInfo.uuid):
  *  - Ask the user if they intend to switch trace and load 2.
  *  b) If 2 == uuid of current trace (e.g., after a new trace has loaded):
  *  - no effect (except redrawing).
  * 5. URL with local_cache_key -> URL without local_cache_key:
  *  - Redirect to ?local_cache_key=1234 where 1234 is the UUID of the previous
- *    URL (this might or might not match globals.state.traceUuid).
+ *    URL (this might or might not match traceInfo.uuid).
  *
  * Backward navigation cases:
  * 6. URL without local_cache_key <- URL with local_cache_key:
@@ -101,7 +98,10 @@
  *  - Same as case 5: re-append the local_cache_key.
  */
 async function maybeOpenCachedTrace(traceUuid: string) {
-  if (traceUuid === globals.state.traceUuid) {
+  const curTrace = AppImpl.instance.trace?.traceInfo;
+  const curCacheUuid = curTrace?.cached ? curTrace.uuid : '';
+
+  if (traceUuid === curCacheUuid) {
     // Do nothing, matches the currently loaded trace.
     return;
   }
@@ -119,22 +119,20 @@
   // This early out prevents to re-trigger the openTraceFromXXX() action if the
   // URL changes (e.g. if the user navigates back/fwd) while the new trace is
   // being loaded.
-  if (globals.state.engine !== undefined) {
-    const eng = globals.state.engine;
-    if (eng.source.type === 'ARRAY_BUFFER' && eng.source.uuid === traceUuid) {
-      return;
-    }
+  if (
+    curTrace !== undefined &&
+    curTrace.source.type === 'ARRAY_BUFFER' &&
+    curTrace.source.uuid === traceUuid
+  ) {
+    return;
   }
 
   // Fetch the trace from the cache storage. If available load it. If not, show
   // a dialog informing the user about the cache miss.
   const maybeTrace = await tryGetTrace(traceUuid);
 
-  const navigateToOldTraceUuid = () => {
-    Router.navigate(
-      `#!/viewer?local_cache_key=${globals.state.traceUuid ?? ''}`,
-    );
-  };
+  const navigateToOldTraceUuid = () =>
+    Router.navigate(`#!/viewer?local_cache_key=${curCacheUuid}`);
 
   if (!maybeTrace) {
     showModal({
@@ -163,8 +161,8 @@
   // the trace without showing any further dialog. This is the case of tab
   // discarding, reloading or pasting a url with a local_cache_key in an empty
   // instance.
-  if (globals.state.traceUuid === undefined) {
-    globals.dispatch(Actions.openTraceFromBuffer(maybeTrace));
+  if (curTrace === undefined) {
+    AppImpl.instance.openTraceFromBuffer(maybeTrace);
     return;
   }
 
@@ -190,7 +188,7 @@
       ),
       m(
         'pre',
-        `Old trace: ${globals.state.traceUuid || '<no trace>'}\n` +
+        `Old trace: ${curTrace !== undefined ? curCacheUuid : '<no trace>'}\n` +
           `New trace: ${traceUuid}`,
       ),
     ),
@@ -201,7 +199,7 @@
         primary: true,
         action: () => {
           hasOpenedNewTrace = true;
-          globals.dispatch(Actions.openTraceFromBuffer(maybeTrace));
+          AppImpl.instance.openTraceFromBuffer(maybeTrace);
         },
       },
       {text: 'Cancel'},
@@ -229,39 +227,20 @@
     const fileName = url.split('/').pop() ?? 'local_trace.pftrace';
     const request = fetch(url)
       .then((response) => response.blob())
-      .then((blob) => {
-        globals.dispatch(
-          Actions.openTraceFromFile({
-            file: new File([blob], fileName),
-          }),
-        );
-      })
+      .then((b) => AppImpl.instance.openTraceFromFile(new File([b], fileName)))
       .catch((e) => alert(`Could not load local trace ${e}`));
     taskTracker.trackPromise(request, 'Downloading local trace');
   } else {
-    globals.dispatch(Actions.openTraceFromUrl({url}));
+    AppImpl.instance.openTraceFromUrl(url);
   }
 }
 
 function openTraceFromAndroidBugTool() {
-  // TODO(hjd): Unify updateStatus and TaskTracker
-  globals.dispatch(
-    Actions.updateStatus({
-      msg: 'Loading trace from ABT extension',
-      timestamp: Date.now() / 1000,
-    }),
-  );
+  const msg = 'Loading trace from ABT extension';
+  AppImpl.instance.omnibox.showStatusMessage(msg);
   const loadInfo = loadAndroidBugToolInfo();
-  taskTracker.trackPromise(loadInfo, 'Loading trace from ABT extension');
+  taskTracker.trackPromise(loadInfo, msg);
   loadInfo
-    .then((info) => {
-      globals.dispatch(
-        Actions.openTraceFromFile({
-          file: info.file,
-        }),
-      );
-    })
-    .catch((e) => {
-      console.error(e);
-    });
+    .then((info) => AppImpl.instance.openTraceFromFile(info.file))
+    .catch((e) => console.error(e));
 }
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index c6462b0..f10e21e 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Engine} from '../trace_processor/engine';
+import {Trace} from '../public/trace';
 
 export interface NewTrackArgs {
-  trackKey: string;
-  engine: Engine;
+  uri: string;
+  trace: Trace;
 }
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
deleted file mode 100644
index 55a5ad5..0000000
--- a/ui/src/frontend/track_group_panel.ts
+++ /dev/null
@@ -1,251 +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 {Icons} from '../base/semantic_icons';
-import {Actions} from '../common/actions';
-import {getContainingGroupKey} from '../common/state';
-import {TrackCacheEntry} from '../common/track_cache';
-import {TrackTags} from '../public';
-
-import {TRACK_SHELL_WIDTH} from './css_constants';
-import {globals} from './globals';
-import {Size} from '../base/geom';
-import {Panel} from './panel_container';
-import {
-  CrashButton,
-  drawGridLines,
-  renderChips,
-  renderHoveredCursorVertical,
-  renderHoveredNoteVertical,
-  renderNoteVerticals,
-  renderWakeupVertical,
-  TrackContent,
-} from './track_panel';
-import {canvasClip} from '../common/canvas_utils';
-import {Button} from '../widgets/button';
-import {TrackRenderContext} from '../public/tracks';
-import {calculateResolution} from '../common/resolution';
-import {PxSpan, TimeScale} from './time_scale';
-import {exists} from '../base/utils';
-import {classNames} from '../base/classnames';
-
-interface Attrs {
-  readonly groupKey: string;
-  readonly title: m.Children;
-  readonly tooltip: string;
-  readonly collapsed: boolean;
-  readonly collapsable: boolean;
-  readonly trackFSM?: TrackCacheEntry;
-  readonly tags?: TrackTags;
-  readonly subtitle?: string;
-  readonly chips?: ReadonlyArray<string>;
-}
-
-export class TrackGroupPanel implements Panel {
-  readonly kind = 'panel';
-  readonly selectable = true;
-  readonly groupKey: string;
-
-  constructor(private attrs: Attrs) {
-    this.groupKey = attrs.groupKey;
-  }
-
-  render(): m.Children {
-    const {groupKey, title, subtitle, chips, collapsed, trackFSM, tooltip} =
-      this.attrs;
-
-    // The shell should be highlighted if the current search result is inside
-    // this track group.
-    let highlightClass = '';
-    const searchIndex = globals.state.searchIndex;
-    if (searchIndex !== -1) {
-      const trackKey = globals.currentSearchResults.trackKeys[searchIndex];
-      const containingGroupKey = getContainingGroupKey(globals.state, trackKey);
-      if (containingGroupKey === groupKey) {
-        highlightClass = 'flash';
-      }
-    }
-
-    const selection = globals.state.selection;
-
-    const trackGroup = globals.state.trackGroups[groupKey];
-    let checkBox = Icons.BlankCheckbox;
-    if (selection.kind === 'area') {
-      if (
-        selection.tracks.includes(groupKey) &&
-        trackGroup.tracks.every((id) => selection.tracks.includes(id))
-      ) {
-        checkBox = Icons.Checkbox;
-      } else if (
-        selection.tracks.includes(groupKey) ||
-        trackGroup.tracks.some((id) => selection.tracks.includes(id))
-      ) {
-        checkBox = Icons.IndeterminateCheckbox;
-      }
-    }
-
-    const error = trackFSM?.getError();
-
-    return m(
-      `.track-group-panel[collapsed=${collapsed}]`,
-      {
-        id: 'track_' + groupKey,
-        oncreate: () => this.onupdate(),
-        onupdate: () => this.onupdate(),
-      },
-      m(
-        `.shell`,
-        {
-          className: classNames(
-            this.attrs.collapsable && 'pf-clickable',
-            highlightClass,
-          ),
-          onclick: (e: MouseEvent) => {
-            if (e.defaultPrevented) return;
-            if (this.attrs.collapsable) {
-              globals.dispatch(
-                Actions.toggleTrackGroupCollapsed({
-                  groupKey,
-                }),
-              );
-            }
-            e.stopPropagation();
-          },
-        },
-        this.attrs.collapsable &&
-          m(
-            '.fold-button',
-            m(
-              'i.material-icons',
-              collapsed ? Icons.ExpandDown : Icons.ExpandUp,
-            ),
-          ),
-        m(
-          '.title-wrapper',
-          m(
-            'h1.track-title',
-            {title: tooltip},
-            title,
-            chips && renderChips(chips),
-          ),
-          collapsed && exists(subtitle) && m('h2.track-subtitle', subtitle),
-        ),
-        m(
-          '.track-buttons',
-          error && m(CrashButton, {error}),
-          selection.kind === 'area' &&
-            m(Button, {
-              onclick: (e: MouseEvent) => {
-                globals.dispatch(
-                  Actions.toggleTrackSelection({
-                    key: groupKey,
-                    isTrackGroup: true,
-                  }),
-                );
-                e.stopPropagation();
-              },
-              icon: checkBox,
-              compact: true,
-            }),
-        ),
-      ),
-      trackFSM
-        ? m(
-            TrackContent,
-            {
-              track: trackFSM.track,
-              hasError: Boolean(trackFSM.getError()),
-              height: this.attrs.trackFSM?.track.getHeight(),
-            },
-            !collapsed && subtitle !== null ? m('span', subtitle) : null,
-          )
-        : null,
-    );
-  }
-
-  private onupdate() {
-    if (this.attrs.trackFSM !== undefined) {
-      this.attrs.trackFSM.track.onFullRedraw?.();
-    }
-  }
-
-  highlightIfTrackSelected(
-    ctx: CanvasRenderingContext2D,
-    timescale: TimeScale,
-    size: Size,
-  ) {
-    const selection = globals.state.selection;
-    if (selection.kind !== 'area') return;
-    const selectedAreaDuration = selection.end - selection.start;
-    if (selection.tracks.includes(this.groupKey)) {
-      ctx.fillStyle = 'rgba(131, 152, 230, 0.3)';
-      ctx.fillRect(
-        timescale.timeToPx(selection.start),
-        0,
-        timescale.durationToPx(selectedAreaDuration),
-        size.height,
-      );
-    }
-  }
-
-  renderCanvas(ctx: CanvasRenderingContext2D, size: Size) {
-    const {collapsed, trackFSM: track} = this.attrs;
-
-    if (!collapsed) return;
-
-    const trackSize = {
-      width: size.width - TRACK_SHELL_WIDTH,
-      height: size.height,
-    };
-
-    ctx.save();
-    ctx.translate(TRACK_SHELL_WIDTH, 0);
-    canvasClip(ctx, 0, 0, trackSize.width, trackSize.height);
-
-    const visibleWindow = globals.timeline.visibleWindow;
-    const timespan = visibleWindow.toTimeSpan();
-    const timescale = new TimeScale(
-      visibleWindow,
-      new PxSpan(0, trackSize.width),
-    );
-
-    drawGridLines(ctx, timespan, timescale, trackSize);
-
-    if (track) {
-      if (!track.getError()) {
-        const trackRenderCtx: TrackRenderContext = {
-          visibleWindow,
-          size: trackSize,
-          ctx,
-          trackKey: track.trackKey,
-          resolution: calculateResolution(visibleWindow, trackSize.width),
-          timescale,
-        };
-        track.render(trackRenderCtx);
-      }
-    }
-
-    this.highlightIfTrackSelected(ctx, timescale, size);
-
-    // Draw vertical line when hovering on the notes panel.
-    renderHoveredNoteVertical(ctx, timescale, size);
-    renderHoveredCursorVertical(ctx, timescale, size);
-    renderWakeupVertical(ctx, timescale, size);
-    renderNoteVerticals(ctx, timescale, size);
-
-    ctx.restore();
-  }
-}
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 878ca4e..7814674 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -12,552 +12,271 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {hex} from 'color-convert';
 import m from 'mithril';
-
-import {currentTargetOffset} from '../base/dom_utils';
-import {Icons} from '../base/semantic_icons';
-import {TimeSpan, time} from '../base/time';
-import {Actions} from '../common/actions';
-import {TrackCacheEntry} from '../common/track_cache';
-import {raf} from '../core/raf_scheduler';
-import {SliceRect, Track, TrackTags} from '../public';
-
-import {checkerboard} from './checkerboard';
-import {
-  SELECTION_FILL_COLOR,
-  TRACK_BORDER_COLOR,
-  TRACK_SHELL_WIDTH,
-} from './css_constants';
-import {globals} from './globals';
-import {generateTicks, TickType, getMaxMajorTicks} from './gridline_helper';
-import {Size} from '../base/geom';
-import {Panel} from './panel_container';
-import {verticalScrollToTrack} from './scroll_helper';
-import {drawVerticalLineAtTime} from './vertical_line_helper';
+import {canvasClip, canvasSave} from '../base/canvas_utils';
 import {classNames} from '../base/classnames';
-import {Button, ButtonBar} from '../widgets/button';
-import {Popup, PopupPosition} from '../widgets/popup';
-import {canvasClip} from '../common/canvas_utils';
-import {PxSpan, TimeScale} from './time_scale';
-import {getLegacySelection} from '../common/state';
-import {CloseTrackButton} from './close_track_button';
-import {exists} from '../base/utils';
-import {Intent} from '../widgets/common';
-import {TrackRenderContext} from '../public/tracks';
+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 {calculateResolution} from '../common/resolution';
 import {featureFlags} from '../core/feature_flags';
+import {TrackRenderer} from '../core/track_manager';
+import {TrackDescriptor, TrackRenderContext} from '../public/track';
+import {TrackNode} from '../public/workspace';
+import {Button} from '../widgets/button';
+import {Popup, PopupPosition} from '../widgets/popup';
 import {Tree, TreeNode} from '../widgets/tree';
+import {SELECTION_FILL_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
+import {Panel} from './panel_container';
+import {TrackWidget} from '../widgets/track_widget';
+import {raf} from '../core/raf_scheduler';
+import {Intent} from '../widgets/common';
+import {TraceImpl} from '../core/trace_impl';
 
-export const SHOW_TRACK_DETAILS_BUTTON = featureFlags.register({
+const SHOW_TRACK_DETAILS_BUTTON = featureFlags.register({
   id: 'showTrackDetailsButton',
   name: 'Show track details button',
   description: 'Show track details button in track shells.',
   defaultValue: false,
 });
 
-export function getTitleFontSize(title: string): string | undefined {
-  const length = title.length;
-  if (length > 55) {
-    return '9px';
-  }
-  if (length > 50) {
-    return '10px';
-  }
-  if (length > 45) {
-    return '11px';
-  }
-  if (length > 40) {
-    return '12px';
-  }
-  if (length > 35) {
-    return '13px';
-  }
-  return undefined;
-}
-
-function isTrackPinned(trackKey: string) {
-  return globals.state.pinnedTracks.indexOf(trackKey) !== -1;
-}
-
-function isTrackSelected(trackKey: string) {
-  const selection = globals.state.selection;
-  if (selection.kind !== 'area') return false;
-  return selection.tracks.includes(trackKey);
-}
-
-interface TrackChipAttrs {
-  text: string;
-}
-
-class TrackChip implements m.ClassComponent<TrackChipAttrs> {
-  view({attrs}: m.CVnode<TrackChipAttrs>) {
-    return m('span.chip', attrs.text);
-  }
-}
-
-export function renderChips(chips: ReadonlyArray<string>) {
-  return chips.map((chip) => m(TrackChip, {text: chip}));
-}
-
-export interface CrashButtonAttrs {
-  error: Error;
-}
-
-export class CrashButton implements m.ClassComponent<CrashButtonAttrs> {
-  view({attrs}: m.Vnode<CrashButtonAttrs>): m.Children {
-    return m(
-      Popup,
-      {
-        trigger: m(Button, {
-          icon: Icons.Crashed,
-          compact: true,
-        }),
-      },
-      this.renderErrorMessage(attrs.error),
-    );
-  }
-
-  private renderErrorMessage(error: Error): m.Children {
-    return m(
-      '',
-      'This track has crashed',
-      m(Button, {
-        label: 'Re-raise exception',
-        intent: Intent.Primary,
-        className: Popup.DISMISS_POPUP_GROUP_CLASS,
-        onclick: () => {
-          throw error;
-        },
-      }),
-    );
-  }
-}
-
-interface TrackShellAttrs {
-  readonly trackKey: string;
-  readonly title: m.Children;
-  readonly buttons: m.Children;
-  readonly tags?: TrackTags;
-  readonly chips?: ReadonlyArray<string>;
-  readonly button?: string;
-  readonly pluginId?: string;
-}
-
-class TrackShell implements m.ClassComponent<TrackShellAttrs> {
-  // Set to true when we click down and drag the
-  private dragging = false;
-  private dropping: 'before' | 'after' | undefined = undefined;
-
-  view({attrs}: m.CVnode<TrackShellAttrs>) {
-    // The shell should be highlighted if the current search result is inside
-    // this track.
-    let highlightClass = undefined;
-    const searchIndex = globals.state.searchIndex;
-    if (searchIndex !== -1) {
-      const trackKey = globals.currentSearchResults.trackKeys[searchIndex];
-      if (trackKey === attrs.trackKey) {
-        highlightClass = 'flash';
-      }
-    }
-
-    const currentSelection = globals.state.selection;
-    const pinned = isTrackPinned(attrs.trackKey);
-
-    return m(
-      `.track-shell[draggable=true]`,
-      {
-        className: classNames(
-          highlightClass,
-          this.dragging && 'drag',
-          this.dropping && `drop-${this.dropping}`,
-        ),
-        ondragstart: (e: DragEvent) => this.ondragstart(e, attrs.trackKey),
-        ondragend: this.ondragend.bind(this),
-        ondragover: this.ondragover.bind(this),
-        ondragleave: this.ondragleave.bind(this),
-        ondrop: (e: DragEvent) => this.ondrop(e, attrs.trackKey),
-      },
-      m(
-        '.track-menubar',
-        m(
-          'h1',
-          {
-            title: attrs.title,
-          },
-          attrs.title,
-          attrs.chips && renderChips(attrs.chips),
-        ),
-        m(
-          ButtonBar,
-          {className: 'track-buttons'},
-          attrs.buttons,
-          SHOW_TRACK_DETAILS_BUTTON.get() &&
-            this.renderTrackDetailsButton(pinned, attrs),
-          m(Button, {
-            className: classNames(!pinned && 'pf-visible-on-hover'),
-            onclick: () => {
-              globals.dispatch(
-                Actions.toggleTrackPinned({trackKey: attrs.trackKey}),
-              );
-            },
-            icon: Icons.Pin,
-            iconFilled: pinned,
-            title: pinned ? 'Unpin' : 'Pin to top',
-            compact: true,
-          }),
-          currentSelection.kind === 'area'
-            ? m(Button, {
-                onclick: (e: MouseEvent) => {
-                  globals.dispatch(
-                    Actions.toggleTrackSelection({
-                      key: attrs.trackKey,
-                      isTrackGroup: false,
-                    }),
-                  );
-                  e.stopPropagation();
-                },
-                compact: true,
-                icon: isTrackSelected(attrs.trackKey)
-                  ? Icons.Checkbox
-                  : Icons.BlankCheckbox,
-                title: isTrackSelected(attrs.trackKey)
-                  ? 'Remove track'
-                  : 'Add track to selection',
-              })
-            : '',
-        ),
-      ),
-    );
-  }
-
-  ondragstart(e: DragEvent, trackKey: string) {
-    const dataTransfer = e.dataTransfer;
-    if (dataTransfer === null) return;
-    this.dragging = true;
-    raf.scheduleFullRedraw();
-    dataTransfer.setData('perfetto/track', `${trackKey}`);
-    dataTransfer.setDragImage(new Image(), 0, 0);
-  }
-
-  ondragend() {
-    this.dragging = false;
-    raf.scheduleFullRedraw();
-  }
-
-  ondragover(e: DragEvent) {
-    if (this.dragging) return;
-    if (!(e.target instanceof HTMLElement)) return;
-    const dataTransfer = e.dataTransfer;
-    if (dataTransfer === null) return;
-    if (!dataTransfer.types.includes('perfetto/track')) return;
-    dataTransfer.dropEffect = 'move';
-    e.preventDefault();
-
-    // Apply some hysteresis to the drop logic so that the lightened border
-    // changes only when we get close enough to the border.
-    if (e.offsetY < e.target.scrollHeight / 3) {
-      this.dropping = 'before';
-    } else if (e.offsetY > (e.target.scrollHeight / 3) * 2) {
-      this.dropping = 'after';
-    }
-    raf.scheduleFullRedraw();
-  }
-
-  ondragleave() {
-    this.dropping = undefined;
-    raf.scheduleFullRedraw();
-  }
-
-  ondrop(e: DragEvent, trackKey: string) {
-    if (this.dropping === undefined) return;
-    const dataTransfer = e.dataTransfer;
-    if (dataTransfer === null) return;
-    raf.scheduleFullRedraw();
-    const srcId = dataTransfer.getData('perfetto/track');
-    const dstId = trackKey;
-    globals.dispatch(Actions.moveTrack({srcId, op: this.dropping, dstId}));
-    this.dropping = undefined;
-  }
-
-  private renderTrackDetailsButton(pinned: boolean, attrs: TrackShellAttrs) {
-    return m(
-      Popup,
-      {
-        trigger: m(Button, {
-          className: classNames(!pinned && 'pf-visible-on-hover'),
-          icon: 'info',
-          title: 'Show track details',
-          compact: true,
-        }),
-        position: PopupPosition.RightStart,
-      },
-      m(
-        '.pf-track-details-dropdown',
-        m(
-          Tree,
-          m(TreeNode, {
-            left: 'URI',
-            right: globals.state.tracks[attrs.trackKey]?.uri,
-          }),
-          m(TreeNode, {left: 'Title', right: attrs.title}),
-          m(TreeNode, {left: 'Track Key', right: attrs.trackKey}),
-          m(TreeNode, {left: 'Plugin ID', right: attrs.pluginId}),
-          m(
-            TreeNode,
-            {left: 'Tags'},
-            attrs.tags &&
-              Object.entries(attrs.tags).map(([key, value]) => {
-                return m(TreeNode, {left: key, right: value?.toString()});
-              }),
-          ),
-        ),
-      ),
-    );
-  }
-}
-
-export interface TrackContentAttrs {
-  track: Track;
-  hasError?: boolean;
-  height?: number;
-}
-export class TrackContent implements m.ClassComponent<TrackContentAttrs> {
-  private mouseDownX?: number;
-  private mouseDownY?: number;
-  private selectionOccurred = false;
-
-  private getTargetContainerSize(event: MouseEvent): number {
-    const target = event.target as HTMLElement;
-    return target.getBoundingClientRect().width;
-  }
-
-  private getTargetTimeScale(event: MouseEvent): TimeScale {
-    const timeWindow = globals.timeline.visibleWindow;
-    return new TimeScale(
-      timeWindow,
-      new PxSpan(0, this.getTargetContainerSize(event)),
-    );
-  }
-
-  view(node: m.CVnode<TrackContentAttrs>) {
-    const attrs = node.attrs;
-    return m(
-      '.track-content',
-      {
-        style: exists(attrs.height) && {
-          height: `${attrs.height}px`,
-        },
-        className: classNames(attrs.hasError && 'pf-track-content-error'),
-        onmousemove: (e: MouseEvent) => {
-          const {x, y} = currentTargetOffset(e);
-          attrs.track.onMouseMove?.({
-            x,
-            y,
-            timescale: this.getTargetTimeScale(e),
-          });
-          raf.scheduleRedraw();
-        },
-        onmouseout: () => {
-          attrs.track.onMouseOut?.();
-          raf.scheduleRedraw();
-        },
-        onmousedown: (e: MouseEvent) => {
-          const {x, y} = currentTargetOffset(e);
-          this.mouseDownX = x;
-          this.mouseDownY = y;
-        },
-        onmouseup: (e: MouseEvent) => {
-          if (this.mouseDownX === undefined || this.mouseDownY === undefined) {
-            return;
-          }
-          const {x, y} = currentTargetOffset(e);
-          if (
-            Math.abs(x - this.mouseDownX) > 1 ||
-            Math.abs(y - this.mouseDownY) > 1
-          ) {
-            this.selectionOccurred = true;
-          }
-          this.mouseDownX = undefined;
-          this.mouseDownY = 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.
-          const {x, y} = currentTargetOffset(e);
-          if (
-            attrs.track.onMouseClick?.({
-              x,
-              y,
-              timescale: this.getTargetTimeScale(e),
-            })
-          ) {
-            e.stopPropagation();
-          }
-          raf.scheduleRedraw();
-        },
-      },
-      node.children,
-    );
-  }
-}
-
-interface TrackComponentAttrs {
-  readonly trackKey: string;
-  readonly heightPx?: number;
-  readonly title: m.Children;
-  readonly buttons?: m.Children;
-  readonly tags?: TrackTags;
-  readonly chips?: ReadonlyArray<string>;
-  readonly track?: Track;
-  readonly error?: Error | undefined;
-  readonly closeable: boolean;
-  readonly pluginId?: string;
-
-  // Issues a scrollTo() on this DOM element at creation time. Default: false.
-  revealOnCreate?: boolean;
-}
-
-class TrackComponent implements m.ClassComponent<TrackComponentAttrs> {
-  view({attrs}: m.CVnode<TrackComponentAttrs>) {
-    // TODO(hjd): The min height below must match the track_shell_title
-    // max height in common.scss so we should read it from CSS to avoid
-    // them going out of sync.
-    const TRACK_HEIGHT_MIN_PX = 18;
-    const TRACK_HEIGHT_DEFAULT_PX = 24;
-    const trackHeightRaw = attrs.heightPx ?? TRACK_HEIGHT_DEFAULT_PX;
-    const trackHeight = Math.max(trackHeightRaw, TRACK_HEIGHT_MIN_PX);
-
-    return m(
-      '.track',
-      {
-        style: {
-          // Note: Sub-pixel track heights can mess with sticky elements.
-          // Round up to the nearest integer number of pixels.
-          height: `${Math.ceil(trackHeight)}px`,
-        },
-        id: 'track_' + attrs.trackKey,
-      },
-      [
-        m(TrackShell, {
-          buttons: [
-            attrs.error && m(CrashButton, {error: attrs.error}),
-            attrs.closeable && m(CloseTrackButton, {trackKey: attrs.trackKey}),
-            attrs.buttons,
-          ],
-          title: attrs.title,
-          trackKey: attrs.trackKey,
-          tags: attrs.tags,
-          chips: attrs.chips,
-          pluginId: attrs.pluginId,
-        }),
-        attrs.track &&
-          m(TrackContent, {
-            track: attrs.track,
-            hasError: Boolean(attrs.error),
-            height: attrs.heightPx,
-          }),
-      ],
-    );
-  }
-
-  oncreate(vnode: m.VnodeDOM<TrackComponentAttrs>) {
-    const {attrs} = vnode;
-    if (globals.scrollToTrackKey === attrs.trackKey) {
-      verticalScrollToTrack(attrs.trackKey);
-      globals.scrollToTrackKey = undefined;
-    }
-    this.onupdate(vnode);
-
-    if (attrs.revealOnCreate) {
-      vnode.dom.scrollIntoView();
-    }
-  }
-
-  onupdate(vnode: m.VnodeDOM<TrackComponentAttrs>) {
-    vnode.attrs.track?.onFullRedraw?.();
-  }
-}
+// 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 trackKey: string;
-  readonly title: m.Children;
-  readonly tags?: TrackTags;
-  readonly chips?: ReadonlyArray<string>;
-  readonly trackFSM?: TrackCacheEntry;
+  readonly trace: TraceImpl;
+  readonly node: TrackNode;
+  readonly indentationLevel: number;
+  readonly trackRenderer?: TrackRenderer;
   readonly revealOnCreate?: boolean;
-  readonly closeable: boolean;
-  readonly pluginId?: string;
+  readonly topOffsetPx: number;
+  readonly reorderable?: boolean;
 }
 
 export class TrackPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = true;
-  private previousTrackContext?: TrackRenderContext;
+  readonly trackNode?: TrackNode;
 
-  constructor(private readonly attrs: TrackPanelAttrs) {}
+  private readonly attrs: TrackPanelAttrs;
 
-  get trackKey(): string {
-    return this.attrs.trackKey;
+  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 attrs = this.attrs;
+    const {
+      node,
+      indentationLevel,
+      trackRenderer,
+      revealOnCreate,
+      topOffsetPx,
+      reorderable = false,
+    } = this.attrs;
 
-    if (attrs.trackFSM) {
-      if (attrs.trackFSM.getError()) {
-        return m(TrackComponent, {
-          title: attrs.title,
-          trackKey: attrs.trackKey,
-          error: attrs.trackFSM.getError(),
-          track: attrs.trackFSM.track,
-          closeable: attrs.closeable,
-          chips: attrs.chips,
-          pluginId: attrs.pluginId,
-        });
-      }
-      return m(TrackComponent, {
-        trackKey: attrs.trackKey,
-        title: attrs.title,
-        heightPx: attrs.trackFSM.track.getHeight(),
-        buttons: attrs.trackFSM.track.getTrackShellButtons?.(),
-        tags: attrs.tags,
-        track: attrs.trackFSM.track,
-        error: attrs.trackFSM.getError(),
-        revealOnCreate: attrs.revealOnCreate,
-        closeable: attrs.closeable,
-        chips: attrs.chips,
-        pluginId: attrs.pluginId,
-      });
-    } else {
-      return m(TrackComponent, {
-        trackKey: attrs.trackKey,
-        title: attrs.title,
-        revealOnCreate: attrs.revealOnCreate,
-        closeable: attrs.closeable,
-        chips: attrs.chips,
-        pluginId: attrs.pluginId,
-      });
+    const error = trackRenderer?.getError();
+
+    const buttons = [
+      SHOW_TRACK_DETAILS_BUTTON.get() &&
+        renderTrackDetailsButton(node, trackRenderer?.desc),
+      trackRenderer?.track.getTrackShellButtons?.(),
+      node.removable && renderCloseButton(node),
+      // Can't pin groups.. yet!
+      !node.hasChildren && 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,
+      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.scheduleRedraw();
+      },
+      onTrackContentMouseOut: () => {
+        trackRenderer?.track.onMouseOut?.();
+        raf.scheduleRedraw();
+      },
+      onTrackContentClick: (pos, bounds) => {
+        const timescale = this.getTimescaleForBounds(bounds);
+        raf.scheduleRedraw();
+        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);
+        }
+      },
+    });
   }
 
-  highlightIfTrackSelected(
+  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.flatTracks.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,
-    size: Size,
+    node: TrackNode,
+    size: Size2D,
   ) {
-    const selection = globals.state.selection;
+    const selection = this.attrs.trace.selection.selection;
     if (selection.kind !== 'area') {
       return;
     }
-    const selectedAreaDuration = selection.end - selection.start;
-    if (selection.tracks.includes(this.attrs.trackKey)) {
+
+    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),
@@ -568,180 +287,180 @@
     }
   }
 
-  renderCanvas(ctx: CanvasRenderingContext2D, size: Size) {
-    const trackSize = {...size, width: size.width - TRACK_SHELL_WIDTH};
-
-    ctx.save();
-    ctx.translate(TRACK_SHELL_WIDTH, 0);
-    canvasClip(ctx, 0, 0, trackSize.width, trackSize.height);
-
-    const visibleWindow = globals.timeline.visibleWindow;
-    const timespan = visibleWindow.toTimeSpan();
-    const timescale = new TimeScale(
-      visibleWindow,
-      new PxSpan(0, trackSize.width),
-    );
-    drawGridLines(ctx, timespan, timescale, trackSize);
-
-    const track = this.attrs.trackFSM;
-
-    if (track !== undefined) {
-      const trackRenderCtx: TrackRenderContext = {
-        trackKey: track.trackKey,
-        visibleWindow,
-        size: trackSize,
-        resolution: calculateResolution(visibleWindow, trackSize.width),
-        ctx,
-        timescale,
-      };
-      this.previousTrackContext = trackRenderCtx;
-      if (!track.getError()) {
-        track.render(trackRenderCtx);
-      }
-    } else {
-      checkerboard(ctx, trackSize.height, 0, trackSize.width);
-    }
-
-    this.highlightIfTrackSelected(ctx, timescale, trackSize);
-
-    // Draw vertical line when hovering on the notes panel.
-    renderHoveredNoteVertical(ctx, timescale, trackSize);
-    renderHoveredCursorVertical(ctx, timescale, trackSize);
-    renderWakeupVertical(ctx, timescale, trackSize);
-    renderNoteVerticals(ctx, timescale, trackSize);
-
-    ctx.restore();
-  }
-
-  getSliceRect(tStart: time, tDur: time, depth: number): SliceRect | undefined {
-    if (
-      this.attrs.trackFSM === undefined ||
-      this.previousTrackContext === undefined
-    ) {
-      return undefined;
-    }
-    return this.attrs.trackFSM.track.getSliceRect?.(
-      this.previousTrackContext,
-      tStart,
-      tDur,
-      depth,
-    );
-  }
-}
-
-export function drawGridLines(
-  ctx: CanvasRenderingContext2D,
-  timespan: TimeSpan,
-  timescale: TimeScale,
-  size: Size,
-): void {
-  ctx.strokeStyle = TRACK_BORDER_COLOR;
-  ctx.lineWidth = 1;
-
-  if (size.width > 0 && timespan.duration > 0n) {
-    const maxMajorTicks = getMaxMajorTicks(size.width);
-    const offset = globals.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();
+  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;
   }
 }
 
-export function renderHoveredCursorVertical(
-  ctx: CanvasRenderingContext2D,
-  timescale: TimeScale,
-  size: Size,
-) {
-  if (globals.state.hoverCursorTimestamp !== -1n) {
-    drawVerticalLineAtTime(
-      ctx,
-      timescale,
-      globals.state.hoverCursorTimestamp,
-      size.height,
-      `#344596`,
-    );
-  }
+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.
+    ),
+  );
 }
 
-export function renderHoveredNoteVertical(
-  ctx: CanvasRenderingContext2D,
-  timescale: TimeScale,
-  size: Size,
-) {
-  if (globals.state.hoveredNoteTimestamp !== -1n) {
-    drawVerticalLineAtTime(
-      ctx,
-      timescale,
-      globals.state.hoveredNoteTimestamp,
-      size.height,
-      `#aaa`,
-    );
-  }
+function renderCloseButton(node: TrackNode) {
+  return m(Button, {
+    onclick: (e) => {
+      node.remove();
+      e.stopPropagation();
+    },
+    icon: Icons.Close,
+    title: 'Close track',
+    compact: true,
+  });
 }
 
-export function renderWakeupVertical(
-  ctx: CanvasRenderingContext2D,
-  timescale: TimeScale,
-  size: Size,
-) {
-  const currentSelection = getLegacySelection(globals.state);
-  if (currentSelection !== null) {
-    if (
-      currentSelection.kind === 'SCHED_SLICE' &&
-      globals.sliceDetails.wakeupTs !== undefined
-    ) {
-      drawVerticalLineAtTime(
-        ctx,
-        timescale,
-        globals.sliceDetails.wakeupTs,
-        size.height,
-        `black`,
-      );
-    }
-  }
+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,
+  });
 }
 
-export function renderNoteVerticals(
-  ctx: CanvasRenderingContext2D,
-  timescale: TimeScale,
-  size: Size,
-) {
-  // All marked areas should have semi-transparent vertical lines
-  // marking the start and end.
-  for (const note of Object.values(globals.state.notes)) {
-    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,
-      );
-    }
+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/tracks/custom_sql_table_slice_track.ts b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
index 832fe44..7d3caa3 100644
--- a/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
+++ b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
@@ -12,18 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Actions} from '../../common/actions';
-import {generateSqlWithInternalLayout} from '../../common/internal_layout_utils';
-import {LegacySelection} from '../../common/state';
-import {OnSliceClickArgs} from '../base_slice_track';
-import {GenericSliceDetailsTabConfigBase} from '../generic_slice_details_tab';
-import {globals} from '../globals';
+import {generateSqlWithInternalLayout} from '../../trace_processor/sql_utils/layout';
 import {NAMED_ROW, NamedRow, NamedSliceTrack} from '../named_slice_track';
 import {NewTrackArgs} from '../track';
 import {createView} from '../../trace_processor/sql_utils';
-import {Slice} from '../../public';
-import {uuidv4} from '../../base/uuid';
+import {Slice} from '../../public/track';
 import {AsyncDisposableStack} from '../../base/disposable_stack';
+import {sqlNameSafe} from '../../base/string_utils';
 
 export interface CustomSqlImportConfig {
   modules: string[];
@@ -38,19 +33,17 @@
   disposable?: AsyncDisposable;
 }
 
-export interface CustomSqlDetailsPanelConfig {
-  // Type of details panel to create
-  kind: string;
-  // Config for the details panel
-  config: GenericSliceDetailsTabConfigBase;
-}
-
 export abstract class CustomSqlTableSliceTrack extends NamedSliceTrack<
   Slice,
   NamedRow
 > {
   protected readonly tableName;
 
+  constructor(args: NewTrackArgs) {
+    super(args);
+    this.tableName = `customsqltableslicetrack_${sqlNameSafe(args.uri)}`;
+  }
+
   getRowSpec(): NamedRow {
     return NAMED_ROW;
   }
@@ -59,22 +52,10 @@
     return this.rowToSliceBase(row);
   }
 
-  constructor(args: NewTrackArgs) {
-    super(args);
-    this.tableName = `customsqltableslicetrack_${uuidv4()
-      .split('-')
-      .join('_')}`;
-  }
-
   abstract getSqlDataSource():
     | CustomSqlTableDefConfig
     | Promise<CustomSqlTableDefConfig>;
 
-  // Override by subclasses.
-  abstract getDetailsPanel(
-    args: OnSliceClickArgs<Slice>,
-  ): CustomSqlDetailsPanelConfig;
-
   getSqlImports(): CustomSqlImportConfig {
     return {
       modules: [] as string[],
@@ -110,34 +91,6 @@
     return `SELECT * FROM ${this.tableName}`;
   }
 
-  isSelectionHandled(selection: LegacySelection) {
-    if (selection.kind !== 'GENERIC_SLICE') {
-      return false;
-    }
-    return selection.trackKey === this.trackKey;
-  }
-
-  onSliceClick(args: OnSliceClickArgs<Slice>) {
-    if (this.getDetailsPanel(args) === undefined) {
-      return;
-    }
-
-    const detailsPanelConfig = this.getDetailsPanel(args);
-    globals.makeSelection(
-      Actions.selectGenericSlice({
-        id: args.slice.id,
-        sqlTableName: this.tableName,
-        start: args.slice.ts,
-        duration: args.slice.dur,
-        trackKey: this.trackKey,
-        detailsPanelConfig: {
-          kind: detailsPanelConfig.kind,
-          config: detailsPanelConfig.config,
-        },
-      }),
-    );
-  }
-
   async loadImports() {
     for (const importModule of this.getSqlImports().modules) {
       await this.engine.query(`INCLUDE PERFETTO MODULE ${importModule};`);
diff --git a/ui/src/frontend/ui_main.ts b/ui/src/frontend/ui_main.ts
new file mode 100644
index 0000000..954da1a
--- /dev/null
+++ b/ui/src/frontend/ui_main.ts
@@ -0,0 +1,746 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {copyToClipboard} from '../base/clipboard';
+import {findRef} from '../base/dom_utils';
+import {FuzzyFinder} from '../base/fuzzy';
+import {assertExists, assertUnreachable} from '../base/logging';
+import {undoCommonChatAppReplacements} from '../base/string_utils';
+import {
+  DurationPrecision,
+  setDurationPrecision,
+  setTimestampFormat,
+  TimestampFormat,
+} 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';
+import {maybeRenderFullscreenModalDialog, showModal} from '../widgets/modal';
+import {CookieConsent} from './cookie_consent';
+import {toggleHelp} from './help_modal';
+import {Omnibox, OmniboxOption} from './omnibox';
+import {addQueryResultsTab} from '../public/lib/query_table/query_result_tab';
+import {Sidebar} from './sidebar';
+import {Topbar} from './topbar';
+import {shareTrace} from './trace_share_utils';
+import {AggregationsTabs} from './aggregation_tab';
+import {OmniboxMode} from '../core/omnibox_manager';
+import {PromptOption} from '../public/omnibox';
+import {DisposableStack} from '../base/disposable_stack';
+import {Spinner} from '../widgets/spinner';
+import {TraceImpl} from '../core/trace_impl';
+import {AppImpl} from '../core/app_impl';
+import {NotesEditorTab} from './notes_panel';
+import {NotesListEditor} from './notes_list_editor';
+import {getTimeSpanOfSelectionOrVisibleWindow} from '../public/utils';
+
+const OMNIBOX_INPUT_REF = 'omnibox';
+
+// This wrapper creates a new instance of UiMainPerTrace for each new trace
+// loaded (including the case of no trace at the beginning).
+export class UiMain implements m.ClassComponent {
+  view({children}: m.CVnode) {
+    const currentTraceId = AppImpl.instance.trace?.engine.engineId ?? '';
+    return [m(UiMainPerTrace, {key: currentTraceId}, children)];
+  }
+}
+
+// This components gets destroyed and recreated every time the current trace
+// changes. Note that in the beginning the current trace is undefined.
+export class UiMainPerTrace implements m.ClassComponent {
+  // NOTE: this should NOT need to be an AsyncDisposableStack. If you feel the
+  // need of making it async because you want to clean up SQL resources, that
+  // will cause bugs (see comments in oncreate()).
+  private trash = new DisposableStack();
+  private omniboxInputEl?: HTMLInputElement;
+  private recentCommands: string[] = [];
+  private trace?: TraceImpl;
+
+  // This function is invoked once per trace.
+  constructor() {
+    const app = AppImpl.instance;
+    const trace = app.trace;
+    this.trace = trace;
+
+    // Register global commands (commands that are useful even without a trace
+    // loaded).
+    const globalCmds: Command[] = [
+      {
+        id: 'perfetto.OpenCommandPalette',
+        name: 'Open command palette',
+        callback: () => app.omnibox.setMode(OmniboxMode.Command),
+        defaultHotkey: '!Mod+Shift+P',
+      },
+
+      {
+        id: 'perfetto.ShowHelp',
+        name: 'Show help',
+        callback: () => toggleHelp(),
+        defaultHotkey: '?',
+      },
+    ];
+    globalCmds.forEach((cmd) => {
+      this.trash.use(app.commands.registerCommand(cmd));
+    });
+
+    // When the UI loads there is no trace. There is no point registering
+    // commands or anything in this state as they will be useless.
+    if (trace === undefined) return;
+    document.title = `${trace.traceInfo.traceTitle || 'Trace'} - Perfetto UI`;
+    this.maybeShowJsonWarning();
+
+    // Register the aggregation tabs.
+    this.trash.use(new AggregationsTabs(trace));
+
+    // Register the notes manager+editor.
+    this.trash.use(trace.tabs.registerDetailsPanel(new NotesEditorTab(trace)));
+
+    this.trash.use(
+      trace.tabs.registerTab({
+        uri: 'notes.manager',
+        isEphemeral: false,
+        content: {
+          getTitle: () => 'Notes & markers',
+          render: () => m(NotesListEditor, {trace}),
+        },
+      }),
+    );
+
+    const cmds: Command[] = [
+      {
+        id: 'perfetto.SetTimestampFormat',
+        name: 'Set timestamp and duration format',
+        callback: async () => {
+          const options: PromptOption[] = [
+            {key: TimestampFormat.Timecode, displayName: 'Timecode'},
+            {key: TimestampFormat.UTC, displayName: 'Realtime (UTC)'},
+            {
+              key: TimestampFormat.TraceTz,
+              displayName: 'Realtime (Trace TZ)',
+            },
+            {key: TimestampFormat.Seconds, displayName: 'Seconds'},
+            {key: TimestampFormat.Milliseoncds, displayName: 'Milliseconds'},
+            {key: TimestampFormat.Microseconds, displayName: 'Microseconds'},
+            {key: TimestampFormat.TraceNs, displayName: 'Trace nanoseconds'},
+            {
+              key: TimestampFormat.TraceNsLocale,
+              displayName:
+                'Trace nanoseconds (with locale-specific formatting)',
+            },
+          ];
+          const promptText = 'Select format...';
+
+          const result = await app.omnibox.prompt(promptText, options);
+          if (result === undefined) return;
+          setTimestampFormat(result as TimestampFormat);
+          raf.scheduleFullRedraw();
+        },
+      },
+      {
+        id: 'perfetto.SetDurationPrecision',
+        name: 'Set duration precision',
+        callback: async () => {
+          const options: PromptOption[] = [
+            {key: DurationPrecision.Full, displayName: 'Full'},
+            {
+              key: DurationPrecision.HumanReadable,
+              displayName: 'Human readable',
+            },
+          ];
+          const promptText = 'Select duration precision mode...';
+
+          const result = await app.omnibox.prompt(promptText, options);
+          if (result === undefined) return;
+          setDurationPrecision(result as DurationPrecision);
+          raf.scheduleFullRedraw();
+        },
+      },
+      {
+        id: 'perfetto.TogglePerformanceMetrics',
+        name: 'Toggle performance metrics',
+        callback: () => app.setPerfDebuggingEnabled(!app.perfDebugging),
+      },
+      {
+        id: 'perfetto.ShareTrace',
+        name: 'Share trace',
+        callback: shareTrace,
+      },
+      {
+        id: 'perfetto.SearchNext',
+        name: 'Go to next search result',
+        callback: () => {
+          trace.search.stepForward();
+        },
+        defaultHotkey: 'Enter',
+      },
+      {
+        id: 'perfetto.SearchPrev',
+        name: 'Go to previous search result',
+        callback: () => {
+          trace.search.stepBackwards();
+        },
+        defaultHotkey: 'Shift+Enter',
+      },
+      {
+        id: 'perfetto.RunQuery',
+        name: 'Run query',
+        callback: () => trace.omnibox.setMode(OmniboxMode.Query),
+      },
+      {
+        id: 'perfetto.Search',
+        name: 'Search',
+        callback: () => trace.omnibox.setMode(OmniboxMode.Search),
+        defaultHotkey: '/',
+      },
+      {
+        id: 'perfetto.CopyTimeWindow',
+        name: `Copy selected time window to clipboard`,
+        callback: async () => {
+          const window = await getTimeSpanOfSelectionOrVisibleWindow(trace);
+          const query = `ts >= ${window.start} and ts < ${window.end}`;
+          copyToClipboard(query);
+        },
+      },
+      {
+        id: 'perfetto.FocusSelection',
+        name: 'Focus current selection',
+        callback: () => trace.selection.scrollToCurrentSelection(),
+        defaultHotkey: 'F',
+      },
+      {
+        id: 'perfetto.Deselect',
+        name: 'Deselect',
+        callback: () => {
+          trace.selection.clear();
+        },
+        defaultHotkey: 'Escape',
+      },
+      {
+        id: 'perfetto.SetTemporarySpanNote',
+        name: 'Set the temporary span note based on the current selection',
+        callback: () => {
+          const range = trace.selection.findTimeRangeOfSelection();
+          if (range) {
+            trace.notes.addSpanNote({
+              start: range.start,
+              end: range.end,
+              id: '__temp__',
+            });
+          }
+        },
+        defaultHotkey: 'M',
+      },
+      {
+        id: 'perfetto.AddSpanNote',
+        name: 'Add a new span note based on the current selection',
+        callback: () => {
+          const range = trace.selection.findTimeRangeOfSelection();
+          if (range) {
+            trace.notes.addSpanNote({
+              start: range.start,
+              end: range.end,
+            });
+          }
+        },
+        defaultHotkey: 'Shift+M',
+      },
+      {
+        id: 'perfetto.RemoveSelectedNote',
+        name: 'Remove selected note',
+        callback: () => {
+          const selection = trace.selection.selection;
+          if (selection.kind === 'note') {
+            trace.notes.removeNote(selection.id);
+          }
+        },
+        defaultHotkey: 'Delete',
+      },
+      {
+        id: 'perfetto.NextFlow',
+        name: 'Next flow',
+        callback: () => trace.flows.focusOtherFlow('Forward'),
+        defaultHotkey: 'Mod+]',
+      },
+      {
+        id: 'perfetto.PrevFlow',
+        name: 'Prev flow',
+        callback: () => trace.flows.focusOtherFlow('Backward'),
+        defaultHotkey: 'Mod+[',
+      },
+      {
+        id: 'perfetto.MoveNextFlow',
+        name: 'Move next flow',
+        callback: () => trace.flows.moveByFocusedFlow('Forward'),
+        defaultHotkey: ']',
+      },
+      {
+        id: 'perfetto.MovePrevFlow',
+        name: 'Move prev flow',
+        callback: () => trace.flows.moveByFocusedFlow('Backward'),
+        defaultHotkey: '[',
+      },
+      {
+        id: 'perfetto.SelectAll',
+        name: 'Select all',
+        callback: () => {
+          // This is a dual state command:
+          // - If one ore more tracks are already area selected, expand the time
+          //   range to include the entire trace, but keep the selection on just
+          //   these tracks.
+          // - If nothing is selected, or all selected tracks are entirely
+          //   selected, then select the entire trace. This allows double tapping
+          //   Ctrl+A to select the entire track, then select the entire trace.
+          let tracksToSelect: string[];
+          const selection = trace.selection.selection;
+          if (selection.kind === 'area') {
+            // Something is already selected, let's see if it covers the entire
+            // span of the trace or not
+            const coversEntireTimeRange =
+              trace.traceInfo.start === selection.start &&
+              trace.traceInfo.end === selection.end;
+            if (!coversEntireTimeRange) {
+              // If the current selection is an area which does not cover the
+              // entire time range, preserve the list of selected tracks and
+              // expand the time range.
+              tracksToSelect = selection.trackUris;
+            } else {
+              // If the entire time range is already covered, update the selection
+              // to cover all tracks.
+              tracksToSelect = trace.workspace.flatTracks
+                .map((t) => t.uri)
+                .filter((uri) => uri !== undefined);
+            }
+          } else {
+            // If the current selection is not an area, select all.
+            tracksToSelect = trace.workspace.flatTracks
+              .map((t) => t.uri)
+              .filter((uri) => uri !== undefined);
+          }
+          const {start, end} = trace.traceInfo;
+          trace.selection.selectArea({
+            start,
+            end,
+            trackUris: tracksToSelect,
+          });
+        },
+        defaultHotkey: 'Mod+A',
+      },
+      {
+        id: 'perfetto.ConvertSelectionToArea',
+        name: 'Convert the current selection to an area selection',
+        callback: () => {
+          const selection = trace.selection.selection;
+          const range = trace.selection.findTimeRangeOfSelection();
+          if (selection.kind === 'track_event' && range) {
+            trace.selection.selectArea({
+              start: range.start,
+              end: range.end,
+              trackUris: [selection.trackUri],
+            });
+          }
+        },
+        // TODO(stevegolton): Decide on a sensible hotkey.
+        // defaultHotkey: 'L',
+      },
+      {
+        id: 'perfetto.ToggleDrawer',
+        name: 'Toggle drawer',
+        defaultHotkey: 'Q',
+        callback: () => trace.tabs.toggleTabPanelVisibility(),
+      },
+    ];
+
+    // Register each command with the command manager
+    cmds.forEach((cmd) => {
+      this.trash.use(trace.commands.registerCommand(cmd));
+    });
+  }
+
+  private renderOmnibox(): m.Children {
+    const omnibox = AppImpl.instance.omnibox;
+    const omniboxMode = omnibox.mode;
+    const statusMessage = omnibox.statusMessage;
+    if (statusMessage !== undefined) {
+      return m(
+        `.omnibox.message-mode`,
+        m(`input[readonly][disabled][ref=omnibox]`, {
+          value: '',
+          placeholder: statusMessage,
+        }),
+      );
+    } else if (omniboxMode === OmniboxMode.Command) {
+      return this.renderCommandOmnibox();
+    } else if (omniboxMode === OmniboxMode.Prompt) {
+      return this.renderPromptOmnibox();
+    } else if (omniboxMode === OmniboxMode.Query) {
+      return this.renderQueryOmnibox();
+    } else if (omniboxMode === OmniboxMode.Search) {
+      return this.renderSearchOmnibox();
+    } else {
+      assertUnreachable(omniboxMode);
+    }
+  }
+
+  renderPromptOmnibox(): m.Children {
+    const omnibox = AppImpl.instance.omnibox;
+    const prompt = assertExists(omnibox.pendingPrompt);
+
+    let options: OmniboxOption[] | undefined = undefined;
+
+    if (prompt.options) {
+      const fuzzy = new FuzzyFinder(
+        prompt.options,
+        ({displayName}) => displayName,
+      );
+      const result = fuzzy.find(omnibox.text);
+      options = result.map((result) => {
+        return {
+          key: result.item.key,
+          displayName: result.segments,
+        };
+      });
+    }
+
+    return m(Omnibox, {
+      value: omnibox.text,
+      placeholder: prompt.text,
+      inputRef: OMNIBOX_INPUT_REF,
+      extraClasses: 'prompt-mode',
+      closeOnOutsideClick: true,
+      options,
+      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);
+      },
+      onClose: () => {
+        omnibox.rejectPrompt();
+      },
+    });
+  }
+
+  renderCommandOmnibox(): m.Children {
+    // Fuzzy-filter commands by the filter string.
+    const {commands, omnibox} = AppImpl.instance;
+    const filteredCmds = commands.fuzzyFilterCommands(omnibox.text);
+
+    // Create an array of commands with attached heuristics from the recent
+    // command register.
+    const commandsWithHeuristics = filteredCmds.map((cmd) => {
+      return {
+        recentsIndex: this.recentCommands.findIndex((id) => id === cmd.id),
+        cmd,
+      };
+    });
+
+    // Sort by recentsIndex then by alphabetical order
+    const sorted = commandsWithHeuristics.sort((a, b) => {
+      if (b.recentsIndex === a.recentsIndex) {
+        return a.cmd.name.localeCompare(b.cmd.name);
+      } else {
+        return b.recentsIndex - a.recentsIndex;
+      }
+    });
+
+    const options = sorted.map(({recentsIndex, cmd}): OmniboxOption => {
+      const {segments, id, defaultHotkey} = cmd;
+      return {
+        key: id,
+        displayName: segments,
+        tag: recentsIndex !== -1 ? 'recently used' : undefined,
+        rightContent: defaultHotkey && m(HotkeyGlyphs, {hotkey: defaultHotkey}),
+      };
+    });
+
+    return m(Omnibox, {
+      value: omnibox.text,
+      placeholder: 'Filter commands...',
+      inputRef: OMNIBOX_INPUT_REF,
+      extraClasses: 'command-mode',
+      options,
+      closeOnSubmit: true,
+      closeOnOutsideClick: true,
+      selectedOptionIndex: omnibox.selectionIndex,
+      onSelectedOptionChanged: (index) => {
+        omnibox.setSelectionIndex(index);
+        raf.scheduleFullRedraw();
+      },
+      onInput: (value) => {
+        omnibox.setText(value);
+        omnibox.setSelectionIndex(0);
+        raf.scheduleFullRedraw();
+      },
+      onClose: () => {
+        if (this.omniboxInputEl) {
+          this.omniboxInputEl.blur();
+        }
+        omnibox.reset();
+      },
+      onSubmit: (key: string) => {
+        this.addRecentCommand(key);
+        commands.runCommand(key);
+      },
+      onGoBack: () => {
+        omnibox.reset();
+      },
+    });
+  }
+
+  private addRecentCommand(id: string): void {
+    this.recentCommands = this.recentCommands.filter((x) => x !== id);
+    this.recentCommands.push(id);
+    while (this.recentCommands.length > 6) {
+      this.recentCommands.shift();
+    }
+  }
+
+  renderQueryOmnibox(): m.Children {
+    const ph = 'e.g. select * from sched left join thread using(utid) limit 10';
+    return m(Omnibox, {
+      value: AppImpl.instance.omnibox.text,
+      placeholder: ph,
+      inputRef: OMNIBOX_INPUT_REF,
+      extraClasses: 'query-mode',
+
+      onInput: (value) => {
+        AppImpl.instance.omnibox.setText(value);
+        raf.scheduleFullRedraw();
+      },
+      onSubmit: (query, alt) => {
+        const config = {
+          query: undoCommonChatAppReplacements(query),
+          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);
+      },
+      onClose: () => {
+        AppImpl.instance.omnibox.setText('');
+        if (this.omniboxInputEl) {
+          this.omniboxInputEl.blur();
+        }
+        AppImpl.instance.omnibox.reset();
+        raf.scheduleFullRedraw();
+      },
+      onGoBack: () => {
+        AppImpl.instance.omnibox.reset();
+      },
+    });
+  }
+
+  renderSearchOmnibox(): m.Children {
+    return m(Omnibox, {
+      value: AppImpl.instance.omnibox.text,
+      placeholder: "Search or type '>' for commands or ':' for SQL mode",
+      inputRef: OMNIBOX_INPUT_REF,
+      onInput: (value, _prev) => {
+        if (value === '>') {
+          AppImpl.instance.omnibox.setMode(OmniboxMode.Command);
+          return;
+        } else if (value === ':') {
+          AppImpl.instance.omnibox.setMode(OmniboxMode.Query);
+          return;
+        }
+        AppImpl.instance.omnibox.setText(value);
+        if (this.trace === undefined) return; // No trace loaded.
+        if (value.length >= 4) {
+          this.trace.search.search(value);
+        } else {
+          this.trace.search.reset();
+        }
+      },
+      onClose: () => {
+        if (this.omniboxInputEl) {
+          this.omniboxInputEl.blur();
+        }
+      },
+      onSubmit: (value, _mod, shift) => {
+        if (this.trace === undefined) return; // No trace loaded.
+        this.trace.search.search(value);
+        if (shift) {
+          this.trace.search.stepBackwards();
+        } else {
+          this.trace.search.stepForward();
+        }
+        if (this.omniboxInputEl) {
+          this.omniboxInputEl.blur();
+        }
+      },
+      rightContent: this.renderStepThrough(),
+    });
+  }
+
+  private renderStepThrough() {
+    const children = [];
+    const results = this.trace?.search.searchResults;
+    if (this.trace?.search.searchInProgress) {
+      children.push(m('.current', m(Spinner)));
+    } else if (results !== undefined) {
+      const searchMgr = assertExists(this.trace).search;
+      const index = searchMgr.resultIndex;
+      const total = results.totalResults ?? 0;
+      children.push(
+        m('.current', `${total === 0 ? '0 / 0' : `${index + 1} / ${total}`}`),
+        m(
+          'button',
+          {
+            onclick: () => searchMgr.stepBackwards(),
+          },
+          m('i.material-icons.left', 'keyboard_arrow_left'),
+        ),
+        m(
+          'button',
+          {
+            onclick: () => searchMgr.stepForward(),
+          },
+          m('i.material-icons.right', 'keyboard_arrow_right'),
+        ),
+      );
+    }
+    return m('.stepthrough', children);
+  }
+
+  oncreate(vnode: m.VnodeDOM) {
+    this.updateOmniboxInputRef(vnode.dom);
+    this.maybeFocusOmnibar();
+  }
+
+  view({children}: m.Vnode): m.Children {
+    const hotkeys: HotkeyConfig[] = [];
+    for (const {id, defaultHotkey} of AppImpl.instance.commands.commands) {
+      if (defaultHotkey) {
+        hotkeys.push({
+          callback: () => AppImpl.instance.commands.runCommand(id),
+          hotkey: defaultHotkey,
+        });
+      }
+    }
+
+    return m(
+      HotkeyContext,
+      {hotkeys},
+      m(
+        'main',
+        m(Sidebar, {trace: this.trace}),
+        m(Topbar, {
+          omnibox: this.renderOmnibox(),
+          trace: this.trace,
+        }),
+        children,
+        m(CookieConsent),
+        maybeRenderFullscreenModalDialog(),
+        AppImpl.instance.perfDebugging && m('.perf-stats'),
+      ),
+    );
+  }
+
+  onupdate({dom}: m.VnodeDOM) {
+    this.updateOmniboxInputRef(dom);
+    this.maybeFocusOmnibar();
+  }
+
+  onremove(_: m.VnodeDOM) {
+    this.omniboxInputEl = undefined;
+
+    // NOTE: if this becomes ever an asyncDispose(), then the promise needs to
+    // be returned to onbeforeremove, so mithril delays the removal until
+    // the promise is resolved, but then also the UiMain wrapper needs to be
+    // more complex to linearize the destruction of the old instane with the
+    // creation of the new one, without overlaps.
+    // However, we should not add disposables that issue cleanup queries on the
+    // Engine. Doing so is: (1) useless: we throw away the whole wasm instance
+    // on each trace load, so what's the point of deleting tables from a TP
+    // instance that is going to be destroyed?; (2) harmful: we don't have
+    // precise linearization with the wasm teardown, so we might end up awaiting
+    // forever for the asyncDispose() because the query will never run.
+    this.trash.dispose();
+  }
+
+  private updateOmniboxInputRef(dom: Element): void {
+    const el = findRef(dom, OMNIBOX_INPUT_REF);
+    if (el && el instanceof HTMLInputElement) {
+      this.omniboxInputEl = el;
+    }
+  }
+
+  private maybeFocusOmnibar() {
+    if (AppImpl.instance.omnibox.focusOmniboxNextRender) {
+      const omniboxEl = this.omniboxInputEl;
+      if (omniboxEl) {
+        omniboxEl.focus();
+        if (AppImpl.instance.omnibox.pendingCursorPlacement === undefined) {
+          omniboxEl.select();
+        } else {
+          omniboxEl.setSelectionRange(
+            AppImpl.instance.omnibox.pendingCursorPlacement,
+            AppImpl.instance.omnibox.pendingCursorPlacement,
+          );
+        }
+      }
+      AppImpl.instance.omnibox.clearFocusFlag();
+    }
+  }
+
+  private async maybeShowJsonWarning() {
+    // Show warning if the trace is in JSON format.
+    const isJsonTrace = this.trace?.traceInfo.traceType === 'json';
+    const SHOWN_JSON_WARNING_KEY = 'shownJsonWarning';
+
+    if (
+      !isJsonTrace ||
+      window.localStorage.getItem(SHOWN_JSON_WARNING_KEY) === 'true' ||
+      AppImpl.instance.embeddedMode
+    ) {
+      // When in embedded mode, the host app will control which trace format
+      // it passes to Perfetto, so we don't need to show this warning.
+      return;
+    }
+
+    // Save that the warning has been shown. Value is irrelevant since only
+    // the presence of key is going to be checked.
+    window.localStorage.setItem(SHOWN_JSON_WARNING_KEY, 'true');
+
+    showModal({
+      title: 'Warning',
+      content: m(
+        'div',
+        m(
+          'span',
+          'Perfetto UI features are limited for JSON traces. ',
+          'We recommend recording ',
+          m(
+            'a',
+            {href: 'https://perfetto.dev/docs/quickstart/chrome-tracing'},
+            'proto-format traces',
+          ),
+          ' from Chrome.',
+        ),
+        m('br'),
+      ),
+      buttons: [],
+    });
+  }
+}
diff --git a/ui/src/frontend/value.ts b/ui/src/frontend/value.ts
index 11043b2..40ad1f4 100644
--- a/ui/src/frontend/value.ts
+++ b/ui/src/frontend/value.ts
@@ -13,10 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {Tree, TreeNode} from '../widgets/tree';
-
-import {PopupMenuButton, PopupMenuItem} from './popup_menu';
+import {PopupMenuButton, PopupMenuItem} from '../widgets/popup_menu';
 
 // This file implements a component for rendering JSON-like values (with
 // customisation options like context menu and action buttons).
diff --git a/ui/src/frontend/vertical_line_helper.ts b/ui/src/frontend/vertical_line_helper.ts
index c4666d6..9492bad 100644
--- a/ui/src/frontend/vertical_line_helper.ts
+++ b/ui/src/frontend/vertical_line_helper.ts
@@ -13,8 +13,7 @@
 // limitations under the License.
 
 import {time} from '../base/time';
-
-import {TimeScale} from './time_scale';
+import {TimeScale} from '../base/time_scale';
 
 export function drawVerticalLineAtTime(
   ctx: CanvasRenderingContext2D,
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index af8d5e9..75f2aba 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -12,39 +12,39 @@
 // 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} from '../base/time';
-import {Actions} from '../common/actions';
-import {TrackCacheEntry} from '../common/track_cache';
+import {Time, TimeSpan} from '../base/time';
+import {TimeScale} from '../base/time_scale';
 import {featureFlags} from '../core/feature_flags';
 import {raf} from '../core/raf_scheduler';
-import {TrackTags} from '../public';
-
-import {TRACK_SHELL_WIDTH} from './css_constants';
-import {globals} from './globals';
+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 {createPage} from './pages';
 import {PanAndZoomHandler} from './pan_and_zoom_handler';
-import {Panel, PanelContainer, PanelOrGroup} from './panel_container';
-import {publishShowPanningHint} from './publish';
+import {
+  PanelContainer,
+  PanelOrGroup,
+  RenderedPanelInfo,
+} from './panel_container';
 import {TabPanel} from './tab_panel';
 import {TickmarkPanel} from './tickmark_panel';
 import {TimeAxisPanel} from './time_axis_panel';
 import {TimeSelectionPanel} from './time_selection_panel';
-import {DISMISSED_PANNING_HINT_KEY} from './topbar';
-import {TrackGroupPanel} from './track_group_panel';
-import {TrackPanel, getTitleFontSize} from './track_panel';
-import {assertExists} from '../base/logging';
-import {PxSpan, TimeScale} from './time_scale';
-import {TrackGroupState} from '../common/state';
-import {FuzzyFinder, fuzzyMatch, FuzzySegment} from '../base/fuzzy';
-import {exists} from '../base/utils';
-import {EmptyState} from '../widgets/empty_state';
-import {removeFalsyValues} from '../base/array_utils';
+import {TrackPanel} from './track_panel';
+import {drawVerticalLineAtTime} from './vertical_line_helper';
+import {TraceImpl} from '../core/trace_impl';
+import {PageWithTraceImplAttrs} from '../core/page_manager';
+import {AppImpl} from '../core/app_impl';
 
 const OVERVIEW_PANEL_FLAG = featureFlags.register({
   id: 'overviewVisible',
@@ -56,15 +56,16 @@
 // 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 = globals.state.selection;
+  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 = globals.timeline.selectedArea
-      ? globals.timeline.selectedArea
+    const area = trace.timeline.selectedArea
+      ? trace.timeline.selectedArea
       : selection;
     const start = timescale.timeToPx(area.start);
     const end = timescale.timeToPx(area.end);
@@ -80,64 +81,79 @@
   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.
  */
-class TraceViewer implements m.ClassComponent {
+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 overviewTimelinePanel = new OverviewTimelinePanel();
-  private timeAxisPanel = new TimeAxisPanel();
-  private timeSelectionPanel = new TimeSelectionPanel();
-  private notesPanel = new NotesPanel();
-  private tickmarkPanel = new TickmarkPanel();
+  private overviewTimelinePanel: OverviewTimelinePanel;
+  private timeAxisPanel: TimeAxisPanel;
+  private timeSelectionPanel: TimeSelectionPanel;
+  private notesPanel: NotesPanel;
+  private tickmarkPanel: TickmarkPanel;
   private timelineWidthPx?: number;
+  private selectedContainer?: SelectedContainer;
+  private showPanningHint = false;
 
   private readonly PAN_ZOOM_CONTENT_REF = 'pan-and-zoom-content';
 
-  oncreate(vnode: m.CVnodeDOM) {
-    const panZoomElRaw = findRef(vnode.dom, this.PAN_ZOOM_CONTENT_REF);
+  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);
+  }
+
+  oncreate({dom, attrs}: m.CVnodeDOM<PageWithTraceImplAttrs>) {
+    const panZoomElRaw = findRef(dom, this.PAN_ZOOM_CONTENT_REF);
     const panZoomEl = toHTMLElement(assertExists(panZoomElRaw));
 
+    const {top: panTop} = panZoomEl.getBoundingClientRect();
     this.zoomContent = new PanAndZoomHandler({
       element: panZoomEl,
       onPanned: (pannedPx: number) => {
-        const timeline = globals.timeline;
+        const timeline = attrs.trace.timeline;
 
         if (this.timelineWidthPx === undefined) return;
 
         this.keepCurrentSelection = true;
-        const timescale = new TimeScale(
-          timeline.visibleWindow,
-          new PxSpan(0, this.timelineWidthPx),
-        );
+        const timescale = new TimeScale(timeline.visibleWindow, {
+          left: 0,
+          right: this.timelineWidthPx,
+        });
         const tDelta = timescale.pxToDuration(pannedPx);
         timeline.panVisibleWindow(tDelta);
-
-        // If the user has panned they no longer need the hint.
-        localStorage.setItem(DISMISSED_PANNING_HINT_KEY, 'true');
-        raf.scheduleRedraw();
       },
       onZoomed: (zoomedPositionPx: number, zoomRatio: number) => {
-        const timeline = globals.timeline;
+        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 = vnode.dom.getBoundingClientRect();
+        const rect = dom.getBoundingClientRect();
         const centerPoint = zoomPx / (rect.width - TRACK_SHELL_WIDTH);
         timeline.zoomVisibleWindow(1 - zoomRatio, centerPoint);
         raf.scheduleRedraw();
       },
       editSelection: (currentPx: number) => {
         if (this.timelineWidthPx === undefined) return false;
-        const timescale = new TimeScale(
-          globals.timeline.visibleWindow,
-          new PxSpan(0, this.timelineWidthPx),
-        );
-        return onTimeRangeBoundary(timescale, currentPx) !== null;
+        const timescale = new TimeScale(attrs.trace.timeline.visibleWindow, {
+          left: 0,
+          right: this.timelineWidthPx,
+        });
+        return onTimeRangeBoundary(attrs.trace, timescale, currentPx) !== null;
       },
       onSelection: (
         dragStartX: number,
@@ -147,8 +163,8 @@
         currentY: number,
         editing: boolean,
       ) => {
-        const traceTime = globals.traceContext;
-        const timeline = globals.timeline;
+        const traceTime = attrs.trace.traceInfo;
+        const timeline = attrs.trace.timeline;
 
         if (this.timelineWidthPx === undefined) return;
 
@@ -158,22 +174,26 @@
         const timespan = visibleWindow.toTimeSpan();
         this.keepCurrentSelection = true;
 
-        const timescale = new TimeScale(
-          timeline.visibleWindow,
-          new PxSpan(0, this.timelineWidthPx),
-        );
+        const timescale = new TimeScale(timeline.visibleWindow, {
+          left: 0,
+          right: this.timelineWidthPx,
+        });
 
         if (editing) {
-          const selection = globals.state.selection;
+          const selection = attrs.trace.selection.selection;
           if (selection.kind === 'area') {
-            const area = globals.timeline.selectedArea
-              ? globals.timeline.selectedArea
+            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(timescale, prevX);
+            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.
@@ -187,7 +207,7 @@
             timeline.selectArea(
               Time.max(Time.min(keepTime, newTime), traceTime.start),
               Time.min(Time.max(keepTime, newTime), traceTime.end),
-              selection.tracks,
+              selection.trackUris,
             );
           }
         } else {
@@ -200,30 +220,62 @@
             timescale.pxToHpTime(startPx).toTime('floor'),
             timescale.pxToHpTime(endPx).toTime('ceil'),
           );
-          timeline.areaY.start = dragStartY;
-          timeline.areaY.end = currentY;
-          publishShowPanningHint();
+
+          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.scheduleRedraw();
       },
       endSelection: (edit: boolean) => {
-        globals.timeline.areaY.start = undefined;
-        globals.timeline.areaY.end = undefined;
-        const area = globals.timeline.selectedArea;
+        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 = globals.state.selection;
+          const selection = attrs.trace.selection.selection;
           if (selection.kind === 'area' && area) {
-            globals.dispatch(Actions.selectArea({...area}));
+            attrs.trace.selection.selectArea({...area});
           }
         } else if (area) {
-          globals.makeSelection(Actions.selectArea({...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.
-        globals.timeline.deselectArea();
+        attrs.trace.timeline.deselectArea();
         // Full redraw to color track shell.
         raf.scheduleFullRedraw();
       },
@@ -234,8 +286,8 @@
     if (this.zoomContent) this.zoomContent[Symbol.dispose]();
   }
 
-  view() {
-    const scrollingPanels = renderToplevelPanels(globals.state.trackFilterTerm);
+  view({attrs}: m.CVnode<PageWithTraceImplAttrs>) {
+    const scrollingPanels = renderToplevelPanels(attrs.trace);
 
     const result = m(
       '.page.viewer-page',
@@ -249,12 +301,13 @@
               this.keepCurrentSelection = false;
               return;
             }
-            globals.clearSelection();
+            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,
@@ -263,229 +316,327 @@
               this.notesPanel,
               this.tickmarkPanel,
             ]),
+            selectedYRange: this.getYRange('header-panel-container'),
           }),
           m('.scrollbar-spacer-vertical'),
         ),
         m(PanelContainer, {
+          trace: attrs.trace,
           className: 'pinned-panel-container',
-          panels: globals.state.pinnedTracks.map((key) => {
-            const trackBundle = resolveTrack(key);
-            return new TrackPanel({
-              trackKey: key,
-              title: trackBundle.title,
-              tags: trackBundle.tags,
-              trackFSM: trackBundle.trackFSM,
-              revealOnCreate: true,
-              closeable: trackBundle.closeable,
-              chips: trackBundle.chips,
-              pluginId: trackBundle.pluginId,
-            });
+          panels: 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),
+          selectedYRange: this.getYRange('pinned-panel-container'),
         }),
-        scrollingPanels.length === 0 &&
-          filterTermIsValid(globals.state.trackFilterTerm)
-          ? m(
-              EmptyState,
-              {title: 'No matching tracks'},
-              `No tracks match filter term "${globals.state.trackFilterTerm}"`,
-            )
-          : m(PanelContainer, {
-              className: 'scrolling-panel-container',
-              panels: scrollingPanels,
-              onPanelStackResize: (width) => {
-                const timelineWidth = width - TRACK_SHELL_WIDTH;
-                this.timelineWidthPx = timelineWidth;
-              },
-            }),
+        m(PanelContainer, {
+          trace: attrs.trace,
+          className: 'scrolling-panel-container',
+          panels: 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),
+          selectedYRange: this.getYRange('scrolling-panel-container'),
+        }),
       ),
-      m(TabPanel),
+      m(TabPanel, {
+        trace: attrs.trace,
+      }),
+      this.showPanningHint && m(HelpPanningNotification),
     );
 
-    globals.trackManager.flushOldTracks();
+    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),
+    };
+  }
 }
 
-// Given a set of fuzzy matched results, render the matching segments in bold
-function renderFuzzyMatchedTrackTitle(title: FuzzySegment[]): m.Children {
-  return title.map(({matching, value}) => {
-    return matching ? m('b', value) : value;
-  });
+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 filterTermIsValid(
-  filterTerm: undefined | string,
-): filterTerm is string {
-  // Note: Boolean(filterTerm) returns the same result, but this is clearer
-  return filterTerm !== undefined && filterTerm !== '';
-}
+function renderOverlay(
+  trace: TraceImpl,
+  ctx: CanvasRenderingContext2D,
+  canvasSize: Size2D,
+  panels: ReadonlyArray<RenderedPanelInfo>,
+): void {
+  const size = {
+    width: canvasSize.width - TRACK_SHELL_WIDTH,
+    height: canvasSize.height,
+  };
 
-// Split filter term on commas into a list of tokens, cleaning up any whitespace
-// before and after the token and removing any blank tokens
-function tokenizeFilterTerm(term: string): ReadonlyArray<string> {
-  return term
-    .split(',')
-    .map((token) => token.trim())
-    .filter((token) => token.length > 0);
+  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);
+
+  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(filterTerm: string | undefined): PanelOrGroup[] {
-  const scrollingPanels: PanelOrGroup[] = renderTrackPanels(
-    globals.state.scrollingTracks,
-    filterTerm,
-  );
-
-  for (const group of Object.values(globals.state.trackGroups)) {
-    if (filterTermIsValid(filterTerm)) {
-      const tokens = tokenizeFilterTerm(filterTerm);
-      // Match group names that match any of the tokens
-      const result = fuzzyMatch(group.name, ...tokens);
-      if (result.matches) {
-        // If the group name matches, render the entire group as normal
-        const title = renderFuzzyMatchedTrackTitle(result.segments);
-        scrollingPanels.push({
-          kind: 'group',
-          collapsed: group.collapsed,
-          childPanels: group.collapsed ? [] : renderTrackPanels(group.tracks),
-          header: renderTrackGroupPanel(group, true, group.collapsed, title),
-        });
-      } else {
-        // If we are filtering, render the group header only if it contains
-        // matching tracks
-        const childPanels = renderTrackPanels(group.tracks, filterTerm);
-        if (childPanels.length === 0) continue;
-        scrollingPanels.push({
-          kind: 'group',
-          collapsed: false,
-          childPanels,
-          header: renderTrackGroupPanel(group, false, false),
-        });
-      }
-    } else {
-      // Always render the group header, but only render child tracks if not
-      // collapsed
-      scrollingPanels.push({
-        kind: 'group',
-        collapsed: group.collapsed,
-        childPanels: group.collapsed ? [] : renderTrackPanels(group.tracks),
-        header: renderTrackGroupPanel(group, true, group.collapsed),
-      });
-    }
-  }
-
-  return scrollingPanels;
+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 renderTrackPanels(trackKeys: string[], filterTerm?: string): Panel[] {
-  if (filterTermIsValid(filterTerm)) {
-    const tokens = tokenizeFilterTerm(filterTerm);
-    const matcher = new FuzzyFinder(trackKeys, (key) => {
-      return globals.state.tracks[key].name;
-    });
-    // Filter tracks which match any of the tokens
-    const filtered = matcher.find(...tokens);
-    return filtered.map(({item: key, segments}) => {
-      return renderTrackPanel(key, renderFuzzyMatchedTrackTitle(segments));
-    });
-  } else {
-    // No point in applying any filtering...
-    return trackKeys.map((key) => {
-      return renderTrackPanel(key);
-    });
-  }
-}
-
-function renderTrackPanel(key: string, title?: m.Children) {
-  const trackBundle = resolveTrack(key);
-  return new TrackPanel({
-    trackKey: key,
-    title: m(
-      'span',
-      {
-        style: {
-          'font-size': getTitleFontSize(trackBundle.title),
-        },
-      },
-      Boolean(title) ? title : trackBundle.title,
-    ),
-    tags: trackBundle.tags,
-    trackFSM: trackBundle.trackFSM,
-    closeable: trackBundle.closeable,
-    chips: trackBundle.chips,
-    pluginId: trackBundle.pluginId,
+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 renderTrackGroupPanel(
-  group: TrackGroupState,
-  collapsable: boolean,
-  collapsed: boolean,
-  title?: m.Children,
-): TrackGroupPanel {
-  const summaryTrackKey = group.summaryTrack;
+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,
+  });
+}
 
-  if (exists(summaryTrackKey)) {
-    const trackBundle = resolveTrack(summaryTrackKey);
-    return new TrackGroupPanel({
-      groupKey: group.key,
-      trackFSM: trackBundle.trackFSM,
-      subtitle: trackBundle.subtitle,
-      tags: trackBundle.tags,
-      chips: trackBundle.chips,
-      collapsed,
-      title: exists(title) ? title : group.name,
-      tooltip: group.name,
-      collapsable,
-    });
-  } else {
-    return new TrackGroupPanel({
-      groupKey: group.key,
-      collapsed,
-      title: exists(title) ? title : group.name,
-      tooltip: group.name,
-      collapsable,
-    });
+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();
+      }
+    }
   }
 }
 
-// Resolve a track and its metadata through the track cache
-function resolveTrack(key: string): TrackBundle {
-  const trackState = globals.state.tracks[key];
-  const {uri, name, closeable} = trackState;
-  const trackDesc = globals.trackManager.resolveTrackInfo(uri);
-  const trackCacheEntry =
-    trackDesc && globals.trackManager.resolveTrack(key, trackDesc);
-  const trackFSM = trackCacheEntry;
-  const tags = trackCacheEntry?.desc.tags;
-  const subtitle = trackCacheEntry?.desc.subtitle;
-  const chips = trackCacheEntry?.desc.chips;
-  const plugin = trackCacheEntry?.desc.pluginId;
-  return {
-    title: name,
-    subtitle,
-    closeable: closeable ?? false,
-    tags,
-    trackFSM,
-    chips,
-    pluginId: plugin,
-  };
+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`,
+    );
+  }
 }
 
-interface TrackBundle {
-  readonly title: string;
-  readonly subtitle?: string;
-  readonly closeable: boolean;
-  readonly trackFSM?: TrackCacheEntry;
-  readonly tags?: TrackTags;
-  readonly chips?: ReadonlyArray<string>;
-  readonly pluginId?: string;
+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 const ViewerPage = createPage({
+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() {
-    return m(TraceViewer);
-  },
-});
+    // 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',
+      ),
+    );
+  }
+}
diff --git a/ui/src/frontend/virtual_canvas.ts b/ui/src/frontend/virtual_canvas.ts
index 0180300..091c11e 100644
--- a/ui/src/frontend/virtual_canvas.ts
+++ b/ui/src/frontend/virtual_canvas.ts
@@ -33,18 +33,11 @@
  */
 
 import {DisposableStack} from '../base/disposable_stack';
-import {
-  Rect,
-  Size,
-  expandRect,
-  intersectRects,
-  rebaseRect,
-  rectSize,
-} from '../base/geom';
+import {Bounds2D, Rect2D, Size2D} from '../base/geom';
 
 export type LayoutShiftListener = (
   canvas: HTMLCanvasElement,
-  rect: Rect,
+  rect: Rect2D,
 ) => void;
 
 export type CanvasResizeListener = (
@@ -71,7 +64,7 @@
   private readonly _targetElement: HTMLElement;
 
   // Describes the offset of the canvas w.r.t. the "target" container
-  private _canvasRect: Rect;
+  private _canvasRect: Rect2D;
   private _layoutShiftListener?: LayoutShiftListener;
   private _canvasResizeListener?: CanvasResizeListener;
 
@@ -93,47 +86,43 @@
 
     // Returns what the canvas rect should look like
     const getCanvasRect = () => {
-      const containerRect = containerElement.getBoundingClientRect();
+      const containerRect = new Rect2D(
+        containerElement.getBoundingClientRect(),
+      );
       const targetElementRect = targetElement.getBoundingClientRect();
 
       // Calculate the intersection of the container's viewport and the target
-      const intersection = intersectRects(containerRect, targetElementRect);
+      const intersection = containerRect.intersect(targetElementRect);
 
       // Pad the intersection by the overdraw amount
-      const intersectionExpanded = expandRect(intersection, overdrawPx);
+      const intersectionExpanded = intersection.expand(overdrawPx);
 
       // Intersect with the original target rect unless we want to avoid resizes
       const canvasTargetRect = avoidOverflowingContainer
-        ? intersectRects(intersectionExpanded, targetElementRect)
+        ? intersectionExpanded.intersect(targetElementRect)
         : intersectionExpanded;
 
-      return rebaseRect(
-        canvasTargetRect,
-        targetElementRect.x,
-        targetElementRect.y,
-      );
+      return canvasTargetRect.reframe(targetElementRect);
     };
 
     const updateCanvas = () => {
       let repaintRequired = false;
 
       const canvasRect = getCanvasRect();
-      const canvasRectSize = rectSize(canvasRect);
       const canvasRectPrev = this._canvasRect;
-      const canvasRectPrevSize = rectSize(canvasRectPrev);
       this._canvasRect = canvasRect;
 
       if (
-        canvasRectPrevSize.width !== canvasRectSize.width ||
-        canvasRectPrevSize.height !== canvasRectSize.height
+        canvasRectPrev.width !== canvasRect.width ||
+        canvasRectPrev.height !== canvasRect.height
       ) {
         // Canvas needs to change size, update its size
-        canvas.style.width = `${canvasRectSize.width}px`;
-        canvas.style.height = `${canvasRectSize.height}px`;
+        canvas.style.width = `${canvasRect.width}px`;
+        canvas.style.height = `${canvasRect.height}px`;
         this._canvasResizeListener?.(
           canvas,
-          canvasRectSize.width,
-          canvasRectSize.height,
+          canvasRect.width,
+          canvasRect.height,
         );
         repaintRequired = true;
       }
@@ -180,12 +169,12 @@
 
     this._canvasElement = canvas;
     this._targetElement = targetElement;
-    this._canvasRect = {
+    this._canvasRect = new Rect2D({
       left: 0,
       top: 0,
       bottom: 0,
       right: 0,
-    };
+    });
   }
 
   /**
@@ -225,7 +214,7 @@
   /**
    * The size of the target element, aka the size of the virtual canvas.
    */
-  get size(): Size {
+  get size(): Size2D {
     return {
       width: this._targetElement.clientWidth,
       height: this._targetElement.clientHeight,
@@ -237,18 +226,11 @@
    * This will need to be subtracted from any drawing operations to get the
    * right alignment within the virtual canvas.
    */
-  get canvasRect(): Rect {
+  get canvasRect(): Rect2D {
     return this._canvasRect;
   }
 
   /**
-   * The size of the floating canvas.
-   */
-  get canvasSize(): Size {
-    return rectSize(this._canvasRect);
-  }
-
-  /**
    * Stop listening to DOM events.
    */
   [Symbol.dispose]() {
@@ -260,7 +242,7 @@
    * @param rect The rect to test.
    * @returns true if rect overlaps, false otherwise.
    */
-  overlapsCanvas(rect: Rect): boolean {
+  overlapsCanvas(rect: Bounds2D): boolean {
     const c = this._canvasRect;
     const y = rect.top < c.bottom && rect.bottom > c.top;
     const x = rect.left < c.right && rect.right > c.left;
diff --git a/ui/src/frontend/visualized_args_track.ts b/ui/src/frontend/visualized_args_track.ts
index d2064bc..fdea5b3 100644
--- a/ui/src/frontend/visualized_args_track.ts
+++ b/ui/src/frontend/visualized_args_track.ts
@@ -13,42 +13,43 @@
 // limitations under the License.
 
 import m from 'mithril';
-
-import {Actions} from '../common/actions';
-import {globals} from './globals';
 import {Button} from '../widgets/button';
 import {Icons} from '../base/semantic_icons';
 import {ThreadSliceTrack} from './thread_slice_track';
 import {uuidv4Sql} from '../base/uuid';
-import {Engine} from '../trace_processor/engine';
 import {createView} from '../trace_processor/sql_utils';
+import {Trace} from '../public/trace';
 
 export interface VisualizedArgsTrackAttrs {
-  readonly trackKey: string;
-  readonly engine: Engine;
+  readonly uri: string;
+  readonly trace: Trace;
   readonly trackId: number;
   readonly maxDepth: number;
   readonly argName: string;
+  readonly onClose: () => void;
 }
 
-export class VisualisedArgsTrack extends ThreadSliceTrack {
+export class VisualizedArgsTrack extends ThreadSliceTrack {
   private readonly viewName: string;
   private readonly argName: string;
+  private readonly onClose: () => void;
 
   constructor({
-    trackKey,
-    engine,
+    uri,
+    trace,
     trackId,
     maxDepth,
     argName,
+    onClose,
   }: VisualizedArgsTrackAttrs) {
     const uuid = uuidv4Sql();
     const escapedArgName = argName.replace(/[^a-zA-Z]/g, '_');
     const viewName = `__arg_visualisation_helper_${escapedArgName}_${uuid}_slice`;
 
-    super({engine, trackKey}, trackId, maxDepth, viewName);
+    super({trace, uri}, trackId, maxDepth, viewName);
     this.viewName = viewName;
     this.argName = argName;
+    this.onClose = onClose;
   }
 
   async onInit() {
@@ -83,17 +84,9 @@
 
   getTrackShellButtons(): m.Children {
     return m(Button, {
-      onclick: () => {
-        // This behavior differs to the original behavior a little.
-        // Originally, hitting the close button on a single track removed ALL
-        // tracks with this argName, whereas this one only closes the single
-        // track.
-        // This will be easily fixable once we transition to using dynamic
-        // tracks instead of this "initial state" approach to add these tracks.
-        globals.dispatch(Actions.removeTracks({trackKeys: [this.trackKey]}));
-      },
+      onclick: () => this.onClose(),
       icon: Icons.Close,
-      title: 'Close',
+      title: 'Close all visualised args tracks for this arg',
       compact: true,
     });
   }
diff --git a/ui/src/frontend/visualized_args_tracks.ts b/ui/src/frontend/visualized_args_tracks.ts
index 9aea25e..985d090 100644
--- a/ui/src/frontend/visualized_args_tracks.ts
+++ b/ui/src/frontend/visualized_args_tracks.ts
@@ -12,37 +12,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertExists} from '../base/logging';
 import {uuidv4} from '../base/uuid';
-import {Actions, AddTrackArgs} from '../common/actions';
-import {InThreadTrackSortKey} from '../common/state';
-import {TrackDescriptor} from '../public/tracks';
-import {Engine} from '../trace_processor/engine';
 import {NUM} from '../trace_processor/query_result';
-import {globals} from './globals';
-import {VisualisedArgsTrack} from './visualized_args_track';
+import {VisualizedArgsTrack} from './visualized_args_track';
+import {TrackNode} from '../public/workspace';
+import {Trace} from '../public/trace';
+import {SLICE_TRACK_KIND} from '../public/track_kinds';
 
-const VISUALISED_ARGS_SLICE_TRACK_URI_PREFIX = 'perfetto.VisualisedArgs';
+const VISUALIZED_ARGS_SLICE_TRACK_URI_PREFIX = 'perfetto.VisualizedArgs';
 
-// We need to add tracks from the core and from plugins. In order to add a debug
-// track we need to pass a context through with we can add the track. This is
-// different for plugins vs the core. This interface defines the generic shape
-// of this context, which can be supplied from a plugin or built from globals.
-//
-// TODO(stevegolton): In the future, both the core and plugins should have
-// access to some Context object which implements the various things we want to
-// do in a generic way, so that we don't have to do this mangling to get this to
-// work.
-interface Context {
-  engine: Engine;
-  registerTrack(track: TrackDescriptor): unknown;
-}
-
-export async function addVisualisedArgTracks(ctx: Context, argName: string) {
+export async function addVisualizedArgTracks(trace: Trace, argName: string) {
   const escapedArgName = argName.replace(/[^a-zA-Z]/g, '_');
   const tableName = `__arg_visualisation_helper_${escapedArgName}_slice`;
 
-  const result = await ctx.engine.query(`
+  const result = await trace.engine.query(`
         drop table if exists ${tableName};
 
         create table ${tableName} as
@@ -75,48 +58,47 @@
         group by track_id;
     `);
 
-  const tracksToAdd: AddTrackArgs[] = [];
+  const addedTracks: TrackNode[] = [];
   const it = result.iter({trackId: NUM, maxDepth: NUM});
-  const addedTrackKeys: string[] = [];
   for (; it.valid(); it.next()) {
     const trackId = it.trackId;
     const maxDepth = it.maxDepth;
-    const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
-    const track = globals.state.tracks[assertExists(trackKey)];
-    const utid = (track.trackSortKey as {utid?: number}).utid;
-    const key = uuidv4();
-    addedTrackKeys.push(key);
 
-    const uri = `${VISUALISED_ARGS_SLICE_TRACK_URI_PREFIX}#${uuidv4()}`;
-    ctx.registerTrack({
+    const uri = `${VISUALIZED_ARGS_SLICE_TRACK_URI_PREFIX}#${uuidv4()}`;
+    trace.tracks.registerTrack({
       uri,
       title: argName,
-      chips: ['metric'],
-      trackFactory: (trackCtx) => {
-        return new VisualisedArgsTrack({
-          engine: ctx.engine,
-          trackKey: trackCtx.trackKey,
-          trackId,
-          maxDepth,
-          argName,
-        });
-      },
+      chips: ['arg'],
+      track: new VisualizedArgsTrack({
+        trace,
+        uri,
+        trackId,
+        maxDepth,
+        argName,
+        onClose: () => {
+          // Remove all added for this argument
+          addedTracks.forEach((t) => t.parent?.removeChild(t));
+        },
+      }),
     });
 
-    tracksToAdd.push({
-      key,
-      trackGroup: track.trackGroup,
-      name: argName,
-      trackSortKey:
-        utid === undefined
-          ? track.trackSortKey
-          : {utid, priority: InThreadTrackSortKey.VISUALISED_ARGS_TRACK},
-      uri,
+    // Find the thread slice track that corresponds with this trackID and insert
+    // this track before it.
+    const threadSliceTrack = trace.workspace.flatTracks.find((trackNode) => {
+      if (!trackNode.uri) return false;
+      const trackDescriptor = trace.tracks.getTrack(trackNode.uri);
+      return (
+        trackDescriptor &&
+        trackDescriptor.tags?.kind === SLICE_TRACK_KIND &&
+        trackDescriptor.tags?.trackIds?.includes(trackId)
+      );
     });
+
+    const parentGroup = threadSliceTrack?.parent;
+    if (parentGroup) {
+      const newTrack = new TrackNode({uri, title: argName});
+      parentGroup.addChildBefore(newTrack, threadSliceTrack);
+      addedTracks.push(newTrack);
+    }
   }
-
-  globals.dispatchMultiple([
-    Actions.addTracks({tracks: tracksToAdd}),
-    Actions.sortThreadTracks({}),
-  ]);
 }
diff --git a/ui/src/frontend/viz_page.ts b/ui/src/frontend/viz_page.ts
deleted file mode 100644
index 6145763..0000000
--- a/ui/src/frontend/viz_page.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {raf} from '../core/raf_scheduler';
-import {Engine} from '../trace_processor/engine';
-import {Editor} from '../widgets/editor';
-import {VegaView} from '../widgets/vega_view';
-
-import {globals} from './globals';
-import {createPage} from './pages';
-
-function getEngine(): Engine | undefined {
-  const engineId = globals.getCurrentEngine()?.id;
-  if (engineId === undefined) {
-    return undefined;
-  }
-  const engine = globals.engines.get(engineId)?.getProxy('VizPage');
-  return engine;
-}
-
-let SPEC = '';
-let ENGINE: Engine | undefined = undefined;
-
-export const VizPage = createPage({
-  oncreate() {
-    ENGINE = getEngine();
-  },
-
-  view() {
-    return m(
-      '.viz-page',
-      m(VegaView, {
-        spec: SPEC,
-        engine: ENGINE,
-        data: {},
-      }),
-      m(Editor, {
-        onUpdate: (text: string) => {
-          SPEC = text;
-          raf.scheduleFullRedraw();
-        },
-      }),
-    );
-  },
-});
diff --git a/ui/src/frontend/widgets/charts/add_chart_menu.ts b/ui/src/frontend/widgets/charts/add_chart_menu.ts
new file mode 100644
index 0000000..384ed9a
--- /dev/null
+++ b/ui/src/frontend/widgets/charts/add_chart_menu.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 m from 'mithril';
+import {MenuItem} from '../../../widgets/menu';
+import {Icons} from '../../../base/semantic_icons';
+import {ChartConfig, ChartOption, toTitleCase} from './chart';
+
+interface AddChartMenuItemAttrs {
+  readonly chartConfig: ChartConfig;
+  readonly chartOptions: Array<ChartOption>;
+  readonly addChart: (option: ChartOption, config: ChartConfig) => void;
+}
+
+export class AddChartMenuItem
+  implements m.ClassComponent<AddChartMenuItemAttrs>
+{
+  private renderAddChartOptions(
+    config: ChartConfig,
+    chartOptions: Array<ChartOption>,
+    addChart: (option: ChartOption, config: ChartConfig) => void,
+  ): m.Children {
+    return chartOptions.map((option) => {
+      return m(MenuItem, {
+        label: toTitleCase(option),
+        onclick: () => addChart(option, config),
+      });
+    });
+  }
+
+  view({attrs}: m.Vnode<AddChartMenuItemAttrs>) {
+    return m(
+      MenuItem,
+      {label: 'Add chart', icon: Icons.Chart},
+      this.renderAddChartOptions(
+        attrs.chartConfig,
+        attrs.chartOptions,
+        attrs.addChart,
+      ),
+    );
+  }
+}
diff --git a/ui/src/frontend/widgets/charts/chart.ts b/ui/src/frontend/widgets/charts/chart.ts
new file mode 100644
index 0000000..ccbf566
--- /dev/null
+++ b/ui/src/frontend/widgets/charts/chart.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 m from 'mithril';
+import {Row} from '../../../trace_processor/query_result';
+import {Engine} from '../../../trace_processor/engine';
+import {Filter, TableColumn, TableColumnSet} from '../sql/table/column';
+import {Histogram} from './histogram/histogram';
+
+export interface VegaLiteChartSpec {
+  $schema: string;
+  width: string | number;
+  mark:
+    | 'area'
+    | 'bar'
+    | 'circle'
+    | 'line'
+    | 'point'
+    | 'rect'
+    | 'rule'
+    | 'square'
+    | 'text'
+    | 'tick'
+    | 'geoshape'
+    | 'boxplot'
+    | 'errorband'
+    | 'errorbar';
+  data: {values?: string | Row[]};
+
+  encoding: {
+    x: {[key: string]: unknown};
+    y: {[key: string]: unknown};
+  };
+}
+
+// Holds the various chart types and human readable string
+export enum ChartOption {
+  HISTOGRAM = 'histogram',
+}
+
+export interface ChartConfig {
+  readonly engine: Engine;
+  readonly columnTitle: string; // Human readable column name (ex: Duration)
+  readonly sqlColumn: string[]; // SQL column name (ex: dur)
+  readonly filters?: Filter[]; // Filters applied to SQL table
+  readonly tableDisplay?: string; // Human readable table name (ex: slices)
+  readonly query: string; // SQL query for the underlying data
+  readonly aggregationType?: 'nominal' | 'quantitative'; // Aggregation type.
+}
+
+export interface ChartData {
+  readonly rows: Row[];
+  readonly error?: string;
+}
+
+export interface ChartState {
+  readonly engine: Engine;
+  readonly query: string;
+  readonly columns: TableColumn[] | TableColumnSet[] | string[];
+  data?: ChartData;
+  spec?: VegaLiteChartSpec;
+  loadData(): Promise<void>;
+  isLoading(): boolean;
+}
+
+export function toTitleCase(s: string): string {
+  const words = s.split(/\s/);
+
+  for (let i = 0; i < words.length; ++i) {
+    words[i] = words[i][0].toUpperCase() + words[i].substring(1);
+  }
+
+  return words.join(' ');
+}
+
+// renderChartComponent will take a chart option and config and map
+// to the corresponding chart class component.
+export function renderChartComponent(option: ChartOption, config: ChartConfig) {
+  switch (option) {
+    case ChartOption.HISTOGRAM:
+      return m(Histogram, config);
+    default:
+      return;
+  }
+}
diff --git a/ui/src/frontend/widgets/charts/chart_tab.ts b/ui/src/frontend/widgets/charts/chart_tab.ts
new file mode 100644
index 0000000..0dcc6d9
--- /dev/null
+++ b/ui/src/frontend/widgets/charts/chart_tab.ts
@@ -0,0 +1,65 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {DetailsShell} from '../../../widgets/details_shell';
+import {filterTitle} from '../sql/table/column';
+import {addEphemeralTab} from '../../../common/add_ephemeral_tab';
+import {Tab} from '../../../public/tab';
+import {
+  ChartConfig,
+  ChartOption,
+  renderChartComponent,
+  toTitleCase,
+} from './chart';
+
+export function addChartTab(
+  chartOption: ChartOption,
+  chartConfig: ChartConfig,
+): void {
+  addEphemeralTab('histogramTab', new ChartTab(chartOption, chartConfig));
+}
+
+export class ChartTab implements Tab {
+  constructor(
+    private readonly chartOption: ChartOption,
+    private readonly chartConfig: ChartConfig,
+  ) {}
+
+  render() {
+    return m(
+      DetailsShell,
+      {
+        title: this.getTitle(),
+        description: this.getDescription(),
+      },
+      renderChartComponent(this.chartOption, this.chartConfig),
+    );
+  }
+
+  getTitle(): string {
+    return `${toTitleCase(this.chartConfig.columnTitle)} Histogram`;
+  }
+
+  private getDescription(): string {
+    let desc = `Count distribution for ${this.chartConfig.tableDisplay ?? ''} table`;
+
+    if (this.chartConfig.filters && this.chartConfig.filters.length > 0) {
+      desc += ' where ';
+      desc += this.chartConfig.filters.map((f) => filterTitle(f)).join(', ');
+    }
+
+    return desc;
+  }
+}
diff --git a/ui/src/frontend/widgets/charts/histogram/histogram.ts b/ui/src/frontend/widgets/charts/histogram/histogram.ts
new file mode 100644
index 0000000..38f4a65
--- /dev/null
+++ b/ui/src/frontend/widgets/charts/histogram/histogram.ts
@@ -0,0 +1,54 @@
+// 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 {stringifyJsonWithBigints} from '../../../../base/json_utils';
+import {VegaView} from '../../../../widgets/vega_view';
+import {HistogramState} from './state';
+import {Spinner} from '../../../../widgets/spinner';
+import {ChartConfig} from '../chart';
+
+export class Histogram implements m.ClassComponent<ChartConfig> {
+  private readonly state: HistogramState;
+
+  constructor({attrs}: m.Vnode<ChartConfig>) {
+    this.state = new HistogramState(
+      attrs.engine,
+      attrs.query,
+      attrs.sqlColumn,
+      attrs.aggregationType,
+    );
+  }
+
+  view() {
+    if (this.state.isLoading()) {
+      return m(Spinner);
+    }
+
+    return m(
+      'figure',
+      {
+        className: 'pf-histogram-view',
+      },
+      m(VegaView, {
+        spec: stringifyJsonWithBigints(this.state.spec),
+        data: {},
+      }),
+    );
+  }
+
+  isLoading(): boolean {
+    return this.state.isLoading();
+  }
+}
diff --git a/ui/src/frontend/widgets/charts/histogram/state.ts b/ui/src/frontend/widgets/charts/histogram/state.ts
new file mode 100644
index 0000000..db6d67f
--- /dev/null
+++ b/ui/src/frontend/widgets/charts/histogram/state.ts
@@ -0,0 +1,122 @@
+// 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 {stringifyJsonWithBigints} from '../../../../base/json_utils';
+import {raf} from '../../../../core/raf_scheduler';
+import {Engine} from '../../../../trace_processor/engine';
+import {Row} from '../../../../trace_processor/query_result';
+import {ChartData, ChartState, VegaLiteChartSpec} from '../chart';
+
+export interface HistogramChartConfig extends VegaLiteChartSpec {
+  binAxisType: 'nominal' | 'quantitative';
+  binAxis: 'x' | 'y';
+  countAxis: 'x' | 'y';
+  sort: string;
+  isBinned: boolean;
+  labelLimit?: number;
+}
+
+export class HistogramState implements ChartState {
+  data?: ChartData;
+  spec?: VegaLiteChartSpec;
+
+  constructor(
+    readonly engine: Engine,
+    readonly query: string,
+    readonly columns: string[],
+    private aggregationType?: 'nominal' | 'quantitative',
+  ) {
+    this.loadData();
+  }
+
+  createHistogramVegaSpec(): VegaLiteChartSpec {
+    const binAxisEncoding = {
+      bin: this.aggregationType !== 'nominal',
+      field: this.columns[0],
+      type: this.aggregationType,
+      title: this.columns[0],
+      sort: this.aggregationType === 'nominal' && {
+        op: 'count',
+        order: 'descending',
+      },
+      axis: {
+        labelLimit: 500,
+      },
+    };
+
+    const countAxisEncoding = {
+      aggregate: 'count',
+      title: 'Count',
+    };
+
+    const spec: VegaLiteChartSpec = {
+      $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
+      width: 'container',
+      mark: 'bar',
+      data: {
+        values: this.data?.rows,
+      },
+      encoding: {
+        x:
+          this.aggregationType !== 'nominal'
+            ? binAxisEncoding
+            : countAxisEncoding,
+        y:
+          this.aggregationType !== 'nominal'
+            ? countAxisEncoding
+            : binAxisEncoding,
+      },
+    };
+
+    return spec;
+  }
+
+  async loadData() {
+    const res = await this.engine.query(`
+      SELECT ${this.columns[0]}
+      FROM (
+        ${this.query}
+      )
+    `);
+
+    const rows: Row[] = [];
+
+    let hasQuantitativeData = false;
+
+    for (const it = res.iter({}); it.valid(); it.next()) {
+      const rowVal = it.get(this.columns[0]);
+      if (typeof rowVal === 'bigint') {
+        hasQuantitativeData = true;
+      }
+
+      rows.push({
+        [this.columns[0]]: rowVal,
+      });
+    }
+
+    if (this.aggregationType === undefined) {
+      this.aggregationType = hasQuantitativeData ? 'quantitative' : 'nominal';
+    }
+
+    this.data = {
+      rows,
+    };
+
+    this.spec = this.createHistogramVegaSpec();
+    raf.scheduleFullRedraw();
+  }
+
+  isLoading(): boolean {
+    return this.data === undefined;
+  }
+}
diff --git a/ui/src/frontend/widgets/duration.ts b/ui/src/frontend/widgets/duration.ts
index 6a05ab4..a623406 100644
--- a/ui/src/frontend/widgets/duration.ts
+++ b/ui/src/frontend/widgets/duration.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {copyToClipboard} from '../../base/clipboard';
 import {Icons} from '../../base/semantic_icons';
 import {Duration, duration} from '../../base/time';
@@ -27,7 +26,6 @@
 import {raf} from '../../core/raf_scheduler';
 import {Anchor} from '../../widgets/anchor';
 import {MenuDivider, MenuItem, PopupMenu2} from '../../widgets/menu';
-
 import {menuItemForFormat} from './timestamp';
 
 interface DurationWidgetAttrs {
@@ -62,9 +60,11 @@
         menuItemForFormat(TimestampFormat.UTC, 'Realtime (UTC)'),
         menuItemForFormat(TimestampFormat.TraceTz, 'Realtime (Trace TZ)'),
         menuItemForFormat(TimestampFormat.Seconds, 'Seconds'),
-        menuItemForFormat(TimestampFormat.Raw, 'Raw'),
+        menuItemForFormat(TimestampFormat.Milliseoncds, 'Milliseconds'),
+        menuItemForFormat(TimestampFormat.Microseconds, 'Microseconds'),
+        menuItemForFormat(TimestampFormat.TraceNs, 'Raw'),
         menuItemForFormat(
-          TimestampFormat.RawLocale,
+          TimestampFormat.TraceNsLocale,
           'Raw (with locale-specific formatting)',
         ),
       ),
@@ -115,12 +115,16 @@
     case TimestampFormat.TraceTz:
     case TimestampFormat.Timecode:
       return renderFormattedDuration(dur);
-    case TimestampFormat.Raw:
+    case TimestampFormat.TraceNs:
       return dur.toString();
-    case TimestampFormat.RawLocale:
+    case TimestampFormat.TraceNsLocale:
       return dur.toLocaleString();
     case TimestampFormat.Seconds:
       return Duration.formatSeconds(dur);
+    case TimestampFormat.Milliseoncds:
+      return Duration.formatMilliseconds(dur);
+    case TimestampFormat.Microseconds:
+      return Duration.formatMicroseconds(dur);
     default:
       const x: never = fmt;
       throw new Error(`Invalid format ${x}`);
diff --git a/ui/src/frontend/widgets/process.ts b/ui/src/frontend/widgets/process.ts
index f7bcb93..0738005 100644
--- a/ui/src/frontend/widgets/process.ts
+++ b/ui/src/frontend/widgets/process.ts
@@ -13,23 +13,58 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {
-  ProcessInfo,
-  getProcessName,
-} from '../../trace_processor/sql_utils/process';
-import {MenuItem, PopupMenu2} from '../../widgets/menu';
-import {Anchor} from '../../widgets/anchor';
-import {exists} from '../../base/utils';
-import {Icons} from '../../base/semantic_icons';
 import {copyToClipboard} from '../../base/clipboard';
+import {Icons} from '../../base/semantic_icons';
+import {exists} from '../../base/utils';
+import {addEphemeralTab} from '../../common/add_ephemeral_tab';
+import {Upid} from '../../trace_processor/sql_utils/core_types';
+import {
+  getProcessInfo,
+  getProcessName,
+  ProcessInfo,
+} from '../../trace_processor/sql_utils/process';
+import {Anchor} from '../../widgets/anchor';
+import {MenuItem, PopupMenu2} from '../../widgets/menu';
+import {ProcessDetailsTab} from '../process_details_tab';
+import {
+  createSqlIdRefRenderer,
+  sqlIdRegistry,
+} from './sql/details/sql_ref_renderer_registry';
+import {asUpid} from '../../trace_processor/sql_utils/core_types';
+import {AppImpl} from '../../core/app_impl';
 
-export function renderProcessRef(info: ProcessInfo): m.Children {
-  const name = info.name;
-  return m(
-    PopupMenu2,
-    {
-      trigger: m(Anchor, getProcessName(info)),
+export function showProcessDetailsMenuItem(
+  upid: Upid,
+  pid?: number,
+): m.Children {
+  return m(MenuItem, {
+    icon: Icons.ExternalLink,
+    label: 'Show process details',
+    onclick: () => {
+      // TODO(primiano): `trace` should be injected, but doing so would require
+      // an invasive refactoring of most classes in frontend/widgets/sql/*.
+      const trace = AppImpl.instance.trace;
+      if (trace === undefined) return;
+      addEphemeralTab(
+        'processDetails',
+        new ProcessDetailsTab({
+          trace,
+          upid,
+          pid,
+        }),
+      );
     },
+  });
+}
+
+export function processRefMenuItems(info: {
+  upid: Upid;
+  name?: string;
+  pid?: number;
+}): m.Children {
+  // We capture a copy to be able to pass it across async boundary to `onclick`.
+  const name = info.name;
+  return [
     exists(name) &&
       m(MenuItem, {
         icon: Icons.Copy,
@@ -47,5 +82,23 @@
       label: 'Copy upid',
       onclick: () => copyToClipboard(`${info.upid}`),
     }),
+    showProcessDetailsMenuItem(info.upid, info.pid),
+  ];
+}
+
+export function renderProcessRef(info: ProcessInfo): m.Children {
+  return m(
+    PopupMenu2,
+    {
+      trigger: m(Anchor, getProcessName(info)),
+    },
+    processRefMenuItems(info),
   );
 }
+
+sqlIdRegistry['process'] = createSqlIdRefRenderer<ProcessInfo>(
+  async (engine, id) => await getProcessInfo(engine, asUpid(Number(id))),
+  (data: ProcessInfo) => ({
+    value: renderProcessRef(data),
+  }),
+);
diff --git a/ui/src/frontend/widgets/sched.ts b/ui/src/frontend/widgets/sched.ts
new file mode 100644
index 0000000..665ca9c
--- /dev/null
+++ b/ui/src/frontend/widgets/sched.ts
@@ -0,0 +1,63 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {SchedSqlId} from '../../trace_processor/sql_utils/core_types';
+import {Anchor} from '../../widgets/anchor';
+import {Icons} from '../../base/semantic_icons';
+import {AppImpl} from '../../core/app_impl';
+
+interface SchedRefAttrs {
+  // The id of the referenced sched slice in the sched_slice table.
+  readonly id: SchedSqlId;
+
+  // If not present, a placeholder name will be used.
+  readonly name?: string;
+
+  // Whether clicking on the reference should change the current tab
+  // to "current selection" tab in addition to updating the selection
+  // and changing the viewport. True by default.
+  readonly switchToCurrentSelectionTab?: boolean;
+}
+
+export function goToSchedSlice(id: SchedSqlId) {
+  // TODO(primiano): the Trace object should be properly injected here.
+  AppImpl.instance.trace?.selection.selectSqlEvent('sched_slice', id, {
+    scrollToSelection: true,
+  });
+}
+
+export class SchedRef implements m.ClassComponent<SchedRefAttrs> {
+  view(vnode: m.Vnode<SchedRefAttrs>) {
+    return m(
+      Anchor,
+      {
+        icon: Icons.UpdateSelection,
+        onclick: () => {
+          // TODO(primiano): the Trace object should be properly injected here.
+          AppImpl.instance.trace?.selection.selectSqlEvent(
+            'sched_slice',
+            vnode.attrs.id,
+            {
+              switchToCurrentSelectionTab:
+                vnode.attrs.switchToCurrentSelectionTab ?? true,
+              scrollToSelection: true,
+            },
+          );
+        },
+      },
+      vnode.attrs.name ?? `Sched ${vnode.attrs.id}`,
+    );
+  }
+}
diff --git a/ui/src/frontend/widgets/slice.ts b/ui/src/frontend/widgets/slice.ts
index 059074e..48ad80a 100644
--- a/ui/src/frontend/widgets/slice.ts
+++ b/ui/src/frontend/widgets/slice.ts
@@ -13,22 +13,22 @@
 // limitations under the License.
 
 import m from 'mithril';
-
-import {Time, duration, time} from '../../base/time';
-import {SliceSqlId} from '../../trace_processor/sql_utils/core_types';
+import {
+  asSliceSqlId,
+  SliceSqlId,
+} from '../../trace_processor/sql_utils/core_types';
 import {Anchor} from '../../widgets/anchor';
 import {Icons} from '../../base/semantic_icons';
-import {globals} from '../globals';
-import {focusHorizontalRange, verticalScrollToTrack} from '../scroll_helper';
-import {BigintMath} from '../../base/bigint_math';
-import {SliceDetails} from '../../trace_processor/sql_utils/slice';
+import {getSlice, SliceDetails} from '../../trace_processor/sql_utils/slice';
+import {
+  createSqlIdRefRenderer,
+  sqlIdRegistry,
+} from './sql/details/sql_ref_renderer_registry';
+import {AppImpl} from '../../core/app_impl';
 
 interface SliceRefAttrs {
   readonly id: SliceSqlId;
   readonly name: string;
-  readonly ts: time;
-  readonly dur: duration;
-  readonly sqlTrackId: number;
 
   // Whether clicking on the reference should change the current tab
   // to "current selection" tab in addition to updating the selection
@@ -38,34 +38,19 @@
 
 export class SliceRef implements m.ClassComponent<SliceRefAttrs> {
   view(vnode: m.Vnode<SliceRefAttrs>) {
-    const switchTab = vnode.attrs.switchToCurrentSelectionTab ?? true;
     return m(
       Anchor,
       {
         icon: Icons.UpdateSelection,
         onclick: () => {
-          const trackKeyByTrackId = globals.trackManager.trackKeyByTrackId;
-          const trackKey = trackKeyByTrackId.get(vnode.attrs.sqlTrackId);
-          if (trackKey === undefined) return;
-          verticalScrollToTrack(trackKey, true);
-          // Clamp duration to 1 - i.e. for instant events
-          const dur = BigintMath.max(1n, vnode.attrs.dur);
-          focusHorizontalRange(
-            vnode.attrs.ts,
-            Time.fromRaw(vnode.attrs.ts + dur),
-          );
-
-          globals.setLegacySelection(
+          // TODO(primiano): the Trace object should be properly injected here.
+          AppImpl.instance.trace?.selection.selectSqlEvent(
+            'slice',
+            vnode.attrs.id,
             {
-              kind: 'SLICE',
-              id: vnode.attrs.id,
-              trackKey,
-              table: 'slice',
-            },
-            {
-              clearSearch: true,
-              pendingScrollId: undefined,
-              switchToCurrentSelectionTab: switchTab,
+              switchToCurrentSelectionTab:
+                vnode.attrs.switchToCurrentSelectionTab,
+              scrollToSelection: true,
             },
           );
         },
@@ -79,8 +64,20 @@
   return m(SliceRef, {
     id: slice.id,
     name: name ?? slice.name,
-    ts: slice.ts,
-    dur: slice.dur,
-    sqlTrackId: slice.trackId,
   });
 }
+
+sqlIdRegistry['slice'] = createSqlIdRefRenderer<{
+  slice: SliceDetails | undefined;
+  id: bigint;
+}>(
+  async (engine, id) => {
+    return {
+      id,
+      slice: await getSlice(engine, asSliceSqlId(Number(id))),
+    };
+  },
+  ({id, slice}) => ({
+    value: slice !== undefined ? sliceRef(slice) : `Unknown slice ${id}`,
+  }),
+);
diff --git a/ui/src/frontend/widgets/sql/details/details.ts b/ui/src/frontend/widgets/sql/details/details.ts
index 09b7f4c..069b723 100644
--- a/ui/src/frontend/widgets/sql/details/details.ts
+++ b/ui/src/frontend/widgets/sql/details/details.ts
@@ -13,26 +13,27 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {Brand} from '../../../../base/brand';
 import {Time} from '../../../../base/time';
 import {exists} from '../../../../base/utils';
 import {raf} from '../../../../core/raf_scheduler';
-import {Engine} from '../../../../public';
+import {Engine} from '../../../../trace_processor/engine';
 import {Row} from '../../../../trace_processor/query_result';
 import {
   SqlValue,
   sqlValueToReadableString,
 } from '../../../../trace_processor/sql_utils';
+import {Arg, getArgs} from '../../../../trace_processor/sql_utils/args';
+import {asArgSetId} from '../../../../trace_processor/sql_utils/core_types';
 import {Anchor} from '../../../../widgets/anchor';
 import {renderError} from '../../../../widgets/error';
 import {SqlRef} from '../../../../widgets/sql_ref';
 import {Tree, TreeNode} from '../../../../widgets/tree';
 import {hasArgs, renderArguments} from '../../../slice_args';
-import {asArgSetId} from '../../../../trace_processor/sql_utils/core_types';
 import {DurationWidget} from '../../../widgets/duration';
 import {Timestamp as TimestampWidget} from '../../../widgets/timestamp';
-import {Arg, getArgs} from '../../../../trace_processor/sql_utils/args';
+import {sqlIdRegistry} from './sql_ref_renderer_registry';
+import {Trace} from '../../../../public/trace';
 
 // This file contains the helper to render the details tree (based on Tree
 // widget) for an object represented by a SQL row in some table. The user passes
@@ -168,6 +169,13 @@
     return new ScalarValueSchema('url', value, args);
   }
 
+  export function Boolean(
+    value: string,
+    args?: ScalarValueParams,
+  ): ScalarValueSchema {
+    return new ScalarValueSchema('boolean', value, args);
+  }
+
   // Create an object representing a reference to a SQL table row in the schema.
   // |table| - name of the table.
   // |id| - SQL expression (e.g. column name) for the id.
@@ -205,17 +213,16 @@
 // Class responsible for fetching the data and rendering the data.
 export class Details {
   constructor(
-    private engine: Engine,
+    private trace: Trace,
     private sqlTable: string,
     private id: number,
     schema: {[key: string]: ValueDesc},
-    sqlIdTypesRenderers: {[key: string]: SqlIdRefRenderer} = {},
   ) {
     this.dataController = new DataController(
-      engine,
+      trace,
       sqlTable,
       id,
-      sqlIdTypesRenderers,
+      sqlIdRegistry,
     );
 
     this.resolvedSchema = {
@@ -242,7 +249,7 @@
     for (const [key, value] of Object.entries(this.resolvedSchema.data)) {
       nodes.push(
         renderValue(
-          this.engine,
+          this.trace,
           key,
           value,
           this.dataController.data,
@@ -285,15 +292,6 @@
   render: (data: {}) => RenderedValue;
 };
 
-// Type-safe helper to create a SqlIdRefRenderer, which ensures that the
-// type returned from the fetch is the same type that renderer takes.
-export function createSqlIdRefRenderer<Data extends {}>(
-  fetch: (engine: Engine, id: bigint) => Promise<Data>,
-  render: (data: Data) => RenderedValue,
-): SqlIdRefRenderer {
-  return {fetch, render: render as (data: {}) => RenderedValue};
-}
-
 // === Impl details ===
 
 // Resolved index into the list of columns / expression to fetch.
@@ -339,7 +337,13 @@
 // from SQL).
 class ScalarValueSchema {
   constructor(
-    public kind: 'timestamp' | 'duration' | 'arg_set_id' | 'value' | 'url',
+    public kind:
+      | 'timestamp'
+      | 'duration'
+      | 'arg_set_id'
+      | 'value'
+      | 'url'
+      | 'boolean',
     public sourceExpression: string,
     public params?: ScalarValueParams,
   ) {}
@@ -347,7 +351,7 @@
 
 // Resolved version of simple scalar values.
 type ResolvedScalarValue = {
-  kind: 'timestamp' | 'duration' | 'value' | 'url';
+  kind: 'timestamp' | 'duration' | 'value' | 'url' | 'boolean';
   source: ExpressionIndex;
 } & ScalarValueParams;
 
@@ -435,7 +439,13 @@
   // Source statements for the SQL references.
   sqlIdRefs: {tableName: string; idExpression: string}[];
   // Fetched data for the SQL references.
-  sqlIdRefData: ({data: {}; id: bigint} | Err)[];
+  sqlIdRefData: (
+    | {
+        data: {};
+        id: bigint | null;
+      }
+    | Err
+  )[];
 }
 
 // Class responsible for collecting the description of the data to fetch and
@@ -457,7 +467,7 @@
   data?: Data;
 
   constructor(
-    private engine: Engine,
+    private trace: Trace,
     private sqlTable: string,
     private id: number,
     public sqlIdRefRenderers: {[table: string]: SqlIdRefRenderer},
@@ -483,7 +493,7 @@
 
     // Fetch the scalar values for the basic expressions.
     const row: Row = (
-      await this.engine.query(`
+      await this.trace.engine.query(`
       SELECT
         ${this.expressions
           .map((value, index) => `${value} as ${label(index)}`)
@@ -501,7 +511,7 @@
       const argSetId = data.values[argSetIndex];
       if (argSetId === null) {
         data.argSets.push([]);
-      } else if (typeof argSetId !== 'number') {
+      } else if (typeof argSetId !== 'number' && typeof argSetId !== 'bigint') {
         data.argSets.push(
           new Err(
             `Incorrect type for arg set ${
@@ -510,7 +520,9 @@
           ),
         );
       } else {
-        data.argSets.push(await getArgs(this.engine, asArgSetId(argSetId)));
+        data.argSets.push(
+          await getArgs(this.trace.engine, asArgSetId(Number(argSetId))),
+        );
       }
     }
 
@@ -522,7 +534,10 @@
         continue;
       }
       const id = data.values[ref.id];
-      if (typeof id !== 'bigint') {
+      if (id === null) {
+        data.sqlIdRefData.push({data: {}, id});
+        continue;
+      } else if (typeof id !== 'bigint') {
         data.sqlIdRefData.push(
           new Err(
             `Incorrect type for SQL reference ${
@@ -532,7 +547,7 @@
         );
         continue;
       }
-      const refData = await renderer.fetch(this.engine, id);
+      const refData = await renderer.fetch(this.trace.engine, id);
       if (refData === undefined) {
         data.sqlIdRefData.push(
           new Err(
@@ -658,7 +673,7 @@
 
 // Generate the vdom for a given value using the fetched `data`.
 function renderValue(
-  engine: Engine,
+  trace: Trace,
   key: string,
   value: ResolvedValue,
   data: Data,
@@ -673,10 +688,10 @@
       });
     case 'url': {
       const url = data.values[value.source];
-      let rhs: m.Child;
+      let rhs: m.Children;
       if (url === null) {
         if (value.skipIfNull) return null;
-        rhs = m('i', 'NULL');
+        rhs = renderNull();
       } else if (typeof url !== 'string') {
         rhs = renderError(
           `Incorrect type for URL ${
@@ -695,6 +710,21 @@
         right: rhs,
       });
     }
+    case 'boolean': {
+      const bool = data.values[value.source];
+      if (bool === null && value.skipIfNull) return null;
+      let rhs: m.Child;
+      if (typeof bool !== 'bigint' && typeof bool !== 'number') {
+        rhs = renderError(
+          `Incorrect type for boolean ${
+            data.valueExpressions[value.source]
+          }: expected bigint or number, got ${typeof bool}`,
+        );
+      } else {
+        rhs = bool ? 'true' : 'false';
+      }
+      return m(TreeNode, {left: key, right: rhs});
+    }
     case 'timestamp': {
       const ts = data.values[value.source];
       let rhs: m.Child;
@@ -747,6 +777,8 @@
       let children: m.Children;
       if (refData instanceof Err) {
         rhs = renderError(refData.message);
+      } else if (refData.id === null && value.skipIfNull === true) {
+        rhs = renderNull();
       } else {
         const renderer = sqlIdRefRenderers[ref.tableName];
         if (renderer === undefined) {
@@ -779,14 +811,14 @@
           {
             left: key,
           },
-          renderArguments(engine, args),
+          renderArguments(trace, args),
         )
       );
     case 'array': {
       const children: m.Children[] = [];
       for (const child of value.data) {
         const renderedChild = renderValue(
-          engine,
+          trace,
           `[${children.length}]`,
           child,
           data,
@@ -810,7 +842,7 @@
     case 'dict': {
       const children: m.Children[] = [];
       for (const [key, val] of Object.entries(value.data)) {
-        const child = renderValue(engine, key, val, data, sqlIdRefRenderers);
+        const child = renderValue(trace, key, val, data, sqlIdRefRenderers);
         if (exists(child)) {
           children.push(child);
         }
@@ -828,3 +860,7 @@
     }
   }
 }
+
+function renderNull(): m.Children {
+  return m('i', 'NULL');
+}
diff --git a/ui/src/frontend/widgets/sql/details/sql_ref_renderer_registry.ts b/ui/src/frontend/widgets/sql/details/sql_ref_renderer_registry.ts
new file mode 100644
index 0000000..08446e5
--- /dev/null
+++ b/ui/src/frontend/widgets/sql/details/sql_ref_renderer_registry.ts
@@ -0,0 +1,27 @@
+// 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 {Engine} from '../../../../trace_processor/engine';
+import type {RenderedValue, SqlIdRefRenderer} from './details';
+
+// Type-safe helper to create a SqlIdRefRenderer, which ensures that the
+// type returned from the fetch is the same type that renderer takes.
+export function createSqlIdRefRenderer<Data extends {}>(
+  fetch: (engine: Engine, id: bigint) => Promise<Data>,
+  render: (data: Data) => RenderedValue,
+): SqlIdRefRenderer {
+  return {fetch, render: render as (data: {}) => RenderedValue};
+}
+
+export const sqlIdRegistry: {[key: string]: SqlIdRefRenderer} = {};
diff --git a/ui/src/frontend/widgets/sql/details/well_known_types.ts b/ui/src/frontend/widgets/sql/details/well_known_types.ts
deleted file mode 100644
index 3794001..0000000
--- a/ui/src/frontend/widgets/sql/details/well_known_types.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {
-  ProcessInfo,
-  getProcessInfo,
-} from '../../../../trace_processor/sql_utils/process';
-import {
-  SliceDetails,
-  getSlice,
-} from '../../../../trace_processor/sql_utils/slice';
-import {
-  asSliceSqlId,
-  asUpid,
-  asUtid,
-} from '../../../../trace_processor/sql_utils/core_types';
-import {
-  ThreadInfo,
-  getThreadInfo,
-} from '../../../../trace_processor/sql_utils/thread';
-import {renderProcessRef} from '../../process';
-import {sliceRef} from '../../slice';
-import {renderThreadRef} from '../../thread';
-import {createSqlIdRefRenderer, SqlIdRefRenderer} from './details';
-
-export const wellKnownTypes: {[key: string]: SqlIdRefRenderer} = {
-  process: createSqlIdRefRenderer<ProcessInfo>(
-    async (engine, id) => await getProcessInfo(engine, asUpid(Number(id))),
-    (data: ProcessInfo) => ({
-      value: renderProcessRef(data),
-    }),
-  ),
-  thread: createSqlIdRefRenderer<ThreadInfo>(
-    async (engine, id) => await getThreadInfo(engine, asUtid(Number(id))),
-    (data: ThreadInfo) => ({
-      value: renderThreadRef(data),
-    }),
-  ),
-  slice: createSqlIdRefRenderer<{slice: SliceDetails | undefined; id: bigint}>(
-    async (engine, id) => {
-      return {
-        id,
-        slice: await getSlice(engine, asSliceSqlId(Number(id))),
-      };
-    },
-    ({id, slice}) => ({
-      value: slice !== undefined ? sliceRef(slice) : `Unknown slice ${id}`,
-    }),
-  ),
-};
diff --git a/ui/src/frontend/widgets/sql/table/argument_selector.ts b/ui/src/frontend/widgets/sql/table/argument_selector.ts
index ddc7052..1500f7d 100644
--- a/ui/src/frontend/widgets/sql/table/argument_selector.ts
+++ b/ui/src/frontend/widgets/sql/table/argument_selector.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 The Android Open Source Project
+// Copyright (C) 2024 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -13,77 +13,125 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {raf} from '../../../../core/raf_scheduler';
-import {Engine} from '../../../../trace_processor/engine';
-import {STR} from '../../../../trace_processor/query_result';
-import {
-  constraintsToQueryPrefix,
-  constraintsToQuerySuffix,
-  SQLConstraints,
-} from '../../../../trace_processor/sql_utils';
-import {FilterableSelect} from '../../../../widgets/select';
 import {Spinner} from '../../../../widgets/spinner';
-
-import {argColumn} from './column';
-import {ArgSetIdColumn} from './table_description';
+import {
+  TableColumn,
+  tableColumnId,
+  TableColumnSet,
+  TableManager,
+} 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';
 
 const MAX_ARGS_TO_DISPLAY = 15;
 
 interface ArgumentSelectorAttrs {
-  engine: Engine;
-  argSetId: ArgSetIdColumn;
-  tableName: string;
-  constraints: SQLConstraints;
-  // List of aliases for existing columns by the table.
-  alreadySelectedColumns: Set<string>;
-  onArgumentSelected: (argument: string) => void;
+  tableManager: TableManager;
+  columnSet: TableColumnSet;
+  alreadySelectedColumnIds: Set<string>;
+  onArgumentSelected: (column: TableColumn) => void;
 }
 
-// A widget which allows the user to select a new argument to display.
-// Dinamically queries Trace Processor to find the relevant set of arg_set_ids
-// and which args are present in these arg sets.
+// This class is responsible for rendering a menu which allows user to select which column out of ColumnSet to add.
 export class ArgumentSelector
   implements m.ClassComponent<ArgumentSelectorAttrs>
 {
-  argList?: string[];
+  searchText = '';
+  columns?: {key: string; column: TableColumn | TableColumnSet}[];
 
   constructor({attrs}: m.Vnode<ArgumentSelectorAttrs>) {
     this.load(attrs);
   }
 
   private async load(attrs: ArgumentSelectorAttrs) {
-    const queryResult = await attrs.engine.query(`
-      -- Encapsulate the query in a CTE to avoid clashes between filters
-      -- and columns of the 'args' table.
-      WITH arg_sets AS (
-        ${constraintsToQueryPrefix(attrs.constraints)}
-        SELECT DISTINCT ${attrs.tableName}.${attrs.argSetId.name} as arg_set_id
-        FROM ${attrs.tableName}
-        ${constraintsToQuerySuffix(attrs.constraints)}
-      )
-      SELECT
-        DISTINCT args.key as key
-      FROM arg_sets
-      JOIN args USING (arg_set_id)
-    `);
-    this.argList = [];
-    const it = queryResult.iter({key: STR});
-    for (; it.valid(); it.next()) {
-      const arg = argColumn(attrs.tableName, attrs.argSetId, it.key);
-      if (attrs.alreadySelectedColumns.has(arg.alias)) continue;
-      this.argList.push(it.key);
-    }
+    this.columns = await attrs.columnSet.discover(attrs.tableManager);
     raf.scheduleFullRedraw();
   }
 
   view({attrs}: m.Vnode<ArgumentSelectorAttrs>) {
-    if (this.argList === undefined) return m(Spinner);
-    return m(FilterableSelect, {
-      values: this.argList,
-      onSelected: (value: string) => attrs.onArgumentSelected(value),
-      maxDisplayedItems: MAX_ARGS_TO_DISPLAY,
-      autofocusInput: true,
+    const columns = this.columns;
+    if (columns === undefined) return m(Spinner);
+
+    // Candidates are the columns which have not been selected yet.
+    const candidates = columns.filter(
+      ({column}) =>
+        column instanceof TableColumnSet ||
+        !attrs.alreadySelectedColumnIds.has(tableColumnId(column)),
+    );
+
+    // Filter the candidates based on the search text.
+    const filtered = candidates.filter(({key}) => {
+      return key.toLowerCase().includes(this.searchText.toLowerCase());
     });
+
+    const displayed = filtered.slice(0, MAX_ARGS_TO_DISPLAY);
+
+    const extraItems = Math.max(0, filtered.length - MAX_ARGS_TO_DISPLAY);
+
+    const firstButtonUuid = uuidv4();
+
+    return [
+      m(
+        '.pf-search-bar',
+        m(TextInput, {
+          autofocus: true,
+          oninput: (event: Event) => {
+            const eventTarget = event.target as HTMLTextAreaElement;
+            this.searchText = eventTarget.value;
+            scheduleFullRedraw();
+          },
+          onkeydown: (event: KeyboardEvent) => {
+            if (filtered.length === 0) return;
+            if (event.key === 'Enter') {
+              // If there is only one item or Mod-Enter was pressed, select the first element.
+              if (filtered.length === 1 || hasModKey(event)) {
+                const params = {bubbles: true};
+                if (hasModKey(event)) {
+                  Object.assign(params, modKey());
+                }
+                const pointerEvent = new PointerEvent('click', params);
+                (
+                  document.getElementById(firstButtonUuid) as HTMLElement | null
+                )?.dispatchEvent(pointerEvent);
+              }
+            }
+          },
+          value: this.searchText,
+          placeholder: 'Filter...',
+          className: 'pf-search-box',
+        }),
+      ),
+      ...displayed.map(({key, column}, index) =>
+        m(
+          MenuItem,
+          {
+            id: index === 0 ? firstButtonUuid : undefined,
+            label: key,
+            onclick: (event) => {
+              if (column instanceof TableColumnSet) return;
+              attrs.onArgumentSelected(column);
+              // For Control-Click, we don't want to close the menu to allow the user
+              // to select multiple items in one go.
+              if (hasModKey(event)) {
+                event.stopPropagation();
+              }
+              // Otherwise this popup will be closed.
+            },
+          },
+          column instanceof TableColumnSet &&
+            m(ArgumentSelector, {
+              columnSet: column,
+              alreadySelectedColumnIds: attrs.alreadySelectedColumnIds,
+              onArgumentSelected: attrs.onArgumentSelected,
+              tableManager: attrs.tableManager,
+            }),
+        ),
+      ),
+      Boolean(extraItems) && m('i', `+${extraItems} more`),
+    ];
   }
 }
diff --git a/ui/src/frontend/widgets/sql/table/column.ts b/ui/src/frontend/widgets/sql/table/column.ts
index 390aa08..9af4cac 100644
--- a/ui/src/frontend/widgets/sql/table/column.ts
+++ b/ui/src/frontend/widgets/sql/table/column.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 The Android Open Source Project
+// Copyright (C) 2024 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,81 +12,215 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {sqliteString} from '../../../../base/string_utils';
+import m from 'mithril';
+import {SqlValue} from '../../../../trace_processor/query_result';
+import {SortDirection} from '../../../../base/comparison_utils';
+import {arrayEquals} from '../../../../base/array_utils';
+import {Trace} from '../../../../public/trace';
 
-import {
-  ArgSetIdColumn,
-  dependendentColumns,
-  DisplayConfig,
-  RegularSqlTableColumn,
-} from './table_description';
+// We are dealing with two types of columns here:
+// - Column, which is shown to a user in table (high-level, ColumnTable).
+// - Column in the underlying SQL data (low-level, SqlColumn).
+// They are related, but somewhat separate due to the fact that some table columns need to work with multiple SQL values to display it properly.
+// For example, a "time range" column would need both timestamp and duration to display interactive experience (e.g. highlight the time range on hover).
+// Each TableColumn has a primary SqlColumn, as well as optional dependent columns.
 
-// This file contains the defintions of different column types that can be
-// displayed in the table viewer.
-
-export interface Column {
-  // SQL expression calculating the value of this column.
-  expression: string;
-  // Unique name for this column.
-  // The relevant bit of SQL fetching this column will be ${expression} as
-  // ${alias}.
-  alias: string;
-  // Title to be displayed in the table header.
-  title: string;
-  // How the value of this column should be rendered.
-  display?: DisplayConfig;
-}
-
-export function columnFromSqlTableColumn(c: RegularSqlTableColumn): Column {
-  return {
-    expression: c.name,
-    alias: c.name,
-    title: c.title || c.name,
-    display: c.display,
-  };
-}
-
-export function argColumn(
-  tableName: string,
-  c: ArgSetIdColumn,
-  argName: string,
-): Column {
-  const escape = (name: string) => name.replace(/[^A-Za-z0-9]/g, '_');
-  return {
-    expression: `extract_arg(${tableName}.${c.name}, ${sqliteString(argName)})`,
-    alias: `_arg_${c.name}_${escape(argName)}`,
-    title: `${c.title ?? c.name} ${argName}`,
-  };
-}
-
-// A single instruction from a select part of the SQL statement, i.e.
-// select `expression` as `alias`.
-export type SqlProjection = {
-  expression: string;
-  alias: string;
+// A source table for a SQL column, representing the joined table and the join constraints.
+export type SourceTable = {
+  table: string;
+  joinOn: {[key: string]: SqlColumn};
+  // Whether more performant 'INNER JOIN' can be used instead of 'LEFT JOIN'.
+  // Special care should be taken to ensure that a) all rows exist in a target table, and b) the source is not null, otherwise the rows will be filtered out.
+  // false by default.
+  innerJoin?: boolean;
 };
 
-export function formatSqlProjection(p: SqlProjection): string {
-  return `${p.expression} as ${p.alias}`;
+// A column in the SQL query. It can be either a column from a base table or a "lookup" column from a joined table.
+export type SqlColumn =
+  | string
+  | {
+      column: string;
+      source: SourceTable;
+    };
+
+// List of columns of args, corresponding to arg values, which cause a short-form of the ID to be generated.
+// (e.g. arg_set_id[foo].int instead of args[arg_set_id,key=foo].int_value).
+const ARG_COLUMN_TO_SUFFIX: {[key: string]: string} = {
+  display_value: '',
+  int_value: '.int',
+  string_value: '.str',
+  real_value: '.real',
+};
+
+// A unique identifier for the SQL column.
+export function sqlColumnId(column: SqlColumn): string {
+  if (typeof column === 'string') {
+    return column;
+  }
+  // Special case: If the join is performed on a single column `id`, we can use a simpler representation (i.e. `table[id].column`).
+  if (arrayEquals(Object.keys(column.source.joinOn), ['id'])) {
+    return `${column.source.table}[${sqlColumnId(Object.values(column.source.joinOn)[0])}].${column.column}`;
+  }
+  // Special case: args lookup. For it, we can use a simpler representation (i.e. `arg_set_id[key]`).
+  if (
+    column.column in ARG_COLUMN_TO_SUFFIX &&
+    column.source.table === 'args' &&
+    arrayEquals(Object.keys(column.source.joinOn).sort(), ['arg_set_id', 'key'])
+  ) {
+    const key = column.source.joinOn['key'];
+    const argSetId = column.source.joinOn['arg_set_id'];
+    return `${sqlColumnId(argSetId)}[${sqlColumnId(key)}]${ARG_COLUMN_TO_SUFFIX[column.column]}`;
+  }
+  // Otherwise, we need to list all the join constraints.
+  const lookup = Object.entries(column.source.joinOn)
+    .map(([key, value]): string => {
+      const valueStr = sqlColumnId(value);
+      if (key === valueStr) return key;
+      return `${key}=${sqlColumnId(value)}`;
+    })
+    .join(', ');
+  return `${column.source.table}[${lookup}].${column.column}`;
 }
 
-// Returns a list of projections (i.e. parts of the SELECT clause) that should
-// be added to the query fetching the data to be able to display the given
-// column (e.g. `foo` or `f(bar) as baz`).
-// Some table columns are backed by multiple SQL columns (e.g. slice_id is
-// backed by id, ts, dur and track_id), so we need to return a list.
-export function sqlProjectionsForColumn(column: Column): SqlProjection[] {
-  const result: SqlProjection[] = [
-    {
-      expression: column.expression,
-      alias: column.alias,
-    },
-  ];
-  for (const dependency of dependendentColumns(column.display)) {
-    result.push({
-      expression: dependency,
-      alias: dependency,
-    });
+export function isSqlColumnEqual(a: SqlColumn, b: SqlColumn): boolean {
+  return sqlColumnId(a) === sqlColumnId(b);
+}
+
+function sqlColumnName(column: SqlColumn): string {
+  if (typeof column === 'string') {
+    return column;
   }
-  return result;
+  return column.column;
+}
+
+// Interface which allows TableColumn and TableColumnSet to interact with the table (e.g. add filters, or run the query).
+export interface TableManager {
+  addFilter(filter: Filter): void;
+
+  trace: Trace;
+  getSqlQuery(data: {[key: string]: SqlColumn}): string;
+}
+
+export interface TableColumnParams {
+  // See TableColumn.tag.
+  tag?: string;
+  // See TableColumn.alias.
+  alias?: string;
+  // See TableColumn.startsHidden.
+  startsHidden?: boolean;
+}
+
+export interface AggregationConfig {
+  dataType?: 'nominal' | 'quantitative';
+}
+
+// Class which represents a column in a table, which can be displayed to the user.
+// It is based on the primary SQL column, but also contains additional information needed for displaying it as a part of a table.
+export abstract class TableColumn {
+  constructor(params?: TableColumnParams) {
+    this.tag = params?.tag;
+    this.alias = params?.alias;
+    this.startsHidden = params?.startsHidden ?? false;
+  }
+
+  // Column title to be displayed.
+  // If not set, then `alias` will be used if it's unique.
+  // If `alias` is not set as well, then `sqlColumnId(primaryColumn())` will be used.
+  // TODO(altimin): This should return m.Children, but a bunch of things, including low-level widgets (Button, MenuItem, Anchor) need to be fixed first.
+  getTitle?(): string | undefined;
+
+  // Some SQL columns can map to multiple table columns. For example, a "utid" can be displayed as an integer column, or as a "thread" column, which displays "$thread_name [$tid]".
+  // Each column should have a unique id, so in these cases `tag` is appended to the primary column id to guarantee uniqueness.
+  readonly tag?: string;
+
+  // Preferred alias to be used in the SQL query. If omitted, column name will be used instead, including postfixing it with an integer if necessary.
+  // However, e.g. explicit aliases like `process_name` and `thread_name` are typically preferred to `name_1`, `name_2`, hence the need for explicit aliasing.
+  readonly alias?: string;
+
+  // Whether the column should be hidden by default.
+  readonly startsHidden: boolean;
+
+  // The SQL column this data corresponds to. Will be also used for sorting and aggregation purposes.
+  abstract primaryColumn(): SqlColumn;
+
+  // Sometimes to display an interactive cell more than a single value is needed (e.g. "time range" corresponds to (ts, dur) pair. While we want to show the duration, we would want to highlight the interval on hover, for which both timestamp and duration are needed.
+  dependentColumns?(): {[key: string]: SqlColumn};
+
+  // The set of underlying sql columns that should be sorted when this column is sorted.
+  sortColumns?(): SqlColumn[];
+
+  // Render a table cell. `value` corresponds to the fetched SQL value for the primary column, `dependentColumns` are the fetched values for the dependent columns.
+  abstract renderCell(
+    value: SqlValue,
+    tableManager: TableManager,
+    dependentColumns: {[key: string]: SqlValue},
+  ): m.Children;
+
+  // Specifies how this column should be aggregated. If not set, then all
+  // numeric columns will be treated as quantitative, and all other columns as
+  // nominal.
+  aggregation?(): AggregationConfig;
+}
+
+// Returns a unique identifier for the table column.
+export function tableColumnId(column: TableColumn): string {
+  const primaryColumnName = sqlColumnId(column.primaryColumn());
+  if (column.tag) {
+    return `${primaryColumnName}#${column.tag}`;
+  }
+  return primaryColumnName;
+}
+
+export function tableColumnAlias(column: TableColumn): string {
+  return column.alias ?? sqlColumnName(column.primaryColumn());
+}
+
+// This class represents a set of columns, from which the user can choose which columns to display. It is typically impossible or impractical to list all possible columns, so this class allows to discover them dynamically.
+// Two examples of canonical TableColumnSet usage are:
+// - Argument sets, where the set of arguments can be arbitrary large (and can change when the user changes filters on the table).
+// - Dependent columns, where the id.
+export abstract class TableColumnSet {
+  // TODO(altimin): This should return m.Children, same comment as in TableColumn.getTitle applies here.
+  abstract getTitle(): string;
+
+  // Returns a list of columns from this TableColumnSet which should be displayed by default.
+  initialColumns?(): TableColumn[];
+
+  // Returns a list of columns which can be added to the table from the current TableColumnSet.
+  abstract discover(manager: TableManager): Promise<
+    {
+      key: string;
+      column: TableColumn | TableColumnSet;
+    }[]
+  >;
+}
+
+// A filter which can be applied to the table.
+export interface Filter {
+  // Operation: it takes a list of column names and should return a valid SQL expression for this filter.
+  op: (cols: string[]) => string;
+  // Columns that the `op` should reference. The number of columns should match the number of interpolations in `op`.
+  columns: SqlColumn[];
+  // Returns a human-readable title for the filter. If not set, `op` will be used.
+  // TODO(altimin): This probably should return m.Children, but currently Button expects its label to be string.
+  getTitle?(): string;
+}
+
+// Returns a default string representation of the filter.
+export function formatFilter(filter: Filter): string {
+  return filter.op(filter.columns.map((c) => sqlColumnId(c)));
+}
+
+// Returns a human-readable title for the filter.
+export function filterTitle(filter: Filter): string {
+  if (filter.getTitle !== undefined) {
+    return filter.getTitle();
+  }
+  return formatFilter(filter);
+}
+
+// A column order clause, which specifies the column and the direction in which it should be sorted.
+export interface ColumnOrderClause {
+  column: SqlColumn;
+  direction: SortDirection;
 }
diff --git a/ui/src/frontend/widgets/sql/table/column_unittest.ts b/ui/src/frontend/widgets/sql/table/column_unittest.ts
index 1d92982..faa9a94 100644
--- a/ui/src/frontend/widgets/sql/table/column_unittest.ts
+++ b/ui/src/frontend/widgets/sql/table/column_unittest.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2022 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.
@@ -12,84 +12,127 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  argColumn,
-  Column,
-  columnFromSqlTableColumn,
-  formatSqlProjection,
-  sqlProjectionsForColumn,
-} from './column';
-import {
-  ArgSetIdColumn,
-  SqlTableColumn,
-  SqlTableDescription,
-} from './table_description';
+import {sqlColumnId} from './column';
+import {argSqlColumn} from './well_known_columns';
 
-const table: SqlTableDescription = {
-  name: 'table',
-  displayName: 'Table',
-  columns: [
-    {
-      name: 'id',
-    },
-    {
-      name: 'name',
-      title: 'Name',
-    },
-    {
-      name: 'ts',
-      display: {
-        type: 'timestamp',
-      },
-    },
-    {
-      name: 'arg_set_id',
-      type: 'arg_set_id',
-      title: 'Arg',
-    },
-  ],
-};
-
-test('fromSqlTableColumn', () => {
-  expect(columnFromSqlTableColumn(table.columns[0])).toEqual({
-    expression: 'id',
-    alias: 'id',
-    title: 'id',
-  });
-
-  expect(columnFromSqlTableColumn(table.columns[1])).toEqual({
-    expression: 'name',
-    alias: 'name',
-    title: 'Name',
-  });
-
-  expect(columnFromSqlTableColumn(table.columns[2])).toEqual({
-    expression: 'ts',
-    alias: 'ts',
-    title: 'ts',
-    display: {
-      type: 'timestamp',
-    },
-  });
-
-  expect(
-    argColumn('slice', table.columns[3] as ArgSetIdColumn, 'foo.bar'),
-  ).toEqual({
-    expression: "extract_arg(slice.arg_set_id, 'foo.bar')",
-    alias: '_arg_arg_set_id_foo_bar',
-    title: 'Arg foo.bar',
-  });
+test('sql_column_id.basic', () => {
+  // Straightforward case: just a column selection.
+  expect(sqlColumnId('utid')).toBe('utid');
 });
 
-function formatSqlProjectionsForColumn(c: Column): string {
-  return sqlProjectionsForColumn(c).map(formatSqlProjection).join(', ');
-}
+test('sql_column_id.single_join', () => {
+  expect(
+    sqlColumnId({
+      column: 'bar',
+      source: {
+        table: 'foo',
+        joinOn: {
+          foo_id: 'id',
+        },
+      },
+    }),
+  ).toBe('foo[foo_id=id].bar');
+});
 
-test('sqlProjections', () => {
-  const format = (c: SqlTableColumn) =>
-    formatSqlProjectionsForColumn(columnFromSqlTableColumn(c));
+test('sql_column_id.double_join', () => {
+  expect(
+    sqlColumnId({
+      column: 'abc',
+      source: {
+        table: 'alphabet',
+        joinOn: {
+          abc_id: {
+            column: 'bar',
+            source: {
+              table: 'foo',
+              joinOn: {
+                foo_id: 'id',
+              },
+            },
+          },
+        },
+      },
+    }),
+  ).toBe('alphabet[abc_id=foo[foo_id=id].bar].abc');
+});
 
-  expect(format(table.columns[0])).toEqual('id as id');
-  expect(format(table.columns[1])).toEqual('name as name');
-  expect(format(table.columns[2])).toEqual('ts as ts');
+test('sql_column_id.join_on_id', () => {
+  // Special case: joins on `id` should be simplified.
+  expect(
+    sqlColumnId({
+      column: 'name',
+      source: {
+        table: 'foo',
+        joinOn: {
+          id: 'foo_id',
+        },
+      },
+    }),
+  ).toBe('foo[foo_id].name');
+});
+
+test('sql_column_id.nested_join_on_id', () => {
+  // Special case: joins on `id` should be simplified in nested joins.
+  expect(
+    sqlColumnId({
+      column: 'name',
+      source: {
+        table: 'foo',
+        joinOn: {
+          id: {
+            column: 'foo_id',
+            source: {
+              table: 'bar',
+              joinOn: {
+                x: 'y',
+              },
+            },
+          },
+        },
+      },
+    }),
+  ).toBe('foo[bar[x=y].foo_id].name');
+});
+
+test('sql_column_id.simplied_join', () => {
+  // Special case: if both sides of the join are the same, only one can be shown.
+  expect(
+    sqlColumnId({
+      column: 'name',
+      source: {
+        table: 'foo',
+        joinOn: {
+          x: 'y',
+          z: 'z',
+        },
+      },
+    }),
+  ).toBe('foo[x=y, z].name');
+});
+
+test('sql_column_id.arg_set_id', () => {
+  // Special case: arg_set_id.
+  expect(sqlColumnId(argSqlColumn('arg_set_id', 'arg1'))).toBe(
+    "arg_set_id['arg1']",
+  );
+});
+
+test('sql_column_id.arg_set_id_with_join', () => {
+  // Special case: arg_set_id.
+  expect(
+    sqlColumnId(
+      argSqlColumn(
+        {
+          column: 'arg_set_id',
+          source: {
+            table: 'foo',
+            joinOn: {
+              x: 'y',
+            },
+          },
+        },
+        'arg1',
+      ),
+    ),
+  ).toBe("foo[x=y].arg_set_id['arg1']");
 });
diff --git a/ui/src/frontend/widgets/sql/table/query_builder.ts b/ui/src/frontend/widgets/sql/table/query_builder.ts
new file mode 100644
index 0000000..be3400d
--- /dev/null
+++ b/ui/src/frontend/widgets/sql/table/query_builder.ts
@@ -0,0 +1,202 @@
+// 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 {ColumnOrderClause, Filter, SqlColumn} from './column';
+
+// The goal of this module is to generate a query statement from the list of columns, filters and order by clauses.
+// The main challenge is that the column definitions are independent, and the columns themselves can reference the same join multiple times:
+//
+// For example, in the following query `parent_slice_ts` and `parent_slice_dur` are both referencing the same join, but we want to include only one join in the final query.
+
+// SELECT
+//    parent.ts AS parent_slice_ts,
+//    parent.dur AS parent_slice_dur
+// FROM slice
+// LEFT JOIN slice AS parent ON slice.parent_id = parent.id
+
+// Normalised sql column, where the source table is resolved to a unique index.
+type NormalisedSqlColumn = {
+  column: string;
+  // If |joinId| is undefined, then the columnName comes from the primary table.
+  sourceTableId?: number;
+};
+
+// Normalised source table, where the join constraints are resolved to a normalised columns.
+type NormalisedSourceTable = {
+  table: string;
+  joinOn: {[key: string]: NormalisedSqlColumn};
+  innerJoin: boolean;
+};
+
+// Checks whether two join constraints are equal.
+function areJoinConstraintsEqual(
+  a: {[key: string]: NormalisedSqlColumn},
+  b: {[key: string]: NormalisedSqlColumn},
+): boolean {
+  if (Object.keys(a).length !== Object.keys(b).length) {
+    return false;
+  }
+
+  for (const key of Object.keys(a)) {
+    if (typeof a[key] !== typeof b[key]) {
+      return false;
+    }
+    if (typeof a[key] === 'string') {
+      return a[key] === b[key];
+    }
+    const aValue = a[key] as NormalisedSqlColumn;
+    const bValue = b[key] as NormalisedSqlColumn;
+    if (
+      aValue.column !== bValue.column ||
+      aValue.sourceTableId !== bValue.sourceTableId
+    ) {
+      return false;
+    }
+  }
+  return true;
+}
+
+// Class responsible for building a query and maintaing a list of normalised join tables.
+class QueryBuilder {
+  tables: NormalisedSourceTable[] = [];
+  tableAlias: string;
+
+  constructor(tableName: string) {
+    this.tableAlias = `${tableName}_0`;
+  }
+
+  // Normalises a column, including adding if necessary the joins to the list of tables.
+  normalise(column: SqlColumn): NormalisedSqlColumn {
+    if (typeof column === 'string') {
+      return {
+        column: column,
+      };
+    }
+    const normalisedJoinOn: {[key: string]: NormalisedSqlColumn} =
+      Object.fromEntries(
+        Object.entries(column.source.joinOn).map(([key, value]) => [
+          key,
+          this.normalise(value),
+        ]),
+      );
+
+    // Check if this join is already present.
+    for (let i = 0; i < this.tables.length; ++i) {
+      const table = this.tables[i];
+      if (
+        table.table === column.source.table &&
+        table.innerJoin === (column.source.innerJoin ?? false) &&
+        areJoinConstraintsEqual(table.joinOn, normalisedJoinOn)
+      ) {
+        return {
+          column: column.column,
+          sourceTableId: i,
+        };
+      }
+    }
+
+    // Otherwise, add a new join.
+    this.tables.push({
+      table: column.source.table,
+      joinOn: normalisedJoinOn,
+      innerJoin: column.source.innerJoin ?? false,
+    });
+    return {
+      column: column.column,
+      sourceTableId: this.tables.length - 1,
+    };
+  }
+
+  // Prints a reference to a column, including properly disambiguated table alias.
+  printColumn(column: NormalisedSqlColumn): string {
+    if (column.sourceTableId === undefined) {
+      if (!/^[A-Za-z0-9_]*$/.test(column.column)) {
+        // If this is an expression, don't prefix it with the table name.
+        return column.column;
+      }
+      return `${this.tableAlias}.${column.column}`;
+    }
+    const table = this.tables[column.sourceTableId];
+    // Dependent tables are 0-indexed, but we want to display them as 1-indexed to reserve 0 for the primary table.
+    return `${table.table}_${column.sourceTableId + 1}.${column.column}`;
+  }
+
+  printJoin(joinIndex: number): string {
+    const join = this.tables[joinIndex];
+    const alias = `${join.table}_${joinIndex + 1}`;
+    const clauses = Object.entries(join.joinOn).map(
+      ([key, value]) => `${alias}.${key} = ${this.printColumn(value)}`,
+    );
+    // Join IDs are 0-indexed, but we want to display them as 1-indexed to reserve 0 for the primary table.
+    return `${join.innerJoin ? '' : 'LEFT '}JOIN ${join.table} AS ${alias} ON ${clauses.join(' AND ')}`;
+  }
+}
+
+// Returns a query fetching the columns from the table, with the specified filters and order by clauses.
+// keys of the `columns` object are the names of the columns in the result set.
+export function buildSqlQuery(args: {
+  table: string;
+  columns: {[key: string]: SqlColumn};
+  filters?: Filter[];
+  orderBy?: ColumnOrderClause[];
+}): string {
+  const builder = new QueryBuilder(args.table);
+
+  const normalisedColumns = Object.fromEntries(
+    Object.entries(args.columns).map(([key, value]) => [
+      key,
+      builder.normalise(value),
+    ]),
+  );
+  const normalisedFilters = (args.filters || []).map((filter) => ({
+    op: filter.op,
+    columns: filter.columns.map((column) => builder.normalise(column)),
+  }));
+  const normalisedOrderBy = (args.orderBy || []).map((orderBy) => ({
+    order: orderBy.direction,
+    column: builder.normalise(orderBy.column),
+  }));
+
+  const formatFilter = (filter: {
+    op: (cols: string[]) => string;
+    columns: NormalisedSqlColumn[];
+  }) => {
+    return filter.op(
+      filter.columns.map((column) => builder.printColumn(column)),
+    );
+  };
+
+  const filterClause =
+    normalisedFilters.length === 0
+      ? ''
+      : `WHERE\n ${normalisedFilters.map(formatFilter).join('\n  AND ')}`;
+  const joinClause = builder.tables
+    .map((_, index) => builder.printJoin(index))
+    .join('\n');
+  const orderByClause =
+    normalisedOrderBy.length === 0
+      ? ''
+      : `ORDER BY\n  ${normalisedOrderBy.map((orderBy) => `${builder.printColumn(orderBy.column)} ${orderBy.order}`).join(',  ')}`;
+
+  return `
+    SELECT
+      ${Object.entries(normalisedColumns)
+        .map(([key, value]) => `${builder.printColumn(value)} AS ${key}`)
+        .join(',\n  ')}
+    FROM ${args.table} AS ${builder.tableAlias}
+    ${joinClause}
+    ${filterClause}
+    ${orderByClause}
+  `;
+}
diff --git a/ui/src/frontend/widgets/sql/table/query_builder_unittest.ts b/ui/src/frontend/widgets/sql/table/query_builder_unittest.ts
new file mode 100644
index 0000000..1403dea
--- /dev/null
+++ b/ui/src/frontend/widgets/sql/table/query_builder_unittest.ts
@@ -0,0 +1,403 @@
+// 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 {sqliteString} from '../../../../base/string_utils';
+import {SourceTable} from './column';
+import {buildSqlQuery} from './query_builder';
+
+function normalise(str: string): string {
+  return str.replace(/\s+/g, ' ').trim();
+}
+
+test('query_builder.basic_select', () => {
+  expect(
+    normalise(
+      buildSqlQuery({
+        table: 'slice',
+        columns: {
+          id: 'id',
+        },
+      }),
+    ),
+  ).toBe('SELECT slice_0.id AS id FROM slice AS slice_0');
+});
+
+test('query_builder.basic_filter', () => {
+  expect(
+    normalise(
+      buildSqlQuery({
+        table: 'slice',
+        columns: {
+          id: 'id',
+        },
+        filters: [
+          {
+            op: (cols) => `${cols[0]} != -1`,
+            columns: ['ts'],
+          },
+        ],
+      }),
+    ),
+  ).toBe(
+    'SELECT slice_0.id AS id FROM slice AS slice_0 WHERE slice_0.ts != -1',
+  );
+});
+
+test('query_builder.basic_order_by', () => {
+  expect(
+    normalise(
+      buildSqlQuery({
+        table: 'slice',
+        columns: {
+          id: 'id',
+        },
+        orderBy: [
+          {
+            column: 'ts',
+            direction: 'ASC',
+          },
+        ],
+      }),
+    ),
+  ).toBe(
+    'SELECT slice_0.id AS id FROM slice AS slice_0 ORDER BY slice_0.ts ASC',
+  );
+});
+
+test('query_builder.simple_join', () => {
+  expect(
+    normalise(
+      buildSqlQuery({
+        table: 'slice',
+        columns: {
+          id: 'id',
+          name: 'name',
+          parent_name: {
+            column: 'name',
+            source: {
+              table: 'slice',
+              joinOn: {
+                id: 'parent_id',
+              },
+            },
+          },
+        },
+      }),
+    ),
+  ).toBe(
+    normalise(`
+    SELECT
+      slice_0.id AS id,
+      slice_0.name AS name,
+      slice_1.name AS parent_name
+    FROM slice AS slice_0
+    LEFT JOIN slice AS slice_1 ON slice_1.id = slice_0.parent_id
+  `),
+  );
+});
+
+// Check a query with INNER JOIN instead of LEFT JOIN.
+test('query_builder.left_join', () => {
+  expect(
+    normalise(
+      buildSqlQuery({
+        table: 'foo',
+        columns: {
+          foo_id: 'id',
+          slice_name: {
+            column: 'name',
+            source: {
+              table: 'slice',
+              innerJoin: true,
+              joinOn: {
+                id: 'slice_id',
+              },
+            },
+          },
+        },
+      }),
+    ),
+  ).toBe(
+    normalise(`
+    SELECT
+      foo_0.id AS foo_id,
+      slice_1.name AS slice_name
+    FROM foo AS foo_0
+    JOIN slice AS slice_1 ON slice_1.id = foo_0.slice_id
+  `),
+  );
+});
+
+// Check a query which has both INNER JOIN and LEFT JOIN on the same table.
+// The correct behaviour here is debatable (probably we can upgrade INNER JOIN to LEFT JOIN),
+// but for now we just generate the query with two separate joins.
+test('query_builder.left_join_and_inner_join', () => {
+  expect(
+    normalise(
+      buildSqlQuery({
+        table: 'foo',
+        columns: {
+          foo_id: 'id',
+          slice_name: {
+            column: 'name',
+            source: {
+              table: 'slice',
+              innerJoin: true,
+              joinOn: {
+                id: 'slice_id',
+              },
+            },
+          },
+          slice_depth: {
+            column: 'depth',
+            source: {
+              table: 'slice',
+              joinOn: {
+                id: 'slice_id',
+              },
+            },
+          },
+        },
+      }),
+    ),
+  ).toBe(
+    normalise(`
+    SELECT
+      foo_0.id AS foo_id,
+      slice_1.name AS slice_name,
+      slice_2.depth AS slice_depth
+    FROM foo AS foo_0
+    JOIN slice AS slice_1 ON slice_1.id = foo_0.slice_id
+    LEFT JOIN slice AS slice_2 ON slice_2.id = foo_0.slice_id
+  `),
+  );
+});
+
+test('query_builder.join_with_multiple_columns', () => {
+  // This test checks that the query builder can correctly deduplicate joins when we request multiple columns from the joined table.
+  const parent: SourceTable = {
+    table: 'slice',
+    joinOn: {
+      id: 'parent_id',
+    },
+  };
+  expect(
+    normalise(
+      buildSqlQuery({
+        table: 'slice',
+        columns: {
+          id: 'id',
+          name: 'name',
+          parent_name: {
+            column: 'name',
+            source: parent,
+          },
+          parent_dur: {
+            column: 'dur',
+            source: parent,
+          },
+        },
+      }),
+    ),
+  ).toBe(
+    normalise(`
+    SELECT
+      slice_0.id AS id,
+      slice_0.name AS name,
+      slice_1.name AS parent_name,
+      slice_1.dur AS parent_dur
+    FROM slice AS slice_0
+    LEFT JOIN slice AS slice_1 ON slice_1.id = slice_0.parent_id
+  `),
+  );
+});
+
+test('query_builder.filter_on_joined_column', () => {
+  // This test checks that the query builder can correctly deduplicate joins when we request multiple columns from the joined table.
+  const parent: SourceTable = {
+    table: 'slice',
+    joinOn: {
+      id: 'parent_id',
+    },
+  };
+  expect(
+    normalise(
+      buildSqlQuery({
+        table: 'slice',
+        columns: {
+          id: 'id',
+          name: 'name',
+          parent_name: {
+            column: 'name',
+            source: parent,
+          },
+        },
+        filters: [
+          {
+            op: (cols) => `${cols[0]} != -1`,
+            columns: [
+              {
+                column: 'dur',
+                source: parent,
+              },
+            ],
+          },
+        ],
+      }),
+    ),
+  ).toBe(
+    normalise(`
+    SELECT
+      slice_0.id AS id,
+      slice_0.name AS name,
+      slice_1.name AS parent_name
+    FROM slice AS slice_0
+    LEFT JOIN slice AS slice_1 ON slice_1.id = slice_0.parent_id
+    WHERE slice_1.dur != -1
+  `),
+  );
+});
+
+test('query_builder.complex_join', () => {
+  const threadTrack: SourceTable = {
+    table: 'thread_track',
+    joinOn: {
+      id: 'track_id',
+    },
+  };
+
+  const thread: SourceTable = {
+    table: 'thread',
+    joinOn: {
+      utid: {
+        column: 'utid',
+        source: threadTrack,
+      },
+    },
+  };
+
+  const process: SourceTable = {
+    table: 'process',
+    joinOn: {
+      upid: {
+        column: 'upid',
+        source: thread,
+      },
+    },
+  };
+
+  expect(
+    normalise(
+      buildSqlQuery({
+        table: 'slice',
+        columns: {
+          id: 'id',
+          name: 'name',
+          tid: {
+            column: 'tid',
+            source: thread,
+          },
+          thread_name: {
+            column: 'name',
+            source: thread,
+          },
+          pid: {
+            column: 'pid',
+            source: process,
+          },
+          process_name: {
+            column: 'name',
+            source: process,
+          },
+        },
+      }),
+    ),
+  ).toBe(
+    normalise(`
+    SELECT
+      slice_0.id AS id,
+      slice_0.name AS name,
+      thread_2.tid AS tid,
+      thread_2.name AS thread_name,
+      process_3.pid AS pid,
+      process_3.name AS process_name
+    FROM slice AS slice_0
+    LEFT JOIN thread_track AS thread_track_1 ON thread_track_1.id = slice_0.track_id
+    LEFT JOIN thread AS thread_2 ON thread_2.utid = thread_track_1.utid
+    LEFT JOIN process AS process_3 ON process_3.upid = thread_2.upid
+  `),
+  );
+});
+
+test('query_builder.multiple_args', () => {
+  expect(
+    normalise(
+      buildSqlQuery({
+        table: 'slice',
+        columns: {
+          count: 'count()',
+          arg1: {
+            column: 'display_value',
+            source: {
+              table: 'args',
+              joinOn: {
+                arg_set_id: 'arg_set_id',
+                key: sqliteString('arg1'),
+              },
+            },
+          },
+          arg2: {
+            column: 'display_value',
+            source: {
+              table: 'args',
+              joinOn: {
+                arg_set_id: 'arg_set_id',
+                key: sqliteString('arg2'),
+              },
+            },
+          },
+        },
+      }),
+    ),
+  ).toBe(
+    normalise(`
+    SELECT
+      count() AS count,
+      args_1.display_value AS arg1,
+      args_2.display_value AS arg2
+    FROM slice AS slice_0
+    LEFT JOIN args AS args_1 ON args_1.arg_set_id = slice_0.arg_set_id AND args_1.key = 'arg1'
+    LEFT JOIN args AS args_2 ON args_2.arg_set_id = slice_0.arg_set_id AND args_2.key = 'arg2'
+  `),
+  );
+});
+
+test('query_builder.expression', () => {
+  expect(
+    normalise(
+      buildSqlQuery({
+        table: 'slice',
+        columns: {
+          count: 'count()',
+        },
+      }),
+    ),
+  ).toBe(
+    normalise(`
+    SELECT
+      count() AS count
+    FROM slice AS slice_0
+  `),
+  );
+});
diff --git a/ui/src/frontend/widgets/sql/table/render_cell.ts b/ui/src/frontend/widgets/sql/table/render_cell.ts
deleted file mode 100644
index 7874852..0000000
--- a/ui/src/frontend/widgets/sql/table/render_cell.ts
+++ /dev/null
@@ -1,258 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {copyToClipboard} from '../../../../base/clipboard';
-import {isString} from '../../../../base/object_utils';
-import {Icons} from '../../../../base/semantic_icons';
-import {sqliteString} from '../../../../base/string_utils';
-import {Duration, Time} from '../../../../base/time';
-import {Row} from '../../../../trace_processor/query_result';
-import {
-  SqlValue,
-  sqlValueToReadableString,
-} from '../../../../trace_processor/sql_utils';
-import {Anchor} from '../../../../widgets/anchor';
-import {renderError} from '../../../../widgets/error';
-import {MenuItem, PopupMenu2} from '../../../../widgets/menu';
-
-import {Column} from './column';
-import {SqlTableState} from './state';
-import {SliceIdDisplayConfig} from './table_description';
-import {Timestamp} from '../../timestamp';
-import {DurationWidget} from '../../duration';
-import {asSliceSqlId} from '../../../../trace_processor/sql_utils/core_types';
-import {SliceRef} from '../../slice';
-
-// This file is responsible for rendering a value in a given sell based on the
-// column type.
-
-function filterOptionMenuItem(
-  label: string,
-  filter: string,
-  state: SqlTableState,
-): m.Child {
-  return m(MenuItem, {
-    label,
-    onclick: () => {
-      state.addFilter(filter);
-    },
-  });
-}
-
-function getStandardFilters(
-  c: Column,
-  value: SqlValue,
-  state: SqlTableState,
-): m.Child[] {
-  if (value === null) {
-    return [
-      filterOptionMenuItem('is null', `${c.expression} is null`, state),
-      filterOptionMenuItem('is not null', `${c.expression} is not null`, state),
-    ];
-  }
-  if (isString(value)) {
-    return [
-      filterOptionMenuItem(
-        'equals to',
-        `${c.expression} = ${sqliteString(value)}`,
-        state,
-      ),
-      filterOptionMenuItem(
-        'not equals to',
-        `${c.expression} != ${sqliteString(value)}`,
-        state,
-      ),
-    ];
-  }
-  if (typeof value === 'bigint' || typeof value === 'number') {
-    return [
-      filterOptionMenuItem('equals to', `${c.expression} = ${value}`, state),
-      filterOptionMenuItem(
-        'not equals to',
-        `${c.expression} != ${value}`,
-        state,
-      ),
-      filterOptionMenuItem('greater than', `${c.expression} > ${value}`, state),
-      filterOptionMenuItem(
-        'greater or equals than',
-        `${c.expression} >= ${value}`,
-        state,
-      ),
-      filterOptionMenuItem('less than', `${c.expression} < ${value}`, state),
-      filterOptionMenuItem(
-        'less or equals than',
-        `${c.expression} <= ${value}`,
-        state,
-      ),
-    ];
-  }
-  return [];
-}
-
-function displayValue(value: SqlValue): m.Child {
-  if (value === null) {
-    return m('i', 'NULL');
-  }
-  return sqlValueToReadableString(value);
-}
-
-function display(column: Column, row: Row): m.Children {
-  const value = row[column.alias];
-  return displayValue(value);
-}
-
-function copyMenuItem(label: string, value: string): m.Child {
-  return m(MenuItem, {
-    icon: Icons.Copy,
-    label,
-    onclick: () => {
-      copyToClipboard(value);
-    },
-  });
-}
-
-function getContextMenuItems(
-  column: Column,
-  row: Row,
-  state: SqlTableState,
-): m.Child[] {
-  const result: m.Child[] = [];
-  const value = row[column.alias];
-
-  if (isString(value)) {
-    result.push(copyMenuItem('Copy', value));
-  }
-
-  const filters = getStandardFilters(column, value, state);
-  if (filters.length > 0) {
-    result.push(
-      m(MenuItem, {label: 'Add filter', icon: Icons.Filter}, ...filters),
-    );
-  }
-
-  return result;
-}
-
-function renderStandardColumn(
-  column: Column,
-  row: Row,
-  state: SqlTableState,
-): m.Children {
-  const displayValue = display(column, row);
-  const contextMenuItems: m.Child[] = getContextMenuItems(column, row, state);
-  return m(
-    PopupMenu2,
-    {
-      trigger: m(Anchor, displayValue),
-    },
-    ...contextMenuItems,
-  );
-}
-
-function renderTimestampColumn(
-  column: Column,
-  row: Row,
-  state: SqlTableState,
-): m.Children {
-  const value = row[column.alias];
-  if (typeof value !== 'bigint') {
-    return renderStandardColumn(column, row, state);
-  }
-
-  return m(Timestamp, {
-    ts: Time.fromRaw(value),
-    extraMenuItems: getContextMenuItems(column, row, state),
-  });
-}
-
-function renderDurationColumn(
-  column: Column,
-  row: Row,
-  state: SqlTableState,
-): m.Children {
-  const value = row[column.alias];
-  if (typeof value !== 'bigint') {
-    return renderStandardColumn(column, row, state);
-  }
-
-  return m(DurationWidget, {
-    dur: Duration.fromRaw(value),
-    extraMenuItems: getContextMenuItems(column, row, state),
-  });
-}
-
-function renderSliceIdColumn(
-  column: {alias: string; display: SliceIdDisplayConfig},
-  row: Row,
-): m.Children {
-  const config = column.display;
-  const id = row[column.alias];
-  const ts = row[config.ts];
-  const dur = row[config.dur] === null ? -1n : row[config.dur];
-  const trackId = row[config.trackId];
-
-  const columnNotFoundError = (type: string, name: string) =>
-    renderError(`${type} column ${name} not found`);
-  const wrongTypeError = (type: string, name: string, value: SqlValue) =>
-    renderError(
-      `Wrong type for ${type} column ${name}: bigint expected, ${typeof value} found`,
-    );
-
-  if (typeof id !== 'bigint') {
-    return sqlValueToReadableString(id);
-  }
-  if (ts === undefined) return columnNotFoundError('Timestamp', config.ts);
-  if (typeof ts !== 'bigint') return wrongTypeError('timestamp', config.ts, ts);
-  if (dur === undefined) return columnNotFoundError('Duration', config.dur);
-  if (typeof dur !== 'bigint') {
-    return wrongTypeError('duration', config.dur, ts);
-  }
-  if (trackId === undefined) return columnNotFoundError('Track id', trackId);
-  if (typeof trackId !== 'bigint') {
-    return wrongTypeError('track id', config.trackId, trackId);
-  }
-
-  return m(SliceRef, {
-    id: asSliceSqlId(Number(id)),
-    name: `${id}`,
-    ts: Time.fromRaw(ts),
-    dur: dur,
-    sqlTrackId: Number(trackId),
-    switchToCurrentSelectionTab: false,
-  });
-}
-
-export function renderCell(
-  column: Column,
-  row: Row,
-  state: SqlTableState,
-): m.Children {
-  if (column.display) {
-    switch (column.display?.type) {
-      case 'slice_id':
-        return renderSliceIdColumn(
-          {alias: column.alias, display: column.display},
-          row,
-        );
-      case 'timestamp':
-        return renderTimestampColumn(column, row, state);
-      case 'duration':
-      case 'thread_duration':
-        return renderDurationColumn(column, row, state);
-    }
-  }
-  return renderStandardColumn(column, row, state);
-}
diff --git a/ui/src/frontend/widgets/sql/table/render_cell_utils.ts b/ui/src/frontend/widgets/sql/table/render_cell_utils.ts
new file mode 100644
index 0000000..459baf2
--- /dev/null
+++ b/ui/src/frontend/widgets/sql/table/render_cell_utils.ts
@@ -0,0 +1,184 @@
+// 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 {TableManager, SqlColumn} from './column';
+import {MenuItem, PopupMenu2} from '../../../../widgets/menu';
+import {SqlValue} from '../../../../trace_processor/query_result';
+import {isString} from '../../../../base/object_utils';
+import {sqliteString} from '../../../../base/string_utils';
+import {Icons} from '../../../../base/semantic_icons';
+import {copyToClipboard} from '../../../../base/clipboard';
+import {sqlValueToReadableString} from '../../../../trace_processor/sql_utils';
+import {Anchor} from '../../../../widgets/anchor';
+
+interface FilterOp {
+  op: string;
+  requiresParam: boolean; // Denotes if the operator acts on an input value
+}
+
+export enum FilterOption {
+  GLOB = 'glob',
+  EQUALS_TO = 'equals to',
+  NOT_EQUALS_TO = 'not equals to',
+  GREATER_THAN = 'greater than',
+  GREATER_OR_EQUALS_THAN = 'greater or equals than',
+  LESS_THAN = 'less than',
+  LESS_OR_EQUALS_THAN = 'less or equals than',
+  IS_NULL = 'is null',
+  IS_NOT_NULL = 'is not null',
+}
+
+export const FILTER_OPTION_TO_OP: Record<FilterOption, FilterOp> = {
+  [FilterOption.GLOB]: {op: 'glob', requiresParam: true},
+  [FilterOption.EQUALS_TO]: {op: '=', requiresParam: true},
+  [FilterOption.NOT_EQUALS_TO]: {op: '!=', requiresParam: true},
+  [FilterOption.GREATER_THAN]: {op: '>', requiresParam: true},
+  [FilterOption.GREATER_OR_EQUALS_THAN]: {op: '>=', requiresParam: true},
+  [FilterOption.LESS_THAN]: {op: '<', requiresParam: true},
+  [FilterOption.LESS_OR_EQUALS_THAN]: {op: '<=', requiresParam: true},
+  [FilterOption.IS_NULL]: {op: 'IS NULL', requiresParam: false},
+  [FilterOption.IS_NOT_NULL]: {op: 'IS NOT NULL', requiresParam: false},
+};
+
+export const NUMERIC_FILTER_OPTIONS = [
+  FilterOption.EQUALS_TO,
+  FilterOption.NOT_EQUALS_TO,
+  FilterOption.GREATER_THAN,
+  FilterOption.GREATER_OR_EQUALS_THAN,
+  FilterOption.LESS_THAN,
+  FilterOption.LESS_OR_EQUALS_THAN,
+];
+
+export const STRING_FILTER_OPTIONS = [
+  FilterOption.EQUALS_TO,
+  FilterOption.NOT_EQUALS_TO,
+];
+
+export const NULL_FILTER_OPTIONS = [
+  FilterOption.IS_NULL,
+  FilterOption.IS_NOT_NULL,
+];
+
+function filterOptionMenuItem(
+  label: string,
+  column: SqlColumn,
+  filterOp: (cols: string[]) => string,
+  tableManager: TableManager,
+): m.Child {
+  return m(MenuItem, {
+    label,
+    onclick: () => {
+      tableManager.addFilter({op: filterOp, columns: [column]});
+    },
+  });
+}
+
+// Return a list of "standard" menu items, adding corresponding filters to the given cell.
+export function getStandardFilters(
+  value: SqlValue,
+  c: SqlColumn,
+  tableManager: TableManager,
+): m.Child[] {
+  if (value === null) {
+    return NULL_FILTER_OPTIONS.map((option) =>
+      filterOptionMenuItem(
+        option,
+        c,
+        (cols) => `${cols[0]} ${FILTER_OPTION_TO_OP[option].op}`,
+        tableManager,
+      ),
+    );
+  }
+  if (isString(value)) {
+    return STRING_FILTER_OPTIONS.map((option) =>
+      filterOptionMenuItem(
+        option,
+        c,
+        (cols) =>
+          `${cols[0]} ${FILTER_OPTION_TO_OP[option].op} ${sqliteString(value)}`,
+        tableManager,
+      ),
+    );
+  }
+  if (typeof value === 'bigint' || typeof value === 'number') {
+    return NUMERIC_FILTER_OPTIONS.map((option) =>
+      filterOptionMenuItem(
+        option,
+        c,
+        (cols) => `${cols[0]} ${FILTER_OPTION_TO_OP[option].op} ${value}`,
+        tableManager,
+      ),
+    );
+  }
+  return [];
+}
+
+function copyMenuItem(label: string, value: string): m.Child {
+  return m(MenuItem, {
+    icon: Icons.Copy,
+    label,
+    onclick: () => {
+      copyToClipboard(value);
+    },
+  });
+}
+
+// Return a list of "standard" menu items for the given cell.
+export function getStandardContextMenuItems(
+  value: SqlValue,
+  column: SqlColumn,
+  tableManager: TableManager,
+): m.Child[] {
+  const result: m.Child[] = [];
+
+  if (isString(value)) {
+    result.push(copyMenuItem('Copy', value));
+  }
+
+  const filters = getStandardFilters(value, column, tableManager);
+  if (filters.length > 0) {
+    result.push(
+      m(MenuItem, {label: 'Add filter', icon: Icons.Filter}, ...filters),
+    );
+  }
+
+  return result;
+}
+
+export function displayValue(value: SqlValue): m.Child {
+  if (value === null) {
+    return m('i', 'NULL');
+  }
+  return sqlValueToReadableString(value);
+}
+
+export function renderStandardCell(
+  value: SqlValue,
+  column: SqlColumn,
+  tableManager: TableManager,
+): m.Children {
+  const contextMenuItems: m.Child[] = getStandardContextMenuItems(
+    value,
+    column,
+    tableManager,
+  );
+  return m(
+    PopupMenu2,
+    {
+      trigger: m(Anchor, displayValue(value)),
+    },
+    ...contextMenuItems,
+  );
+}
diff --git a/ui/src/frontend/widgets/sql/table/sql_table_registry.ts b/ui/src/frontend/widgets/sql/table/sql_table_registry.ts
new file mode 100644
index 0000000..6aba357
--- /dev/null
+++ b/ui/src/frontend/widgets/sql/table/sql_table_registry.ts
@@ -0,0 +1,23 @@
+// 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 type {SqlTableDescription} from './table_description';
+
+export const sqlTableRegistry: {[tableName: string]: SqlTableDescription} = {};
+
+export function getSqlTableDescription(
+  name: string,
+): SqlTableDescription | undefined {
+  return sqlTableRegistry[name];
+}
diff --git a/ui/src/frontend/widgets/sql/table/state.ts b/ui/src/frontend/widgets/sql/table/state.ts
index e4c9015..1f513b8 100644
--- a/ui/src/frontend/widgets/sql/table/state.ts
+++ b/ui/src/frontend/widgets/sql/table/state.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 The Android Open Source Project
+// Copyright (C) 2024 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,38 +12,35 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {arrayEquals} from '../../../../base/array_utils';
-import {SortDirection} from '../../../../base/comparison_utils';
-import {isString} from '../../../../base/object_utils';
-import {sqliteString} from '../../../../base/string_utils';
-import {raf} from '../../../../core/raf_scheduler';
-import {Engine} from '../../../../trace_processor/engine';
 import {NUM, Row} from '../../../../trace_processor/query_result';
 import {
-  constraintsToQueryPrefix,
-  constraintsToQuerySuffix,
-  SQLConstraints,
-} from '../../../../trace_processor/sql_utils';
-
-import {
-  Column,
-  columnFromSqlTableColumn,
-  formatSqlProjection,
-  SqlProjection,
-  sqlProjectionsForColumn,
+  tableColumnAlias,
+  ColumnOrderClause,
+  Filter,
+  isSqlColumnEqual,
+  SqlColumn,
+  sqlColumnId,
+  TableColumn,
+  tableColumnId,
 } from './column';
-import {SqlTableDescription, startsHidden} from './table_description';
-
-interface ColumnOrderClause {
-  // We only allow the table to be sorted by the columns which are displayed to
-  // the user to avoid confusion, so we use a reference to the underlying Column
-  // here and compare it by reference down the line.
-  column: Column;
-  direction: SortDirection;
-}
+import {buildSqlQuery} from './query_builder';
+import {raf} from '../../../../core/raf_scheduler';
+import {SortDirection} from '../../../../base/comparison_utils';
+import {assertTrue} from '../../../../base/logging';
+import {SqlTableDescription} from './table_description';
+import {Trace} from '../../../../public/trace';
 
 const ROW_LIMIT = 100;
 
+interface Request {
+  // Select statement, without the includes and the LIMIT and OFFSET clauses.
+  selectStatement: string;
+  // Query, including the LIMIT and OFFSET clauses.
+  query: string;
+  // Map of SqlColumn's id to the column name in the query.
+  columns: {[key: string]: string};
+}
+
 // Result of the execution of the query.
 interface Data {
   // Rows to show, including pagination.
@@ -51,20 +48,6 @@
   error?: string;
 }
 
-// In the common case, filter is an expression which evaluates to a boolean.
-// However, when filtering args, it's substantially (10x) cheaper to do a
-// join with the args table, as it means that trace processor can cache the
-// query on the key instead of invoking a function for each row of the entire
-// `slice` table.
-export type Filter =
-  | string
-  | {
-      type: 'arg_filter';
-      argSetIdColumn: string;
-      argName: string;
-      op: string;
-    };
-
 interface RowCount {
   // Total number of rows in view, excluding the pagination.
   // Undefined if the query returned an error.
@@ -75,130 +58,180 @@
   filters: Filter[];
 }
 
+function isFilterEqual(a: Filter, b: Filter) {
+  return (
+    a.op === b.op &&
+    a.columns.length === b.columns.length &&
+    a.columns.every((c, i) => isSqlColumnEqual(c, b.columns[i]))
+  );
+}
+
+function areFiltersEqual(a: Filter[], b: Filter[]) {
+  if (a.length !== b.length) return false;
+  return a.every((f, i) => isFilterEqual(f, b[i]));
+}
+
 export class SqlTableState {
-  private readonly engine_: Engine;
-  private readonly table_: SqlTableDescription;
   private readonly additionalImports: string[];
 
-  get engine() {
-    return this.engine_;
-  }
-  get table() {
-    return this.table_;
-  }
-
+  // Columns currently displayed to the user. All potential columns can be found `this.table.columns`.
+  private columns: TableColumn[];
   private filters: Filter[];
-  private columns: Column[];
-  private orderBy: ColumnOrderClause[];
+  private orderBy: {
+    column: TableColumn;
+    direction: SortDirection;
+  }[];
   private offset = 0;
+  private request: Request;
   private data?: Data;
   private rowCount?: RowCount;
 
   constructor(
-    engine: Engine,
-    table: SqlTableDescription,
-    filters?: Filter[],
-    imports?: string[],
+    readonly trace: Trace,
+    readonly config: SqlTableDescription,
+    private readonly args?: {
+      initialColumns?: TableColumn[];
+      additionalColumns?: TableColumn[];
+      imports?: string[];
+      filters?: Filter[];
+      orderBy?: {
+        column: TableColumn;
+        direction: SortDirection;
+      }[];
+    },
   ) {
-    this.engine_ = engine;
-    this.table_ = table;
-    this.additionalImports = imports || [];
+    this.additionalImports = args?.imports || [];
 
-    this.filters = filters || [];
+    this.filters = args?.filters || [];
     this.columns = [];
-    for (const column of this.table.columns) {
-      if (startsHidden(column)) continue;
-      this.columns.push(columnFromSqlTableColumn(column));
-    }
-    this.orderBy = [];
 
+    if (args?.initialColumns !== undefined) {
+      assertTrue(
+        args?.additionalColumns === undefined,
+        'Only one of `initialColumns` and `additionalColumns` can be set',
+      );
+      this.columns.push(...args.initialColumns);
+    } else {
+      for (const column of this.config.columns) {
+        if (column instanceof TableColumn) {
+          if (column.startsHidden !== true) {
+            this.columns.push(column);
+          }
+        } else {
+          const cols = column.initialColumns?.();
+          for (const col of cols ?? []) {
+            this.columns.push(col);
+          }
+        }
+      }
+      if (args?.additionalColumns !== undefined) {
+        this.columns.push(...args.additionalColumns);
+      }
+    }
+
+    this.orderBy = args?.orderBy ?? [];
+
+    this.request = this.buildRequest();
     this.reload();
   }
 
-  // Compute the actual columns to fetch. Some columns can appear multiple times
-  // (e.g. we might need "ts" to be able to show it, as well as a dependency for
-  // "slice_id" to be able to jump to it, so this function will deduplicate
-  // projections by alias.
-  private getSQLProjections(): SqlProjection[] {
-    const projections = [];
-    const aliases = new Set<string>();
-    for (const column of this.columns) {
-      for (const p of sqlProjectionsForColumn(column)) {
-        if (aliases.has(p.alias)) continue;
-        aliases.add(p.alias);
-        projections.push(p);
-      }
-    }
-    return projections;
-  }
-
-  getQueryConstraints(): SQLConstraints {
-    const result: SQLConstraints = {
-      commonTableExpressions: {},
-      joins: [],
-      filters: [],
-    };
-    let cteId = 0;
-    for (const filter of this.filters) {
-      if (isString(filter)) {
-        result.filters!.push(filter);
-      } else {
-        const cteName = `arg_sets_${cteId++}`;
-        result.commonTableExpressions![cteName] = `
-          SELECT DISTINCT arg_set_id
-          FROM args
-          WHERE key = ${sqliteString(filter.argName)}
-            AND display_value ${filter.op}
-        `;
-        result.joins!.push(
-          `JOIN ${cteName} ON ${cteName}.arg_set_id = ${this.table.name}.${filter.argSetIdColumn}`,
-        );
-      }
-    }
-    return result;
+  clone(): SqlTableState {
+    return new SqlTableState(this.trace, this.config, {
+      initialColumns: this.columns,
+      imports: this.args?.imports,
+      filters: this.filters,
+      orderBy: this.orderBy,
+    });
   }
 
   private getSQLImports() {
-    const tableImports = this.table.imports || [];
+    const tableImports = this.config.imports || [];
     return [...tableImports, ...this.additionalImports]
       .map((i) => `INCLUDE PERFETTO MODULE ${i};`)
       .join('\n');
   }
 
   private getCountRowsSQLQuery(): string {
-    const constraints = this.getQueryConstraints();
     return `
       ${this.getSQLImports()}
 
-      ${constraintsToQueryPrefix(constraints)}
-      SELECT
-        COUNT() AS count
-      FROM ${this.table.name}
-      ${constraintsToQuerySuffix(constraints)}
+      ${this.getSqlQuery({count: 'COUNT()'})}
     `;
   }
 
-  buildSqlSelectStatement(): {
+  // Return a query which selects the given columns, applying the filters and ordering currently in effect.
+  getSqlQuery(columns: {[key: string]: SqlColumn}): string {
+    return buildSqlQuery({
+      table: this.config.name,
+      columns,
+      filters: this.filters,
+      orderBy: this.getOrderedBy(),
+    });
+  }
+
+  // We need column names to pass to the debug track creation logic.
+  private buildSqlSelectStatement(): {
     selectStatement: string;
-    columns: string[];
+    columns: {[key: string]: string};
   } {
-    const projections = this.getSQLProjections();
-    const orderBy = this.orderBy.map((c) => ({
-      fieldName: c.column.alias,
-      direction: c.direction,
-    }));
-    const constraints = this.getQueryConstraints();
-    constraints.orderBy = orderBy;
-    const statement = `
-      ${constraintsToQueryPrefix(constraints)}
-      SELECT
-        ${projections.map(formatSqlProjection).join(',\n')}
-      FROM ${this.table.name}
-      ${constraintsToQuerySuffix(constraints)}
-    `;
+    const columns: {[key: string]: SqlColumn} = {};
+    // A set of columnIds for quick lookup.
+    const sqlColumnIds: Set<string> = new Set();
+    // We want to use the shortest posible name for each column, but we also need to mindful of potential collisions.
+    // To avoid collisions, we append a number to the column name if there are multiple columns with the same name.
+    const columnNameCount: {[key: string]: number} = {};
+
+    const tableColumns: {column: TableColumn; name: string; alias: string}[] =
+      [];
+
+    for (const column of this.columns) {
+      // If TableColumn has an alias, use it. Otherwise, use the column name.
+      const name = tableColumnAlias(column);
+      if (!(name in columnNameCount)) {
+        columnNameCount[name] = 0;
+      }
+
+      // Note: this can break if the user specifies a column which ends with `__<number>`.
+      // We intentionally use two underscores to avoid collisions and will fix it down the line if it turns out to be a problem.
+      const alias = `${name}__${++columnNameCount[name]}`;
+      tableColumns.push({column, name, alias});
+    }
+
+    for (const column of tableColumns) {
+      const sqlColumn = column.column.primaryColumn();
+      // If we have only one column with this name, we don't need to disambiguate it.
+      if (columnNameCount[column.name] === 1) {
+        columns[column.name] = sqlColumn;
+      } else {
+        columns[column.alias] = sqlColumn;
+      }
+      sqlColumnIds.add(sqlColumnId(sqlColumn));
+    }
+
+    // We are going to be less fancy for the dependendent columns can just always suffix them with a unique integer.
+    let dependentColumnCount = 0;
+    for (const column of tableColumns) {
+      const dependentColumns =
+        column.column.dependentColumns !== undefined
+          ? column.column.dependentColumns()
+          : {};
+      for (const col of Object.values(dependentColumns)) {
+        if (sqlColumnIds.has(sqlColumnId(col))) continue;
+        const name = typeof col === 'string' ? col : col.column;
+        const alias = `__${name}_${dependentColumnCount++}`;
+        columns[alias] = col;
+        sqlColumnIds.add(sqlColumnId(col));
+      }
+    }
+
     return {
-      selectStatement: statement,
-      columns: projections.map((p) => p.alias),
+      selectStatement: this.getSqlQuery(columns),
+      columns: Object.fromEntries(
+        Object.entries(columns).map(([key, value]) => [
+          sqlColumnId(value),
+          key,
+        ]),
+      ),
     };
   }
 
@@ -210,13 +243,8 @@
     `;
   }
 
-  getPaginatedSQLQuery(): string {
-    // We fetch one more row to determine if we can go forward.
-    return `
-      ${this.getNonPaginatedSQLQuery()}
-      LIMIT ${ROW_LIMIT + 1}
-      OFFSET ${this.offset}
-    `;
+  getPaginatedSQLQuery(): Request {
+    return this.request;
   }
 
   canGoForward(): boolean {
@@ -251,7 +279,7 @@
 
   private async loadRowCount(): Promise<RowCount | undefined> {
     const filters = Array.from(this.filters);
-    const res = await this.engine.query(this.getCountRowsSQLQuery());
+    const res = await this.trace.engine.query(this.getCountRowsSQLQuery());
     if (res.error() !== undefined) return undefined;
     return {
       count: res.firstRow({count: NUM}).count,
@@ -259,8 +287,20 @@
     };
   }
 
+  private buildRequest(): Request {
+    const {selectStatement, columns} = this.buildSqlSelectStatement();
+    // We fetch one more row to determine if we can go forward.
+    const query = `
+      ${this.getSQLImports()}
+      ${selectStatement}
+      LIMIT ${ROW_LIMIT + 1}
+      OFFSET ${this.offset}
+    `;
+    return {selectStatement, query, columns};
+  }
+
   private async loadData(): Promise<Data> {
-    const queryRes = await this.engine.query(this.getPaginatedSQLQuery());
+    const queryRes = await this.trace.engine.query(this.request.query);
     const rows: Row[] = [];
     for (const it = queryRes.iter({}); it.valid(); it.next()) {
       const row: Row = {};
@@ -282,20 +322,27 @@
     }
 
     const newFilters = this.rowCount?.filters;
-    const filtersMatch = newFilters && arrayEquals(newFilters, this.filters);
+    const filtersMatch =
+      newFilters && areFiltersEqual(newFilters, this.filters);
     this.data = undefined;
+    const request = this.buildRequest();
+    this.request = request;
     if (!filtersMatch) {
       this.rowCount = undefined;
     }
 
-    // Delay the visual update by 50ms to avoid flickering (if the query returns
-    // before the data is loaded.
-    setTimeout(() => raf.scheduleFullRedraw(), 50);
+    // Run a delayed UI update to avoid flickering if the query returns quickly.
+    raf.scheduleDelayedFullRedraw();
 
     if (!filtersMatch) {
       this.rowCount = await this.loadRowCount();
     }
-    this.data = await this.loadData();
+
+    const data = await this.loadData();
+
+    // If the request has changed since we started loading the data, do not update the state.
+    if (this.request !== request) return;
+    this.data = data;
 
     raf.scheduleFullRedraw();
   }
@@ -304,6 +351,10 @@
     return this.rowCount?.count;
   }
 
+  getCurrentRequest(): Request {
+    return this.request;
+  }
+
   getDisplayedRows(): Row[] {
     return this.data?.rows || [];
   }
@@ -316,15 +367,13 @@
     return this.data === undefined;
   }
 
-  // Filters are compared by reference, so the caller is required to pass an
-  // object which was previously returned by getFilters.
-  removeFilter(filter: Filter) {
-    this.filters = this.filters.filter((f) => f !== filter);
+  addFilter(filter: Filter) {
+    this.filters.push(filter);
     this.reload();
   }
 
-  addFilter(filter: string) {
-    this.filters.push(filter);
+  removeFilter(filter: Filter) {
+    this.filters = this.filters.filter((f) => !isFilterEqual(f, filter));
     this.reload();
   }
 
@@ -332,9 +381,11 @@
     return this.filters;
   }
 
-  sortBy(clause: ColumnOrderClause) {
+  sortBy(clause: {column: TableColumn; direction: SortDirection}) {
     // Remove previous sort by the same column.
-    this.orderBy = this.orderBy.filter((c) => c.column !== clause.column);
+    this.orderBy = this.orderBy.filter(
+      (c) => tableColumnId(c.column) != tableColumnId(clause.column),
+    );
     // Add the new sort clause to the front, so we effectively stable-sort the
     // data currently displayed to the user.
     this.orderBy.unshift(clause);
@@ -346,13 +397,28 @@
     this.reload();
   }
 
-  isSortedBy(column: Column): SortDirection | undefined {
+  isSortedBy(column: TableColumn): SortDirection | undefined {
     if (this.orderBy.length === 0) return undefined;
-    if (this.orderBy[0].column !== column) return undefined;
+    if (tableColumnId(this.orderBy[0].column) !== tableColumnId(column)) {
+      return undefined;
+    }
     return this.orderBy[0].direction;
   }
 
-  addColumn(column: Column, index: number) {
+  getOrderedBy(): ColumnOrderClause[] {
+    const result: ColumnOrderClause[] = [];
+    for (const orderBy of this.orderBy) {
+      const sortColumns = orderBy.column.sortColumns?.() ?? [
+        orderBy.column.primaryColumn(),
+      ];
+      for (const column of sortColumns) {
+        result.push({column, direction: orderBy.direction});
+      }
+    }
+    return result;
+  }
+
+  addColumn(column: TableColumn, index: number) {
     this.columns.splice(index + 1, 0, column);
     this.reload({offset: 'keep'});
   }
@@ -362,12 +428,26 @@
     this.columns.splice(index, 1);
     // We can only filter by the visibile columns to avoid confusing the user,
     // so we remove order by clauses that refer to the hidden column.
-    this.orderBy = this.orderBy.filter((c) => c.column !== column);
+    this.orderBy = this.orderBy.filter(
+      (c) => tableColumnId(c.column) !== tableColumnId(column),
+    );
     // TODO(altimin): we can avoid the fetch here if the orderBy hasn't changed.
     this.reload({offset: 'keep'});
   }
 
-  getSelectedColumns(): Column[] {
+  moveColumn(fromIndex: number, toIndex: number) {
+    if (fromIndex === toIndex) return;
+    const column = this.columns[fromIndex];
+    this.columns.splice(fromIndex, 1);
+    if (fromIndex < toIndex) {
+      // We have deleted a column, therefore we need to adjust the target index.
+      --toIndex;
+    }
+    this.columns.splice(toIndex, 0, column);
+    raf.scheduleFullRedraw();
+  }
+
+  getSelectedColumns(): TableColumn[] {
     return this.columns;
   }
 }
diff --git a/ui/src/frontend/widgets/sql/table/state_unittest.ts b/ui/src/frontend/widgets/sql/table/state_unittest.ts
index 45dc882..e6161d9 100644
--- a/ui/src/frontend/widgets/sql/table/state_unittest.ts
+++ b/ui/src/frontend/widgets/sql/table/state_unittest.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2022 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.
@@ -12,91 +12,67 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {EngineBase} from '../../../../trace_processor/engine';
-import {Column} from './column';
+import {createFakeTraceImpl} from '../../../../core/fake_trace_impl';
+import {tableColumnId} from './column';
 import {SqlTableState} from './state';
 import {SqlTableDescription} from './table_description';
+import {
+  ArgSetColumnSet,
+  StandardColumn,
+  TimestampColumn,
+} from './well_known_columns';
+
+const idColumn = new StandardColumn('id');
+const nameColumn = new StandardColumn('name', {title: 'Name'});
+const tsColumn = new TimestampColumn('ts', {
+  title: 'Timestamp',
+  startsHidden: true,
+});
 
 const table: SqlTableDescription = {
   name: 'table',
   displayName: 'Table',
-  columns: [
-    {
-      name: 'id',
-    },
-    {
-      name: 'name',
-      title: 'Name',
-    },
-    {
-      name: 'ts',
-      display: {
-        type: 'timestamp',
-      },
-      startsHidden: true,
-    },
-    {
-      name: 'arg_set_id',
-      type: 'arg_set_id',
-      title: 'Arg',
-    },
-  ],
+  columns: [idColumn, nameColumn, tsColumn, new ArgSetColumnSet('arg_set_id')],
 };
 
-class FakeEngine extends EngineBase {
-  id: string = 'TestEngine';
-
-  rpcSendRequestBytes(_data: Uint8Array) {}
-}
-
 test('sqlTableState: columnManupulation', () => {
-  const engine = new FakeEngine();
-  const state = new SqlTableState(engine, table);
-
-  const idColumn = {
-    alias: 'id',
-    expression: 'id',
-    title: 'id',
-  };
-  const nameColumn = {
-    alias: 'name',
-    expression: 'name',
-    title: 'Name',
-  };
-  const tsColumn: Column = {
-    alias: 'ts',
-    expression: 'ts',
-    title: 'ts',
-    display: {
-      type: 'timestamp',
-    },
-  };
+  const trace = createFakeTraceImpl({allowQueries: true});
+  const state = new SqlTableState(trace, table);
 
   // The initial set of columns should include "id" and "name",
   // but not "ts" (as it is marked as startsHidden) and not "arg_set_id"
   // (as it is a special column).
-  expect(state.getSelectedColumns()).toEqual([idColumn, nameColumn]);
+  expect(state.getSelectedColumns().map((c) => tableColumnId(c))).toEqual([
+    'id',
+    'name',
+  ]);
 
   state.addColumn(tsColumn, 0);
 
-  expect(state.getSelectedColumns()).toEqual([idColumn, tsColumn, nameColumn]);
+  expect(state.getSelectedColumns().map((c) => tableColumnId(c))).toEqual([
+    'id',
+    'ts',
+    'name',
+  ]);
 
   state.hideColumnAtIndex(0);
 
-  expect(state.getSelectedColumns()).toEqual([tsColumn, nameColumn]);
+  expect(state.getSelectedColumns().map((c) => tableColumnId(c))).toEqual([
+    'ts',
+    'name',
+  ]);
 });
 
 test('sqlTableState: sortedColumns', () => {
-  const engine = new FakeEngine();
-  const state = new SqlTableState(engine, table);
+  const trace = createFakeTraceImpl({allowQueries: true});
+  const state = new SqlTableState(trace, table);
 
   // Verify that we have two columns: "id" and "name" and
   // save references to them.
-  expect(state.getSelectedColumns().length).toBe(2);
-  const idColumn = state.getSelectedColumns()[0];
-  expect(idColumn.alias).toBe('id');
-  const nameColumn = state.getSelectedColumns()[1];
-  expect(nameColumn.alias).toBe('name');
+  expect(state.getSelectedColumns().map((c) => tableColumnId(c))).toEqual([
+    'id',
+    'name',
+  ]);
 
   // Sort by name column and verify that it is sorted by.
   state.sortBy({
@@ -138,11 +114,11 @@
 }
 
 test('sqlTableState: sqlStatement', () => {
-  const engine = new FakeEngine();
-  const state = new SqlTableState(engine, table);
+  const trace = createFakeTraceImpl({allowQueries: true});
+  const state = new SqlTableState(trace, table);
 
   // Check the generated SQL statement.
-  expect(normalize(state.buildSqlSelectStatement().selectStatement)).toBe(
-    'SELECT id as id, name as name FROM table',
+  expect(normalize(state.getCurrentRequest().query)).toBe(
+    'SELECT table_0.id AS id, table_0.name AS name FROM table AS table_0 LIMIT 101 OFFSET 0',
   );
 });
diff --git a/ui/src/frontend/widgets/sql/table/table.ts b/ui/src/frontend/widgets/sql/table/table.ts
index 890800a..28cc81c 100644
--- a/ui/src/frontend/widgets/sql/table/table.ts
+++ b/ui/src/frontend/widgets/sql/table/table.ts
@@ -13,47 +13,199 @@
 // limitations under the License.
 
 import m from 'mithril';
-
-import {isString} from '../../../../base/object_utils';
-import {Icons} from '../../../../base/semantic_icons';
-import {Engine} from '../../../../trace_processor/engine';
-import {Row} from '../../../../trace_processor/query_result';
-import {Anchor} from '../../../../widgets/anchor';
-import {BasicTable} from '../../../../widgets/basic_table';
+import {
+  filterTitle,
+  SqlColumn,
+  sqlColumnId,
+  TableColumn,
+  tableColumnId,
+  TableManager,
+} from './column';
 import {Button} from '../../../../widgets/button';
 import {MenuDivider, MenuItem, PopupMenu2} from '../../../../widgets/menu';
+import {buildSqlQuery} from './query_builder';
+import {Icons} from '../../../../base/semantic_icons';
+import {sqliteString} from '../../../../base/string_utils';
+import {
+  ColumnType,
+  Row,
+  SqlValue,
+} from '../../../../trace_processor/query_result';
+import {Anchor} from '../../../../widgets/anchor';
+import {BasicTable, ReorderableColumns} from '../../../../widgets/basic_table';
 import {Spinner} from '../../../../widgets/spinner';
 
 import {ArgumentSelector} from './argument_selector';
-import {argColumn, Column, columnFromSqlTableColumn} from './column';
-import {renderCell} from './render_cell';
+import {FILTER_OPTION_TO_OP, FilterOption} from './render_cell_utils';
 import {SqlTableState} from './state';
-import {isArgSetIdColumn, SqlTableDescription} from './table_description';
+import {SqlTableDescription} from './table_description';
 import {Intent} from '../../../../widgets/common';
-import {addHistogramTab} from '../../../charts/histogram/tab';
+import {addChartTab} from '../../charts/chart_tab';
+import {Form} from '../../../../widgets/form';
+import {TextInput} from '../../../../widgets/text_input';
+import {AddChartMenuItem} from '../../charts/add_chart_menu';
+import {ChartConfig, ChartOption} from '../../charts/chart';
 
 export interface SqlTableConfig {
   readonly state: SqlTableState;
 }
 
+function renderCell(
+  column: TableColumn,
+  row: Row,
+  state: SqlTableState,
+): m.Children {
+  const {columns} = state.getCurrentRequest();
+  const sqlValue = row[columns[sqlColumnId(column.primaryColumn())]];
+
+  const additionalValues: {[key: string]: SqlValue} = {};
+  const dependentColumns = column.dependentColumns?.() ?? {};
+  for (const [key, col] of Object.entries(dependentColumns)) {
+    additionalValues[key] = row[columns[sqlColumnId(col)]];
+  }
+
+  return column.renderCell(sqlValue, getTableManager(state), additionalValues);
+}
+
+export function columnTitle(column: TableColumn): string {
+  if (column.getTitle !== undefined) {
+    const title = column.getTitle();
+    if (title !== undefined) return title;
+  }
+  return sqlColumnId(column.primaryColumn());
+}
+
+interface AddColumnMenuItemAttrs {
+  table: SqlTable;
+  state: SqlTableState;
+  index: number;
+}
+
+// This is separated into a separate class to store the index of the column to be
+// added and increment it when multiple columns are added from the same popup menu.
+class AddColumnMenuItem implements m.ClassComponent<AddColumnMenuItemAttrs> {
+  // Index where the new column should be inserted.
+  // In the regular case, a click would close the popup (destroying this class) and
+  // the `index` would not change during its lifetime.
+  // However, for mod-click, we want to keep adding columns to the right of the recently
+  // added column, so to achieve that we keep track of the index and increment it for
+  // each new column added.
+  index: number;
+
+  constructor({attrs}: m.Vnode<AddColumnMenuItemAttrs>) {
+    this.index = attrs.index;
+  }
+
+  view({attrs}: m.Vnode<AddColumnMenuItemAttrs>) {
+    return m(
+      MenuItem,
+      {label: 'Add column', icon: Icons.AddColumn},
+      attrs.table.renderAddColumnOptions((column) => {
+        attrs.state.addColumn(column, this.index++);
+      }),
+    );
+  }
+}
+
+interface ColumnFilterAttrs {
+  filterOption: FilterOption;
+  columns: SqlColumn[];
+  state: SqlTableState;
+}
+
+// Separating out an individual column filter into a class
+// so that we can store the raw input value.
+class ColumnFilter implements m.ClassComponent<ColumnFilterAttrs> {
+  // Holds the raw string value from the filter text input element
+  private inputValue: string;
+
+  constructor() {
+    this.inputValue = '';
+  }
+
+  view({attrs}: m.Vnode<ColumnFilterAttrs>) {
+    const {filterOption, columns, state} = attrs;
+
+    const {op, requiresParam} = FILTER_OPTION_TO_OP[filterOption];
+
+    return m(
+      MenuItem,
+      {
+        label: filterOption,
+        // Filter options that do not need an input value will filter the
+        // table directly when clicking on the menu item
+        // (ex: IS NULL or IS NOT NULL)
+        onclick: !requiresParam
+          ? () => {
+              state.addFilter({
+                op: (cols) => `${cols[0]} ${op}`,
+                columns,
+              });
+            }
+          : undefined,
+      },
+      // All non-null filter options will have a submenu that allows
+      // the user to enter a value into textfield and filter using
+      // the Filter button.
+      requiresParam &&
+        m(
+          Form,
+          {
+            onSubmit: () => {
+              // Convert the string extracted from
+              // the input text field into the correct data type for
+              // filtering. The order in which each data type is
+              // checked matters: string, number (floating), and bigint.
+              if (this.inputValue === '') return;
+
+              let filterValue: ColumnType;
+
+              if (Number.isNaN(Number.parseFloat(this.inputValue))) {
+                filterValue = sqliteString(this.inputValue);
+              } else if (
+                !Number.isInteger(Number.parseFloat(this.inputValue))
+              ) {
+                filterValue = Number(this.inputValue);
+              } else {
+                filterValue = BigInt(this.inputValue);
+              }
+
+              state.addFilter({
+                op: (cols) => `${cols[0]} ${op} ${filterValue}`,
+                columns,
+              });
+            },
+            submitLabel: 'Filter',
+          },
+          m(TextInput, {
+            id: 'column_filter_value',
+            ref: 'COLUMN_FILTER_VALUE',
+            autofocus: true,
+            oninput: (e: KeyboardEvent) => {
+              if (!e.target) return;
+
+              this.inputValue = (e.target as HTMLInputElement).value;
+            },
+          }),
+        ),
+    );
+  }
+}
+
 export class SqlTable implements m.ClassComponent<SqlTableConfig> {
   private readonly table: SqlTableDescription;
-  private readonly engine: Engine;
 
   private state: SqlTableState;
 
   constructor(vnode: m.Vnode<SqlTableConfig>) {
     this.state = vnode.attrs.state;
-    this.table = this.state.table;
-    this.engine = this.state.engine;
+    this.table = this.state.config;
   }
 
   renderFilters(): m.Children {
     const filters: m.Child[] = [];
     for (const filter of this.state.getFilters()) {
-      const label = isString(filter)
-        ? filter
-        : `Arg(${filter.argName}) ${filter.op}`;
+      const label = filterTitle(filter);
       filters.push(
         m(Button, {
           label,
@@ -68,52 +220,63 @@
     return filters;
   }
 
-  renderAddColumnOptions(addColumn: (column: Column) => void): m.Children {
+  renderAddColumnOptions(addColumn: (column: TableColumn) => void): m.Children {
     // We do not want to add columns which already exist, so we track the
     // columns which we are already showing here.
     // TODO(altimin): Theoretically a single table can have two different
     // arg_set_ids, so we should track (arg_set_id_column, arg_name) pairs here.
-    const existingColumns = new Set<string>();
+    const existingColumnIds = new Set<string>();
 
     for (const column of this.state.getSelectedColumns()) {
-      existingColumns.add(column.alias);
+      existingColumnIds.add(tableColumnId(column));
     }
 
     const result = [];
     for (const column of this.table.columns) {
-      if (existingColumns.has(column.name)) continue;
-      if (isArgSetIdColumn(column)) {
+      if (column instanceof TableColumn) {
+        if (existingColumnIds.has(tableColumnId(column))) continue;
+        result.push(
+          m(MenuItem, {
+            label: columnTitle(column),
+            onclick: () => addColumn(column),
+          }),
+        );
+      } else {
         result.push(
           m(
             MenuItem,
             {
-              label: column.name,
+              label: column.getTitle(),
             },
             m(ArgumentSelector, {
-              engine: this.engine,
-              argSetId: column,
-              tableName: this.table.name,
-              constraints: this.state.getQueryConstraints(),
-              alreadySelectedColumns: existingColumns,
-              onArgumentSelected: (argument: string) => {
-                addColumn(argColumn(this.table.name, column, argument));
+              alreadySelectedColumnIds: existingColumnIds,
+              tableManager: getTableManager(this.state),
+              columnSet: column,
+              onArgumentSelected: (column: TableColumn) => {
+                addColumn(column);
               },
             }),
           ),
         );
         continue;
       }
-      result.push(
-        m(MenuItem, {
-          label: column.name,
-          onclick: () => addColumn(columnFromSqlTableColumn(column)),
-        }),
-      );
     }
     return result;
   }
 
-  renderColumnHeader(column: Column, index: number) {
+  renderColumnFilterOptions(
+    c: TableColumn,
+  ): m.Vnode<ColumnFilterAttrs, unknown>[] {
+    return Object.values(FilterOption).map((filterOption) =>
+      m(ColumnFilter, {
+        filterOption,
+        columns: [c.primaryColumn()],
+        state: this.state,
+      }),
+    );
+  }
+
+  renderColumnHeader(column: TableColumn, index: number) {
     const sorted = this.state.isSortedBy(column);
     const icon =
       sorted === 'ASC'
@@ -121,17 +284,37 @@
         : sorted === 'DESC'
           ? Icons.SortedDesc
           : Icons.ContextMenu;
+
+    const columnAlias =
+      this.state.getCurrentRequest().columns[
+        sqlColumnId(column.primaryColumn())
+      ];
+    const chartConfig: ChartConfig = {
+      engine: this.state.trace.engine,
+      columnTitle: columnTitle(column),
+      sqlColumn: [columnAlias],
+      filters: this.state.getFilters(),
+      tableDisplay: this.table.displayName ?? this.table.name,
+      query: this.state.getSqlQuery(
+        Object.fromEntries([[columnAlias, column.primaryColumn()]]),
+      ),
+      aggregationType: column.aggregation?.().dataType,
+    };
+
     return m(
       PopupMenu2,
       {
-        trigger: m(Anchor, {icon}, column.title),
+        trigger: m(Anchor, {icon}, columnTitle(column)),
       },
       sorted !== 'DESC' &&
         m(MenuItem, {
           label: 'Sort: highest first',
           icon: Icons.SortedDesc,
           onclick: () => {
-            this.state.sortBy({column, direction: 'DESC'});
+            this.state.sortBy({
+              column: column,
+              direction: 'DESC',
+            });
           },
         }),
       sorted !== 'ASC' &&
@@ -139,7 +322,10 @@
           label: 'Sort: lowest first',
           icon: Icons.SortedAsc,
           onclick: () => {
-            this.state.sortBy({column, direction: 'ASC'});
+            this.state.sortBy({
+              column: column,
+              direction: 'ASC',
+            });
           },
         }),
       sorted !== undefined &&
@@ -154,52 +340,67 @@
           icon: Icons.Hide,
           onclick: () => this.state.hideColumnAtIndex(index),
         }),
-      m(MenuItem, {
-        label: 'Create histogram',
-        icon: Icons.Chart,
-        onclick: () => {
-          addHistogramTab(
-            {
-              sqlColumn: column.alias,
-              columnTitle: column.title,
-              filters: this.state.getFilters(),
-              tableDisplay: this.table.displayName,
-              query: this.state.buildSqlSelectStatement().selectStatement,
-            },
-            this.engine,
-          );
-        },
+      m(
+        MenuItem,
+        {label: 'Add filter', icon: Icons.Filter},
+        this.renderColumnFilterOptions(column),
+      ),
+      m(AddChartMenuItem, {
+        chartConfig,
+        chartOptions: [ChartOption.HISTOGRAM],
+        addChart: (option, config) => addChartTab(option, config),
       }),
       // Menu items before divider apply to selected column
       m(MenuDivider),
       // Menu items after divider apply to entire table
-      m(
-        MenuItem,
-        {label: 'Add column', icon: Icons.AddColumn},
-        this.renderAddColumnOptions((column) => {
-          this.state.addColumn(column, index);
-        }),
-      ),
+      m(AddColumnMenuItem, {table: this, state: this.state, index}),
     );
   }
 
   view() {
     const rows = this.state.getDisplayedRows();
 
+    const columns = this.state.getSelectedColumns();
+    const columnDescriptors = columns.map((column, i) => {
+      return {
+        title: this.renderColumnHeader(column, i),
+        render: (row: Row) => renderCell(column, row, this.state),
+      };
+    });
+
     return [
       m('div', this.renderFilters()),
-      m(BasicTable<Row>, {
-        data: rows,
-        columns: this.state.getSelectedColumns().map((column, i) => ({
-          title: this.renderColumnHeader(column, i),
-          render: (row: Row) => renderCell(column, row, this.state),
-        })),
-      }),
-      this.state.isLoading() && m(Spinner),
-      this.state.getQueryError() !== undefined &&
-        m('.query-error', this.state.getQueryError()),
+      m(
+        BasicTable<Row>,
+        {
+          data: rows,
+          columns: [
+            new ReorderableColumns(
+              columnDescriptors,
+              (from: number, to: number) => this.state.moveColumn(from, to),
+            ),
+          ],
+        },
+        this.state.isLoading() && m(Spinner),
+        this.state.getQueryError() !== undefined &&
+          m('.query-error', this.state.getQueryError()),
+      ),
     ];
   }
 }
 
-export {SqlTableDescription};
+function getTableManager(state: SqlTableState): TableManager {
+  return {
+    addFilter: (filter) => {
+      state.addFilter(filter);
+    },
+    trace: state.trace,
+    getSqlQuery: (columns: {[key: string]: SqlColumn}) =>
+      buildSqlQuery({
+        table: state.config.name,
+        columns,
+        filters: state.getFilters(),
+        orderBy: state.getOrderedBy(),
+      }),
+  };
+}
diff --git a/ui/src/frontend/widgets/sql/table/table_description.ts b/ui/src/frontend/widgets/sql/table/table_description.ts
index fdeb626..9c8a255 100644
--- a/ui/src/frontend/widgets/sql/table/table_description.ts
+++ b/ui/src/frontend/widgets/sql/table/table_description.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 The Android Open Source Project
+// Copyright (C) 2024 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,92 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// Definition of the SQL table to be displayed in the SQL table widget,
-// including the semantic definitions of the columns (e.g. timestamp
-// column which requires special formatting). Also note that some of the
-// columns require other columns for advanced display features (e.g. timestamp
-// and duration taken together define "a time range", which can be used for
-// additional filtering.
-
-export type DisplayConfig =
-  | SliceIdDisplayConfig
-  | Timestamp
-  | Duration
-  | ThreadDuration;
-
-// Common properties for all columns.
-interface SqlTableColumnBase {
-  // Name of the column in the SQL table.
-  name: string;
-  // Display name of the column in the UI.
-  title?: string;
-}
-
-export interface ArgSetIdColumn extends SqlTableColumnBase {
-  type: 'arg_set_id';
-}
-
-export interface RegularSqlTableColumn extends SqlTableColumnBase {
-  // Special rendering instructions for this column, including the list
-  // of additional columns required for the rendering.
-  display?: DisplayConfig;
-  // Whether the column should be hidden by default.
-  startsHidden?: boolean;
-}
-
-export type SqlTableColumn = RegularSqlTableColumn | ArgSetIdColumn;
-
-export function startsHidden(c: SqlTableColumn): boolean {
-  if (isArgSetIdColumn(c)) return true;
-  return c.startsHidden ?? false;
-}
-
-export function isArgSetIdColumn(c: SqlTableColumn): c is ArgSetIdColumn {
-  return (c as {type?: string}).type === 'arg_set_id';
-}
+import {TableColumn, TableColumnSet} from './column';
 
 export interface SqlTableDescription {
   readonly imports?: string[];
   name: string;
+  // In some cases, the name of the table we are querying is different from the name of the table we want to display to the user -- typically because the underlying table is wrapped into a view.
   displayName?: string;
-  columns: SqlTableColumn[];
-}
-
-export function tableDisplayName(table: SqlTableDescription): string {
-  return table.displayName ?? table.name;
-}
-
-// Additional columns needed to display the given column.
-export function dependendentColumns(display?: DisplayConfig): string[] {
-  switch (display?.type) {
-    case 'slice_id':
-      return [display.ts, display.dur, display.trackId];
-    default:
-      return [];
-  }
-}
-
-// Column displaying ids into the `slice` table. Requires the ts, dur and
-// track_id columns to be able to display the value, including the
-// "go-to-slice-on-click" functionality.
-export interface SliceIdDisplayConfig {
-  type: 'slice_id';
-  ts: string;
-  dur: string;
-  trackId: string;
-}
-
-// Column displaying timestamps.
-interface Timestamp {
-  type: 'timestamp';
-}
-
-// Column displaying durations.
-export interface Duration {
-  type: 'duration';
-}
-
-// Column displaying thread durations.
-export interface ThreadDuration {
-  type: 'thread_duration';
+  columns: (TableColumn | TableColumnSet)[];
 }
diff --git a/ui/src/frontend/widgets/sql/table/well_known_columns.ts b/ui/src/frontend/widgets/sql/table/well_known_columns.ts
new file mode 100644
index 0000000..7c1eb98
--- /dev/null
+++ b/ui/src/frontend/widgets/sql/table/well_known_columns.ts
@@ -0,0 +1,1166 @@
+// 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 {Icons} from '../../../../base/semantic_icons';
+import {sqliteString} from '../../../../base/string_utils';
+import {Duration, Time} from '../../../../base/time';
+import {SqlValue, STR} from '../../../../trace_processor/query_result';
+import {
+  asSchedSqlId,
+  asSliceSqlId,
+  asThreadStateSqlId,
+  asUpid,
+  asUtid,
+} from '../../../../trace_processor/sql_utils/core_types';
+import {getProcessName} from '../../../../trace_processor/sql_utils/process';
+import {getThreadName} from '../../../../trace_processor/sql_utils/thread';
+import {Anchor} from '../../../../widgets/anchor';
+import {renderError} from '../../../../widgets/error';
+import {MenuDivider, MenuItem, PopupMenu2} from '../../../../widgets/menu';
+import {DurationWidget} from '../../duration';
+import {processRefMenuItems, showProcessDetailsMenuItem} from '../../process';
+import {SchedRef} from '../../sched';
+import {SliceRef} from '../../slice';
+import {showThreadDetailsMenuItem, threadRefMenuItems} from '../../thread';
+import {ThreadStateRef} from '../../thread_state';
+import {Timestamp} from '../../timestamp';
+import {
+  AggregationConfig,
+  SourceTable,
+  SqlColumn,
+  sqlColumnId,
+  TableColumn,
+  TableColumnSet,
+  TableManager,
+} from './column';
+import {
+  displayValue,
+  getStandardContextMenuItems,
+  getStandardFilters,
+  renderStandardCell,
+} from './render_cell_utils';
+
+export type ColumnParams = {
+  alias?: string;
+  startsHidden?: boolean;
+  title?: string;
+};
+
+export type StandardColumnParams = ColumnParams & {
+  aggregationType?: 'nominal' | 'quantitative';
+};
+
+export interface IdColumnParams {
+  // Whether the column is guaranteed not to have null values.
+  // (this will allow us to upgrage the joins on this column to more performant INNER JOINs).
+  notNull?: boolean;
+}
+
+type ColumnSetParams = {
+  title: string;
+  startsHidden?: boolean;
+};
+
+export class StandardColumn extends TableColumn {
+  constructor(
+    private column: SqlColumn,
+    private params?: StandardColumnParams,
+  ) {
+    super(params);
+  }
+
+  primaryColumn(): SqlColumn {
+    return this.column;
+  }
+
+  aggregation(): AggregationConfig {
+    return {dataType: this.params?.aggregationType};
+  }
+
+  getTitle() {
+    return this.params?.title;
+  }
+
+  renderCell(value: SqlValue, tableManager: TableManager): m.Children {
+    return renderStandardCell(value, this.column, tableManager);
+  }
+}
+
+export class TimestampColumn extends TableColumn {
+  constructor(
+    private column: SqlColumn,
+    private params?: ColumnParams,
+  ) {
+    super(params);
+  }
+
+  primaryColumn(): SqlColumn {
+    return this.column;
+  }
+
+  getTitle() {
+    return this.params?.title;
+  }
+
+  renderCell(value: SqlValue, tableManager: TableManager): m.Children {
+    if (typeof value !== 'bigint') {
+      return renderStandardCell(value, this.column, tableManager);
+    }
+    return m(Timestamp, {
+      ts: Time.fromRaw(value),
+      extraMenuItems: getStandardContextMenuItems(
+        value,
+        this.column,
+        tableManager,
+      ),
+    });
+  }
+}
+
+export class DurationColumn extends TableColumn {
+  constructor(
+    private column: SqlColumn,
+    private params?: ColumnParams,
+  ) {
+    super(params);
+  }
+
+  primaryColumn(): SqlColumn {
+    return this.column;
+  }
+
+  getTitle() {
+    return this.params?.title;
+  }
+
+  renderCell(value: SqlValue, tableManager: TableManager): m.Children {
+    if (typeof value !== 'bigint') {
+      return renderStandardCell(value, this.column, tableManager);
+    }
+
+    return m(DurationWidget, {
+      dur: Duration.fromRaw(value),
+      extraMenuItems: getStandardContextMenuItems(
+        value,
+        this.column,
+        tableManager,
+      ),
+    });
+  }
+}
+
+function wrongTypeError(type: string, name: SqlColumn, value: SqlValue) {
+  return renderError(
+    `Wrong type for ${type} column ${sqlColumnId(name)}: bigint expected, ${typeof value} found`,
+  );
+}
+
+export class SliceIdColumn extends TableColumn {
+  private columns: {ts: SqlColumn; dur: SqlColumn; trackId: SqlColumn};
+
+  constructor(
+    private id: SqlColumn,
+    private params?: ColumnParams & IdColumnParams,
+  ) {
+    super(params);
+
+    const sliceTable: SourceTable = {
+      table: 'slice',
+      joinOn: {id: this.id},
+      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
+      innerJoin: this.params?.notNull === true,
+    };
+
+    this.columns = {
+      ts: {
+        column: 'ts',
+        source: sliceTable,
+      },
+      dur: {
+        column: 'dur',
+        source: sliceTable,
+      },
+      trackId: {
+        column: 'track_id',
+        source: sliceTable,
+      },
+    };
+  }
+
+  primaryColumn(): SqlColumn {
+    return this.id;
+  }
+
+  getTitle() {
+    return this.params?.title;
+  }
+
+  dependentColumns() {
+    return this.columns;
+  }
+
+  renderCell(
+    value: SqlValue,
+    manager: TableManager,
+    data: {[key: string]: SqlValue},
+  ): m.Children {
+    const id = value;
+    const ts = data['ts'];
+    const dur = data['dur'] === null ? -1n : data['dur'];
+    const trackId = data['trackId'];
+
+    if (id === null) {
+      return renderStandardCell(id, this.id, manager);
+    }
+    if (ts === null || trackId === null) {
+      return renderError(`Slice with id ${id} not found`);
+    }
+    if (typeof id !== 'bigint') return wrongTypeError('id', this.id, id);
+    if (typeof ts !== 'bigint') {
+      return wrongTypeError('timestamp', this.columns.ts, ts);
+    }
+    if (typeof dur !== 'bigint') {
+      return wrongTypeError('duration', this.columns.dur, dur);
+    }
+    if (typeof trackId !== 'bigint') {
+      return wrongTypeError('track id', this.columns.trackId, trackId);
+    }
+
+    return m(SliceRef, {
+      id: asSliceSqlId(Number(id)),
+      name: `${id}`,
+      switchToCurrentSelectionTab: false,
+    });
+  }
+}
+
+export class SliceColumnSet extends TableColumnSet {
+  constructor(
+    private id: SqlColumn,
+    private params?: ColumnSetParams,
+  ) {
+    super();
+  }
+
+  getTitle(): string {
+    return this.params?.title ?? `${sqlColumnId(this.id)} (slice)`;
+  }
+
+  async discover(): Promise<
+    {key: string; column: TableColumn | TableColumnSet}[]
+  > {
+    const column: (name: string) => SqlColumn = (name) => {
+      return {
+        column: name,
+        source: {
+          table: 'slice',
+          joinOn: {id: this.id},
+        },
+      };
+    };
+
+    return [
+      {
+        key: 'id',
+        column: new SliceIdColumn(this.id),
+      },
+      {
+        key: 'ts',
+        column: new TimestampColumn(column('ts')),
+      },
+      {
+        key: 'dur',
+        column: new DurationColumn(column('dur')),
+      },
+      {
+        key: 'name',
+        column: new StandardColumn(column('name')),
+      },
+      {
+        key: 'thread_dur',
+        column: new StandardColumn(column('thread_dur')),
+      },
+      {
+        key: 'parent_id',
+        column: new SliceColumnSet(column('parent_id')),
+      },
+    ];
+  }
+
+  initialColumns(): TableColumn[] {
+    if (this.params?.startsHidden) return [];
+    return [new SliceIdColumn(this.id)];
+  }
+}
+
+export class SchedIdColumn extends TableColumn {
+  private columns: {ts: SqlColumn; dur: SqlColumn; cpu: SqlColumn};
+
+  constructor(
+    private id: SqlColumn,
+    private params?: ColumnParams & IdColumnParams,
+  ) {
+    super(params);
+
+    const schedTable: SourceTable = {
+      table: 'sched',
+      joinOn: {id: this.id},
+      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
+      innerJoin: this.params?.notNull === true,
+    };
+
+    this.columns = {
+      ts: {
+        column: 'ts',
+        source: schedTable,
+      },
+      dur: {
+        column: 'dur',
+        source: schedTable,
+      },
+      cpu: {
+        column: 'cpu',
+        source: schedTable,
+      },
+    };
+  }
+
+  primaryColumn(): SqlColumn {
+    return this.id;
+  }
+
+  getTitle() {
+    return this.params?.title;
+  }
+
+  dependentColumns() {
+    return {
+      ts: this.columns.ts,
+      dur: this.columns.dur,
+      cpu: this.columns.cpu,
+    };
+  }
+
+  renderCell(
+    value: SqlValue,
+    manager: TableManager,
+    data: {[key: string]: SqlValue},
+  ): m.Children {
+    const id = value;
+    const ts = data['ts'];
+    const dur = data['dur'] === null ? -1n : data['dur'];
+    const cpu = data['cpu'];
+
+    if (id === null) {
+      return renderStandardCell(id, this.id, manager);
+    }
+    if (ts === null || cpu === null) {
+      return renderError(`Sched with id ${id} not found`);
+    }
+    if (typeof id !== 'bigint') return wrongTypeError('id', this.id, id);
+    if (typeof ts !== 'bigint') {
+      return wrongTypeError('timestamp', this.columns.ts, ts);
+    }
+    if (typeof dur !== 'bigint') {
+      return wrongTypeError('duration', this.columns.dur, dur);
+    }
+    if (typeof cpu !== 'bigint') {
+      return wrongTypeError('track id', this.columns.cpu, cpu);
+    }
+
+    return m(SchedRef, {
+      id: asSchedSqlId(Number(id)),
+      name: `${id}`,
+      switchToCurrentSelectionTab: false,
+    });
+  }
+}
+
+export class ThreadStateIdColumn extends TableColumn {
+  private columns: {ts: SqlColumn; dur: SqlColumn; utid: SqlColumn};
+
+  constructor(
+    private id: SqlColumn,
+    private params?: ColumnParams & IdColumnParams,
+  ) {
+    super(params);
+
+    const threadStateTable: SourceTable = {
+      table: 'thread_state',
+      joinOn: {id: this.id},
+      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
+      innerJoin: this.params?.notNull === true,
+    };
+
+    this.columns = {
+      ts: {
+        column: 'ts',
+        source: threadStateTable,
+      },
+      dur: {
+        column: 'dur',
+        source: threadStateTable,
+      },
+      utid: {
+        column: 'utid',
+        source: threadStateTable,
+      },
+    };
+  }
+
+  primaryColumn(): SqlColumn {
+    return this.id;
+  }
+
+  getTitle() {
+    return this.params?.title;
+  }
+
+  dependentColumns() {
+    return {
+      ts: this.columns.ts,
+      dur: this.columns.dur,
+      utid: this.columns.utid,
+    };
+  }
+
+  renderCell(
+    value: SqlValue,
+    manager: TableManager,
+    data: {[key: string]: SqlValue},
+  ): m.Children {
+    const id = value;
+    const ts = data['ts'];
+    const dur = data['dur'] === null ? -1n : data['dur'];
+    const utid = data['utid'];
+
+    if (id === null) {
+      return renderStandardCell(id, this.id, manager);
+    }
+    if (ts === null || utid === null) {
+      return renderError(`Thread state with id ${id} not found`);
+    }
+    if (typeof id !== 'bigint') return wrongTypeError('id', this.id, id);
+    if (typeof ts !== 'bigint') {
+      return wrongTypeError('timestamp', this.columns.ts, ts);
+    }
+    if (typeof dur !== 'bigint') {
+      return wrongTypeError('duration', this.columns.dur, dur);
+    }
+    if (typeof utid !== 'bigint') {
+      return wrongTypeError('track id', this.columns.utid, utid);
+    }
+
+    return m(ThreadStateRef, {
+      id: asThreadStateSqlId(Number(id)),
+      name: `${id}`,
+      switchToCurrentSelectionTab: false,
+    });
+  }
+}
+
+export class ThreadColumn extends TableColumn {
+  private columns: {name: SqlColumn; tid: SqlColumn};
+
+  constructor(
+    private utid: SqlColumn,
+    private params?: ColumnParams & IdColumnParams,
+  ) {
+    // Both ThreadColumn and ThreadIdColumn are referencing the same underlying SQL column as primary,
+    // so we have to use tag to distinguish them.
+    super({tag: 'thread', ...params});
+
+    const threadTable: SourceTable = {
+      table: 'thread',
+      joinOn: {id: this.utid},
+      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
+      innerJoin: this.params?.notNull === true,
+    };
+
+    this.columns = {
+      name: {
+        column: 'name',
+        source: threadTable,
+      },
+      tid: {
+        column: 'tid',
+        source: threadTable,
+      },
+    };
+  }
+
+  primaryColumn(): SqlColumn {
+    return this.utid;
+  }
+
+  getTitle() {
+    if (this.params?.title !== undefined) return this.params.title;
+    return `${sqlColumnId(this.utid)} (thread)`;
+  }
+
+  dependentColumns() {
+    return {
+      tid: this.columns.tid,
+      name: this.columns.name,
+    };
+  }
+
+  renderCell(
+    value: SqlValue,
+    manager: TableManager,
+    data: {[key: string]: SqlValue},
+  ): m.Children {
+    const utid = value;
+    const rawTid = data['tid'];
+    const rawName = data['name'];
+
+    if (utid === null) {
+      return renderStandardCell(utid, this.utid, manager);
+    }
+    if (typeof utid !== 'bigint') {
+      return wrongTypeError('utid', this.utid, utid);
+    }
+    if (rawTid !== null && typeof rawTid !== 'bigint') {
+      return wrongTypeError('tid', this.columns.tid, rawTid);
+    }
+    if (rawName !== null && typeof rawName !== 'string') {
+      return wrongTypeError('name', this.columns.name, rawName);
+    }
+
+    const name: string | undefined = rawName ?? undefined;
+    const tid: number | undefined =
+      rawTid !== null ? Number(rawTid) : undefined;
+
+    return m(
+      PopupMenu2,
+      {
+        trigger: m(
+          Anchor,
+          getThreadName({
+            name: name ?? undefined,
+            tid: tid !== null ? Number(tid) : undefined,
+          }),
+        ),
+      },
+      threadRefMenuItems({utid: asUtid(Number(utid)), name, tid}),
+      m(MenuDivider),
+      m(
+        MenuItem,
+        {
+          label: 'Add filter',
+          icon: Icons.Filter,
+        },
+        m(
+          MenuItem,
+          {
+            label: 'utid',
+          },
+          getStandardFilters(utid, this.utid, manager),
+        ),
+        m(
+          MenuItem,
+          {
+            label: 'thread name',
+          },
+          getStandardFilters(rawName, this.columns.name, manager),
+        ),
+        m(
+          MenuItem,
+          {
+            label: 'tid',
+          },
+          getStandardFilters(rawTid, this.columns.tid, manager),
+        ),
+      ),
+    );
+  }
+
+  aggregation(): AggregationConfig {
+    return {
+      dataType: 'nominal',
+    };
+  }
+}
+
+// ThreadIdColumn is a column type for displaying primary key of the `thread` table.
+// All other references (foreign keys) should use `ThreadColumn` instead.
+export class ThreadIdColumn extends TableColumn {
+  private columns: {tid: SqlColumn};
+
+  constructor(private utid: SqlColumn) {
+    super({});
+
+    const threadTable: SourceTable = {
+      table: 'thread',
+      joinOn: {id: this.utid},
+      innerJoin: true,
+    };
+
+    this.columns = {
+      tid: {
+        column: 'tid',
+        source: threadTable,
+      },
+    };
+  }
+
+  primaryColumn(): SqlColumn {
+    return this.utid;
+  }
+
+  getTitle() {
+    return 'utid';
+  }
+
+  dependentColumns() {
+    return {
+      tid: this.columns.tid,
+    };
+  }
+
+  renderCell(
+    value: SqlValue,
+    manager: TableManager,
+    data: {[key: string]: SqlValue},
+  ): m.Children {
+    const utid = value;
+    const rawTid = data['tid'];
+
+    if (utid === null) {
+      return renderStandardCell(utid, this.utid, manager);
+    }
+
+    if (typeof utid !== 'bigint') {
+      throw new Error(
+        `thread.utid is expected to be bigint, got ${typeof utid}`,
+      );
+    }
+
+    return m(
+      PopupMenu2,
+      {
+        trigger: m(Anchor, `${utid}`),
+      },
+
+      showThreadDetailsMenuItem(
+        asUtid(Number(utid)),
+        rawTid === null ? undefined : Number(rawTid),
+      ),
+      getStandardContextMenuItems(utid, this.utid, manager),
+    );
+  }
+
+  aggregation(): AggregationConfig {
+    return {dataType: 'nominal'};
+  }
+}
+
+export class ThreadColumnSet extends TableColumnSet {
+  constructor(
+    private id: SqlColumn,
+    private params: ColumnSetParams & {
+      notNull?: boolean;
+    },
+  ) {
+    super();
+  }
+
+  getTitle(): string {
+    return `${this.params.title} (thread)`;
+  }
+
+  initialColumns(): TableColumn[] {
+    if (this.params.startsHidden === true) return [];
+    return [new ThreadColumn(this.id)];
+  }
+
+  async discover() {
+    const column: (name: string) => SqlColumn = (name) => ({
+      column: name,
+      source: {
+        table: 'thread',
+        joinOn: {id: this.id},
+      },
+      innerJoin: this.params.notNull === true,
+    });
+
+    return [
+      {
+        key: 'thread',
+        column: new ThreadColumn(this.id),
+      },
+      {
+        key: 'utid',
+        column: new ThreadIdColumn(this.id),
+      },
+      {
+        key: 'tid',
+        column: new StandardColumn(column('tid'), {aggregationType: 'nominal'}),
+      },
+      {
+        key: 'name',
+        column: new StandardColumn(column('name')),
+      },
+      {
+        key: 'start_ts',
+        column: new TimestampColumn(column('start_ts')),
+      },
+      {
+        key: 'end_ts',
+        column: new TimestampColumn(column('end_ts')),
+      },
+      {
+        key: 'upid',
+        column: new ProcessColumnSet(column('upid'), {title: 'upid'}),
+      },
+      {
+        key: 'is_main_thread',
+        column: new StandardColumn(column('is_main_thread'), {
+          aggregationType: 'nominal',
+        }),
+      },
+    ];
+  }
+}
+
+export class ProcessColumn extends TableColumn {
+  private columns: {name: SqlColumn; pid: SqlColumn};
+
+  constructor(
+    private upid: SqlColumn,
+    private params?: ColumnParams & IdColumnParams,
+  ) {
+    // Both ProcessColumn and ProcessIdColumn are referencing the same underlying SQL column as primary,
+    // so we have to use tag to distinguish them.
+    super({tag: 'process', ...params});
+
+    const processTable: SourceTable = {
+      table: 'process',
+      joinOn: {id: this.upid},
+      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
+      innerJoin: this.params?.notNull === true,
+    };
+
+    this.columns = {
+      name: {
+        column: 'name',
+        source: processTable,
+      },
+      pid: {
+        column: 'pid',
+        source: processTable,
+      },
+    };
+  }
+
+  primaryColumn(): SqlColumn {
+    return this.upid;
+  }
+
+  getTitle() {
+    if (this.params?.title !== undefined) return this.params.title;
+    return `${sqlColumnId(this.upid)} (process)`;
+  }
+
+  dependentColumns() {
+    return this.columns;
+  }
+
+  renderCell(
+    value: SqlValue,
+    manager: TableManager,
+    data: {[key: string]: SqlValue},
+  ): m.Children {
+    const upid = value;
+    const rawPid = data['pid'];
+    const rawName = data['name'];
+
+    if (upid === null) {
+      return renderStandardCell(upid, this.upid, manager);
+    }
+    if (typeof upid !== 'bigint') {
+      return wrongTypeError('upid', this.upid, upid);
+    }
+    if (rawPid !== null && typeof rawPid !== 'bigint') {
+      return wrongTypeError('pid', this.columns.pid, rawPid);
+    }
+    if (rawName !== null && typeof rawName !== 'string') {
+      return wrongTypeError('name', this.columns.name, rawName);
+    }
+
+    const name: string | undefined = rawName ?? undefined;
+    const pid: number | undefined =
+      rawPid !== null ? Number(rawPid) : undefined;
+
+    return m(
+      PopupMenu2,
+      {
+        trigger: m(
+          Anchor,
+          getProcessName({
+            name: name ?? undefined,
+            pid: pid !== null ? Number(pid) : undefined,
+          }),
+        ),
+      },
+      processRefMenuItems({upid: asUpid(Number(upid)), name, pid}),
+      m(MenuDivider),
+      m(
+        MenuItem,
+        {
+          label: 'Add filter',
+          icon: Icons.Filter,
+        },
+        m(
+          MenuItem,
+          {
+            label: 'upid',
+          },
+          getStandardFilters(upid, this.upid, manager),
+        ),
+        m(
+          MenuItem,
+          {
+            label: 'process name',
+          },
+          getStandardFilters(rawName, this.columns.name, manager),
+        ),
+        m(
+          MenuItem,
+          {
+            label: 'tid',
+          },
+          getStandardFilters(rawPid, this.columns.pid, manager),
+        ),
+      ),
+    );
+  }
+
+  aggregation(): AggregationConfig {
+    return {
+      dataType: 'nominal',
+    };
+  }
+}
+
+// ProcessIdColumn is a column type for displaying primary key of the `process` table.
+// All other references (foreign keys) should use `ProcessColumn` instead.
+export class ProcessIdColumn extends TableColumn {
+  private columns: {pid: SqlColumn};
+
+  constructor(private upid: SqlColumn) {
+    super({});
+
+    const processTable: SourceTable = {
+      table: 'process',
+      joinOn: {id: this.upid},
+      innerJoin: true,
+    };
+
+    this.columns = {
+      pid: {
+        column: 'pid',
+        source: processTable,
+      },
+    };
+  }
+
+  primaryColumn(): SqlColumn {
+    return this.upid;
+  }
+
+  getTitle() {
+    return 'upid';
+  }
+
+  dependentColumns() {
+    return {
+      pid: this.columns.pid,
+    };
+  }
+
+  renderCell(
+    value: SqlValue,
+    manager: TableManager,
+    data: {[key: string]: SqlValue},
+  ): m.Children {
+    const upid = value;
+    const rawPid = data['pid'];
+
+    if (upid === null) {
+      return renderStandardCell(upid, this.upid, manager);
+    }
+
+    if (typeof upid !== 'bigint') {
+      throw new Error(
+        `process.upid is expected to be bigint, got ${typeof upid}`,
+      );
+    }
+
+    return m(
+      PopupMenu2,
+      {
+        trigger: m(Anchor, `${upid}`),
+      },
+
+      showProcessDetailsMenuItem(
+        asUpid(Number(upid)),
+        rawPid === null ? undefined : Number(rawPid),
+      ),
+      getStandardContextMenuItems(upid, this.upid, manager),
+    );
+  }
+
+  aggregation(): AggregationConfig {
+    return {dataType: 'nominal'};
+  }
+}
+
+export class ProcessColumnSet extends TableColumnSet {
+  constructor(
+    private id: SqlColumn,
+    private params: ColumnSetParams & {
+      notNull?: boolean;
+    },
+  ) {
+    super();
+  }
+
+  getTitle(): string {
+    return `${this.params.title} (process)`;
+  }
+
+  initialColumns(): TableColumn[] {
+    if (this.params.startsHidden === true) return [];
+    return [new ProcessColumn(this.id)];
+  }
+
+  async discover() {
+    const column: (name: string) => SqlColumn = (name) => ({
+      column: name,
+      source: {
+        table: 'process',
+        joinOn: {id: this.id},
+      },
+      innerJoin: this.params.notNull === true,
+    });
+
+    return [
+      {
+        key: 'process',
+        column: new ProcessColumn(this.id),
+      },
+      {
+        key: 'upid',
+        column: new ProcessIdColumn(this.id),
+      },
+      {
+        key: 'pid',
+        column: new StandardColumn(column('pid'), {aggregationType: 'nominal'}),
+      },
+      {
+        key: 'name',
+        column: new StandardColumn(column('name')),
+      },
+      {
+        key: 'start_ts',
+        column: new TimestampColumn(column('start_ts')),
+      },
+      {
+        key: 'end_ts',
+        column: new TimestampColumn(column('end_ts')),
+      },
+      {
+        key: 'parent_upid',
+        column: new ProcessColumnSet(column('parent_upid'), {
+          title: 'parent_upid',
+        }),
+      },
+      {
+        key: 'uid',
+        column: new StandardColumn(column('uid'), {aggregationType: 'nominal'}),
+      },
+      {
+        key: 'android_appid',
+        column: new StandardColumn(column('android_appid'), {
+          aggregationType: 'nominal',
+        }),
+      },
+      {
+        key: 'cmdline',
+        column: new StandardColumn(column('cmdline')),
+      },
+      {
+        key: 'arg_set_id (args)',
+        column: new ArgSetColumnSet(column('arg_set_id')),
+      },
+    ];
+  }
+}
+
+class ArgColumn extends TableColumn {
+  private displayValue: SqlColumn;
+  private stringValue: SqlColumn;
+  private intValue: SqlColumn;
+  private realValue: SqlColumn;
+
+  constructor(
+    private argSetId: SqlColumn,
+    private key: string,
+  ) {
+    super();
+
+    const argTable: SourceTable = {
+      table: 'args',
+      joinOn: {
+        arg_set_id: argSetId,
+        key: sqliteString(key),
+      },
+    };
+
+    this.displayValue = {
+      column: 'display_value',
+      source: argTable,
+    };
+    this.stringValue = {
+      column: 'string_value',
+      source: argTable,
+    };
+    this.intValue = {
+      column: 'int_value',
+      source: argTable,
+    };
+    this.realValue = {
+      column: 'real_value',
+      source: argTable,
+    };
+  }
+
+  override primaryColumn(): SqlColumn {
+    return this.displayValue;
+  }
+
+  override sortColumns(): SqlColumn[] {
+    return [this.stringValue, this.intValue, this.realValue];
+  }
+
+  override dependentColumns() {
+    return {
+      stringValue: this.stringValue,
+      intValue: this.intValue,
+      realValue: this.realValue,
+    };
+  }
+
+  getTitle() {
+    return `${sqlColumnId(this.argSetId)}[${this.key}]`;
+  }
+
+  renderCell(
+    value: SqlValue,
+    tableManager: TableManager,
+    dependentColumns: {[key: string]: SqlValue},
+  ): m.Children {
+    const strValue = dependentColumns['stringValue'];
+    const intValue = dependentColumns['intValue'];
+    const realValue = dependentColumns['realValue'];
+
+    let contextMenuItems: m.Child[] = [];
+    if (strValue !== null) {
+      contextMenuItems = getStandardContextMenuItems(
+        strValue,
+        this.stringValue,
+        tableManager,
+      );
+    } else if (intValue !== null) {
+      contextMenuItems = getStandardContextMenuItems(
+        intValue,
+        this.intValue,
+        tableManager,
+      );
+    } else if (realValue !== null) {
+      contextMenuItems = getStandardContextMenuItems(
+        realValue,
+        this.realValue,
+        tableManager,
+      );
+    } else {
+      contextMenuItems = getStandardContextMenuItems(
+        value,
+        this.displayValue,
+        tableManager,
+      );
+    }
+    return m(
+      PopupMenu2,
+      {
+        trigger: m(Anchor, displayValue(value)),
+      },
+      ...contextMenuItems,
+    );
+  }
+}
+
+export class ArgSetColumnSet extends TableColumnSet {
+  constructor(
+    private column: SqlColumn,
+    private title?: string,
+  ) {
+    super();
+  }
+
+  getTitle(): string {
+    return this.title ?? sqlColumnId(this.column);
+  }
+
+  async discover(
+    manager: TableManager,
+  ): Promise<{key: string; column: TableColumn}[]> {
+    const queryResult = await manager.trace.engine.query(`
+      -- Encapsulate the query in a CTE to avoid clashes between filters
+      -- and columns of the 'args' table.
+      SELECT
+        DISTINCT args.key
+      FROM (${manager.getSqlQuery({arg_set_id: this.column})}) data
+      JOIN args USING (arg_set_id)
+    `);
+    const result = [];
+    const it = queryResult.iter({key: STR});
+    for (; it.valid(); it.next()) {
+      result.push({
+        key: it.key,
+        column: argTableColumn(this.column, it.key),
+      });
+    }
+    return result;
+  }
+}
+
+export function argSqlColumn(argSetId: SqlColumn, key: string): SqlColumn {
+  return {
+    column: 'display_value',
+    source: {
+      table: 'args',
+      joinOn: {
+        arg_set_id: argSetId,
+        key: sqliteString(key),
+      },
+    },
+  };
+}
+
+export function argTableColumn(argSetId: SqlColumn, key: string) {
+  return new ArgColumn(argSetId, key);
+}
diff --git a/ui/src/frontend/widgets/sql/table/well_known_sql_tables.ts b/ui/src/frontend/widgets/sql/table/well_known_sql_tables.ts
deleted file mode 100644
index 6bc8c81..0000000
--- a/ui/src/frontend/widgets/sql/table/well_known_sql_tables.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {SqlTableDescription} from './table_description';
-
-const sliceTable: SqlTableDescription = {
-  imports: ['slices.slices'],
-  name: '_slice_with_thread_and_process_info',
-  displayName: 'slice',
-  columns: [
-    {
-      name: 'id',
-      title: 'ID',
-      display: {
-        type: 'slice_id',
-        ts: 'ts',
-        dur: 'dur',
-        trackId: 'track_id',
-      },
-    },
-    {
-      name: 'ts',
-      title: 'Timestamp',
-      display: {
-        type: 'timestamp',
-      },
-    },
-    {
-      name: 'dur',
-      title: 'Duration',
-      display: {
-        type: 'duration',
-      },
-    },
-    {
-      name: 'thread_dur',
-      title: 'Thread duration',
-      display: {
-        type: 'thread_duration',
-      },
-    },
-    {
-      name: 'category',
-      title: 'Category',
-    },
-    {
-      name: 'name',
-      title: 'Name',
-    },
-    {
-      name: 'track_id',
-      title: 'Track ID',
-      startsHidden: true,
-    },
-    {
-      name: 'track_name',
-      title: 'Track name',
-      startsHidden: true,
-    },
-    {
-      name: 'thread_name',
-      title: 'Thread name',
-    },
-    {
-      name: 'utid',
-      startsHidden: true,
-    },
-    {
-      name: 'tid',
-    },
-    {
-      name: 'process_name',
-      title: 'Process name',
-    },
-    {
-      name: 'upid',
-      startsHidden: true,
-    },
-    {
-      name: 'pid',
-    },
-    {
-      name: 'depth',
-      title: 'Depth',
-      startsHidden: true,
-    },
-    {
-      name: 'parent_id',
-      title: 'Parent slice ID',
-      startsHidden: true,
-    },
-    {
-      name: 'arg_set_id',
-      title: 'Arg',
-      type: 'arg_set_id',
-    },
-  ],
-};
-
-export class SqlTables {
-  static readonly slice = sliceTable;
-}
diff --git a/ui/src/frontend/widgets/thread.ts b/ui/src/frontend/widgets/thread.ts
index b668d1c..5d8018d 100644
--- a/ui/src/frontend/widgets/thread.ts
+++ b/ui/src/frontend/widgets/thread.ts
@@ -13,24 +13,58 @@
 // limitations under the License.
 
 import m from 'mithril';
-
-import {
-  ThreadInfo,
-  getThreadName,
-} from '../../trace_processor/sql_utils/thread';
-import {MenuItem, PopupMenu2} from '../../widgets/menu';
-import {Anchor} from '../../widgets/anchor';
-import {exists} from '../../base/utils';
-import {Icons} from '../../base/semantic_icons';
 import {copyToClipboard} from '../../base/clipboard';
+import {Icons} from '../../base/semantic_icons';
+import {exists} from '../../base/utils';
+import {addEphemeralTab} from '../../common/add_ephemeral_tab';
+import {
+  getThreadInfo,
+  getThreadName,
+  ThreadInfo,
+} from '../../trace_processor/sql_utils/thread';
+import {Anchor} from '../../widgets/anchor';
+import {MenuItem, PopupMenu2} from '../../widgets/menu';
+import {ThreadDetailsTab} from '../thread_details_tab';
+import {
+  createSqlIdRefRenderer,
+  sqlIdRegistry,
+} from './sql/details/sql_ref_renderer_registry';
+import {asUtid} from '../../trace_processor/sql_utils/core_types';
+import {Utid} from '../../trace_processor/sql_utils/core_types';
+import {AppImpl} from '../../core/app_impl';
 
-export function renderThreadRef(info: ThreadInfo): m.Children {
-  const name = info.name;
-  return m(
-    PopupMenu2,
-    {
-      trigger: m(Anchor, getThreadName(info)),
+export function showThreadDetailsMenuItem(
+  utid: Utid,
+  tid?: number,
+): m.Children {
+  return m(MenuItem, {
+    icon: Icons.ExternalLink,
+    label: 'Show thread details',
+    onclick: () => {
+      // TODO(primiano): `trace` should be injected, but doing so would require
+      // an invasive refactoring of most classes in frontend/widgets/sql/*.
+      const trace = AppImpl.instance.trace;
+      if (trace === undefined) return;
+      addEphemeralTab(
+        'threadDetails',
+        new ThreadDetailsTab({
+          trace,
+          utid,
+          tid,
+        }),
+      );
     },
+  });
+}
+
+export function threadRefMenuItems(info: {
+  utid: Utid;
+  name?: string;
+  tid?: number;
+}): m.Children {
+  // We capture a copy to be able to pass it across async boundary to `onclick`.
+  const name = info.name;
+  return [
     exists(name) &&
       m(MenuItem, {
         icon: Icons.Copy,
@@ -48,5 +82,27 @@
       label: 'Copy utid',
       onclick: () => copyToClipboard(`${info.utid}`),
     }),
+    showThreadDetailsMenuItem(info.utid, info.tid),
+  ];
+}
+
+export function renderThreadRef(info: {
+  utid: Utid;
+  name?: string;
+  tid?: number;
+}): m.Children {
+  return m(
+    PopupMenu2,
+    {
+      trigger: m(Anchor, getThreadName(info)),
+    },
+    threadRefMenuItems(info),
   );
 }
+
+sqlIdRegistry['thread'] = createSqlIdRefRenderer<ThreadInfo>(
+  async (engine, id) => await getThreadInfo(engine, asUtid(Number(id))),
+  (data: ThreadInfo) => ({
+    value: renderThreadRef(data),
+  }),
+);
diff --git a/ui/src/frontend/widgets/thread_state.ts b/ui/src/frontend/widgets/thread_state.ts
index 78b44c6..b7cfd69 100644
--- a/ui/src/frontend/widgets/thread_state.ts
+++ b/ui/src/frontend/widgets/thread_state.ts
@@ -13,26 +13,21 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {
-  ThreadStateSqlId,
-  Utid,
-} from '../../trace_processor/sql_utils/core_types';
-import {duration, time} from '../../base/time';
+import {ThreadStateSqlId} from '../../trace_processor/sql_utils/core_types';
 import {Anchor} from '../../widgets/anchor';
 import {Icons} from '../../base/semantic_icons';
-import {globals} from '../globals';
-import {THREAD_STATE_TRACK_KIND} from '../../core/track_kinds';
-import {Actions} from '../../common/actions';
-import {scrollToTrackAndTs} from '../scroll_helper';
 import {ThreadState} from '../../trace_processor/sql_utils/thread_state';
+import {AppImpl} from '../../core/app_impl';
 
 interface ThreadStateRefAttrs {
   id: ThreadStateSqlId;
-  ts: time;
-  dur: duration;
-  utid: Utid;
   // If not present, a placeholder name will be used.
   name?: string;
+
+  // Whether clicking on the reference should change the current tab
+  // to "current selection" tab in addition to updating the selection
+  // and changing the viewport. True by default.
+  readonly switchToCurrentSelectionTab?: boolean;
 }
 
 export class ThreadStateRef implements m.ClassComponent<ThreadStateRefAttrs> {
@@ -42,28 +37,16 @@
       {
         icon: Icons.UpdateSelection,
         onclick: () => {
-          let trackKey: string | undefined;
-          for (const track of Object.values(globals.state.tracks)) {
-            const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
-            if (
-              trackDesc &&
-              trackDesc.tags?.kind === THREAD_STATE_TRACK_KIND &&
-              trackDesc.tags?.utid === vnode.attrs.utid
-            ) {
-              trackKey = track.key;
-            }
-          }
-
-          if (trackKey) {
-            globals.makeSelection(
-              Actions.selectThreadState({
-                id: vnode.attrs.id,
-                trackKey: trackKey.toString(),
-              }),
-            );
-
-            scrollToTrackAndTs(trackKey, vnode.attrs.ts, true);
-          }
+          // TODO(primiano): the Trace object should be properly injected here.
+          AppImpl.instance.trace?.selection.selectSqlEvent(
+            'thread_state',
+            vnode.attrs.id,
+            {
+              switchToCurrentSelectionTab:
+                vnode.attrs.switchToCurrentSelectionTab,
+              scrollToSelection: true,
+            },
+          );
         },
       },
       vnode.attrs.name ?? `Thread State ${vnode.attrs.id}`,
@@ -75,9 +58,6 @@
   if (state.thread === undefined) return null;
 
   return m(ThreadStateRef, {
-    id: state.threadStateSqlId,
-    ts: state.ts,
-    dur: state.dur,
-    utid: state.thread?.utid,
+    id: state.id,
   });
 }
diff --git a/ui/src/frontend/widgets/timestamp.ts b/ui/src/frontend/widgets/timestamp.ts
index 9a89e3c..7fd44f1 100644
--- a/ui/src/frontend/widgets/timestamp.ts
+++ b/ui/src/frontend/widgets/timestamp.ts
@@ -13,11 +13,9 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {copyToClipboard} from '../../base/clipboard';
 import {Icons} from '../../base/semantic_icons';
 import {time, Time} from '../../base/time';
-import {Actions} from '../../common/actions';
 import {
   setTimestampFormat,
   TimestampFormat,
@@ -26,7 +24,9 @@
 import {raf} from '../../core/raf_scheduler';
 import {Anchor} from '../../widgets/anchor';
 import {MenuDivider, MenuItem, PopupMenu2} from '../../widgets/menu';
-import {globals} from '../globals';
+import {Trace} from '../../public/trace';
+import {AppImpl} from '../../core/app_impl';
+import {assertExists} from '../../base/logging';
 
 // import {MenuItem, PopupMenu2} from './menu';
 
@@ -41,24 +41,29 @@
 }
 
 export class Timestamp implements m.ClassComponent<TimestampAttrs> {
+  private readonly trace: Trace;
+
+  constructor() {
+    // TODO(primiano): the Trace object should be injected into the attrs, but
+    // there are too many users of this class and doing so requires a larger
+    // refactoring CL. Either that or we should find a different way to plumb
+    // the hoverCursorTimestamp.
+    this.trace = assertExists(AppImpl.instance.trace);
+  }
+
   view({attrs}: m.Vnode<TimestampAttrs>) {
     const {ts} = attrs;
+    const timeline = this.trace.timeline;
     return m(
       PopupMenu2,
       {
         trigger: m(
           Anchor,
           {
-            onmouseover: () => {
-              globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
-            },
-            onmouseout: () => {
-              globals.dispatch(
-                Actions.setHoverCursorTimestamp({ts: Time.INVALID}),
-              );
-            },
+            onmouseover: () => (timeline.hoverCursorTimestamp = ts),
+            onmouseout: () => (timeline.hoverCursorTimestamp = undefined),
           },
-          attrs.display ?? renderTimestamp(ts),
+          attrs.display ?? renderTimestamp(timeline.toDomainTime(ts)),
         ),
       },
       m(MenuItem, {
@@ -77,9 +82,11 @@
         menuItemForFormat(TimestampFormat.UTC, 'Realtime (UTC)'),
         menuItemForFormat(TimestampFormat.TraceTz, 'Realtime (Trace TZ)'),
         menuItemForFormat(TimestampFormat.Seconds, 'Seconds'),
-        menuItemForFormat(TimestampFormat.Raw, 'Raw'),
+        menuItemForFormat(TimestampFormat.Milliseoncds, 'Milliseconds'),
+        menuItemForFormat(TimestampFormat.Microseconds, 'Microseconds'),
+        menuItemForFormat(TimestampFormat.TraceNs, 'Raw'),
         menuItemForFormat(
-          TimestampFormat.RawLocale,
+          TimestampFormat.TraceNsLocale,
           'Raw (with locale-specific formatting)',
         ),
       ),
@@ -102,20 +109,23 @@
   });
 }
 
-function renderTimestamp(time: time): m.Children {
+function renderTimestamp(domainTime: time): m.Children {
   const fmt = timestampFormat();
-  const domainTime = globals.toDomainTime(time);
   switch (fmt) {
     case TimestampFormat.UTC:
     case TimestampFormat.TraceTz:
     case TimestampFormat.Timecode:
       return renderTimecode(domainTime);
-    case TimestampFormat.Raw:
+    case TimestampFormat.TraceNs:
       return domainTime.toString();
-    case TimestampFormat.RawLocale:
+    case TimestampFormat.TraceNsLocale:
       return domainTime.toLocaleString();
     case TimestampFormat.Seconds:
       return Time.formatSeconds(domainTime);
+    case TimestampFormat.Milliseoncds:
+      return Time.formatMilliseconds(domainTime);
+    case TimestampFormat.Microseconds:
+      return Time.formatMicroseconds(domainTime);
     default:
       const x: never = fmt;
       throw new Error(`Invalid timestamp ${x}`);
diff --git a/ui/src/frontend/widgets/treetable.ts b/ui/src/frontend/widgets/treetable.ts
index 17399c0..46f78a8 100644
--- a/ui/src/frontend/widgets/treetable.ts
+++ b/ui/src/frontend/widgets/treetable.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {classNames} from '../../base/classnames';
 import {raf} from '../../core/raf_scheduler';
 
diff --git a/ui/src/frontend/widgets_page.ts b/ui/src/frontend/widgets_page.ts
deleted file mode 100644
index 9b258b1..0000000
--- a/ui/src/frontend/widgets_page.ts
+++ /dev/null
@@ -1,1389 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {classNames} from '../base/classnames';
-import {Hotkey, Platform} from '../base/hotkeys';
-import {isString} from '../base/object_utils';
-import {Icons} from '../base/semantic_icons';
-import {raf} from '../core/raf_scheduler';
-import {Anchor} from '../widgets/anchor';
-import {Button} from '../widgets/button';
-import {Callout} from '../widgets/callout';
-import {Checkbox} from '../widgets/checkbox';
-import {Editor} from '../widgets/editor';
-import {EmptyState} from '../widgets/empty_state';
-import {Form, FormLabel} from '../widgets/form';
-import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
-import {Icon} from '../widgets/icon';
-import {Menu, MenuDivider, MenuItem, PopupMenu2} from '../widgets/menu';
-import {showModal} from '../widgets/modal';
-import {
-  MultiSelect,
-  MultiSelectDiff,
-  PopupMultiSelect,
-} from '../widgets/multiselect';
-import {Popup, PopupPosition} from '../widgets/popup';
-import {Portal} from '../widgets/portal';
-import {FilterableSelect, Select} from '../widgets/select';
-import {Spinner} from '../widgets/spinner';
-import {Switch} from '../widgets/switch';
-import {TextInput} from '../widgets/text_input';
-import {MultiParagraphText, TextParagraph} from '../widgets/text_paragraph';
-import {LazyTreeNode, Tree, TreeNode} from '../widgets/tree';
-import {VegaView} from '../widgets/vega_view';
-
-import {createPage} from './pages';
-import {PopupMenuButton} from './popup_menu';
-import {TableShowcase} from './tables/table_showcase';
-import {TreeTable, TreeTableAttrs} from './widgets/treetable';
-import {Intent} from '../widgets/common';
-import {
-  VirtualTable,
-  VirtualTableAttrs,
-  VirtualTableRow,
-} from '../widgets/virtual_table';
-import {TagInput} from '../widgets/tag_input';
-import {SegmentedButtons} from '../widgets/segmented_buttons';
-
-const DATA_ENGLISH_LETTER_FREQUENCY = {
-  table: [
-    {category: 'a', amount: 8.167},
-    {category: 'b', amount: 1.492},
-    {category: 'c', amount: 2.782},
-    {category: 'd', amount: 4.253},
-    {category: 'e', amount: 12.7},
-    {category: 'f', amount: 2.228},
-    {category: 'g', amount: 2.015},
-    {category: 'h', amount: 6.094},
-    {category: 'i', amount: 6.966},
-    {category: 'j', amount: 0.253},
-    {category: 'k', amount: 1.772},
-    {category: 'l', amount: 4.025},
-    {category: 'm', amount: 2.406},
-    {category: 'n', amount: 6.749},
-    {category: 'o', amount: 7.507},
-    {category: 'p', amount: 1.929},
-    {category: 'q', amount: 0.095},
-    {category: 'r', amount: 5.987},
-    {category: 's', amount: 6.327},
-    {category: 't', amount: 9.056},
-    {category: 'u', amount: 2.758},
-    {category: 'v', amount: 0.978},
-    {category: 'w', amount: 2.36},
-    {category: 'x', amount: 0.25},
-    {category: 'y', amount: 1.974},
-    {category: 'z', amount: 0.074},
-  ],
-};
-
-const DATA_POLISH_LETTER_FREQUENCY = {
-  table: [
-    {category: 'a', amount: 8.965},
-    {category: 'b', amount: 1.482},
-    {category: 'c', amount: 3.988},
-    {category: 'd', amount: 3.293},
-    {category: 'e', amount: 7.921},
-    {category: 'f', amount: 0.312},
-    {category: 'g', amount: 1.377},
-    {category: 'h', amount: 1.072},
-    {category: 'i', amount: 8.286},
-    {category: 'j', amount: 2.343},
-    {category: 'k', amount: 3.411},
-    {category: 'l', amount: 2.136},
-    {category: 'm', amount: 2.911},
-    {category: 'n', amount: 5.6},
-    {category: 'o', amount: 7.59},
-    {category: 'p', amount: 3.101},
-    {category: 'q', amount: 0.003},
-    {category: 'r', amount: 4.571},
-    {category: 's', amount: 4.263},
-    {category: 't', amount: 3.966},
-    {category: 'u', amount: 2.347},
-    {category: 'v', amount: 0.034},
-    {category: 'w', amount: 4.549},
-    {category: 'x', amount: 0.019},
-    {category: 'y', amount: 3.857},
-    {category: 'z', amount: 5.62},
-  ],
-};
-
-const DATA_EMPTY = {};
-
-const SPEC_BAR_CHART = `
-{
-  "$schema": "https://vega.github.io/schema/vega/v5.json",
-  "description": "A basic bar chart example, with value labels shown upon mouse hover.",
-  "width": 400,
-  "height": 200,
-  "padding": 5,
-
-  "data": [
-    {
-      "name": "table"
-    }
-  ],
-
-  "signals": [
-    {
-      "name": "tooltip",
-      "value": {},
-      "on": [
-        {"events": "rect:mouseover", "update": "datum"},
-        {"events": "rect:mouseout",  "update": "{}"}
-      ]
-    }
-  ],
-
-  "scales": [
-    {
-      "name": "xscale",
-      "type": "band",
-      "domain": {"data": "table", "field": "category"},
-      "range": "width",
-      "padding": 0.05,
-      "round": true
-    },
-    {
-      "name": "yscale",
-      "domain": {"data": "table", "field": "amount"},
-      "nice": true,
-      "range": "height"
-    }
-  ],
-
-  "axes": [
-    { "orient": "bottom", "scale": "xscale" },
-    { "orient": "left", "scale": "yscale" }
-  ],
-
-  "marks": [
-    {
-      "type": "rect",
-      "from": {"data":"table"},
-      "encode": {
-        "enter": {
-          "x": {"scale": "xscale", "field": "category"},
-          "width": {"scale": "xscale", "band": 1},
-          "y": {"scale": "yscale", "field": "amount"},
-          "y2": {"scale": "yscale", "value": 0}
-        },
-        "update": {
-          "fill": {"value": "steelblue"}
-        },
-        "hover": {
-          "fill": {"value": "red"}
-        }
-      }
-    },
-    {
-      "type": "text",
-      "encode": {
-        "enter": {
-          "align": {"value": "center"},
-          "baseline": {"value": "bottom"},
-          "fill": {"value": "#333"}
-        },
-        "update": {
-          "x": {"scale": "xscale", "signal": "tooltip.category", "band": 0.5},
-          "y": {"scale": "yscale", "signal": "tooltip.amount", "offset": -2},
-          "text": {"signal": "tooltip.amount"},
-          "fillOpacity": [
-            {"test": "datum === tooltip", "value": 0},
-            {"value": 1}
-          ]
-        }
-      }
-    }
-  ]
-}
-`;
-
-const SPEC_BAR_CHART_LITE = `
-{
-  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
-  "description": "A simple bar chart with embedded data.",
-  "data": {
-    "name": "table"
-  },
-  "mark": "bar",
-  "encoding": {
-    "x": {"field": "category", "type": "nominal", "axis": {"labelAngle": 0}},
-    "y": {"field": "amount", "type": "quantitative"}
-  }
-}
-`;
-
-const SPEC_BROKEN = `{
-  "description": 123
-}
-`;
-
-enum SpecExample {
-  BarChart = 'Barchart',
-  BarChartLite = 'Barchart (Lite)',
-  Broken = 'Broken',
-}
-
-enum DataExample {
-  English = 'English',
-  Polish = 'Polish',
-  Empty = 'Empty',
-}
-
-function arg<T>(
-  anyArg: unknown,
-  valueIfTrue: T,
-  valueIfFalse: T | undefined = undefined,
-): T | undefined {
-  return Boolean(anyArg) ? valueIfTrue : valueIfFalse;
-}
-
-function getExampleSpec(example: SpecExample): string {
-  switch (example) {
-    case SpecExample.BarChart:
-      return SPEC_BAR_CHART;
-    case SpecExample.BarChartLite:
-      return SPEC_BAR_CHART_LITE;
-    case SpecExample.Broken:
-      return SPEC_BROKEN;
-    default:
-      const exhaustiveCheck: never = example;
-      throw new Error(`Unhandled case: ${exhaustiveCheck}`);
-  }
-}
-
-function getExampleData(example: DataExample) {
-  switch (example) {
-    case DataExample.English:
-      return DATA_ENGLISH_LETTER_FREQUENCY;
-    case DataExample.Polish:
-      return DATA_POLISH_LETTER_FREQUENCY;
-    case DataExample.Empty:
-      return DATA_EMPTY;
-    default:
-      const exhaustiveCheck: never = example;
-      throw new Error(`Unhandled case: ${exhaustiveCheck}`);
-  }
-}
-
-const options: {[key: string]: boolean} = {
-  foobar: false,
-  foo: false,
-  bar: false,
-  baz: false,
-  qux: false,
-  quux: false,
-  corge: false,
-  grault: false,
-  garply: false,
-  waldo: false,
-  fred: false,
-  plugh: false,
-  xyzzy: false,
-  thud: false,
-};
-
-function PortalButton() {
-  let portalOpen = false;
-
-  return {
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    view: function ({attrs}: any) {
-      const {zIndex = true, absolute = true, top = true} = attrs;
-      return [
-        m(Button, {
-          label: 'Toggle Portal',
-          intent: Intent.Primary,
-          onclick: () => {
-            portalOpen = !portalOpen;
-            raf.scheduleFullRedraw();
-          },
-        }),
-        portalOpen &&
-          m(
-            Portal,
-            {
-              style: {
-                position: arg(absolute, 'absolute'),
-                top: arg(top, '0'),
-                zIndex: arg(zIndex, '10', '0'),
-                background: 'white',
-              },
-            },
-            m(
-              '',
-              `A very simple portal - a div rendered outside of the normal
-              flow of the page`,
-            ),
-          ),
-      ];
-    },
-  };
-}
-
-function lorem() {
-  const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
-      tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
-      veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
-      commodo consequat.Duis aute irure dolor in reprehenderit in voluptate
-      velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
-      cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
-      est laborum.`;
-  return m('', {style: {width: '200px'}}, text);
-}
-
-function ControlledPopup() {
-  let popupOpen = false;
-
-  return {
-    view: function () {
-      return m(
-        Popup,
-        {
-          trigger: m(Button, {label: `${popupOpen ? 'Close' : 'Open'} Popup`}),
-          isOpen: popupOpen,
-          onChange: (shouldOpen: boolean) => (popupOpen = shouldOpen),
-        },
-        m(Button, {
-          label: 'Close Popup',
-          onclick: () => {
-            popupOpen = !popupOpen;
-            raf.scheduleFullRedraw();
-          },
-        }),
-      );
-    },
-  };
-}
-
-type Options = {
-  [key: string]: EnumOption | boolean | string;
-};
-
-class EnumOption {
-  constructor(
-    public initial: string,
-    public options: string[],
-  ) {}
-}
-
-interface WidgetTitleAttrs {
-  label: string;
-}
-
-function recursiveTreeNode(): m.Children {
-  return m(LazyTreeNode, {
-    left: 'Recursive',
-    right: '...',
-    fetchData: async () => {
-      // await new Promise((r) => setTimeout(r, 1000));
-      return () => recursiveTreeNode();
-    },
-  });
-}
-
-class WidgetTitle implements m.ClassComponent<WidgetTitleAttrs> {
-  view({attrs}: m.CVnode<WidgetTitleAttrs>) {
-    const {label} = attrs;
-    const id = label.replaceAll(' ', '').toLowerCase();
-    const href = `#!/widgets#${id}`;
-    return m(Anchor, {id, href}, m('h2', label));
-  }
-}
-
-interface WidgetShowcaseAttrs {
-  label: string;
-  description?: string;
-  initialOpts?: Options;
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  renderWidget: (options: any) => any;
-  wide?: boolean;
-}
-
-// A little helper class to render any vnode with a dynamic set of options
-class WidgetShowcase implements m.ClassComponent<WidgetShowcaseAttrs> {
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  private optValues: any = {};
-  private opts?: Options;
-
-  renderOptions(listItems: m.Child[]): m.Child {
-    if (listItems.length === 0) {
-      return null;
-    }
-    return m('.widget-controls', m('h3', 'Options'), m('ul', listItems));
-  }
-
-  oninit({attrs: {initialOpts: opts}}: m.Vnode<WidgetShowcaseAttrs, this>) {
-    this.opts = opts;
-    if (opts) {
-      // Make the initial options values
-      for (const key in opts) {
-        if (Object.prototype.hasOwnProperty.call(opts, key)) {
-          const option = opts[key];
-          if (option instanceof EnumOption) {
-            this.optValues[key] = option.initial;
-          } else if (typeof option === 'boolean') {
-            this.optValues[key] = option;
-          } else if (isString(option)) {
-            this.optValues[key] = option;
-          }
-        }
-      }
-    }
-  }
-
-  view({attrs}: m.CVnode<WidgetShowcaseAttrs>) {
-    const {renderWidget, wide, label, description} = attrs;
-    const listItems = [];
-
-    if (this.opts) {
-      for (const key in this.opts) {
-        if (Object.prototype.hasOwnProperty.call(this.opts, key)) {
-          listItems.push(m('li', this.renderControlForOption(key)));
-        }
-      }
-    }
-
-    return [
-      m(WidgetTitle, {label}),
-      description && m('p', description),
-      m(
-        '.widget-block',
-        m(
-          'div',
-          {
-            class: classNames(
-              'widget-container',
-              wide && 'widget-container-wide',
-            ),
-          },
-          renderWidget(this.optValues),
-        ),
-        this.renderOptions(listItems),
-      ),
-    ];
-  }
-
-  private renderControlForOption(key: string) {
-    if (!this.opts) return null;
-    const value = this.opts[key];
-    if (value instanceof EnumOption) {
-      return this.renderEnumOption(key, value);
-    } else if (typeof value === 'boolean') {
-      return this.renderBooleanOption(key);
-    } else if (isString(value)) {
-      return this.renderStringOption(key);
-    } else {
-      return null;
-    }
-  }
-
-  private renderBooleanOption(key: string) {
-    return m(Checkbox, {
-      checked: this.optValues[key],
-      label: key,
-      onchange: () => {
-        this.optValues[key] = !Boolean(this.optValues[key]);
-        raf.scheduleFullRedraw();
-      },
-    });
-  }
-
-  private renderStringOption(key: string) {
-    return m(TextInput, {
-      placeholder: key,
-      value: this.optValues[key],
-      oninput: (e: Event) => {
-        this.optValues[key] = (e.target as HTMLInputElement).value;
-        raf.scheduleFullRedraw();
-      },
-    });
-  }
-
-  private renderEnumOption(key: string, opt: EnumOption) {
-    const optionElements = opt.options.map((option: string) => {
-      return m('option', {value: option}, option);
-    });
-    return m(
-      Select,
-      {
-        value: this.optValues[key],
-        onchange: (e: Event) => {
-          const el = e.target as HTMLSelectElement;
-          this.optValues[key] = el.value;
-          raf.scheduleFullRedraw();
-        },
-      },
-      optionElements,
-    );
-  }
-}
-
-interface File {
-  name: string;
-  size: string;
-  date: string;
-  children?: File[];
-}
-
-const files: File[] = [
-  {
-    name: 'foo',
-    size: '10MB',
-    date: '2023-04-02',
-  },
-  {
-    name: 'bar',
-    size: '123KB',
-    date: '2023-04-08',
-    children: [
-      {
-        name: 'baz',
-        size: '4KB',
-        date: '2023-05-07',
-      },
-      {
-        name: 'qux',
-        size: '18KB',
-        date: '2023-05-28',
-        children: [
-          {
-            name: 'quux',
-            size: '4KB',
-            date: '2023-05-07',
-          },
-          {
-            name: 'corge',
-            size: '18KB',
-            date: '2023-05-28',
-            children: [
-              {
-                name: 'grault',
-                size: '4KB',
-                date: '2023-05-07',
-              },
-              {
-                name: 'garply',
-                size: '18KB',
-                date: '2023-05-28',
-              },
-              {
-                name: 'waldo',
-                size: '87KB',
-                date: '2023-05-02',
-              },
-            ],
-          },
-        ],
-      },
-    ],
-  },
-  {
-    name: 'fred',
-    size: '8KB',
-    date: '2022-12-27',
-  },
-];
-
-let virtualTableData: {offset: number; rows: VirtualTableRow[]} = {
-  offset: 0,
-  rows: [],
-};
-
-function TagInputDemo() {
-  const tags: string[] = ['foo', 'bar', 'baz'];
-  let tagInputValue: string = '';
-
-  return {
-    view: () => {
-      return m(TagInput, {
-        tags,
-        value: tagInputValue,
-        onTagAdd: (tag) => {
-          tags.push(tag);
-          tagInputValue = '';
-          raf.scheduleFullRedraw();
-        },
-        onChange: (value) => {
-          tagInputValue = value;
-        },
-        onTagRemove: (index) => {
-          tags.splice(index, 1);
-          raf.scheduleFullRedraw();
-        },
-      });
-    },
-  };
-}
-
-function SegmentedButtonsDemo({attrs}: {attrs: {}}) {
-  let selectedIdx = 0;
-  return {
-    view: () => {
-      return m(SegmentedButtons, {
-        ...attrs,
-        options: [{label: 'Yes'}, {label: 'Maybe'}, {label: 'No'}],
-        selectedOption: selectedIdx,
-        onOptionSelected: (num) => {
-          selectedIdx = num;
-          raf.scheduleFullRedraw();
-        },
-      });
-    },
-  };
-}
-
-export const WidgetsPage = createPage({
-  view() {
-    return m(
-      '.widgets-page',
-      m('h1', 'Widgets'),
-      m(WidgetShowcase, {
-        label: 'Button',
-        renderWidget: ({label, icon, rightIcon, ...rest}) =>
-          m(Button, {
-            icon: arg(icon, 'send'),
-            rightIcon: arg(rightIcon, 'arrow_forward'),
-            label: arg(label, 'Button', ''),
-            ...rest,
-          }),
-        initialOpts: {
-          label: true,
-          icon: true,
-          rightIcon: false,
-          disabled: false,
-          intent: new EnumOption(Intent.None, Object.values(Intent)),
-          active: false,
-          compact: false,
-          loading: false,
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Segmented Buttons',
-        description: `
-          Segmented buttons are a group of buttons where one of them is
-          'selected'; they act similar to a set of radio buttons.
-        `,
-        renderWidget: (opts) => m(SegmentedButtonsDemo, opts),
-        initialOpts: {
-          disabled: false,
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Checkbox',
-        renderWidget: (opts) => m(Checkbox, {label: 'Checkbox', ...opts}),
-        initialOpts: {
-          disabled: false,
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Switch',
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        renderWidget: ({label, ...rest}: any) =>
-          m(Switch, {label: arg(label, 'Switch'), ...rest}),
-        initialOpts: {
-          label: true,
-          disabled: false,
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Text Input',
-        renderWidget: ({placeholder, ...rest}) =>
-          m(TextInput, {
-            placeholder: arg(placeholder, 'Placeholder...', ''),
-            ...rest,
-          }),
-        initialOpts: {
-          placeholder: true,
-          disabled: false,
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Select',
-        renderWidget: (opts) =>
-          m(Select, opts, [
-            m('option', {value: 'foo', label: 'Foo'}),
-            m('option', {value: 'bar', label: 'Bar'}),
-            m('option', {value: 'baz', label: 'Baz'}),
-          ]),
-        initialOpts: {
-          disabled: false,
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Filterable Select',
-        renderWidget: () =>
-          m(FilterableSelect, {
-            values: ['foo', 'bar', 'baz'],
-            onSelected: () => {},
-          }),
-      }),
-      m(WidgetShowcase, {
-        label: 'Empty State',
-        renderWidget: ({header, content}) =>
-          m(
-            EmptyState,
-            {
-              title: arg(header, 'No search results found...'),
-            },
-            arg(content, m(Button, {label: 'Try again'})),
-          ),
-        initialOpts: {
-          header: true,
-          content: true,
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Anchor',
-        renderWidget: ({icon}) =>
-          m(
-            Anchor,
-            {
-              icon: arg(icon, 'open_in_new'),
-              href: 'https://perfetto.dev/docs/',
-              target: '_blank',
-            },
-            'Docs',
-          ),
-        initialOpts: {
-          icon: true,
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Table',
-        renderWidget: () => m(TableShowcase),
-        initialOpts: {},
-        wide: true,
-      }),
-      m(WidgetShowcase, {
-        label: 'Portal',
-        description: `A portal is a div rendered out of normal flow
-          of the hierarchy.`,
-        renderWidget: (opts) => m(PortalButton, opts),
-        initialOpts: {
-          absolute: true,
-          zIndex: true,
-          top: true,
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Popup',
-        description: `A popup is a nicely styled portal element whose position is
-        dynamically updated to appear to float alongside a specific element on
-        the page, even as the element is moved and scrolled around.`,
-        renderWidget: (opts) =>
-          m(
-            Popup,
-            {
-              trigger: m(Button, {label: 'Toggle Popup'}),
-              ...opts,
-            },
-            lorem(),
-          ),
-        initialOpts: {
-          position: new EnumOption(
-            PopupPosition.Auto,
-            Object.values(PopupPosition),
-          ),
-          closeOnEscape: true,
-          closeOnOutsideClick: true,
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Controlled Popup',
-        description: `The open/close state of a controlled popup is passed in via
-        the 'isOpen' attribute. This means we can get open or close the popup
-        from wherever we like. E.g. from a button inside the popup.
-        Keeping this state external also means we can modify other parts of the
-        page depending on whether the popup is open or not, such as the text
-        on this button.
-        Note, this is the same component as the popup above, but used in
-        controlled mode.`,
-        renderWidget: (opts) => m(ControlledPopup, opts),
-        initialOpts: {},
-      }),
-      m(WidgetShowcase, {
-        label: 'Icon',
-        renderWidget: (opts) => m(Icon, {icon: 'star', ...opts}),
-        initialOpts: {filled: false},
-      }),
-      m(WidgetShowcase, {
-        label: 'MultiSelect panel',
-        renderWidget: ({...rest}) =>
-          m(MultiSelect, {
-            options: Object.entries(options).map(([key, value]) => {
-              return {
-                id: key,
-                name: key,
-                checked: value,
-              };
-            }),
-            onChange: (diffs: MultiSelectDiff[]) => {
-              diffs.forEach(({id, checked}) => {
-                options[id] = checked;
-              });
-              raf.scheduleFullRedraw();
-            },
-            ...rest,
-          }),
-        initialOpts: {
-          repeatCheckedItemsAtTop: false,
-          fixedSize: false,
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Popup with MultiSelect',
-        renderWidget: ({icon, ...rest}) =>
-          m(PopupMultiSelect, {
-            options: Object.entries(options).map(([key, value]) => {
-              return {
-                id: key,
-                name: key,
-                checked: value,
-              };
-            }),
-            popupPosition: PopupPosition.Top,
-            label: 'Multi Select',
-            icon: arg(icon, Icons.LibraryAddCheck),
-            onChange: (diffs: MultiSelectDiff[]) => {
-              diffs.forEach(({id, checked}) => {
-                options[id] = checked;
-              });
-              raf.scheduleFullRedraw();
-            },
-            ...rest,
-          }),
-        initialOpts: {
-          icon: true,
-          showNumSelected: true,
-          repeatCheckedItemsAtTop: false,
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'PopupMenu',
-        renderWidget: () => {
-          return m(PopupMenuButton, {
-            icon: 'description',
-            items: [
-              {itemType: 'regular', text: 'New', callback: () => {}},
-              {itemType: 'regular', text: 'Open', callback: () => {}},
-              {itemType: 'regular', text: 'Save', callback: () => {}},
-              {itemType: 'regular', text: 'Delete', callback: () => {}},
-              {
-                itemType: 'group',
-                text: 'Share',
-                itemId: 'foo',
-                children: [
-                  {itemType: 'regular', text: 'Friends', callback: () => {}},
-                  {itemType: 'regular', text: 'Family', callback: () => {}},
-                  {itemType: 'regular', text: 'Everyone', callback: () => {}},
-                ],
-              },
-            ],
-          });
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Menu',
-        renderWidget: () =>
-          m(
-            Menu,
-            m(MenuItem, {label: 'New', icon: 'add'}),
-            m(MenuItem, {label: 'Open', icon: 'folder_open'}),
-            m(MenuItem, {label: 'Save', icon: 'save', disabled: true}),
-            m(MenuDivider),
-            m(MenuItem, {label: 'Delete', icon: 'delete'}),
-            m(MenuDivider),
-            m(
-              MenuItem,
-              {label: 'Share', icon: 'share'},
-              m(MenuItem, {label: 'Everyone', icon: 'public'}),
-              m(MenuItem, {label: 'Friends', icon: 'group'}),
-              m(
-                MenuItem,
-                {label: 'Specific people', icon: 'person_add'},
-                m(MenuItem, {label: 'Alice', icon: 'person'}),
-                m(MenuItem, {label: 'Bob', icon: 'person'}),
-              ),
-            ),
-            m(
-              MenuItem,
-              {label: 'More', icon: 'more_horiz'},
-              m(MenuItem, {label: 'Query', icon: 'database'}),
-              m(MenuItem, {label: 'Download', icon: 'download'}),
-              m(MenuItem, {label: 'Clone', icon: 'copy_all'}),
-            ),
-          ),
-      }),
-      m(WidgetShowcase, {
-        label: 'PopupMenu2',
-        renderWidget: (opts) =>
-          m(
-            PopupMenu2,
-            {
-              trigger: m(Button, {
-                label: 'Menu',
-                rightIcon: Icons.ContextMenu,
-              }),
-              ...opts,
-            },
-            m(MenuItem, {label: 'New', icon: 'add'}),
-            m(MenuItem, {label: 'Open', icon: 'folder_open'}),
-            m(MenuItem, {label: 'Save', icon: 'save', disabled: true}),
-            m(MenuDivider),
-            m(MenuItem, {label: 'Delete', icon: 'delete'}),
-            m(MenuDivider),
-            m(
-              MenuItem,
-              {label: 'Share', icon: 'share'},
-              m(MenuItem, {label: 'Everyone', icon: 'public'}),
-              m(MenuItem, {label: 'Friends', icon: 'group'}),
-              m(
-                MenuItem,
-                {label: 'Specific people', icon: 'person_add'},
-                m(MenuItem, {label: 'Alice', icon: 'person'}),
-                m(MenuItem, {label: 'Bob', icon: 'person'}),
-              ),
-            ),
-            m(
-              MenuItem,
-              {label: 'More', icon: 'more_horiz'},
-              m(MenuItem, {label: 'Query', icon: 'database'}),
-              m(MenuItem, {label: 'Download', icon: 'download'}),
-              m(MenuItem, {label: 'Clone', icon: 'copy_all'}),
-            ),
-          ),
-        initialOpts: {
-          popupPosition: new EnumOption(
-            PopupPosition.Bottom,
-            Object.values(PopupPosition),
-          ),
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Spinner',
-        description: `Simple spinner, rotates forever.
-            Width and height match the font size.`,
-        renderWidget: ({fontSize, easing}) =>
-          m('', {style: {fontSize}}, m(Spinner, {easing})),
-        initialOpts: {
-          fontSize: new EnumOption('16px', [
-            '12px',
-            '16px',
-            '24px',
-            '32px',
-            '64px',
-            '128px',
-          ]),
-          easing: false,
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Tree',
-        description: `Hierarchical tree with left and right values aligned to
-        a grid.`,
-        renderWidget: (opts) =>
-          m(
-            Tree,
-            opts,
-            m(TreeNode, {left: 'Name', right: 'my_event', icon: 'badge'}),
-            m(TreeNode, {left: 'CPU', right: '2', icon: 'memory'}),
-            m(TreeNode, {
-              left: 'Start time',
-              right: '1s 435ms',
-              icon: 'schedule',
-            }),
-            m(TreeNode, {left: 'Duration', right: '86ms', icon: 'timer'}),
-            m(TreeNode, {
-              left: 'SQL',
-              right: m(
-                PopupMenu2,
-                {
-                  popupPosition: PopupPosition.RightStart,
-                  trigger: m(
-                    Anchor,
-                    {
-                      icon: Icons.ContextMenu,
-                    },
-                    'SELECT * FROM raw WHERE id = 123',
-                  ),
-                },
-                m(MenuItem, {
-                  label: 'Copy SQL Query',
-                  icon: 'content_copy',
-                }),
-                m(MenuItem, {
-                  label: 'Execute Query in new tab',
-                  icon: 'open_in_new',
-                }),
-              ),
-            }),
-            m(TreeNode, {
-              icon: 'account_tree',
-              left: 'Process',
-              right: m(Anchor, {icon: 'open_in_new'}, '/bin/foo[789]'),
-            }),
-            m(TreeNode, {
-              left: 'Thread',
-              right: m(Anchor, {icon: 'open_in_new'}, 'my_thread[456]'),
-            }),
-            m(
-              TreeNode,
-              {
-                left: 'Args',
-                summary: 'foo: string, baz: string, quux: string[4]',
-              },
-              m(TreeNode, {left: 'foo', right: 'bar'}),
-              m(TreeNode, {left: 'baz', right: 'qux'}),
-              m(
-                TreeNode,
-                {left: 'quux', summary: 'string[4]'},
-                m(TreeNode, {left: '[0]', right: 'corge'}),
-                m(TreeNode, {left: '[1]', right: 'grault'}),
-                m(TreeNode, {left: '[2]', right: 'garply'}),
-                m(TreeNode, {left: '[3]', right: 'waldo'}),
-              ),
-            ),
-            m(LazyTreeNode, {
-              left: 'Lazy',
-              icon: 'bedtime',
-              fetchData: async () => {
-                await new Promise((r) => setTimeout(r, 1000));
-                return () => m(TreeNode, {left: 'foo'});
-              },
-            }),
-            m(LazyTreeNode, {
-              left: 'Dynamic',
-              unloadOnCollapse: true,
-              icon: 'bedtime',
-              fetchData: async () => {
-                await new Promise((r) => setTimeout(r, 1000));
-                return () => m(TreeNode, {left: 'foo'});
-              },
-            }),
-            recursiveTreeNode(),
-          ),
-        wide: true,
-      }),
-      m(WidgetShowcase, {
-        label: 'Form',
-        renderWidget: () => renderForm('form'),
-      }),
-      m(WidgetShowcase, {
-        label: 'Nested Popups',
-        renderWidget: () =>
-          m(
-            Popup,
-            {
-              trigger: m(Button, {label: 'Open the popup'}),
-            },
-            m(
-              PopupMenu2,
-              {
-                trigger: m(Button, {label: 'Select an option'}),
-              },
-              m(MenuItem, {label: 'Option 1'}),
-              m(MenuItem, {label: 'Option 2'}),
-            ),
-            m(Button, {
-              label: 'Done',
-              dismissPopup: true,
-            }),
-          ),
-      }),
-      m(WidgetShowcase, {
-        label: 'Callout',
-        renderWidget: () =>
-          m(
-            Callout,
-            {
-              icon: 'info',
-            },
-            'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' +
-              'Nulla rhoncus tempor neque, sed malesuada eros dapibus vel. ' +
-              'Aliquam in ligula vitae tortor porttitor laoreet iaculis ' +
-              'finibus est.',
-          ),
-      }),
-      m(WidgetShowcase, {
-        label: 'Editor',
-        renderWidget: () => m(Editor),
-      }),
-      m(WidgetShowcase, {
-        label: 'VegaView',
-        renderWidget: (opt) =>
-          m(VegaView, {
-            spec: getExampleSpec(opt.exampleSpec),
-            data: getExampleData(opt.exampleData),
-          }),
-        initialOpts: {
-          exampleSpec: new EnumOption(
-            SpecExample.BarChart,
-            Object.values(SpecExample),
-          ),
-          exampleData: new EnumOption(
-            DataExample.English,
-            Object.values(DataExample),
-          ),
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Form within PopupMenu2',
-        description: `A form placed inside a popup menu works just fine,
-              and the cancel/submit buttons also dismiss the popup. A bit more
-              margin is added around it too, which improves the look and feel.`,
-        renderWidget: () =>
-          m(
-            PopupMenu2,
-            {
-              trigger: m(Button, {label: 'Popup!'}),
-            },
-            m(
-              MenuItem,
-              {
-                label: 'Open form...',
-              },
-              renderForm('popup-form'),
-            ),
-          ),
-      }),
-      m(WidgetShowcase, {
-        label: 'Hotkey',
-        renderWidget: (opts) => {
-          if (opts.platform === 'auto') {
-            return m(HotkeyGlyphs, {hotkey: opts.hotkey as Hotkey});
-          } else {
-            const platform = opts.platform as Platform;
-            return m(HotkeyGlyphs, {
-              hotkey: opts.hotkey as Hotkey,
-              spoof: platform,
-            });
-          }
-        },
-        initialOpts: {
-          hotkey: 'Mod+Shift+P',
-          platform: new EnumOption('auto', ['auto', 'Mac', 'PC']),
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Text Paragraph',
-        description: `A basic formatted text paragraph with wrapping. If
-              it is desirable to preserve the original text format/line breaks,
-              set the compressSpace attribute to false.`,
-        renderWidget: (opts) => {
-          return m(TextParagraph, {
-            text: `Lorem ipsum dolor sit amet, consectetur adipiscing
-                         elit. Nulla rhoncus tempor neque, sed malesuada eros
-                         dapibus vel. Aliquam in ligula vitae tortor porttitor
-                         laoreet iaculis finibus est.`,
-            compressSpace: opts.compressSpace,
-          });
-        },
-        initialOpts: {
-          compressSpace: true,
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Multi Paragraph Text',
-        description: `A wrapper for multiple paragraph widgets.`,
-        renderWidget: () => {
-          return m(
-            MultiParagraphText,
-            m(TextParagraph, {
-              text: `Lorem ipsum dolor sit amet, consectetur adipiscing
-                         elit. Nulla rhoncus tempor neque, sed malesuada eros
-                         dapibus vel. Aliquam in ligula vitae tortor porttitor
-                         laoreet iaculis finibus est.`,
-              compressSpace: true,
-            }),
-            m(TextParagraph, {
-              text: `Sed ut perspiciatis unde omnis iste natus error sit
-                         voluptatem accusantium doloremque laudantium, totam rem
-                         aperiam, eaque ipsa quae ab illo inventore veritatis et
-                         quasi architecto beatae vitae dicta sunt explicabo.
-                         Nemo enim ipsam voluptatem quia voluptas sit aspernatur
-                         aut odit aut fugit, sed quia consequuntur magni dolores
-                         eos qui ratione voluptatem sequi nesciunt.`,
-              compressSpace: true,
-            }),
-          );
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Modal',
-        description: `A helper for modal dialog.`,
-        renderWidget: () => m(ModalShowcase),
-      }),
-      m(WidgetShowcase, {
-        label: 'TreeTable',
-        description: `Hierarchical tree with multiple columns`,
-        renderWidget: () => {
-          const attrs: TreeTableAttrs<File> = {
-            rows: files,
-            getChildren: (file) => file.children,
-            columns: [
-              {name: 'Name', getData: (file) => file.name},
-              {name: 'Size', getData: (file) => file.size},
-              {name: 'Date', getData: (file) => file.date},
-            ],
-          };
-          return m(TreeTable<File>, attrs);
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'VirtualTable',
-        description: `Virtualized table for efficient rendering of large datasets`,
-        renderWidget: () => {
-          const attrs: VirtualTableAttrs = {
-            columns: [
-              {header: 'x', width: '4em'},
-              {header: 'x^2', width: '8em'},
-            ],
-            rows: virtualTableData.rows,
-            firstRowOffset: virtualTableData.offset,
-            rowHeight: 20,
-            numRows: 500_000,
-            style: {height: '200px'},
-            onReload: (rowOffset, rowCount) => {
-              const rows = [];
-              for (let i = rowOffset; i < rowOffset + rowCount; i++) {
-                rows.push({id: i, cells: [i, i ** 2]});
-              }
-              virtualTableData = {
-                offset: rowOffset,
-                rows,
-              };
-              raf.scheduleFullRedraw();
-            },
-          };
-          return m(VirtualTable, attrs);
-        },
-      }),
-      m(WidgetShowcase, {
-        label: 'Tag Input',
-        description: `
-          TagInput displays Tag elements inside an input, followed by an
-          interactive text input. The container is styled to look like a
-          TextInput, but the actual editable element appears after the last tag.
-          Clicking anywhere on the container will focus the text input.`,
-        renderWidget: () => m(TagInputDemo),
-      }),
-    );
-  },
-});
-class ModalShowcase implements m.ClassComponent {
-  private static counter = 0;
-
-  private static log(txt: string) {
-    const mwlogs = document.getElementById('mwlogs');
-    if (!mwlogs || !(mwlogs instanceof HTMLTextAreaElement)) return;
-    const time = new Date().toLocaleTimeString();
-    mwlogs.value += `[${time}] ${txt}\n`;
-    mwlogs.scrollTop = mwlogs.scrollHeight;
-  }
-
-  private static showModalDialog(staticContent = false) {
-    const id = `N=${++ModalShowcase.counter}`;
-    ModalShowcase.log(`Open ${id}`);
-    const logOnClose = () => ModalShowcase.log(`Close ${id}`);
-
-    let content;
-    if (staticContent) {
-      content = m('.modal-pre', 'Content of the modal dialog.\nEnd of content');
-    } else {
-      const component = {
-        oninit: function (vnode: m.Vnode<{}, {progress: number}>) {
-          vnode.state.progress = ((vnode.state.progress as number) || 0) + 1;
-        },
-        view: function (vnode: m.Vnode<{}, {progress: number}>) {
-          vnode.state.progress = (vnode.state.progress + 1) % 100;
-          raf.scheduleFullRedraw();
-          return m(
-            'div',
-            m('div', 'You should see an animating progress bar'),
-            m('progress', {value: vnode.state.progress, max: 100}),
-          );
-        },
-      } as m.Component<{}, {progress: number}>;
-      content = () => m(component);
-    }
-    const closePromise = showModal({
-      title: `Modal dialog ${id}`,
-      buttons: [
-        {text: 'OK', action: () => ModalShowcase.log(`OK ${id}`)},
-        {text: 'Cancel', action: () => ModalShowcase.log(`Cancel ${id}`)},
-        {
-          text: 'Show another now',
-          action: () => ModalShowcase.showModalDialog(),
-        },
-        {
-          text: 'Show another in 2s',
-          action: () => setTimeout(() => ModalShowcase.showModalDialog(), 2000),
-        },
-      ],
-      content,
-    });
-    closePromise.then(logOnClose);
-  }
-
-  view() {
-    return m(
-      'div',
-      {
-        style: {
-          'display': 'flex',
-          'flex-direction': 'column',
-          'width': '100%',
-        },
-      },
-      m('textarea', {
-        id: 'mwlogs',
-        readonly: 'readonly',
-        rows: '8',
-        placeholder: 'Logs will appear here',
-      }),
-      m('input[type=button]', {
-        value: 'Show modal (static)',
-        onclick: () => ModalShowcase.showModalDialog(true),
-      }),
-      m('input[type=button]', {
-        value: 'Show modal (dynamic)',
-        onclick: () => ModalShowcase.showModalDialog(false),
-      }),
-    );
-  }
-} // class ModalShowcase
-
-function renderForm(id: string) {
-  return m(
-    Form,
-    {
-      submitLabel: 'Submit',
-      submitIcon: 'send',
-      cancelLabel: 'Cancel',
-      resetLabel: 'Reset',
-      onSubmit: () => window.alert('Form submitted!'),
-    },
-    m(FormLabel, {for: `${id}-foo`}, 'Foo'),
-    m(TextInput, {id: `${id}-foo`}),
-    m(FormLabel, {for: `${id}-bar`}, 'Bar'),
-    m(Select, {id: `${id}-bar`}, [
-      m('option', {value: 'foo', label: 'Foo'}),
-      m('option', {value: 'bar', label: 'Bar'}),
-      m('option', {value: 'baz', label: 'Baz'}),
-    ]),
-  );
-}
diff --git a/ui/src/plugins/com.android.GpuWorkPeriod/OWNERS b/ui/src/plugins/com.android.GpuWorkPeriod/OWNERS
new file mode 100644
index 0000000..0ac4632
--- /dev/null
+++ b/ui/src/plugins/com.android.GpuWorkPeriod/OWNERS
@@ -0,0 +1,2 @@
+varadgautam@google.com
+vitalyvv@google.com
diff --git a/ui/src/plugins/com.android.GpuWorkPeriod/index.ts b/ui/src/plugins/com.android.GpuWorkPeriod/index.ts
new file mode 100644
index 0000000..1bae632
--- /dev/null
+++ b/ui/src/plugins/com.android.GpuWorkPeriod/index.ts
@@ -0,0 +1,91 @@
+// 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 {NUM, STR} from '../../trace_processor/query_result';
+import {PerfettoPlugin} from '../../public/plugin';
+import {Trace} from '../../public/trace';
+import {TrackNode} from '../../public/workspace';
+import {SLICE_TRACK_KIND} from '../../public/track_kinds';
+import {createQuerySliceTrack} from '../../public/lib/tracks/query_slice_track';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'com.android.GpuWorkPeriod';
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const {engine} = ctx;
+    const result = await engine.query(`
+      include perfetto module android.gpu.work_period;
+
+      with grouped_packages as materialized (
+        select
+          uid,
+          group_concat(package_name, ',') as package_name,
+          count() as cnt
+        from package_list
+        group by uid
+      )
+      select
+        t.id trackId,
+        t.uid as uid,
+        t.gpu_id as gpuId,
+        iif(g.cnt = 1, g.package_name, 'UID ' || t.uid) as packageName
+      from android_gpu_work_period_track t
+      left join grouped_packages g using (uid)
+      order by uid
+    `);
+
+    const it = result.iter({
+      trackId: NUM,
+      uid: NUM,
+      gpuId: NUM,
+      packageName: STR,
+    });
+
+    const workPeriodByGpu = new Map<number, TrackNode>();
+    for (; it.valid(); it.next()) {
+      const {trackId, gpuId, uid, packageName} = it;
+      const uri = `/gpu_work_period_${gpuId}_${uid}`;
+      const track = await createQuerySliceTrack({
+        trace: ctx,
+        uri,
+        data: {
+          sqlSource: `
+            select ts, dur, name
+            from slice
+            where track_id = ${trackId}
+          `,
+        },
+      });
+      ctx.tracks.registerTrack({
+        uri,
+        title: packageName,
+        tags: {
+          trackIds: [trackId],
+          kind: SLICE_TRACK_KIND,
+        },
+        track,
+      });
+      let workPeriod = workPeriodByGpu.get(gpuId);
+      if (workPeriod === undefined) {
+        workPeriod = new TrackNode({
+          title: `GPU Work Period (GPU ${gpuId})`,
+          isSummary: true,
+        });
+        workPeriodByGpu.set(gpuId, workPeriod);
+        ctx.workspace.addChildInOrder(workPeriod);
+      }
+      workPeriod.addChildInOrder(new TrackNode({title: packageName, uri}));
+    }
+  }
+}
diff --git a/ui/src/plugins/com.android.InputEvents/OWNERS b/ui/src/plugins/com.android.InputEvents/OWNERS
new file mode 100644
index 0000000..0e76d10
--- /dev/null
+++ b/ui/src/plugins/com.android.InputEvents/OWNERS
@@ -0,0 +1,2 @@
+kholm@google.com
+sadrul@google.com
diff --git a/ui/src/plugins/com.android.InputEvents/index.ts b/ui/src/plugins/com.android.InputEvents/index.ts
new file mode 100644
index 0000000..b8e7184
--- /dev/null
+++ b/ui/src/plugins/com.android.InputEvents/index.ts
@@ -0,0 +1,64 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {LONG} from '../../trace_processor/query_result';
+import {PerfettoPlugin} from '../../public/plugin';
+import {Trace} from '../../public/trace';
+import {createQuerySliceTrack} from '../../public/lib/tracks/query_slice_track';
+import {TrackNode} from '../../public/workspace';
+import {getOrCreateUserInteractionGroup} from '../../public/standard_groups';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'com.android.InputEvents';
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const cnt = await ctx.engine.query(`
+      SELECT
+        count(*) as cnt
+      FROM slice
+      WHERE name GLOB 'UnwantedInteractionBlocker::notifyMotion*'
+    `);
+    if (cnt.firstRow({cnt: LONG}).cnt == 0n) {
+      return;
+    }
+
+    const SQL_SOURCE = `
+      SELECT
+        read_time as ts,
+        end_to_end_latency_dur as dur,
+        CONCAT(event_type, ' ', event_action, ': ', process_name, ' (', input_event_id, ')') as name
+      FROM android_input_events
+      WHERE end_to_end_latency_dur IS NOT NULL
+      `;
+
+    await ctx.engine.query('INCLUDE PERFETTO MODULE android.input;');
+    const uri = 'com.android.InputEvents#InputEventsTrack';
+    const title = 'Input Events';
+    const track = await createQuerySliceTrack({
+      trace: ctx,
+      uri,
+      data: {
+        sqlSource: SQL_SOURCE,
+      },
+    });
+    ctx.tracks.registerTrack({
+      uri,
+      title: title,
+      track,
+    });
+    const node = new TrackNode({uri, title});
+    const group = getOrCreateUserInteractionGroup(ctx.workspace);
+    group.addChildInOrder(node);
+  }
+}
diff --git a/ui/src/plugins/com.example.ExampleNestedTracks/index.ts b/ui/src/plugins/com.example.ExampleNestedTracks/index.ts
new file mode 100644
index 0000000..be99948
--- /dev/null
+++ b/ui/src/plugins/com.example.ExampleNestedTracks/index.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 {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {createQuerySliceTrack} from '../../public/lib/tracks/query_slice_track';
+import {TrackNode} from '../../public/workspace';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'com.example.ExampleNestedTracks';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const traceStartTime = ctx.traceInfo.start;
+    const traceDur = ctx.traceInfo.end - ctx.traceInfo.start;
+    await ctx.engine.query(`
+      create table example_events (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        name TEXT,
+        ts INTEGER,
+        dur INTEGER,
+        arg INTEGER
+      );
+
+      insert into example_events (name, ts, dur, arg)
+      values
+        ('Foo', ${traceStartTime}, ${traceDur}, 'aaa'),
+        ('Bar', ${traceStartTime}, ${traceDur / 2n}, 'bbb'),
+        ('Baz', ${traceStartTime}, ${traceDur / 3n}, 'bbb');
+    `);
+
+    const title = 'Test Track';
+    const uri = `com.example.ExampleNestedTracks#TestTrack`;
+    const track = await createQuerySliceTrack({
+      trace: ctx,
+      uri,
+      data: {
+        sqlSource: 'select * from example_events',
+      },
+    });
+    ctx.tracks.registerTrack({
+      uri,
+      title,
+      track,
+    });
+
+    this.addNestedTracks(ctx, uri);
+  }
+
+  private addNestedTracks(ctx: Trace, uri: string): void {
+    const trackRoot = new TrackNode({uri, title: 'Root'});
+    const track1 = new TrackNode({uri, title: '1'});
+    const track2 = new TrackNode({uri, title: '2'});
+    const track11 = new TrackNode({uri, title: '1.1'});
+    const track12 = new TrackNode({uri, title: '1.2'});
+    const track121 = new TrackNode({uri, title: '1.2.1'});
+    const track21 = new TrackNode({uri, title: '2.1'});
+
+    ctx.workspace.addChildInOrder(trackRoot);
+    trackRoot.addChildLast(track1);
+    trackRoot.addChildLast(track2);
+    track1.addChildLast(track11);
+    track1.addChildLast(track12);
+    track12.addChildLast(track121);
+    track2.addChildLast(track21);
+  }
+}
diff --git a/ui/src/plugins/com.example.ExampleSimpleCommand/index.ts b/ui/src/plugins/com.example.ExampleSimpleCommand/index.ts
new file mode 100644
index 0000000..cf473b3
--- /dev/null
+++ b/ui/src/plugins/com.example.ExampleSimpleCommand/index.ts
@@ -0,0 +1,28 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {App} from '../../public/app';
+import {PerfettoPlugin} from '../../public/plugin';
+
+// This is just an example plugin, used to prove that the plugin system works.
+export default class implements PerfettoPlugin {
+  static readonly id = 'com.example.ExampleSimpleCommand';
+  static onActivate(ctx: App): void {
+    ctx.commands.registerCommand({
+      id: 'com.example.ExampleSimpleCommand#LogHelloWorld',
+      name: 'Log "Hello, world!"',
+      callback: () => console.log('Hello, world!'),
+    });
+  }
+}
diff --git a/ui/src/plugins/com.example.ExampleState/index.ts b/ui/src/plugins/com.example.ExampleState/index.ts
new file mode 100644
index 0000000..9e1c296
--- /dev/null
+++ b/ui/src/plugins/com.example.ExampleState/index.ts
@@ -0,0 +1,63 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {createStore, Store} from '../../base/store';
+import {exists} from '../../base/utils';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {addQueryResultsTab} from '../../public/lib/query_table/query_result_tab';
+
+interface State {
+  counter: number;
+}
+
+// This example plugin shows using state that is persisted in the
+// permalink.
+export default class implements PerfettoPlugin {
+  static readonly id = 'com.example.ExampleState';
+  private store: Store<State> = createStore({counter: 0});
+
+  private migrate(initialState: unknown): State {
+    if (
+      exists(initialState) &&
+      typeof initialState === 'object' &&
+      'counter' in initialState &&
+      typeof initialState.counter === 'number'
+    ) {
+      return {counter: initialState.counter};
+    } else {
+      return {counter: 0};
+    }
+  }
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    this.store = ctx.mountStore((init: unknown) => this.migrate(init));
+    ctx.trash.use(this.store);
+
+    ctx.commands.registerCommand({
+      id: 'com.example.ExampleState#ShowCounter',
+      name: 'Show ExampleState counter',
+      callback: () => {
+        const counter = this.store.state.counter;
+        addQueryResultsTab(ctx, {
+          query: `SELECT ${counter} as counter;`,
+          title: `Show counter ${counter}`,
+        });
+        this.store.edit((draft) => {
+          ++draft.counter;
+        });
+      },
+    });
+  }
+}
diff --git a/ui/src/plugins/com.example.Skeleton/index.ts b/ui/src/plugins/com.example.Skeleton/index.ts
index 283cf97..5746950 100644
--- a/ui/src/plugins/com.example.Skeleton/index.ts
+++ b/ui/src/plugins/com.example.Skeleton/index.ts
@@ -12,23 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  createStore,
-  MetricVisualisation,
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-  Store,
-} from '../../public';
-
-interface State {
-  foo: string;
-}
+import {Trace} from '../../public/trace';
+import {App} from '../../public/app';
+import {MetricVisualisation} from '../../public/plugin';
+import {PerfettoPlugin} from '../../public/plugin';
 
 // SKELETON: Rename this class to match your plugin.
-class Skeleton implements Plugin {
-  private store: Store<State> = createStore({foo: 'foo'});
+export default class implements PerfettoPlugin {
+  // SKELETON: Update pluginId to match the directory of the plugin.
+  static readonly id = 'com.example.Skeleton';
 
   /**
    * This hook is called when the plugin is activated manually, or when the UI
@@ -39,8 +31,8 @@
    * This hook should be used for adding commands that don't depend on the
    * trace.
    */
-  onActivate(_: PluginContext): void {
-    //
+  static onActivate(app: App): void {
+    console.log('SkeletonPlugin::onActivate()', app.pluginId);
   }
 
   /**
@@ -51,73 +43,53 @@
    * It should not be used for finding tracks from other plugins as there is no
    * guarantee those tracks will have been added yet.
    */
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    this.store = ctx.mountStore((_: unknown): State => {
-      return {foo: 'bar'};
-    });
-
-    this.store.edit((state) => {
-      state.foo = 'baz';
-    });
+  async onTraceLoad(trace: Trace): Promise<void> {
+    console.log('SkeletonPlugin::onTraceLoad()', trace.traceInfo.traceTitle);
 
     // This is an example of how to access the pluginArgs pushed by the
     // postMessage when deep-linking to the UI.
-    if (ctx.openerPluginArgs !== undefined) {
-      console.log(`Postmessage args for ${ctx.pluginId}`, ctx.openerPluginArgs);
+    if (trace.openerPluginArgs !== undefined) {
+      console.log(
+        `Postmessage args for ${trace.pluginId}`,
+        trace.openerPluginArgs,
+      );
     }
+
+    /**
+     * The 'traceready' event is fired when the trace has finished loading, and
+     * all plugins have returned from their onTraceLoad calls. The UI can be
+     * considered 'ready' at this point. All tracks and commands should now be
+     * available, and the timeline is ready to use.
+     *
+     * This is where any automations should be done - things that you would
+     * usually do manually after the trace has loaded but you'd like to automate
+     * them.
+     *
+     * Examples of things that could be done here:
+     * - Pinning tracks
+     * - Focusing on a slice
+     * - Adding debug tracks
+     *
+     * Postmessage args might be useful here - e.g. if you would like to pin a
+     * specific track, pass the track details through the postmessage args
+     * interface and react to it here.
+     *
+     * Note: Any tracks registered in this hook will not be displayed in the
+     * timeline, unless they are manually added through the ctx.timeline API.
+     * However this part of the code is in flux at the moment and the semantics
+     * of how this works might change, though it's still good practice to use
+     * the onTraceLoad hook to add tracks as it means that all tracks are
+     * available by the time this hook gets called.
+     *
+     * TODO(stevegolton): Update this comment if the semantics of track adding
+     * changes.
+     */
+    trace.addEventListener('traceready', async () => {
+      console.log('SkeletonPlugin::traceready');
+    });
   }
 
-  /**
-   * This hook is called when the trace has finished loading, and all plugins
-   * have returned from their onTraceLoad calls. The UI can be considered
-   * 'ready' at this point. All tracks and commands should now be available, and
-   * the timeline is ready to use.
-   *
-   * This is where any automations should be done - things that you would
-   * usually do manually after the trace has loaded but you'd like to automate
-   * them.
-   *
-   * Examples of things that could be done here:
-   * - Pinning tracks
-   * - Focusing on a slice
-   * - Adding debug tracks
-   *
-   * Postmessage args might be useful here - e.g. if you would like to pin a
-   * specific track, pass the track details through the postmessage args
-   * interface and react to it here.
-   *
-   * Note: Any tracks registered in this hook will not be displayed in the
-   * timeline, unless they are manually added through the ctx.timeline API.
-   * However this part of the code is in flux at the moment and the semantics of
-   * how this works might change, though it's still good practice to use the
-   * onTraceLoad hook to add tracks as it means that all tracks are available by
-   * the time this hook gets called.
-   *
-   * TODO(stevegolton): Update this comment if the semantics of track adding
-   * changes.
-   */
-  async onTraceReady(_ctx: PluginContextTrace): Promise<void> {}
-
-  /**
-   * This hook is called as the trace is being unloaded, such as when switching
-   * traces. It should be used to clean up any trace related resources.
-   */
-  async onTraceUnload(_: PluginContextTrace): Promise<void> {
-    this.store[Symbol.dispose]();
-  }
-
-  /**
-   * This hook is called when this plugin is manually deactivated by the user.
-   */
-  onDeactivate(_: PluginContext): void {}
-
-  metricVisualisations(_: PluginContextTrace): MetricVisualisation[] {
+  static metricVisualisations(): MetricVisualisation[] {
     return [];
   }
 }
-
-export const plugin: PluginDescriptor = {
-  // SKELETON: Update pluginId to match the directory of the plugin.
-  pluginId: 'com.example.Skeleton',
-  plugin: Skeleton,
-};
diff --git a/ui/src/plugins/com.google.PixelCpmTrace/OWNERS b/ui/src/plugins/com.google.PixelCpmTrace/OWNERS
new file mode 100644
index 0000000..9833dcb
--- /dev/null
+++ b/ui/src/plugins/com.google.PixelCpmTrace/OWNERS
@@ -0,0 +1,2 @@
+sashwinbalaji@google.com
+spirani@google.com
diff --git a/ui/src/plugins/com.google.PixelCpmTrace/index.ts b/ui/src/plugins/com.google.PixelCpmTrace/index.ts
new file mode 100644
index 0000000..dcb3934
--- /dev/null
+++ b/ui/src/plugins/com.google.PixelCpmTrace/index.ts
@@ -0,0 +1,74 @@
+// 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 {createQueryCounterTrack} from '../../public/lib/tracks/query_counter_track';
+import {PerfettoPlugin} from '../../public/plugin';
+import {Trace} from '../../public/trace';
+import {COUNTER_TRACK_KIND} from '../../public/track_kinds';
+import {TrackNode} from '../../public/workspace';
+import {NUM, STR} from '../../trace_processor/query_result';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'com.google.PixelCpmTrace';
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const group = new TrackNode({
+      title: 'Central Power Manager',
+      isSummary: true,
+    });
+
+    const {engine} = ctx;
+    const result = await engine.query(`
+      select
+        id AS trackId,
+        extract_arg(dimension_arg_set_id, 'name') AS trackName
+      FROM track
+      WHERE classification = 'pixel_cpm_trace'
+      ORDER BY trackName
+    `);
+
+    const it = result.iter({trackId: NUM, trackName: STR});
+    for (let group_added = false; it.valid(); it.next()) {
+      const {trackId, trackName} = it;
+      const uri = `/cpm_trace_${trackName}`;
+      const track = await createQueryCounterTrack({
+        trace: ctx,
+        uri,
+        data: {
+          sqlSource: `
+             select ts, value
+             from counter
+             where track_id = ${trackId}
+           `,
+          columns: ['ts', 'value'],
+        },
+        columns: {ts: 'ts', value: 'value'},
+      });
+      ctx.tracks.registerTrack({
+        uri,
+        title: trackName,
+        tags: {
+          kind: COUNTER_TRACK_KIND,
+          trackIds: [trackId],
+        },
+        track,
+      });
+      group.addChildInOrder(new TrackNode({uri, title: trackName}));
+      if (!group_added) {
+        ctx.workspace.addChildInOrder(group);
+        group_added = true;
+      }
+    }
+  }
+}
diff --git a/ui/src/plugins/com.google.PixelMemory/index.ts b/ui/src/plugins/com.google.PixelMemory/index.ts
index 80ac5af..30d1520 100644
--- a/ui/src/plugins/com.google.PixelMemory/index.ts
+++ b/ui/src/plugins/com.google.PixelMemory/index.ts
@@ -12,13 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {addDebugCounterTrack} from '../../public/lib/tracks/debug_tracks';
 
-import {addDebugCounterTrack} from '../../frontend/debug_tracks/debug_tracks';
+export default class implements PerfettoPlugin {
+  static readonly id = 'com.google.PixelMemory';
 
-class PixelMemory implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerCommand({
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.PixelMemory#ShowTotalMemory',
       name: 'Add tracks: show a process total memory',
       callback: async (pid) => {
@@ -40,9 +42,9 @@
             );
         `;
         await ctx.engine.query(RSS_ALL);
-        await addDebugCounterTrack(
-          ctx,
-          {
+        await addDebugCounterTrack({
+          trace: ctx,
+          data: {
             sqlSource: `
                 SELECT
                   ts,
@@ -52,15 +54,9 @@
             `,
             columns: ['ts', 'value'],
           },
-          pid + '_rss_anon_file_swap_shmem_gpu',
-          {ts: 'ts', value: 'value'},
-        );
+          title: pid + '_rss_anon_file_swap_shmem_gpu',
+        });
       },
     });
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'com.google.PixelMemory',
-  plugin: PixelMemory,
-};
diff --git a/ui/src/plugins/com.google.android.GoogleCamera/index.ts b/ui/src/plugins/com.google.android.GoogleCamera/index.ts
index fb24735..1debd41 100644
--- a/ui/src/plugins/com.google.android.GoogleCamera/index.ts
+++ b/ui/src/plugins/com.google.android.GoogleCamera/index.ts
@@ -12,30 +12,22 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  TrackRef,
-} from '../../public';
-
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
 import * as cameraConstants from './googleCameraConstants';
 
-class GoogleCamera implements Plugin {
-  private ctx!: PluginContextTrace;
-
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    this.ctx = ctx;
-
-    ctx.registerCommand({
+export default class implements PerfettoPlugin {
+  static readonly id = 'com.google.android.GoogleCamera';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    ctx.commands.registerCommand({
       id: 'com.google.android.GoogleCamera#LoadGoogleCameraStartupView',
       name: 'Load google camera startup view',
       callback: () => {
-        this.loadGCAStartupView();
+        this.loadGCAStartupView(ctx);
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'com.google.android.GoogleCamera#PinCameraRelatedTracks',
       name: 'Pin camera related tracks',
       callback: (trackNames) => {
@@ -48,30 +40,23 @@
         ) {
           return item.trim();
         });
-        this.pinTracks(trackNameList);
+        this.pinTracks(ctx, trackNameList);
       },
     });
   }
 
-  private loadGCAStartupView() {
-    this.pinTracks(cameraConstants.MAIN_THREAD_TRACK);
-    this.pinTracks(cameraConstants.STARTUP_RELATED_TRACKS);
+  private loadGCAStartupView(ctx: Trace) {
+    this.pinTracks(ctx, cameraConstants.MAIN_THREAD_TRACK);
+    this.pinTracks(ctx, cameraConstants.STARTUP_RELATED_TRACKS);
   }
 
-  private pinTracks(trackNames: string[]) {
-    const tracks: TrackRef[] = this.ctx.timeline.tracks;
-    trackNames.forEach((trackName) => {
-      const desiredTracks = tracks.filter((track) => {
-        return track.title.match(trackName);
-      });
-      desiredTracks.forEach((desiredTrack) => {
-        this.ctx.timeline.pinTrack(desiredTrack.key!);
+  private pinTracks(ctx: Trace, trackNames: ReadonlyArray<string>) {
+    ctx.workspace.flatTracks.forEach((track) => {
+      trackNames.forEach((trackName) => {
+        if (track.title.match(trackName)) {
+          track.pin();
+        }
       });
     });
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'com.google.android.GoogleCamera',
-  plugin: GoogleCamera,
-};
diff --git a/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts b/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts
index ffda5a3..1525057 100644
--- a/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts
@@ -12,7 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {MetricVisualisation, Plugin, PluginDescriptor} from '../../public';
+import {MetricVisualisation} from '../../public/plugin';
+import {PerfettoPlugin} from '../../public/plugin';
 
 const SPEC = `
 {
@@ -31,8 +32,10 @@
 }
 `;
 
-class AndroidBinderVizPlugin implements Plugin {
-  metricVisualisations(): MetricVisualisation[] {
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.AndroidBinderVizPlugin';
+
+  static metricVisualisations(): MetricVisualisation[] {
     return [
       {
         metric: 'android_binder',
@@ -42,8 +45,3 @@
     ];
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.AndroidBinderVizPlugin',
-  plugin: AndroidBinderVizPlugin,
-};
diff --git a/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts b/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts
index a63f8ac..ab095b4 100644
--- a/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts
@@ -12,18 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  NUM,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  STR,
-} from '../../public';
-import {addDebugSliceTrack} from '../../public';
+import {NUM, STR} from '../../trace_processor/query_result';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {addDebugSliceTrack} from '../../public/debug_tracks';
 
-class AndroidClientServer implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerCommand({
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.AndroidClientServer';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidClientServer#ThreadRuntimeIPC',
       name: 'Show dependencies in client server model',
       callback: async (sliceId) => {
@@ -191,26 +188,19 @@
           name: STR,
         });
         for (; it.valid(); it.next()) {
-          await addDebugSliceTrack(
-            ctx,
-            {
+          await addDebugSliceTrack({
+            trace: ctx,
+            data: {
               sqlSource: `
                 SELECT ts, dur, name
                 FROM __enhanced_binder_for_slice_${sliceId}
                 WHERE binder_id = ${it.id}
               `,
             },
-            it.name,
-            {ts: 'ts', dur: 'dur', name: 'name'},
-            [],
-          );
+            title: it.name,
+          });
         }
       },
     });
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.AndroidClientServer',
-  plugin: AndroidClientServer,
-};
diff --git a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
index 24d90bf..2fef0d4 100644
--- a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
@@ -12,30 +12,25 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {SimpleSliceTrackConfig} from '../../frontend/simple_slice_track';
-import {addDebugSliceTrack} from '../../public';
-import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {addAndPinSliceTrack, TrackType} from './trackUtils';
+import {addDebugSliceTrack} from '../../public/debug_tracks';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {addQueryResultsTab} from '../../public/lib/query_table/query_result_tab';
 
 /**
  * Adds the Debug Slice Track for given Jank CUJ name
  *
- * @param {PluginContextTrace} ctx For properties and methods of trace viewer
+ * @param {Trace} ctx For properties and methods of trace viewer
  * @param {string} trackName Display Name of the track
- * @param {TrackType} type Track type for jank CUJ slice track
  * @param {string | string[]} cujNames List of Jank CUJs to pin
- * @param {string} uri Identifier for the track in case of 'static' type
  */
 export function addJankCUJDebugTrack(
-  ctx: PluginContextTrace,
+  ctx: Trace,
   trackName: string,
-  type: TrackType,
   cujNames?: string | string[],
-  uri?: string,
 ) {
-  const jankCujTrackConfig: SimpleSliceTrackConfig =
-    generateJankCujTrackConfig(cujNames);
-  addAndPinSliceTrack(ctx, jankCujTrackConfig, trackName, type, uri);
+  const jankCujTrackConfig = generateJankCujTrackConfig(cujNames);
+  addDebugSliceTrack({trace: ctx, title: trackName, ...jankCujTrackConfig});
 }
 
 const JANK_CUJ_QUERY_PRECONDITIONS = `
@@ -47,11 +42,9 @@
  * Generate the Track config for a multiple Jank CUJ slices
  *
  * @param {string | string[]} cujNames List of Jank CUJs to pin, default empty
- * @returns {SimpleSliceTrackConfig} Returns the track config for given CUJs
+ * @returns Returns the track config for given CUJs
  */
-function generateJankCujTrackConfig(
-  cujNames: string | string[] = [],
-): SimpleSliceTrackConfig {
+function generateJankCujTrackConfig(cujNames: string | string[] = []) {
   // This method expects the caller to have run JANK_CUJ_QUERY_PRECONDITIONS
   // Not running the precondition query here to save time in case already run
   const jankCujQuery = JANK_CUJ_QUERY;
@@ -64,15 +57,13 @@
           .join(',')})`
       : '';
 
-  const jankCujTrackConfig: SimpleSliceTrackConfig = {
+  return {
     data: {
       sqlSource: `${jankCujQuery}${filterCuj}`,
       columns: jankCujColumns,
     },
-    columns: {ts: 'ts', dur: 'dur', name: 'name'},
     argColumns: jankCujColumns,
   };
-  return jankCujTrackConfig;
 }
 
 const JANK_CUJ_QUERY = `
@@ -162,7 +153,7 @@
                 cuj_state_marker.ts >= cuj.ts
                 AND cuj_state_marker.ts + cuj_state_marker.dur <= cuj.ts + cuj.dur
                 AND marker_track.name = cuj.name AND (
-                    cuj_state_marker.name GLOB 'cancel' 
+                    cuj_state_marker.name GLOB 'cancel'
                     OR cuj_state_marker.name GLOB 'timeout')
             )
           THEN ' ❌ '
@@ -217,74 +208,73 @@
   'table_name',
 ];
 
-class AndroidCujs implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerCommand({
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.AndroidCujs';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidCujs#PinJankCUJs',
       name: 'Add track: Android jank CUJs',
       callback: () => {
         ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() => {
-          addJankCUJDebugTrack(ctx, 'Jank CUJs', 'debug');
+          addJankCUJDebugTrack(ctx, 'Jank CUJs');
         });
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidCujs#ListJankCUJs',
       name: 'Run query: Android jank CUJs',
       callback: () => {
-        ctx.engine
-          .query(JANK_CUJ_QUERY_PRECONDITIONS)
-          .then(() => ctx.tabs.openQuery(JANK_CUJ_QUERY, 'Android Jank CUJs'));
-      },
-    });
-
-    ctx.registerCommand({
-      id: 'dev.perfetto.AndroidCujs#PinLatencyCUJs',
-      name: 'Add track: Android latency CUJs',
-      callback: () => {
-        addDebugSliceTrack(
-          ctx,
-          {
-            sqlSource: LATENCY_CUJ_QUERY,
-            columns: LATENCY_COLUMNS,
-          },
-          'Latency CUJs',
-          {ts: 'ts', dur: 'dur', name: 'name'},
-          [],
+        ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() =>
+          addQueryResultsTab(ctx, {
+            query: JANK_CUJ_QUERY,
+            title: 'Android Jank CUJs',
+          }),
         );
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
+      id: 'dev.perfetto.AndroidCujs#PinLatencyCUJs',
+      name: 'Add track: Android latency CUJs',
+      callback: () => {
+        addDebugSliceTrack({
+          trace: ctx,
+          data: {
+            sqlSource: LATENCY_CUJ_QUERY,
+            columns: LATENCY_COLUMNS,
+          },
+          title: 'Latency CUJs',
+        });
+      },
+    });
+
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidCujs#ListLatencyCUJs',
       name: 'Run query: Android Latency CUJs',
       callback: () =>
-        ctx.tabs.openQuery(LATENCY_CUJ_QUERY, 'Android Latency CUJs'),
+        addQueryResultsTab(ctx, {
+          query: LATENCY_CUJ_QUERY,
+          title: 'Android Latency CUJs',
+        }),
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidCujs#PinBlockingCalls',
       name: 'Add track: Android Blocking calls during CUJs',
       callback: () => {
         ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() =>
-          addDebugSliceTrack(
-            ctx,
-            {
+          addDebugSliceTrack({
+            trace: ctx,
+            data: {
               sqlSource: BLOCKING_CALLS_DURING_CUJS_QUERY,
               columns: BLOCKING_CALLS_DURING_CUJS_COLUMNS,
             },
-            'Blocking calls during CUJs',
-            {ts: 'ts', dur: 'dur', name: 'name'},
-            BLOCKING_CALLS_DURING_CUJS_COLUMNS,
-          ),
+            title: 'Blocking calls during CUJs',
+            argColumns: BLOCKING_CALLS_DURING_CUJS_COLUMNS,
+          }),
         );
       },
     });
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.AndroidCujs',
-  plugin: AndroidCujs,
-};
diff --git a/ui/src/plugins/dev.perfetto.AndroidCujs/trackUtils.ts b/ui/src/plugins/dev.perfetto.AndroidCujs/trackUtils.ts
index 2b108d2..ca01328 100644
--- a/ui/src/plugins/dev.perfetto.AndroidCujs/trackUtils.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidCujs/trackUtils.ts
@@ -12,190 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {globals} from '../../frontend/globals';
-import {
-  SimpleSliceTrack,
-  SimpleSliceTrackConfig,
-} from '../../frontend/simple_slice_track';
-import {addDebugSliceTrack, PluginContextTrace} from '../../public';
-import {findCurrentSelection} from '../../frontend/keyboard_event_handler';
-import {time, Time} from '../../base/time';
-import {BigintMath} from '../../base/bigint_math';
-import {reveal} from '../../frontend/scroll_helper';
-
-// Common TrackType for tracks when using registerStatic or addDebug
-// TODO: b/349502258 - to be removed after single refactoring to single API
-export type TrackType = 'static' | 'debug';
-
-/**
- * Adds debug tracks from SimpleSliceTrackConfig
- * Static tracks cannot be added on command
- * TODO: b/349502258 - To be removed later
- *
- * @param {PluginContextTrace} ctx Context for trace methods and properties
- * @param {SimpleSliceTrackConfig} config Track config to add
- * @param {string} trackName Track name to display
- */
-export function addDebugTrackOnCommand(
-  ctx: PluginContextTrace,
-  config: SimpleSliceTrackConfig,
-  trackName: string,
-) {
-  addDebugSliceTrack(
-    ctx,
-    config.data,
-    trackName,
-    config.columns,
-    config.argColumns,
-  );
-}
-
-/**
- * Registers and pins tracks on traceload, given params
- * TODO: b/349502258 - Refactor to single API
- *
- * @param {PluginContextTrace} ctx Context for trace methods and properties
- * @param {SimpleSliceTrackConfig} config Track config to add
- * @param {string} trackName Track name to display
- * @param {string} uri Unique identifier for the track
- */
-export function addDebugTrackOnTraceLoad(
-  ctx: PluginContextTrace,
-  config: SimpleSliceTrackConfig,
-  trackName: string,
-  uri: string,
-) {
-  ctx.registerStaticTrack({
-    uri: uri,
-    title: trackName,
-    isPinned: true,
-    trackFactory: (trackCtx) => {
-      return new SimpleSliceTrack(ctx.engine, trackCtx, config);
-    },
-  });
-}
-
-/**
- * Registers and pins tracks on traceload or command
- * Every enabled plugins' onTraceload is executed when the trace is first loaded
- * To add and pin tracks on traceload, need to use registerStaticTrack
- * After traceload, if plugin registered command invocated, then addDebugSliceTrack
- * TODO: b/349502258 - Refactor to single API
- *
- * @param {PluginContextTrace} ctx Context for trace methods and properties
- * @param {SimpleSliceTrackConfig} config Track config to add
- * @param {string} trackName Track name to display
- * @param {TrackType} type Whether to registerStaticTrack or addDebugSliceTrack
- * type 'static' expects caller to pass uri string
- * @param {string} uri Unique track identifier expected when type is 'static'
- */
-export function addAndPinSliceTrack(
-  ctx: PluginContextTrace,
-  config: SimpleSliceTrackConfig,
-  trackName: string,
-  type: TrackType,
-  uri?: string,
-) {
-  if (type == 'static') {
-    addDebugTrackOnTraceLoad(ctx, config, trackName, uri ?? '');
-  } else if (type == 'debug') {
-    addDebugTrackOnCommand(ctx, config, trackName);
-  }
-}
-
-/**
- * Interface for slice identifier
- */
-export interface SliceIdentifier {
-  sliceId?: number;
-  trackId?: number;
-  ts?: time;
-  dur?: bigint;
-}
+import {Trace} from '../../public/trace';
 
 /**
  * Sets focus on a specific slice within the trace data.
  *
  * Takes and adds desired slice to current selection
  * Retrieves the track key and scrolls to the desired slice
- *
- * @param {SliceIdentifier} slice slice to focus on with trackId and sliceId
  */
-
-export function focusOnSlice(slice: SliceIdentifier) {
-  if (slice.sliceId == undefined || slice.trackId == undefined) {
-    return;
-  }
-  const trackId = slice.trackId;
-  const trackKey = getTrackKey(trackId);
-  globals.setLegacySelection(
-    {
-      kind: 'SLICE',
-      id: slice.sliceId,
-      trackKey: trackKey,
-      table: 'slice',
-    },
-    {
-      clearSearch: true,
-      pendingScrollId: slice.sliceId,
-      switchToCurrentSelectionTab: true,
-    },
-  );
-  findCurrentSelection;
-}
-
-/**
- * Given the trackId of the track, retrieves its trackKey
- *
- * @param {number} trackId track_id of the track
- * @returns {string} trackKey given to the track with queried trackId
- */
-function getTrackKey(trackId: number): string | undefined {
-  return globals.trackManager.trackKeyByTrackId.get(trackId);
-}
-
-/**
- * Sets focus on a specific time span and a track
- *
- * Takes a row object pans the view to that time span
- * Retrieves the track key and scrolls to the desired track
- *
- * @param {SliceIdentifier} slice slice to focus on with trackId and time data
- */
-
-export async function focusOnTimeAndTrack(slice: SliceIdentifier) {
-  if (
-    slice.trackId == undefined ||
-    slice.ts == undefined ||
-    slice.dur == undefined
-  ) {
-    return;
-  }
-  const trackId = slice.trackId;
-  const sliceStart = slice.ts;
-  // row.dur can be negative. Clamp to 1ns.
-  const sliceDur = BigintMath.max(slice.dur, 1n);
-  const trackKey = getTrackKey(trackId);
-  // true for whether to expand the process group the track belongs to
-  if (trackKey == undefined) {
-    return;
-  }
-  reveal(trackKey, sliceStart, Time.add(sliceStart, sliceDur), true);
-}
-
-/**
- * Function to check keep checking for object values at set intervals
- *
- * @param {T | undefined} getValue Function to retrieve object value
- * @returns {T} Value returned by getValue when available
- */
-export async function waitForValue<T>(getValue: () => T): Promise<T> {
-  while (true) {
-    // TODO: b/353466921 - update when waiForTraceLoad function added
-    const value = getValue();
-    if (value !== undefined) {
-      return value;
-    }
-    await new Promise((resolve) => setTimeout(resolve, 100));
-  }
+export function focusOnSlice(ctx: Trace, sqlSliceId: number) {
+  ctx.selection.selectSqlEvent('slice', sqlSliceId, {
+    scrollToSelection: true,
+  });
 }
diff --git a/ui/src/plugins/dev.perfetto.AndroidDesktopMode/OWNERS b/ui/src/plugins/dev.perfetto.AndroidDesktopMode/OWNERS
new file mode 100644
index 0000000..d0be0cc
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AndroidDesktopMode/OWNERS
@@ -0,0 +1,2 @@
+benm@google.com
+
diff --git a/ui/src/plugins/dev.perfetto.AndroidDesktopMode/index.ts b/ui/src/plugins/dev.perfetto.AndroidDesktopMode/index.ts
new file mode 100644
index 0000000..59f6bbf
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AndroidDesktopMode/index.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 {createQuerySliceTrack} from '../../public/lib/tracks/query_slice_track';
+import {PerfettoPlugin} from '../../public/plugin';
+import {Trace} from '../../public/trace';
+import {TrackNode} from '../../public/workspace';
+
+const INCLUDE_DESKTOP_MODULE_QUERY = `INCLUDE PERFETTO MODULE android.desktop_mode`;
+
+const QUERY = `
+SELECT
+  ROW_NUMBER() OVER (ORDER BY ts) AS id,
+  ts,
+  dur,
+  ifnull(p.package_name, 'uid=' || dw.uid) AS name
+FROM android_desktop_mode_windows dw
+LEFT JOIN package_list p ON CAST (dw.uid AS INT) % 100000 = p.uid AND p.uid != 1000
+`;
+
+const COLUMNS = ['id', 'ts', 'dur', 'name'];
+const TRACK_NAME = 'Desktop Mode Windows';
+const TRACK_URI = '/desktop_windows';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.AndroidDesktopMode';
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    await ctx.engine.query(INCLUDE_DESKTOP_MODULE_QUERY);
+    await this.registerTrack(ctx, QUERY);
+    ctx.commands.registerCommand({
+      id: 'dev.perfetto.DesktopMode#AddTrackDesktopWindowss',
+      name: 'Add Track: ' + TRACK_NAME,
+      callback: () => this.addSimpleTrack(ctx),
+    });
+  }
+
+  private async registerTrack(ctx: Trace, sql: string) {
+    const track = await createQuerySliceTrack({
+      trace: ctx,
+      uri: TRACK_URI,
+      data: {
+        sqlSource: sql,
+        columns: COLUMNS,
+      },
+    });
+    ctx.tracks.registerTrack({
+      uri: TRACK_URI,
+      title: TRACK_NAME,
+      track,
+    });
+  }
+
+  private addSimpleTrack(ctx: Trace) {
+    const trackNode = new TrackNode({uri: TRACK_URI, title: TRACK_NAME});
+    ctx.workspace.addChildInOrder(trackNode);
+    trackNode.pin();
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.AndroidDmabuf/OWNERS b/ui/src/plugins/dev.perfetto.AndroidDmabuf/OWNERS
new file mode 100644
index 0000000..0bcdb4d
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AndroidDmabuf/OWNERS
@@ -0,0 +1 @@
+ilkos@google.com
diff --git a/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts b/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts
new file mode 100644
index 0000000..a946cc2
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts
@@ -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.
+
+import {
+  createQueryCounterTrack,
+  SqlDataSource,
+} from '../../public/lib/tracks/query_counter_track';
+import {PerfettoPlugin} from '../../public/plugin';
+import {
+  getOrCreateGroupForProcess,
+  getOrCreateGroupForThread,
+} from '../../public/standard_groups';
+import {Trace} from '../../public/trace';
+import {TrackNode} from '../../public/workspace';
+import {NUM_NULL} from '../../trace_processor/query_result';
+
+async function registerAllocsTrack(
+  ctx: Trace,
+  uri: string,
+  dataSource: SqlDataSource,
+) {
+  const track = await createQueryCounterTrack({
+    trace: ctx,
+    uri,
+    data: dataSource,
+  });
+  ctx.tracks.registerTrack({
+    uri,
+    title: `dmabuf allocs`,
+    track: track,
+  });
+}
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.AndroidDmabuf';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const e = ctx.engine;
+    await e.query(`INCLUDE PERFETTO MODULE android.memory.dmabuf`);
+    await e.query(`
+      CREATE PERFETTO TABLE _android_memory_cumulative_dmabuf AS
+      SELECT
+        upid, utid, ts,
+        SUM(buf_size) OVER(PARTITION BY COALESCE(upid, utid) ORDER BY ts) AS value
+      FROM android_dmabuf_allocs;`);
+
+    const pids = await e.query(
+      `SELECT DISTINCT upid, IIF(upid IS NULL, utid, NULL) AS utid FROM _android_memory_cumulative_dmabuf`,
+    );
+    const it = pids.iter({upid: NUM_NULL, utid: NUM_NULL});
+    for (; it.valid(); it.next()) {
+      if (it.upid != null) {
+        const uri = `/android_process_dmabuf_upid_${it.upid}`;
+        const config: SqlDataSource = {
+          sqlSource: `SELECT ts, value FROM _android_memory_cumulative_dmabuf
+                 WHERE upid = ${it.upid}`,
+        };
+        await registerAllocsTrack(ctx, uri, config);
+        getOrCreateGroupForProcess(ctx.workspace, it.upid).addChildInOrder(
+          new TrackNode({uri, title: 'dmabuf allocs'}),
+        );
+      } else if (it.utid != null) {
+        const uri = `/android_process_dmabuf_utid_${it.utid}`;
+        const config: SqlDataSource = {
+          sqlSource: `SELECT ts, value FROM _android_memory_cumulative_dmabuf
+                 WHERE utid = ${it.utid}`,
+        };
+        await registerAllocsTrack(ctx, uri, config);
+        getOrCreateGroupForThread(ctx.workspace, it.utid).addChildInOrder(
+          new TrackNode({uri, title: 'dmabuf allocs'}),
+        );
+      }
+    }
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.AndroidLog/index.ts b/ui/src/plugins/dev.perfetto.AndroidLog/index.ts
new file mode 100644
index 0000000..8736fcd
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AndroidLog/index.ts
@@ -0,0 +1,112 @@
+// 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 m from 'mithril';
+import {LogFilteringCriteria, LogPanel} from './logs_panel';
+import {ANDROID_LOGS_TRACK_KIND} from '../../public/track_kinds';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {sqlTableRegistry} from '../../frontend/widgets/sql/table/sql_table_registry';
+import {NUM} from '../../trace_processor/query_result';
+import {AndroidLogTrack} from './logs_track';
+import {exists} from '../../base/utils';
+import {TrackNode} from '../../public/workspace';
+import {getAndroidLogsTable} from './table';
+import {extensions} from '../../public/lib/extensions';
+
+const VERSION = 1;
+
+const DEFAULT_STATE: AndroidLogPluginState = {
+  version: VERSION,
+  filter: {
+    // The first two log priorities are ignored.
+    minimumLevel: 2,
+    tags: [],
+    textEntry: '',
+    hideNonMatching: true,
+  },
+};
+
+interface AndroidLogPluginState {
+  version: number;
+  filter: LogFilteringCriteria;
+}
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.AndroidLog';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const store = ctx.mountStore<AndroidLogPluginState>((init) => {
+      return exists(init) && (init as {version: unknown}).version === VERSION
+        ? (init as AndroidLogPluginState)
+        : DEFAULT_STATE;
+    });
+
+    const result = await ctx.engine.query(
+      `select count(1) as cnt from android_logs`,
+    );
+    const logCount = result.firstRow({cnt: NUM}).cnt;
+    const uri = 'perfetto.AndroidLog';
+    const title = 'Android logs';
+    if (logCount > 0) {
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        tags: {kind: ANDROID_LOGS_TRACK_KIND},
+        track: new AndroidLogTrack(ctx.engine),
+      });
+      const track = new TrackNode({title, uri});
+      ctx.workspace.addChildInOrder(track);
+    }
+
+    const androidLogsTabUri = 'perfetto.AndroidLog#tab';
+
+    // Eternal tabs should always be available even if there is nothing to show
+    const filterStore = store.createSubStore(
+      ['filter'],
+      (x) => x as LogFilteringCriteria,
+    );
+
+    ctx.tabs.registerTab({
+      isEphemeral: false,
+      uri: androidLogsTabUri,
+      content: {
+        render: () => m(LogPanel, {filterStore: filterStore, trace: ctx}),
+        getTitle: () => 'Android Logs',
+      },
+    });
+
+    if (logCount > 0) {
+      ctx.tabs.addDefaultTab(androidLogsTabUri);
+    }
+
+    ctx.commands.registerCommand({
+      id: 'perfetto.AndroidLog#ShowLogsTab',
+      name: 'Show android logs tab',
+      callback: () => {
+        ctx.tabs.showTab(androidLogsTabUri);
+      },
+    });
+
+    sqlTableRegistry['android_logs'] = getAndroidLogsTable();
+    ctx.commands.registerCommand({
+      id: 'perfetto.ShowTable.android_logs',
+      name: 'Open table: android_logs',
+      callback: () => {
+        extensions.addSqlTableTab(ctx, {
+          table: getAndroidLogsTable(),
+        });
+      },
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.AndroidLog/logs_panel.ts b/ui/src/plugins/dev.perfetto.AndroidLog/logs_panel.ts
new file mode 100644
index 0000000..48714ba
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AndroidLog/logs_panel.ts
@@ -0,0 +1,442 @@
+// Copyright (C) 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
+//
+//      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 {time, Time, TimeSpan} from '../../base/time';
+import {DetailsShell} from '../../widgets/details_shell';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {Engine} from '../../trace_processor/engine';
+import {LONG, NUM, NUM_NULL, STR} from '../../trace_processor/query_result';
+import {Monitor} from '../../base/monitor';
+import {AsyncLimiter} from '../../base/async_limiter';
+import {escapeGlob, escapeQuery} from '../../trace_processor/query_utils';
+import {Select} from '../../widgets/select';
+import {Button} from '../../widgets/button';
+import {TextInput} from '../../widgets/text_input';
+import {VirtualTable, VirtualTableRow} from '../../widgets/virtual_table';
+import {classNames} from '../../base/classnames';
+import {TagInput} from '../../widgets/tag_input';
+import {Store} from '../../base/store';
+import {Trace} from '../../public/trace';
+
+const ROW_H = 20;
+
+export interface LogFilteringCriteria {
+  minimumLevel: number;
+  tags: string[];
+  textEntry: string;
+  hideNonMatching: boolean;
+}
+
+export interface LogPanelAttrs {
+  filterStore: Store<LogFilteringCriteria>;
+  trace: Trace;
+}
+
+interface Pagination {
+  offset: number;
+  count: number;
+}
+
+interface LogEntries {
+  offset: number;
+  timestamps: time[];
+  priorities: number[];
+  tags: string[];
+  messages: string[];
+  isHighlighted: boolean[];
+  processName: string[];
+  totalEvents: number; // Count of the total number of events within this window
+}
+
+export class LogPanel implements m.ClassComponent<LogPanelAttrs> {
+  private entries?: LogEntries;
+
+  private pagination: Pagination = {
+    offset: 0,
+    count: 0,
+  };
+  private readonly rowsMonitor: Monitor;
+  private readonly filterMonitor: Monitor;
+  private readonly queryLimiter = new AsyncLimiter();
+
+  constructor({attrs}: m.CVnode<LogPanelAttrs>) {
+    this.rowsMonitor = new Monitor([
+      () => attrs.filterStore.state,
+      () => attrs.trace.timeline.visibleWindow.toTimeSpan().start,
+      () => attrs.trace.timeline.visibleWindow.toTimeSpan().end,
+    ]);
+
+    this.filterMonitor = new Monitor([() => attrs.filterStore.state]);
+  }
+
+  view({attrs}: m.CVnode<LogPanelAttrs>) {
+    if (this.rowsMonitor.ifStateChanged()) {
+      this.reloadData(attrs);
+    }
+
+    const hasProcessNames =
+      this.entries &&
+      this.entries.processName.filter((name) => name).length > 0;
+    const totalEvents = this.entries?.totalEvents ?? 0;
+
+    return m(
+      DetailsShell,
+      {
+        title: 'Android Logs',
+        description: `Total messages: ${totalEvents}`,
+        buttons: m(LogsFilters, {trace: attrs.trace, store: attrs.filterStore}),
+      },
+      m(VirtualTable, {
+        className: 'pf-android-logs-table',
+        columns: [
+          {header: 'Timestamp', width: '13em'},
+          {header: 'Level', width: '4em'},
+          {header: 'Tag', width: '13em'},
+          ...(hasProcessNames ? [{header: 'Process', width: '18em'}] : []),
+          // '' means column width can vary depending on the content.
+          // This works as this is the last column, but using this for other
+          // columns will pull the columns to the right out of line.
+          {header: 'Message', width: ''},
+        ],
+        rows: this.renderRows(hasProcessNames),
+        firstRowOffset: this.entries?.offset ?? 0,
+        numRows: this.entries?.totalEvents ?? 0,
+        rowHeight: ROW_H,
+        onReload: (offset, count) => {
+          this.pagination = {offset, count};
+          this.reloadData(attrs);
+        },
+        onRowHover: (id) => {
+          const timestamp = this.entries?.timestamps[id];
+          if (timestamp !== undefined) {
+            attrs.trace.timeline.hoverCursorTimestamp = timestamp;
+          }
+        },
+        onRowOut: () => {
+          attrs.trace.timeline.hoverCursorTimestamp = undefined;
+        },
+      }),
+    );
+  }
+
+  private reloadData(attrs: LogPanelAttrs) {
+    this.queryLimiter.schedule(async () => {
+      const visibleSpan = attrs.trace.timeline.visibleWindow.toTimeSpan();
+
+      if (this.filterMonitor.ifStateChanged()) {
+        await updateLogView(attrs.trace.engine, attrs.filterStore.state);
+      }
+
+      this.entries = await updateLogEntries(
+        attrs.trace.engine,
+        visibleSpan,
+        this.pagination,
+      );
+
+      attrs.trace.scheduleFullRedraw();
+    });
+  }
+
+  private renderRows(hasProcessNames: boolean | undefined): VirtualTableRow[] {
+    if (!this.entries) {
+      return [];
+    }
+
+    const timestamps = this.entries.timestamps;
+    const priorities = this.entries.priorities;
+    const tags = this.entries.tags;
+    const messages = this.entries.messages;
+    const processNames = this.entries.processName;
+
+    const rows: VirtualTableRow[] = [];
+    for (let i = 0; i < this.entries.timestamps.length; i++) {
+      const priorityLetter = LOG_PRIORITIES[priorities[i]][0];
+      const ts = timestamps[i];
+      const prioClass = priorityLetter ?? '';
+
+      rows.push({
+        id: i,
+        className: classNames(
+          prioClass,
+          this.entries.isHighlighted[i] && 'pf-highlighted',
+        ),
+        cells: [
+          m(Timestamp, {ts}),
+          priorityLetter || '?',
+          tags[i],
+          ...(hasProcessNames ? [processNames[i]] : []),
+          messages[i],
+        ],
+      });
+    }
+
+    return rows;
+  }
+}
+
+export const LOG_PRIORITIES = [
+  '-',
+  '-',
+  'Verbose',
+  'Debug',
+  'Info',
+  'Warn',
+  'Error',
+  'Fatal',
+];
+const IGNORED_STATES = 2;
+
+interface LogPriorityWidgetAttrs {
+  readonly trace: Trace;
+  readonly options: string[];
+  readonly selectedIndex: number;
+  readonly onSelect: (id: number) => void;
+}
+
+class LogPriorityWidget implements m.ClassComponent<LogPriorityWidgetAttrs> {
+  view(vnode: m.Vnode<LogPriorityWidgetAttrs>) {
+    const attrs = vnode.attrs;
+    const optionComponents = [];
+    for (let i = IGNORED_STATES; i < attrs.options.length; i++) {
+      const selected = i === attrs.selectedIndex;
+      optionComponents.push(
+        m('option', {value: i, selected}, attrs.options[i]),
+      );
+    }
+    return m(
+      Select,
+      {
+        onchange: (e: Event) => {
+          const selectionValue = (e.target as HTMLSelectElement).value;
+          attrs.onSelect(Number(selectionValue));
+          attrs.trace.scheduleFullRedraw();
+        },
+      },
+      optionComponents,
+    );
+  }
+}
+
+interface LogTextWidgetAttrs {
+  readonly trace: Trace;
+  readonly onChange: (value: string) => void;
+}
+
+class LogTextWidget implements m.ClassComponent<LogTextWidgetAttrs> {
+  view({attrs}: m.CVnode<LogTextWidgetAttrs>) {
+    return m(TextInput, {
+      placeholder: 'Search logs...',
+      onkeyup: (e: KeyboardEvent) => {
+        // We want to use the value of the input field after it has been
+        // updated with the latest key (onkeyup).
+        const htmlElement = e.target as HTMLInputElement;
+        attrs.onChange(htmlElement.value);
+        attrs.trace.scheduleFullRedraw();
+      },
+    });
+  }
+}
+
+interface FilterByTextWidgetAttrs {
+  readonly hideNonMatching: boolean;
+  readonly disabled: boolean;
+  readonly onClick: () => void;
+}
+
+class FilterByTextWidget implements m.ClassComponent<FilterByTextWidgetAttrs> {
+  view({attrs}: m.Vnode<FilterByTextWidgetAttrs>) {
+    const icon = attrs.hideNonMatching ? 'unfold_less' : 'unfold_more';
+    const tooltip = attrs.hideNonMatching
+      ? 'Expand all and view highlighted'
+      : 'Collapse all';
+    return m(Button, {
+      icon,
+      title: tooltip,
+      disabled: attrs.disabled,
+      onclick: attrs.onClick,
+    });
+  }
+}
+
+interface LogsFiltersAttrs {
+  readonly trace: Trace;
+  readonly store: Store<LogFilteringCriteria>;
+}
+
+export class LogsFilters implements m.ClassComponent<LogsFiltersAttrs> {
+  view({attrs}: m.CVnode<LogsFiltersAttrs>) {
+    return [
+      m('.log-label', 'Log Level'),
+      m(LogPriorityWidget, {
+        trace: attrs.trace,
+        options: LOG_PRIORITIES,
+        selectedIndex: attrs.store.state.minimumLevel,
+        onSelect: (minimumLevel) => {
+          attrs.store.edit((draft) => {
+            draft.minimumLevel = minimumLevel;
+          });
+        },
+      }),
+      m(TagInput, {
+        placeholder: 'Filter by tag...',
+        tags: attrs.store.state.tags,
+        onTagAdd: (tag) => {
+          attrs.store.edit((draft) => {
+            draft.tags.push(tag);
+          });
+        },
+        onTagRemove: (index) => {
+          attrs.store.edit((draft) => {
+            draft.tags.splice(index, 1);
+          });
+        },
+      }),
+      m(LogTextWidget, {
+        trace: attrs.trace,
+        onChange: (text) => {
+          attrs.store.edit((draft) => {
+            draft.textEntry = text;
+          });
+        },
+      }),
+      m(FilterByTextWidget, {
+        hideNonMatching: attrs.store.state.hideNonMatching,
+        onClick: () => {
+          attrs.store.edit((draft) => {
+            draft.hideNonMatching = !draft.hideNonMatching;
+          });
+        },
+        disabled: attrs.store.state.textEntry === '',
+      }),
+    ];
+  }
+}
+
+async function updateLogEntries(
+  engine: Engine,
+  span: TimeSpan,
+  pagination: Pagination,
+): Promise<LogEntries> {
+  const rowsResult = await engine.query(`
+        select
+          ts,
+          prio,
+          ifnull(tag, '[NULL]') as tag,
+          ifnull(msg, '[NULL]') as msg,
+          is_msg_highlighted as isMsgHighlighted,
+          is_process_highlighted as isProcessHighlighted,
+          ifnull(process_name, '') as processName
+        from filtered_logs
+        where ts >= ${span.start} and ts <= ${span.end}
+        order by ts
+        limit ${pagination.offset}, ${pagination.count}
+    `);
+
+  const timestamps: time[] = [];
+  const priorities = [];
+  const tags = [];
+  const messages = [];
+  const isHighlighted = [];
+  const processName = [];
+
+  const it = rowsResult.iter({
+    ts: LONG,
+    prio: NUM,
+    tag: STR,
+    msg: STR,
+    isMsgHighlighted: NUM_NULL,
+    isProcessHighlighted: NUM,
+    processName: STR,
+  });
+  for (; it.valid(); it.next()) {
+    timestamps.push(Time.fromRaw(it.ts));
+    priorities.push(it.prio);
+    tags.push(it.tag);
+    messages.push(it.msg);
+    isHighlighted.push(
+      it.isMsgHighlighted === 1 || it.isProcessHighlighted === 1,
+    );
+    processName.push(it.processName);
+  }
+
+  const queryRes = await engine.query(`
+    select
+      count(*) as totalEvents
+    from filtered_logs
+    where ts >= ${span.start} and ts <= ${span.end}
+  `);
+  const {totalEvents} = queryRes.firstRow({totalEvents: NUM});
+
+  return {
+    offset: pagination.offset,
+    timestamps,
+    priorities,
+    tags,
+    messages,
+    isHighlighted,
+    processName,
+    totalEvents,
+  };
+}
+
+async function updateLogView(engine: Engine, filter: LogFilteringCriteria) {
+  await engine.query('drop view if exists filtered_logs');
+
+  const globMatch = composeGlobMatch(filter.hideNonMatching, filter.textEntry);
+  let selectedRows = `select prio, ts, tag, msg,
+      process.name as process_name, ${globMatch}
+      from android_logs
+      left join thread using(utid)
+      left join process using(upid)
+      where prio >= ${filter.minimumLevel}`;
+  if (filter.tags.length) {
+    selectedRows += ` and tag in (${serializeTags(filter.tags)})`;
+  }
+
+  // We extract only the rows which will be visible.
+  await engine.query(`create view filtered_logs as select *
+    from (${selectedRows})
+    where is_msg_chosen is 1 or is_process_chosen is 1`);
+}
+
+function serializeTags(tags: string[]) {
+  return tags.map((tag) => escapeQuery(tag)).join();
+}
+
+function composeGlobMatch(isCollaped: boolean, textEntry: string) {
+  if (isCollaped) {
+    // If the entries are collapsed, we won't highlight any lines.
+    return `msg glob ${escapeGlob(textEntry)} as is_msg_chosen,
+      (process.name is not null and process.name glob ${escapeGlob(
+        textEntry,
+      )}) as is_process_chosen,
+      0 as is_msg_highlighted,
+      0 as is_process_highlighted`;
+  } else if (!textEntry) {
+    // If there is no text entry, we will show all lines, but won't highlight.
+    // any.
+    return `1 as is_msg_chosen,
+      1 as is_process_chosen,
+      0 as is_msg_highlighted,
+      0 as is_process_highlighted`;
+  } else {
+    return `1 as is_msg_chosen,
+      1 as is_process_chosen,
+      msg glob ${escapeGlob(textEntry)} as is_msg_highlighted,
+      (process.name is not null and process.name glob ${escapeGlob(
+        textEntry,
+      )}) as is_process_highlighted`;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.AndroidLog/logs_track.ts b/ui/src/plugins/dev.perfetto.AndroidLog/logs_track.ts
new file mode 100644
index 0000000..451a422
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AndroidLog/logs_track.ts
@@ -0,0 +1,145 @@
+// 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 {Time, duration, time} from '../../base/time';
+import {LIMIT, TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {checkerboardExcept} from '../../frontend/checkerboard';
+import {Engine} from '../../trace_processor/engine';
+import {LONG, NUM} from '../../trace_processor/query_result';
+import {Track} from '../../public/track';
+import {TrackRenderContext} from '../../public/track';
+
+export interface Data extends TrackData {
+  // Total number of log events within [start, end], before any quantization.
+  numEvents: number;
+
+  // Below: data quantized by resolution and aggregated by event priority.
+  timestamps: BigInt64Array;
+
+  // Each Uint8 value has the i-th bit is set if there is at least one log
+  // event at the i-th priority level at the corresponding time in |timestamps|.
+  priorities: Uint8Array;
+}
+
+const LEVELS: LevelCfg[] = [
+  {color: 'hsl(122, 39%, 49%)', prios: [0, 1, 2, 3]}, // Up to DEBUG: Green.
+  {color: 'hsl(0, 0%, 70%)', prios: [4]}, // 4 (INFO) -> Gray.
+  {color: 'hsl(45, 100%, 51%)', prios: [5]}, // 5 (WARN) -> Amber.
+  {color: 'hsl(4, 90%, 58%)', prios: [6]}, // 6 (ERROR) -> Red.
+  {color: 'hsl(291, 64%, 42%)', prios: [7]}, // 7 (FATAL) -> Purple
+];
+
+const MARGIN_TOP = 2;
+const RECT_HEIGHT = 35;
+const EVT_PX = 2; // Width of an event tick in pixels.
+
+interface LevelCfg {
+  color: string;
+  prios: number[];
+}
+
+export class AndroidLogTrack implements Track {
+  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+
+  constructor(private engine: Engine) {}
+
+  async onUpdate({
+    visibleWindow,
+    resolution,
+  }: TrackRenderContext): Promise<void> {
+    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
+  }
+
+  async onDestroy(): Promise<void> {
+    this.fetcher[Symbol.dispose]();
+  }
+
+  getHeight(): number {
+    return 40;
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    const queryRes = await this.engine.query(`
+      select
+        cast(ts / ${resolution} as integer) * ${resolution} as tsQuant,
+        prio,
+        count(prio) as numEvents
+      from android_logs
+      where ts >= ${start} and ts <= ${end}
+      group by tsQuant, prio
+      order by tsQuant, prio limit ${LIMIT};`);
+
+    const rowCount = queryRes.numRows();
+    const result = {
+      start,
+      end,
+      resolution,
+      length: rowCount,
+      numEvents: 0,
+      timestamps: new BigInt64Array(rowCount),
+      priorities: new Uint8Array(rowCount),
+    };
+
+    const it = queryRes.iter({tsQuant: LONG, prio: NUM, numEvents: NUM});
+    for (let row = 0; it.valid(); it.next(), row++) {
+      result.timestamps[row] = it.tsQuant;
+      const prio = Math.min(it.prio, 7);
+      result.priorities[row] |= 1 << prio;
+      result.numEvents += it.numEvents;
+    }
+    return result;
+  }
+
+  render({ctx, size, timescale}: TrackRenderContext): void {
+    const data = this.fetcher.data;
+
+    if (data === undefined) return; // Can't possibly draw anything.
+
+    const dataStartPx = timescale.timeToPx(data.start);
+    const dataEndPx = timescale.timeToPx(data.end);
+
+    checkerboardExcept(
+      ctx,
+      this.getHeight(),
+      0,
+      size.width,
+      dataStartPx,
+      dataEndPx,
+    );
+
+    const quantWidth = Math.max(
+      EVT_PX,
+      timescale.durationToPx(data.resolution),
+    );
+    const blockH = RECT_HEIGHT / LEVELS.length;
+    for (let i = 0; i < data.timestamps.length; i++) {
+      for (let lev = 0; lev < LEVELS.length; lev++) {
+        let hasEventsForCurColor = false;
+        for (const prio of LEVELS[lev].prios) {
+          if (data.priorities[i] & (1 << prio)) hasEventsForCurColor = true;
+        }
+        if (!hasEventsForCurColor) continue;
+        ctx.fillStyle = LEVELS[lev].color;
+        const timestamp = Time.fromRaw(data.timestamps[i]);
+        const px = Math.floor(timescale.timeToPx(timestamp));
+        ctx.fillRect(px, MARGIN_TOP + blockH * lev, quantWidth, blockH);
+      } // for(lev)
+    } // for (timestamps)
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.AndroidLog/table.ts b/ui/src/plugins/dev.perfetto.AndroidLog/table.ts
new file mode 100644
index 0000000..bbb0926
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AndroidLog/table.ts
@@ -0,0 +1,45 @@
+// 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 {SqlTableDescription} from '../../frontend/widgets/sql/table/table_description';
+import {
+  ProcessColumnSet,
+  StandardColumn,
+  ThreadColumnSet,
+  TimestampColumn,
+} from '../../frontend/widgets/sql/table/well_known_columns';
+
+export function getAndroidLogsTable(): SqlTableDescription {
+  return {
+    name: 'android_logs',
+    columns: [
+      new StandardColumn('id', {aggregationType: 'nominal'}),
+      new TimestampColumn('ts'),
+      new StandardColumn('tag'),
+      new StandardColumn('prio', {aggregationType: 'nominal'}),
+      new ThreadColumnSet('utid', {title: 'utid', notNull: true}),
+      new ProcessColumnSet(
+        {
+          column: 'upid',
+          source: {
+            table: 'thread',
+            joinOn: {utid: 'utid'},
+          },
+        },
+        {title: 'upid', notNull: true},
+      ),
+      new StandardColumn('msg'),
+    ],
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
index 9584442..45c471c 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
@@ -12,17 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
 import {Engine} from '../../trace_processor/engine';
-import {
-  SimpleSliceTrack,
-  SimpleSliceTrackConfig,
-} from '../../frontend/simple_slice_track';
+import {createQuerySliceTrack} from '../../public/lib/tracks/query_slice_track';
 import {CounterOptions} from '../../frontend/base_counter_track';
-import {
-  SimpleCounterTrack,
-  SimpleCounterTrackConfig,
-} from '../../frontend/simple_counter_track';
+import {createQueryCounterTrack} from '../../public/lib/tracks/query_counter_track';
+import {TrackNode} from '../../public/workspace';
 
 interface ContainedTrace {
   uuid: string;
@@ -169,54 +165,6 @@
   )
   select * from final where ts is not null`;
 
-const MODEM_ACTIVITY_INFO = `
-  drop table if exists modem_activity_info;
-  create table modem_activity_info as
-  with modem_raw as (
-    select
-      ts,
-      EXTRACT_ARG(arg_set_id, 'modem_activity_info.timestamp_millis') as timestamp_millis,
-      EXTRACT_ARG(arg_set_id, 'modem_activity_info.sleep_time_millis') as sleep_time_millis,
-      EXTRACT_ARG(arg_set_id, 'modem_activity_info.controller_idle_time_millis') as controller_idle_time_millis,
-      EXTRACT_ARG(arg_set_id, 'modem_activity_info.controller_tx_time_pl0_millis') as controller_tx_time_pl0_millis,
-      EXTRACT_ARG(arg_set_id, 'modem_activity_info.controller_tx_time_pl1_millis') as controller_tx_time_pl1_millis,
-      EXTRACT_ARG(arg_set_id, 'modem_activity_info.controller_tx_time_pl2_millis') as controller_tx_time_pl2_millis,
-      EXTRACT_ARG(arg_set_id, 'modem_activity_info.controller_tx_time_pl3_millis') as controller_tx_time_pl3_millis,
-      EXTRACT_ARG(arg_set_id, 'modem_activity_info.controller_tx_time_pl4_millis') as controller_tx_time_pl4_millis,
-      EXTRACT_ARG(arg_set_id, 'modem_activity_info.controller_rx_time_millis') as controller_rx_time_millis
-    from track t join slice s on t.id = s.track_id
-    where t.name = 'Statsd Atoms'
-      and s.name = 'modem_activity_info'
-  ),
-  deltas as (
-      select
-          timestamp_millis * 1000000 as ts,
-          lead(timestamp_millis) over (order by ts) - timestamp_millis as dur_millis,
-          lead(sleep_time_millis) over (order by ts) - sleep_time_millis as sleep_time_millis,
-          lead(controller_idle_time_millis) over (order by ts) - controller_idle_time_millis as controller_idle_time_millis,
-          lead(controller_tx_time_pl0_millis) over (order by ts) - controller_tx_time_pl0_millis as controller_tx_time_pl0_millis,
-          lead(controller_tx_time_pl1_millis) over (order by ts) - controller_tx_time_pl1_millis as controller_tx_time_pl1_millis,
-          lead(controller_tx_time_pl2_millis) over (order by ts) - controller_tx_time_pl2_millis as controller_tx_time_pl2_millis,
-          lead(controller_tx_time_pl3_millis) over (order by ts) - controller_tx_time_pl3_millis as controller_tx_time_pl3_millis,
-          lead(controller_tx_time_pl4_millis) over (order by ts) - controller_tx_time_pl4_millis as controller_tx_time_pl4_millis,
-          lead(controller_rx_time_millis) over (order by ts) - controller_rx_time_millis as controller_rx_time_millis
-      from modem_raw
-  ),
-  ratios as (
-      select
-          ts,
-          100.0 * sleep_time_millis / dur_millis as sleep_time_ratio,
-          100.0 * controller_idle_time_millis / dur_millis as controller_idle_time_ratio,
-          100.0 * controller_tx_time_pl0_millis / dur_millis as controller_tx_time_pl0_ratio,
-          100.0 * controller_tx_time_pl1_millis / dur_millis as controller_tx_time_pl1_ratio,
-          100.0 * controller_tx_time_pl2_millis / dur_millis as controller_tx_time_pl2_ratio,
-          100.0 * controller_tx_time_pl3_millis / dur_millis as controller_tx_time_pl3_ratio,
-          100.0 * controller_tx_time_pl4_millis / dur_millis as controller_tx_time_pl4_ratio,
-          100.0 * controller_rx_time_millis / dur_millis as controller_rx_time_ratio
-      from deltas
-  )
-  select * from ratios where sleep_time_ratio is not null and sleep_time_ratio >= 0`;
-
 const MODEM_RIL_STRENGTH = `
   DROP VIEW IF EXISTS ScreenOn;
   CREATE VIEW ScreenOn AS
@@ -1146,83 +1094,89 @@
   from step2
 `;
 
-class AndroidLongBatteryTracing implements Plugin {
-  addSliceTrack(
-    ctx: PluginContextTrace,
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.AndroidLongBatteryTracing';
+  private readonly groups = new Map<string, TrackNode>();
+
+  private addTrack(ctx: Trace, track: TrackNode, groupName?: string): void {
+    if (groupName) {
+      const existingGroup = this.groups.get(groupName);
+      if (existingGroup) {
+        existingGroup.addChildInOrder(track);
+      } else {
+        const group = new TrackNode({title: groupName, isSummary: true});
+        group.addChildInOrder(track);
+        this.groups.set(groupName, group);
+        ctx.workspace.addChildInOrder(group);
+      }
+    } else {
+      ctx.workspace.addChildInOrder(track);
+    }
+  }
+
+  async addSliceTrack(
+    ctx: Trace,
     name: string,
     query: string,
     groupName?: string,
     columns: string[] = [],
-  ): void {
-    const config: SimpleSliceTrackConfig = {
+  ) {
+    const uri = `/long_battery_tracing_${name}`;
+    const track = await createQuerySliceTrack({
+      trace: ctx,
+      uri,
       data: {
         sqlSource: query,
         columns: ['ts', 'dur', 'name', ...columns],
       },
-      columns: {ts: 'ts', dur: 'dur', name: 'name'},
       argColumns: columns,
-    };
-
-    let uri;
-    if (groupName) {
-      uri = `/${groupName}/long_battery_tracing_${name}`;
-    } else {
-      uri = `/long_battery_tracing_${name}`;
-    }
-    ctx.registerStaticTrack({
+    });
+    ctx.tracks.registerTrack({
       uri,
       title: name,
-      trackFactory: (trackCtx) => {
-        return new SimpleSliceTrack(ctx.engine, trackCtx, config);
-      },
-      groupName,
+      track,
     });
+    const trackNode = new TrackNode({uri, title: name});
+    this.addTrack(ctx, trackNode, groupName);
   }
 
-  addCounterTrack(
-    ctx: PluginContextTrace,
+  async addCounterTrack(
+    ctx: Trace,
     name: string,
     query: string,
     groupName: string,
     options?: Partial<CounterOptions>,
-  ): void {
-    const config: SimpleCounterTrackConfig = {
+  ) {
+    const uri = `/long_battery_tracing_${name}`;
+    const track = await createQueryCounterTrack({
+      trace: ctx,
+      uri,
       data: {
         sqlSource: query,
         columns: ['ts', 'value'],
       },
-      columns: {ts: 'ts', value: 'value'},
       options,
-    };
-
-    let uri;
-    if (groupName) {
-      uri = `/${groupName}/long_battery_tracing_${name}`;
-    } else {
-      uri = `/long_battery_tracing_${name}`;
-    }
-
-    ctx.registerStaticTrack({
+    });
+    ctx.tracks.registerTrack({
       uri,
       title: name,
-      trackFactory: (trackCtx) => {
-        return new SimpleCounterTrack(ctx.engine, trackCtx, config);
-      },
-      groupName,
+      track,
     });
+    const trackNode = new TrackNode({uri, title: name});
+    this.addTrack(ctx, trackNode, groupName);
   }
 
-  addBatteryStatsState(
-    ctx: PluginContextTrace,
+  async addBatteryStatsState(
+    ctx: Trace,
     name: string,
     track: string,
     groupName: string,
     features: Set<string>,
-  ): void {
+  ) {
     if (!features.has(`track.${track}`)) {
       return;
     }
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       name,
       `SELECT ts, safe_dur AS dur, value_name AS name
@@ -1232,18 +1186,18 @@
     );
   }
 
-  addBatteryStatsEvent(
-    ctx: PluginContextTrace,
+  async addBatteryStatsEvent(
+    ctx: Trace,
     name: string,
     track: string,
     groupName: string | undefined,
     features: Set<string>,
-  ): void {
+  ) {
     if (!features.has(`track.${track}`)) {
       return;
     }
 
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       name,
       `SELECT ts, safe_dur AS dur, str_value AS name
@@ -1253,10 +1207,7 @@
     );
   }
 
-  async addDeviceState(
-    ctx: PluginContextTrace,
-    features: Set<string>,
-  ): Promise<void> {
+  async addDeviceState(ctx: Trace, features: Set<string>): Promise<void> {
     if (!features.has('track.battery_stats.*')) {
       return;
     }
@@ -1269,15 +1220,19 @@
     await e.query(`INCLUDE PERFETTO MODULE android.suspend;`);
     await e.query(`INCLUDE PERFETTO MODULE counters.intervals;`);
 
-    this.addSliceTrack(ctx, 'Device State: Screen state', SCREEN_STATE);
-    this.addSliceTrack(ctx, 'Device State: Charging', CHARGING);
-    this.addSliceTrack(ctx, 'Device State: Suspend / resume', SUSPEND_RESUME);
-    this.addSliceTrack(ctx, 'Device State: Doze light state', DOZE_LIGHT);
-    this.addSliceTrack(ctx, 'Device State: Doze deep state', DOZE_DEEP);
+    await this.addSliceTrack(ctx, 'Device State: Screen state', SCREEN_STATE);
+    await this.addSliceTrack(ctx, 'Device State: Charging', CHARGING);
+    await this.addSliceTrack(
+      ctx,
+      'Device State: Suspend / resume',
+      SUSPEND_RESUME,
+    );
+    await this.addSliceTrack(ctx, 'Device State: Doze light state', DOZE_LIGHT);
+    await this.addSliceTrack(ctx, 'Device State: Doze deep state', DOZE_DEEP);
 
     query('Device State: Top app', 'battery_stats.top');
 
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'Device State: Long wakelocks',
       `SELECT
@@ -1298,7 +1253,7 @@
     query('Device State: Jobs', 'battery_stats.job');
 
     if (features.has('atom.thermal_throttling_severity_state_changed')) {
-      this.addSliceTrack(
+      await this.addSliceTrack(
         ctx,
         'Device State: Thermal throttling',
         THERMAL_THROTTLING,
@@ -1306,10 +1261,123 @@
     }
   }
 
-  async addNetworkSummary(
-    ctx: PluginContextTrace,
-    features: Set<string>,
-  ): Promise<void> {
+  async addAtomCounters(ctx: Trace): Promise<void> {
+    const e = ctx.engine;
+
+    try {
+      await e.query(
+        `INCLUDE PERFETTO MODULE
+            google3.wireless.android.telemetry.trace_extractor.modules.atom_counters_slices`,
+      );
+    } catch (e) {
+      return;
+    }
+
+    const counters = await e.query(
+      `select distinct ui_group, ui_name, ui_unit, counter_name
+       from atom_counters
+       where ui_name is not null`,
+    );
+    const countersIt = counters.iter({
+      ui_group: 'str',
+      ui_name: 'str',
+      ui_unit: 'str',
+      counter_name: 'str',
+    });
+    for (; countersIt.valid(); countersIt.next()) {
+      const unit = countersIt.ui_unit;
+      const opts =
+        unit === '%'
+          ? {yOverrideMaximum: 100, unit: '%'}
+          : unit !== undefined
+            ? {unit}
+            : undefined;
+
+      await this.addCounterTrack(
+        ctx,
+        countersIt.ui_name,
+        `select ts, ${unit === '%' ? 100.0 : 1.0} * counter_value as value
+         from atom_counters
+         where counter_name = '${countersIt.counter_name}'`,
+        countersIt.ui_group,
+        opts,
+      );
+    }
+  }
+
+  async addAtomSlices(ctx: Trace): Promise<void> {
+    const e = ctx.engine;
+
+    try {
+      await e.query(
+        `INCLUDE PERFETTO MODULE
+            google3.wireless.android.telemetry.trace_extractor.modules.atom_counters_slices`,
+      );
+    } catch (e) {
+      return;
+    }
+
+    const sliceTracks = await e.query(
+      `select distinct ui_group, ui_name, atom, field
+       from atom_slices
+       where ui_name is not null
+       order by 1, 2, 3, 4`,
+    );
+    const slicesIt = sliceTracks.iter({
+      atom: 'str',
+      ui_group: 'str',
+      ui_name: 'str',
+      field: 'str',
+    });
+
+    const tracks = new Map<
+      string,
+      {
+        ui_group: string;
+        ui_name: string;
+      }
+    >();
+    const fields = new Map<string, string[]>();
+    for (; slicesIt.valid(); slicesIt.next()) {
+      const atom = slicesIt.atom;
+      let args = fields.get(atom);
+      if (args === undefined) {
+        args = [];
+        fields.set(atom, args);
+      }
+      args.push(slicesIt.field);
+      tracks.set(atom, {
+        ui_group: slicesIt.ui_group,
+        ui_name: slicesIt.ui_name,
+      });
+    }
+
+    for (const [atom, args] of fields) {
+      function safeArg(arg: string) {
+        return arg.replaceAll(/[[\]]/g, '').replaceAll(/\./g, '_');
+      }
+
+      // We need to make arg names compatible with SQL here because they pass through several
+      // layers of SQL without being quoted in "".
+      function argSql(arg: string) {
+        return `max(case when field = '${arg}' then ifnull(string_value, int_value) end)
+                as ${safeArg(arg)}`;
+      }
+
+      await this.addSliceTrack(
+        ctx,
+        tracks.get(atom)!.ui_name,
+        `select ts, dur, slice_name as name, ${args.map((a) => argSql(a)).join(', ')}
+         from atom_slices
+         where atom = '${atom}'
+         group by ts, dur, name`,
+        tracks.get(atom)!.ui_group,
+        args.map((a) => safeArg(a)),
+      );
+    }
+  }
+
+  async addNetworkSummary(ctx: Trace, features: Set<string>): Promise<void> {
     if (!features.has('net.modem') && !features.has('net.wifi')) {
       return;
     }
@@ -1322,13 +1390,18 @@
     await e.query(NETWORK_SUMMARY);
     await e.query(RADIO_TRANSPORT_TYPE);
 
-    this.addSliceTrack(ctx, 'Default network', DEFAULT_NETWORK, groupName);
+    await this.addSliceTrack(
+      ctx,
+      'Default network',
+      DEFAULT_NETWORK,
+      groupName,
+    );
 
     if (features.has('atom.network_tethering_reported')) {
-      this.addSliceTrack(ctx, 'Tethering', TETHERING, groupName);
+      await this.addSliceTrack(ctx, 'Tethering', TETHERING, groupName);
     }
     if (features.has('net.wifi')) {
-      this.addCounterTrack(
+      await this.addCounterTrack(
         ctx,
         'Wifi total bytes',
         `select ts, sum(value) as value from network_summary where dev_type = 'wifi' group by 1`,
@@ -1340,7 +1413,7 @@
       );
       const it = result.iter({pkg: 'str'});
       for (; it.valid(); it.next()) {
-        this.addCounterTrack(
+        await this.addCounterTrack(
           ctx,
           `Top wifi: ${it.pkg}`,
           `select ts, value from network_summary where dev_type = 'wifi' and pkg = '${it.pkg}'`,
@@ -1371,7 +1444,7 @@
       features,
     );
     if (features.has('net.modem')) {
-      this.addCounterTrack(
+      await this.addCounterTrack(
         ctx,
         'Modem total bytes',
         `select ts, sum(value) as value from network_summary where dev_type = 'modem' group by 1`,
@@ -1383,7 +1456,7 @@
       );
       const it = result.iter({pkg: 'str'});
       for (; it.valid(); it.next()) {
-        this.addCounterTrack(
+        await this.addCounterTrack(
           ctx,
           `Top modem: ${it.pkg}`,
           `select ts, value from network_summary where dev_type = 'modem' and pkg = '${it.pkg}'`,
@@ -1399,7 +1472,7 @@
       groupName,
       features,
     );
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'Cellular connection',
       `select ts, dur, name from radio_transport`,
@@ -1414,47 +1487,17 @@
     );
   }
 
-  async addModemDetail(
-    ctx: PluginContextTrace,
-    features: Set<string>,
-  ): Promise<void> {
-    if (!features.has('atom.modem_activity_info')) {
-      return;
-    }
+  async addModemDetail(ctx: Trace, features: Set<string>): Promise<void> {
     const groupName = 'Modem Detail';
-    await this.addModemActivityInfo(ctx, groupName);
     if (features.has('track.ril')) {
       await this.addModemRil(ctx, groupName);
     }
+    await this.addModemTeaData(ctx, groupName);
   }
 
-  async addModemActivityInfo(
-    ctx: PluginContextTrace,
-    groupName: string,
-  ): Promise<void> {
-    const query = (name: string, col: string): void =>
-      this.addCounterTrack(
-        ctx,
-        name,
-        `select ts, ${col}_ratio as value from modem_activity_info`,
-        groupName,
-        {yOverrideMaximum: 100, unit: '%'},
-      );
-
-    await ctx.engine.query(MODEM_ACTIVITY_INFO);
-    query('Modem sleep', 'sleep_time');
-    query('Modem controller idle', 'controller_idle_time');
-    query('Modem RX time', 'controller_rx_time');
-    query('Modem TX time power 0', 'controller_tx_time_pl0');
-    query('Modem TX time power 1', 'controller_tx_time_pl1');
-    query('Modem TX time power 2', 'controller_tx_time_pl2');
-    query('Modem TX time power 3', 'controller_tx_time_pl3');
-    query('Modem TX time power 4', 'controller_tx_time_pl4');
-  }
-
-  async addModemRil(ctx: PluginContextTrace, groupName: string): Promise<void> {
-    const rilStrength = (band: string, value: string): void =>
-      this.addSliceTrack(
+  async addModemRil(ctx: Trace, groupName: string): Promise<void> {
+    const rilStrength = async (band: string, value: string) =>
+      await this.addSliceTrack(
         ctx,
         `Modem signal strength ${band} ${value}`,
         `SELECT ts, dur, name FROM RilScreenOn WHERE band_name = '${band}' AND value_name = '${value}'`,
@@ -1462,22 +1505,23 @@
       );
 
     const e = ctx.engine;
+
     await e.query(MODEM_RIL_STRENGTH);
     await e.query(MODEM_RIL_CHANNELS_PREAMBLE);
 
-    rilStrength('LTE', 'rsrp');
-    rilStrength('LTE', 'rssi');
-    rilStrength('NR', 'rsrp');
-    rilStrength('NR', 'rssi');
+    await rilStrength('LTE', 'rsrp');
+    await rilStrength('LTE', 'rssi');
+    await rilStrength('NR', 'rsrp');
+    await rilStrength('NR', 'rssi');
 
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'Modem channel config',
       MODEM_RIL_CHANNELS,
       groupName,
     );
 
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'Modem cell reselection',
       MODEM_CELL_RESELECTION,
@@ -1486,10 +1530,46 @@
     );
   }
 
-  async addKernelWakelocks(
-    ctx: PluginContextTrace,
-    features: Set<string>,
-  ): Promise<void> {
+  async addModemTeaData(ctx: Trace, groupName: string): Promise<void> {
+    const e = ctx.engine;
+
+    try {
+      await e.query(
+        `INCLUDE PERFETTO MODULE
+            google3.wireless.android.telemetry.trace_extractor.modules.modem_tea_metrics`,
+      );
+    } catch {
+      return;
+    }
+
+    const counters = await e.query(
+      `select distinct name from pixel_modem_counters`,
+    );
+    const countersIt = counters.iter({name: 'str'});
+    for (; countersIt.valid(); countersIt.next()) {
+      await this.addCounterTrack(
+        ctx,
+        countersIt.name,
+        `select ts, value from pixel_modem_counters where name = '${countersIt.name}'`,
+        groupName,
+      );
+    }
+    const slices = await e.query(
+      `select distinct track_name from pixel_modem_slices`,
+    );
+    const slicesIt = slices.iter({track_name: 'str'});
+    for (; slicesIt.valid(); slicesIt.next()) {
+      await this.addSliceTrack(
+        ctx,
+        slicesIt.track_name,
+        `select ts, dur, slice_name as name from pixel_modem_slices
+            where track_name = '${slicesIt.track_name}'`,
+        groupName,
+      );
+    }
+  }
+
+  async addKernelWakelocks(ctx: Trace, features: Set<string>): Promise<void> {
     if (!features.has('atom.kernel_wakelock')) {
       return;
     }
@@ -1501,7 +1581,7 @@
     const result = await e.query(KERNEL_WAKELOCKS_SUMMARY);
     const it = result.iter({wakelock_name: 'str'});
     for (; it.valid(); it.next()) {
-      this.addCounterTrack(
+      await this.addCounterTrack(
         ctx,
         it.wakelock_name,
         `select ts, dur, value from kernel_wakelocks where wakelock_name = "${it.wakelock_name}"`,
@@ -1511,10 +1591,7 @@
     }
   }
 
-  async addWakeups(
-    ctx: PluginContextTrace,
-    features: Set<string>,
-  ): Promise<void> {
+  async addWakeups(ctx: Trace, features: Set<string>): Promise<void> {
     if (!features.has('track.suspend_backoff')) {
       return;
     }
@@ -1552,7 +1629,7 @@
     let labelOther = false;
     for (; it.valid(); it.next()) {
       labelOther = true;
-      this.addSliceTrack(
+      await this.addSliceTrack(
         ctx,
         `Wakeup ${it.item}`,
         `${sqlPrefix} where item="${it.item}"`,
@@ -1561,7 +1638,7 @@
       );
       items.push(it.item);
     }
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       labelOther ? 'Other wakeups' : 'Wakeups',
       `${sqlPrefix} where item not in ('${items.join("','")}')`,
@@ -1570,10 +1647,7 @@
     );
   }
 
-  async addHighCpu(
-    ctx: PluginContextTrace,
-    features: Set<string>,
-  ): Promise<void> {
+  async addHighCpu(ctx: Trace, features: Set<string>): Promise<void> {
     if (!features.has('atom.cpu_cycles_per_uid_cluster')) {
       return;
     }
@@ -1587,7 +1661,7 @@
     );
     const it = result.iter({pkg: 'str', cluster: 'str'});
     for (; it.valid(); it.next()) {
-      this.addCounterTrack(
+      await this.addCounterTrack(
         ctx,
         `CPU (${it.cluster}): ${it.pkg}`,
         `select ts, value from high_cpu where pkg = "${it.pkg}" and cluster="${it.cluster}"`,
@@ -1597,10 +1671,7 @@
     }
   }
 
-  async addBluetooth(
-    ctx: PluginContextTrace,
-    features: Set<string>,
-  ): Promise<void> {
+  async addBluetooth(ctx: Trace, features: Set<string>): Promise<void> {
     if (
       !Array.from(features.values()).some(
         (f) => f.startsWith('atom.bluetooth_') || f.startsWith('atom.ble_'),
@@ -1609,140 +1680,140 @@
       return;
     }
     const groupName = 'Bluetooth';
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'BLE Scans (opportunistic)',
       bleScanQuery('opportunistic'),
       groupName,
     );
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'BLE Scans (filtered)',
       bleScanQuery('filtered'),
       groupName,
     );
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'BLE Scans (unfiltered)',
       bleScanQuery('not filtered'),
       groupName,
     );
-    this.addSliceTrack(ctx, 'BLE Scan Results', BLE_RESULTS, groupName);
-    this.addSliceTrack(ctx, 'Connections (ACL)', BT_CONNS_ACL, groupName);
-    this.addSliceTrack(ctx, 'Connections (SCO)', BT_CONNS_SCO, groupName);
-    this.addSliceTrack(
+    await this.addSliceTrack(ctx, 'BLE Scan Results', BLE_RESULTS, groupName);
+    await this.addSliceTrack(ctx, 'Connections (ACL)', BT_CONNS_ACL, groupName);
+    await this.addSliceTrack(ctx, 'Connections (SCO)', BT_CONNS_SCO, groupName);
+    await this.addSliceTrack(
       ctx,
       'Link-level Events',
       BT_LINK_LEVEL_EVENTS,
       groupName,
       BT_LINK_LEVEL_EVENTS_COLUMNS,
     );
-    this.addSliceTrack(ctx, 'A2DP Audio', BT_A2DP_AUDIO, groupName);
-    this.addSliceTrack(
+    await this.addSliceTrack(ctx, 'A2DP Audio', BT_A2DP_AUDIO, groupName);
+    await this.addSliceTrack(
       ctx,
       'Bytes Transferred (L2CAP/RFCOMM)',
       BT_BYTES,
       groupName,
     );
     await ctx.engine.query(BT_ACTIVITY);
-    this.addCounterTrack(
+    await this.addCounterTrack(
       ctx,
       'ACL Classic Active Count',
       'select ts, dur, acl_active_count as value from bt_activity',
       groupName,
     );
-    this.addCounterTrack(
+    await this.addCounterTrack(
       ctx,
       'ACL Classic Sniff Count',
       'select ts, dur, acl_sniff_count as value from bt_activity',
       groupName,
     );
-    this.addCounterTrack(
+    await this.addCounterTrack(
       ctx,
       'ACL BLE Count',
       'select ts, dur, acl_ble_count as value from bt_activity',
       groupName,
     );
-    this.addCounterTrack(
+    await this.addCounterTrack(
       ctx,
       'Advertising Instance Count',
       'select ts, dur, advertising_count as value from bt_activity',
       groupName,
     );
-    this.addCounterTrack(
+    await this.addCounterTrack(
       ctx,
       'LE Scan Duty Cycle Maximum',
       'select ts, dur, le_scan_duty_cycle as value from bt_activity',
       groupName,
       {unit: '%'},
     );
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'Inquiry Active',
       "select ts, dur, 'Active' as name from bt_activity where inquiry_active",
       groupName,
     );
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'SCO Active',
       "select ts, dur, 'Active' as name from bt_activity where sco_active",
       groupName,
     );
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'A2DP Active',
       "select ts, dur, 'Active' as name from bt_activity where a2dp_active",
       groupName,
     );
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'LE Audio Active',
       "select ts, dur, 'Active' as name from bt_activity where le_audio_active",
       groupName,
     );
-    this.addCounterTrack(
+    await this.addCounterTrack(
       ctx,
       'Controller Idle Time',
       'select ts, dur, controller_idle_pct as value from bt_activity',
       groupName,
       {yRangeSharingKey: 'bt_controller_time', unit: '%'},
     );
-    this.addCounterTrack(
+    await this.addCounterTrack(
       ctx,
       'Controller TX Time',
       'select ts, dur, controller_tx_pct as value from bt_activity',
       groupName,
       {yRangeSharingKey: 'bt_controller_time', unit: '%'},
     );
-    this.addCounterTrack(
+    await this.addCounterTrack(
       ctx,
       'Controller RX Time',
       'select ts, dur, controller_rx_pct as value from bt_activity',
       groupName,
       {yRangeSharingKey: 'bt_controller_time', unit: '%'},
     );
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'Quality reports',
       BT_QUALITY_REPORTS,
       groupName,
       BT_QUALITY_REPORTS_COLUMNS,
     );
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'RSSI Reports',
       BT_RSSI_REPORTS,
       groupName,
       BT_RSSI_REPORTS_COLUMNS,
     );
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'HAL Crashes',
       BT_HAL_CRASHES,
       groupName,
       BT_HAL_CRASHES_COLUMNS,
     );
-    this.addSliceTrack(
+    await this.addSliceTrack(
       ctx,
       'Code Path Counter',
       BT_CODE_PATH_COUNTER,
@@ -1752,7 +1823,7 @@
   }
 
   async addContainedTraces(
-    ctx: PluginContextTrace,
+    ctx: Trace,
     containedTraces: ContainedTrace[],
   ): Promise<void> {
     const bySubscription = new Map<string, ContainedTrace[]>();
@@ -1763,8 +1834,8 @@
       bySubscription.get(trace.subscription)!.push(trace);
     }
 
-    bySubscription.forEach((traces, subscription) =>
-      this.addSliceTrack(
+    for (const [subscription, traces] of bySubscription) {
+      await this.addSliceTrack(
         ctx,
         subscription,
         traces
@@ -1779,8 +1850,8 @@
           .join(' UNION ALL '),
         'Other traces',
         ['link'],
-      ),
-    );
+      );
+    }
   }
 
   async findFeatures(e: Engine): Promise<Set<string>> {
@@ -1819,7 +1890,7 @@
     return features;
   }
 
-  async addTracks(ctx: PluginContextTrace): Promise<void> {
+  async addTracks(ctx: Trace): Promise<void> {
     const features: Set<string> = await this.findFeatures(ctx.engine);
 
     const containedTraces = (ctx.openerPluginArgs?.containedTraces ??
@@ -1827,21 +1898,18 @@
 
     await ctx.engine.query(PACKAGE_LOOKUP);
     await this.addNetworkSummary(ctx, features);
+    await this.addBluetooth(ctx, features);
+    await this.addAtomCounters(ctx);
+    await this.addAtomSlices(ctx);
     await this.addModemDetail(ctx, features);
     await this.addKernelWakelocks(ctx, features);
     await this.addWakeups(ctx, features);
     await this.addDeviceState(ctx, features);
     await this.addHighCpu(ctx, features);
-    await this.addBluetooth(ctx, features);
     await this.addContainedTraces(ctx, containedTraces);
   }
 
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+  async onTraceLoad(ctx: Trace): Promise<void> {
     await this.addTracks(ctx);
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.AndroidLongBatteryTracing',
-  plugin: AndroidLongBatteryTracing,
-};
diff --git a/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts b/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
index a2f24c2..56e57fb 100644
--- a/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
@@ -12,33 +12,35 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {addDebugSliceTrack} from '../../public';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {addDebugSliceTrack} from '../../public/debug_tracks';
 
-class AndroidNetwork implements Plugin {
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.AndroidNetwork';
   // Adds a debug track using the provided query and given columns. The columns
   // must be start with ts, dur, and a name column. The name column and all
   // following columns are shown as arguments in slice details.
   async addSimpleTrack(
-    ctx: PluginContextTrace,
+    ctx: Trace,
     trackName: string,
     tableOrQuery: string,
     columns: string[],
   ): Promise<void> {
-    await addDebugSliceTrack(
-      ctx,
-      {
+    await addDebugSliceTrack({
+      trace: ctx,
+      data: {
         sqlSource: `SELECT ${columns.join(',')} FROM ${tableOrQuery}`,
         columns: columns,
       },
-      trackName,
-      {ts: columns[0], dur: columns[1], name: columns[2]},
-      columns.slice(2),
-    );
+      title: trackName,
+      columns: {ts: columns[0], dur: columns[1], name: columns[2]},
+      argColumns: columns.slice(2),
+    });
   }
 
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerCommand({
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidNetwork#batteryEvents',
       name: 'Add track: battery events',
       callback: async (track) => {
@@ -59,7 +61,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidNetwork#activityTrack',
       name: 'Add track: network activity',
       callback: async (groupby, filter, trackName) => {
@@ -97,8 +99,3 @@
     });
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.AndroidNetwork',
-  plugin: AndroidNetwork,
-};
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
index 215d39e..ab56328 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
@@ -12,16 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  addDebugSliceTrack,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {addDebugSliceTrack} from '../../public/debug_tracks';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {getTimeSpanOfSelectionOrVisibleWindow} from '../../public/utils';
+import {addQueryResultsTab} from '../../public/lib/query_table/query_result_tab';
 
-class AndroidPerf implements Plugin {
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.AndroidPerf';
   async addAppProcessStartsDebugTrack(
-    ctx: PluginContextTrace,
+    ctx: Trace,
     reason: string,
     sliceName: string,
   ): Promise<void> {
@@ -34,9 +34,9 @@
       'intent',
       'table_name',
     ];
-    await addDebugSliceTrack(
-      ctx,
-      {
+    await addDebugSliceTrack({
+      trace: ctx,
+      data: {
         sqlSource: `
                     SELECT
                       start_id AS id,
@@ -46,63 +46,62 @@
                       process_name,
                       intent,
                       'slice' AS table_name
-                    FROM _android_app_process_starts
+                    FROM android_app_process_starts
                     WHERE reason = '${reason}'
                  `,
         columns: sliceColumns,
       },
-      'app_' + sliceName + '_start reason: ' + reason,
-      {ts: 'ts', dur: 'dur', name: sliceName},
-      sliceColumns,
-    );
+      title: 'app_' + sliceName + '_start reason: ' + reason,
+      argColumns: sliceColumns,
+    });
   }
 
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerCommand({
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#BinderSystemServerIncoming',
       name: 'Run query: system_server incoming binder graph',
       callback: () =>
-        ctx.tabs.openQuery(
-          `INCLUDE PERFETTO MODULE android.binder;
+        addQueryResultsTab(ctx, {
+          query: `INCLUDE PERFETTO MODULE android.binder;
            SELECT * FROM android_binder_incoming_graph((SELECT upid FROM process WHERE name = 'system_server'))`,
-          'system_server incoming binder graph',
-        ),
+          title: 'system_server incoming binder graph',
+        }),
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#BinderSystemServerOutgoing',
       name: 'Run query: system_server outgoing binder graph',
       callback: () =>
-        ctx.tabs.openQuery(
-          `INCLUDE PERFETTO MODULE android.binder;
+        addQueryResultsTab(ctx, {
+          query: `INCLUDE PERFETTO MODULE android.binder;
            SELECT * FROM android_binder_outgoing_graph((SELECT upid FROM process WHERE name = 'system_server'))`,
-          'system_server outgoing binder graph',
-        ),
+          title: 'system_server outgoing binder graph',
+        }),
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#MonitorContentionSystemServer',
       name: 'Run query: system_server monitor_contention graph',
       callback: () =>
-        ctx.tabs.openQuery(
-          `INCLUDE PERFETTO MODULE android.monitor_contention;
+        addQueryResultsTab(ctx, {
+          query: `INCLUDE PERFETTO MODULE android.monitor_contention;
            SELECT * FROM android_monitor_contention_graph((SELECT upid FROM process WHERE name = 'system_server'))`,
-          'system_server monitor_contention graph',
-        ),
+          title: 'system_server monitor_contention graph',
+        }),
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#BinderAll',
       name: 'Run query: all process binder graph',
       callback: () =>
-        ctx.tabs.openQuery(
-          `INCLUDE PERFETTO MODULE android.binder;
+        addQueryResultsTab(ctx, {
+          query: `INCLUDE PERFETTO MODULE android.binder;
            SELECT * FROM android_binder_graph(-1000, 1000, -1000, 1000)`,
-          'all process binder graph',
-        ),
+          title: 'all process binder graph',
+        }),
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#ThreadClusterDistribution',
       name: 'Run query: runtime cluster distribution for a thread',
       callback: async (tid) => {
@@ -110,8 +109,8 @@
           tid = prompt('Enter a thread tid', '');
           if (tid === null) return;
         }
-        ctx.tabs.openQuery(
-          `
+        addQueryResultsTab(ctx, {
+          query: `
           INCLUDE PERFETTO MODULE android.cpu.cluster_type;
           WITH
             total_runtime AS (
@@ -122,8 +121,7 @@
               WHERE t.tid = ${tid}
             )
             SELECT
-              c.cluster_type AS cluster,
-              sum(dur)/1e6 AS total_dur_ms,
+              c.cluster_type AS cluster, sum(dur)/1e6 AS total_dur_ms,
               sum(dur) * 1.0 / (SELECT * FROM total_runtime) AS percentage
             FROM sched s
             LEFT JOIN thread t
@@ -132,12 +130,12 @@
               USING (cpu)
             WHERE t.tid = ${tid}
             GROUP BY 1`,
-          `runtime cluster distrubtion for tid ${tid}`,
-        );
+          title: `runtime cluster distrubtion for tid ${tid}`,
+        });
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#SchedLatency',
       name: 'Run query: top 50 sched latency for a thread',
       callback: async (tid) => {
@@ -145,8 +143,8 @@
           tid = prompt('Enter a thread tid', '');
           if (tid === null) return;
         }
-        ctx.tabs.openQuery(
-          `
+        addQueryResultsTab(ctx, {
+          query: `
           SELECT ts.*, t.tid, t.name, tt.id AS track_id
           FROM thread_state ts
           LEFT JOIN thread_track tt
@@ -156,12 +154,40 @@
           WHERE ts.state IN ('R', 'R+') AND tid = ${tid}
            ORDER BY dur DESC
           LIMIT 50`,
-          `top 50 sched latency slice for tid ${tid}`,
-        );
+          title: `top 50 sched latency slice for tid ${tid}`,
+        });
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
+      id: 'dev.perfetto.AndroidPerf#SchedLatencyInSelectedWindow',
+      name: 'Top 50 sched latency in selected time window',
+      callback: async () => {
+        const window = await getTimeSpanOfSelectionOrVisibleWindow(ctx);
+        addQueryResultsTab(ctx, {
+          title: 'top 50 sched latency slice in selcted time window',
+          query: `SELECT
+            ts.*,
+            t.tid,
+            t.name AS thread_name,
+            tt.id AS track_id,
+            p.name AS process_name
+          FROM thread_state ts
+          LEFT JOIN thread_track tt
+           USING (utid)
+          LEFT JOIN thread t
+           USING (utid)
+          LEFT JOIN process p
+           USING (upid)
+          WHERE ts.state IN ('R', 'R+')
+           AND ts.ts >= ${window.start} and ts.ts < ${window.end}
+          ORDER BY dur DESC
+          LIMIT 50`,
+        });
+      },
+    });
+
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#AppProcessStarts',
       name: 'Add tracks: app process starts',
       callback: async () => {
@@ -176,7 +202,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#AppIntentStarts',
       name: 'Add tracks: app intent starts',
       callback: async () => {
@@ -192,8 +218,3 @@
     });
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.AndroidPerf',
-  plugin: AndroidPerf,
-};
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
index 4270a76..c78abf8 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
@@ -12,8 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {addDebugSliceTrack} from '../../public';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {addDebugSliceTrack} from '../../public/debug_tracks';
+import {addQueryResultsTab} from '../../public/lib/query_table/query_result_tab';
 
 const PERF_TRACE_COUNTERS_PRECONDITION = `
   SELECT
@@ -24,11 +26,12 @@
     AND str_value GLOB '*ftrace_events: "perf_trace_counters/sched_switch_with_ctrs"*'
 `;
 
-class AndroidPerfTraceCounters implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.AndroidPerfTraceCounters';
+  async onTraceLoad(ctx: Trace): Promise<void> {
     const resp = await ctx.engine.query(PERF_TRACE_COUNTERS_PRECONDITION);
     if (resp.numRows() === 0) return;
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerfTraceCounters#ThreadRuntimeIPC',
       name: 'Add a track to show a thread runtime ipc',
       callback: async (tid) => {
@@ -82,20 +85,26 @@
             )
         `;
 
-        await addDebugSliceTrack(
-          ctx,
-          {
+        await addDebugSliceTrack({
+          trace: ctx,
+          data: {
             sqlSource:
               sqlPrefix +
               `
               SELECT * FROM target_thread_ipc_slice WHERE ts IS NOT NULL`,
           },
-          'Rutime IPC:' + tid,
-          {ts: 'ts', dur: 'dur', name: 'ipc'},
-          ['instruction', 'cycle', 'stall_backend_mem', 'l3_cache_miss'],
-        );
-        ctx.tabs.openQuery(
-          sqlPrefix +
+          title: 'Rutime IPC:' + tid,
+          columns: {ts: 'ts', dur: 'dur', name: 'ipc'},
+          argColumns: [
+            'instruction',
+            'cycle',
+            'stall_backend_mem',
+            'l3_cache_miss',
+          ],
+        });
+        addQueryResultsTab(ctx, {
+          query:
+            sqlPrefix +
             `
             SELECT
               (sum(instruction) * 1.0 / sum(cycle)*1.0) AS avg_ipc,
@@ -105,14 +114,9 @@
               sum(stall_backend_mem) as total_stall_backend_mem,
               sum(l3_cache_miss) as total_l3_cache_miss
             FROM target_thread_ipc_slice WHERE ts IS NOT NULL`,
-          'target thread ipc statistic',
-        );
+          title: 'target thread ipc statistic',
+        });
       },
     });
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.AndroidPerfTraceCounters',
-  plugin: AndroidPerfTraceCounters,
-};
diff --git a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
index 1a2eb0b..88cf8b6 100644
--- a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
@@ -12,44 +12,74 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {LONG, Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {
-  SimpleSliceTrack,
-  SimpleSliceTrackConfig,
-} from '../../frontend/simple_slice_track';
-
-class AndroidStartup implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+import {LONG} from '../../trace_processor/query_result';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {createQuerySliceTrack} from '../../public/lib/tracks/query_slice_track';
+import {TrackNode} from '../../public/workspace';
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.AndroidStartup';
+  async onTraceLoad(ctx: Trace): Promise<void> {
     const e = ctx.engine;
-    await e.query(`include perfetto module android.startup.startups;`);
+    await e.query(`
+          include perfetto module android.startup.startups;
+          include perfetto module android.startup.startup_breakdowns;
+         `);
 
     const cnt = await e.query('select count() cnt from android_startups');
     if (cnt.firstRow({cnt: LONG}).cnt === 0n) {
       return;
     }
 
-    const config: SimpleSliceTrackConfig = {
-      data: {
-        sqlSource: `
+    const trackSource = `
           SELECT l.ts AS ts, l.dur AS dur, l.package AS name
           FROM android_startups l
-        `,
+    `;
+    const trackBreakdownSource = `
+        SELECT
+          ts,
+          dur,
+          reason AS name
+          FROM android_startup_opinionated_breakdown
+    `;
+
+    const trackNode = await this.loadStartupTrack(
+      ctx,
+      trackSource,
+      `/android_startups`,
+      'Android App Startups',
+    );
+    const trackBreakdownNode = await this.loadStartupTrack(
+      ctx,
+      trackBreakdownSource,
+      `/android_startups_breakdown`,
+      'Android App Startups Breakdown',
+    );
+
+    ctx.workspace.addChildInOrder(trackNode);
+    trackNode.addChildLast(trackBreakdownNode);
+  }
+
+  private async loadStartupTrack(
+    ctx: Trace,
+    sqlSource: string,
+    uri: string,
+    title: string,
+  ): Promise<TrackNode> {
+    const track = await createQuerySliceTrack({
+      trace: ctx,
+      uri,
+      data: {
+        sqlSource,
         columns: ['ts', 'dur', 'name'],
       },
-      columns: {ts: 'ts', dur: 'dur', name: 'name'},
-      argColumns: [],
-    };
-    ctx.registerStaticTrack({
-      uri: `/android_startups`,
-      title: 'Android App Startups',
-      trackFactory: (trackCtx) => {
-        return new SimpleSliceTrack(ctx.engine, trackCtx, config);
-      },
     });
+    ctx.tracks.registerTrack({
+      uri,
+      title,
+      track,
+    });
+    // Needs a sort order lower than 'Ftrace Events' so that it is prioritized in the UI.
+    return new TrackNode({title, uri, sortOrder: -6});
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.AndroidStartup',
-  plugin: AndroidStartup,
-};
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts b/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts
new file mode 100644
index 0000000..1bb31a5
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts
@@ -0,0 +1,107 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {BigintMath as BIMath} from '../../base/bigint_math';
+import {clamp} from '../../base/math_utils';
+import {NAMED_ROW, NamedSliceTrack} from '../../frontend/named_slice_track';
+import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../frontend/slice_layout';
+import {NewTrackArgs} from '../../frontend/track';
+import {TrackEventDetails} from '../../public/selection';
+import {Slice} from '../../public/track';
+import {LONG_NULL} from '../../trace_processor/query_result';
+
+export const THREAD_SLICE_ROW = {
+  // Base columns (tsq, ts, dur, id, depth).
+  ...NAMED_ROW,
+
+  // Thread-specific columns.
+  threadDur: LONG_NULL,
+};
+export type ThreadSliceRow = typeof THREAD_SLICE_ROW;
+
+export class AsyncSliceTrack extends NamedSliceTrack<Slice, ThreadSliceRow> {
+  constructor(
+    args: NewTrackArgs,
+    maxDepth: number,
+    private readonly trackIds: number[],
+  ) {
+    super(args);
+    this.sliceLayout = {
+      ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
+      depthGuess: maxDepth,
+    };
+  }
+
+  getRowSpec(): ThreadSliceRow {
+    return THREAD_SLICE_ROW;
+  }
+
+  rowToSlice(row: ThreadSliceRow): Slice {
+    const namedSlice = this.rowToSliceBase(row);
+
+    if (row.dur > 0n && row.threadDur !== null) {
+      const fillRatio = clamp(BIMath.ratio(row.threadDur, row.dur), 0, 1);
+      return {...namedSlice, fillRatio};
+    } else {
+      return namedSlice;
+    }
+  }
+
+  getSqlSource(): string {
+    // If we only have one track ID we can avoid the overhead of
+    // experimental_slice_layout, and just go straight to the slice table.
+    if (this.trackIds.length === 1) {
+      return `
+        select
+          ts,
+          dur,
+          id,
+          depth,
+          ifnull(name, '[null]') as name,
+          thread_dur as threadDur
+        from slice
+        where track_id = ${this.trackIds[0]}
+      `;
+    } else {
+      return `
+        select
+          id,
+          ts,
+          dur,
+          layout_depth as depth,
+          ifnull(name, '[null]') as name,
+          thread_dur as threadDur
+        from experimental_slice_layout
+        where filter_track_ids = '${this.trackIds.join(',')}'
+      `;
+    }
+  }
+
+  onUpdatedSlices(slices: Slice[]) {
+    for (const slice of slices) {
+      slice.isHighlighted = slice === this.hoveredSlice;
+    }
+  }
+
+  async getSelectionDetails(
+    id: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const baseDetails = await super.getSelectionDetails(id);
+    if (!baseDetails) return undefined;
+    return {
+      ...baseDetails,
+      tableName: 'slice',
+    };
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts b/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts
new file mode 100644
index 0000000..9b0c6e7
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts
@@ -0,0 +1,407 @@
+// 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 {
+  getOrCreateGroupForProcess,
+  getOrCreateGroupForThread,
+} from '../../public/standard_groups';
+import {exists} from '../../base/utils';
+import {assertExists, assertTrue} from '../../base/logging';
+import {SliceSelectionAggregator} from './slice_selection_aggregator';
+import {sqlTableRegistry} from '../../frontend/widgets/sql/table/sql_table_registry';
+import {getSliceTable} from './table';
+import {extensions} from '../../public/lib/extensions';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.AsyncSlices';
+  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.registerAreaSelectionAggreagtor(
+      new SliceSelectionAggregator(),
+    );
+
+    sqlTableRegistry['slice'] = getSliceTable();
+
+    ctx.commands.registerCommand({
+      id: 'perfetto.ShowTable.slice',
+      name: 'Open table: slice',
+      callback: () => {
+        extensions.addSqlTableTab(ctx, {
+          table: getSliceTable(),
+        });
+      },
+    });
+  }
+
+  async addGlobalAsyncTracks(
+    ctx: Trace,
+    trackIdsToUris: Map<number, string>,
+  ): Promise<void> {
+    const {engine} = ctx;
+    // TODO(stevegolton): The track exclusion logic is currently a hack. This will be replaced
+    // by a mechanism for more specific plugins to override tracks from more generic plugins.
+    const suspendResumeLatencyTrackName = 'Suspend/Resume Latency';
+    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(id) as trackIds,
+          count() as trackCount,
+          min(a.order_id) as order_id
+        from track t
+        join _slice_track_summary using (id)
+        left join _track_event_tracks_ordered a USING (id)
+        where
+          t.type in ('__intrinsic_track', 'gpu_track', '__intrinsic_cpu_track')
+          and (name != '${suspendResumeLatencyTrackName}' or name is null)
+          and classification not in (
+            'linux_rpm',
+            'linux_device_frequency',
+            'irq_counter',
+            'softirq_counter',
+            'android_energy_estimation_breakdown',
+            'android_energy_estimation_breakdown_per_uid'
+          )
+        group by parent_id, name
+        order by parent_id, order_id
+      ),
+      intermediate_groups as (
+        select
+          t.name,
+          t.id,
+          t.parent_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
+      )
+      select
+        t.name as name,
+        t.parent_id as parentId,
+        t.trackIds as trackIds,
+        __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
+      from global_tracks_grouped t
+      union all
+      select
+        t.name as name,
+        t.parent_id as parentId,
+        cast_string!(t.id) as trackIds,
+        NULL as maxDepth
+      from intermediate_groups t
+      left join _slice_track_summary s using (id)
+      where s.id is null
+    `);
+    const it = rawGlobalAsyncTracks.iter({
+      name: STR_NULL,
+      parentId: NUM_NULL,
+      trackIds: STR,
+      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({trace: ctx, uri}, maxDepth, trackIds),
+        });
+        const trackNode = new TrackNode({
+          uri,
+          title,
+          sortOrder: it.parentId === undefined ? -25 : 0,
+        });
+        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 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({trace: 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 = getOrCreateGroupForProcess(ctx.workspace, 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({trace: 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 = getOrCreateGroupForThread(ctx.workspace, t.utid);
+        group.addChildInOrder(t.trackNode);
+      }
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts
new file mode 100644
index 0000000..55f7c95
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts
@@ -0,0 +1,96 @@
+// 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.
+
+import {ColumnDef, Sorting} from '../../public/aggregation';
+import {AreaSelection} from '../../public/selection';
+import {Engine} from '../../trace_processor/engine';
+import {AreaSelectionAggregator} from '../../public/selection';
+import {SLICE_TRACK_KIND} from '../../public/track_kinds';
+
+export class SliceSelectionAggregator implements AreaSelectionAggregator {
+  readonly id = 'slice_aggregation';
+
+  async createAggregateView(engine: Engine, area: AreaSelection) {
+    const selectedTrackKeys = getSelectedTrackSqlIds(area);
+
+    if (selectedTrackKeys.length === 0) return false;
+
+    await engine.query(`
+      create or replace perfetto table ${this.id} as
+      select
+        name,
+        sum(dur) AS total_dur,
+        sum(dur)/count() as avg_dur,
+        count() as occurrences
+        from slices
+      where track_id in (${selectedTrackKeys})
+        and ts + dur > ${area.start}
+        and ts < ${area.end}
+      group by name
+    `);
+    return true;
+  }
+
+  getTabName() {
+    return 'Slices';
+  }
+
+  async getExtra() {}
+
+  getDefaultSorting(): Sorting {
+    return {column: 'total_dur', direction: 'DESC'};
+  }
+
+  getColumnDefinitions(): ColumnDef[] {
+    return [
+      {
+        title: 'Name',
+        kind: 'STRING',
+        columnConstructor: Uint32Array,
+        columnId: 'name',
+      },
+      {
+        title: 'Wall duration (ms)',
+        kind: 'TIMESTAMP_NS',
+        columnConstructor: Float64Array,
+        columnId: 'total_dur',
+        sum: true,
+      },
+      {
+        title: 'Avg Wall duration (ms)',
+        kind: 'TIMESTAMP_NS',
+        columnConstructor: Float64Array,
+        columnId: 'avg_dur',
+      },
+      {
+        title: 'Occurrences',
+        kind: 'NUMBER',
+        columnConstructor: Uint32Array,
+        columnId: 'occurrences',
+        sum: true,
+      },
+    ];
+  }
+}
+
+function getSelectedTrackSqlIds(area: AreaSelection): number[] {
+  const selectedTrackKeys: number[] = [];
+  for (const trackInfo of area.tracks) {
+    if (trackInfo?.tags?.kind === SLICE_TRACK_KIND) {
+      trackInfo.tags.trackIds &&
+        selectedTrackKeys.push(...trackInfo.tags.trackIds);
+    }
+  }
+  return selectedTrackKeys;
+}
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/table.ts b/ui/src/plugins/dev.perfetto.AsyncSlices/table.ts
new file mode 100644
index 0000000..cee67c3
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AsyncSlices/table.ts
@@ -0,0 +1,52 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {SqlTableDescription} from '../../frontend/widgets/sql/table/table_description';
+import {
+  ArgSetColumnSet,
+  DurationColumn,
+  ProcessColumnSet,
+  SliceIdColumn,
+  StandardColumn,
+  ThreadColumnSet,
+  TimestampColumn,
+} from '../../frontend/widgets/sql/table/well_known_columns';
+
+export function getSliceTable(): SqlTableDescription {
+  return {
+    imports: ['slices.slices'],
+    name: '_slice_with_thread_and_process_info',
+    displayName: 'slice',
+    columns: [
+      new SliceIdColumn('id', {notNull: true}),
+      new TimestampColumn('ts', {title: 'Timestamp'}),
+      new DurationColumn('dur', {title: 'Duration'}),
+      new DurationColumn('thread_dur', {title: 'Thread duration'}),
+      new StandardColumn('category', {title: 'Category'}),
+      new StandardColumn('name', {title: 'Name'}),
+      new StandardColumn('track_id', {
+        title: 'Track ID',
+        aggregationType: 'nominal',
+        startsHidden: true,
+      }),
+      new ThreadColumnSet('utid', {title: 'utid'}),
+      new ProcessColumnSet('upid', {title: 'upid'}),
+      new StandardColumn('depth', {title: 'Depth', startsHidden: true}),
+      new SliceIdColumn('parent_id', {
+        startsHidden: true,
+      }),
+      new ArgSetColumnSet('arg_set_id'),
+    ],
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.BookmarkletApi/index.ts b/ui/src/plugins/dev.perfetto.BookmarkletApi/index.ts
index 767e29d..7b1769e 100644
--- a/ui/src/plugins/dev.perfetto.BookmarkletApi/index.ts
+++ b/ui/src/plugins/dev.perfetto.BookmarkletApi/index.ts
@@ -12,41 +12,29 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {Trace} from '../../public/trace';
+import {App} from '../../public/app';
+import {PerfettoPlugin} from '../../public/plugin';
 
 declare global {
   interface Window {
-    ctx: PluginContext | PluginContextTrace | undefined;
+    ctx: App | Trace | undefined;
   }
 }
 
-class BookmarkletApi implements Plugin {
-  private pluginCtx?: PluginContext;
+export default class Plugin implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.BookmarkletApi';
+  static bookmarkletPluginCtx: App;
 
-  onActivate(pluginCtx: PluginContext): void {
-    this.pluginCtx = pluginCtx;
+  static onActivate(pluginCtx: App): void {
+    this.bookmarkletPluginCtx = pluginCtx;
     window.ctx = pluginCtx;
   }
 
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    window.ctx = ctx;
-  }
-
-  async onTraceUnload(_: PluginContextTrace): Promise<void> {
-    window.ctx = this.pluginCtx;
-  }
-
-  onDeactivate(_: PluginContext): void {
-    window.ctx = undefined;
+  async onTraceLoad(trace: Trace): Promise<void> {
+    window.ctx = trace;
+    trace.trash.defer(() => {
+      window.ctx = Plugin.bookmarkletPluginCtx;
+    });
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.BookmarkletApi',
-  plugin: BookmarkletApi,
-};
diff --git a/ui/src/plugins/dev.perfetto.Chaos/index.ts b/ui/src/plugins/dev.perfetto.Chaos/index.ts
index 028b8dd..358ecca 100644
--- a/ui/src/plugins/dev.perfetto.Chaos/index.ts
+++ b/ui/src/plugins/dev.perfetto.Chaos/index.ts
@@ -12,17 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-  addDebugSliceTrack,
-} from '../../public';
+import {Trace} from '../../public/trace';
+import {App} from '../../public/app';
+import {addDebugSliceTrack} from '../../public/debug_tracks';
+import {PerfettoPlugin} from '../../public/plugin';
 
-class Chaos implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.registerCommand({
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.Chaos';
+
+  static onActivate(ctx: App): void {
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.Chaos#CrashNow',
       name: 'Chaos: crash now',
       callback: () => {
@@ -31,8 +30,8 @@
     });
   }
 
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerCommand({
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.Chaos#CrashNowQuery',
       name: 'Chaos: run crashing query',
       callback: () => {
@@ -47,35 +46,24 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.Chaos#AddCrashingDebugTrack',
       name: 'Chaos: add crashing debug track',
       callback: () => {
-        addDebugSliceTrack(
-          ctx,
-          {
+        addDebugSliceTrack({
+          trace: ctx,
+          data: {
             sqlSource: `
-            syntactically
-            invalid
-            query
-            over
-            many
-          `,
+              syntactically
+              invalid
+              query
+              over
+              many
+            `,
           },
-          `Chaos track`,
-          {ts: 'ts', dur: 'dur', name: 'name'},
-          [],
-        );
+          title: `Chaos track`,
+        });
       },
     });
   }
-
-  async onTraceUnload(_: PluginContextTrace): Promise<void> {}
-
-  onDeactivate(_: PluginContext): void {}
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.Chaos',
-  plugin: Chaos,
-};
diff --git a/ui/src/plugins/dev.perfetto.Counter/counter_details_panel.ts b/ui/src/plugins/dev.perfetto.Counter/counter_details_panel.ts
new file mode 100644
index 0000000..e0ff568
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Counter/counter_details_panel.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 {Time, duration, time} from '../../base/time';
+import {Engine} from '../../trace_processor/engine';
+import {Trace} from '../../public/trace';
+import {
+  LONG,
+  LONG_NULL,
+  NUM,
+  NUM_NULL,
+} from '../../trace_processor/query_result';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import m from 'mithril';
+import {DetailsShell} from '../../widgets/details_shell';
+import {GridLayout} from '../../widgets/grid_layout';
+import {Section} from '../../widgets/section';
+import {Tree, TreeNode} from '../../widgets/tree';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {DurationWidget} from '../../frontend/widgets/duration';
+import {TrackEventSelection} from '../../public/selection';
+import {hasArgs, renderArguments} from '../../frontend/slice_args';
+import {asArgSetId} from '../../trace_processor/sql_utils/core_types';
+import {Arg, getArgs} from '../../trace_processor/sql_utils/args';
+
+interface CounterDetails {
+  // The "left" timestamp of the counter sample T(N)
+  ts: time;
+
+  // The delta between this sample and the next one's timestamps T(N+1) - T(N)
+  duration: duration;
+
+  // The value of the counter sample F(N)
+  value: number;
+
+  // The delta between this sample's value and the previous one F(N) - F(N-1)
+  delta: number;
+
+  args?: Arg[];
+}
+
+export class CounterDetailsPanel implements TrackEventDetailsPanel {
+  private readonly trace: Trace;
+  private readonly engine: Engine;
+  private readonly trackId: number;
+  private readonly rootTable: string;
+  private readonly trackName: string;
+  private counterDetails?: CounterDetails;
+
+  constructor(
+    trace: Trace,
+    trackId: number,
+    trackName: string,
+    rootTable = 'counter',
+  ) {
+    this.trace = trace;
+    this.engine = trace.engine;
+    this.trackId = trackId;
+    this.trackName = trackName;
+    this.rootTable = rootTable;
+  }
+
+  async load({eventId}: TrackEventSelection) {
+    this.counterDetails = await loadCounterDetails(
+      this.engine,
+      this.trackId,
+      eventId,
+      this.rootTable,
+    );
+  }
+
+  render() {
+    const counterInfo = this.counterDetails;
+    if (counterInfo) {
+      const args =
+        hasArgs(counterInfo.args) &&
+        m(
+          Section,
+          {title: 'Arguments'},
+          m(Tree, renderArguments(this.trace, counterInfo.args)),
+        );
+
+      return m(
+        DetailsShell,
+        {title: 'Counter', description: `${this.trackName}`},
+        m(
+          GridLayout,
+          m(
+            Section,
+            {title: 'Properties'},
+            m(
+              Tree,
+              m(TreeNode, {left: 'Name', right: `${this.trackName}`}),
+              m(TreeNode, {
+                left: 'Start time',
+                right: m(Timestamp, {ts: counterInfo.ts}),
+              }),
+              m(TreeNode, {
+                left: 'Value',
+                right: `${counterInfo.value.toLocaleString()}`,
+              }),
+              m(TreeNode, {
+                left: 'Delta',
+                right: `${counterInfo.delta.toLocaleString()}`,
+              }),
+              m(TreeNode, {
+                left: 'Duration',
+                right: m(DurationWidget, {dur: counterInfo.duration}),
+              }),
+            ),
+          ),
+          args,
+        ),
+      );
+    } else {
+      return m(DetailsShell, {title: 'Counter', description: 'Loading...'});
+    }
+  }
+
+  isLoading(): boolean {
+    return this.counterDetails === undefined;
+  }
+}
+
+async function loadCounterDetails(
+  engine: Engine,
+  trackId: number,
+  id: number,
+  rootTable: string,
+): Promise<CounterDetails> {
+  const query = `
+    WITH CTE AS (
+      SELECT
+        id,
+        ts as leftTs,
+        value,
+        LAG(value) OVER (ORDER BY ts) AS prevValue,
+        LEAD(ts) OVER (ORDER BY ts) AS rightTs,
+        arg_set_id AS argSetId
+      FROM ${rootTable}
+      WHERE track_id = ${trackId}
+    )
+    SELECT * FROM CTE WHERE id = ${id}
+  `;
+
+  const counter = await engine.query(query);
+  const row = counter.iter({
+    value: NUM,
+    prevValue: NUM_NULL,
+    leftTs: LONG,
+    rightTs: LONG_NULL,
+    argSetId: NUM_NULL,
+  });
+  const value = row.value;
+  const leftTs = Time.fromRaw(row.leftTs);
+  const rightTs = row.rightTs !== null ? Time.fromRaw(row.rightTs) : leftTs;
+  const prevValue = row.prevValue !== null ? row.prevValue : value;
+
+  const delta = value - prevValue;
+  const duration = rightTs - leftTs;
+  const argSetId = row.argSetId;
+  const args =
+    argSetId == null ? undefined : await getArgs(engine, asArgSetId(argSetId));
+  return {ts: leftTs, value, delta, duration, args};
+}
diff --git a/ui/src/plugins/dev.perfetto.Counter/counter_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.Counter/counter_selection_aggregator.ts
new file mode 100644
index 0000000..6a2bd3f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Counter/counter_selection_aggregator.ts
@@ -0,0 +1,175 @@
+// 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.
+
+import {Duration} from '../../base/time';
+import {ColumnDef, Sorting} from '../../public/aggregation';
+import {AreaSelection} from '../../public/selection';
+import {COUNTER_TRACK_KIND} from '../../public/track_kinds';
+import {Engine} from '../../trace_processor/engine';
+import {AreaSelectionAggregator} from '../../public/selection';
+
+export class CounterSelectionAggregator implements AreaSelectionAggregator {
+  readonly id = 'counter_aggregation';
+
+  async createAggregateView(engine: Engine, area: AreaSelection) {
+    const trackIds: (string | number)[] = [];
+    for (const trackInfo of area.tracks) {
+      if (trackInfo?.tags?.kind === COUNTER_TRACK_KIND) {
+        trackInfo.tags?.trackIds && trackIds.push(...trackInfo.tags.trackIds);
+      }
+    }
+    if (trackIds.length === 0) return false;
+    const duration = area.end - area.start;
+    const durationSec = Duration.toSeconds(duration);
+
+    // 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})
+      SELECT
+        (SELECT name FROM counter_track WHERE id = ${trackIds[0]}) AS name,
+        *,
+        MAX(last_value) - MIN(first_value) AS delta_value,
+        ROUND((MAX(last_value) - MIN(first_value))/${durationSec}, 2) AS rate
+      FROM aggregated`;
+    } else {
+      // Slower, but general purspose query that can aggregate multiple tracks
+      query = `CREATE 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
+      )
+      SELECT
+        name,
+        count,
+        avg_value,
+        last AS last_value,
+        first AS first_value,
+        last - first AS delta_value,
+        ROUND((last - first)/${durationSec}, 2) AS rate,
+        min_value,
+        max_value
+      FROM aggregated JOIN counter_track ON
+        track_id = counter_track.id
+      GROUP BY track_id`;
+    }
+    await engine.query(query);
+    return true;
+  }
+
+  getColumnDefinitions(): ColumnDef[] {
+    return [
+      {
+        title: 'Name',
+        kind: 'STRING',
+        columnConstructor: Uint16Array,
+        columnId: 'name',
+      },
+      {
+        title: 'Delta value',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'delta_value',
+      },
+      {
+        title: 'Rate /s',
+        kind: 'Number',
+        columnConstructor: Float64Array,
+        columnId: 'rate',
+      },
+      {
+        title: 'Weighted avg value',
+        kind: 'Number',
+        columnConstructor: Float64Array,
+        columnId: 'avg_value',
+      },
+      {
+        title: 'Count',
+        kind: 'Number',
+        columnConstructor: Float64Array,
+        columnId: 'count',
+        sum: true,
+      },
+      {
+        title: 'First value',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'first_value',
+      },
+      {
+        title: 'Last value',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'last_value',
+      },
+      {
+        title: 'Min value',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'min_value',
+      },
+      {
+        title: 'Max value',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'max_value',
+      },
+    ];
+  }
+
+  async getExtra() {}
+
+  getTabName() {
+    return 'Counters';
+  }
+
+  getDefaultSorting(): Sorting {
+    return {column: 'name', direction: 'DESC'};
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Counter/index.ts b/ui/src/plugins/dev.perfetto.Counter/index.ts
new file mode 100644
index 0000000..074e2c4
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Counter/index.ts
@@ -0,0 +1,417 @@
+// 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 '../../frontend/base_counter_track';
+import {TraceProcessorCounterTrack} from './trace_processor_counter_track';
+import {exists} from '../../base/utils';
+import {TrackNode} from '../../public/workspace';
+import {
+  getOrCreateGroupForProcess,
+  getOrCreateGroupForThread,
+} from '../../public/standard_groups';
+import {CounterSelectionAggregator} from './counter_selection_aggregator';
+
+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';
+  // }
+
+  if (name.startsWith('battery_stats.')) {
+    options.yRangeSharingKey = 'battery_stats';
+  }
+
+  // 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';
+  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.registerAreaSelectionAggreagtor(
+      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 type = 'counter_track'
+        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({
+          trace: ctx,
+          uri,
+          trackId,
+          trackName: title,
+          options: {
+            ...getDefaultCounterOptions(title),
+            unit,
+          },
+        }),
+      });
+      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({
+          trace: ctx,
+          uri,
+          trackId: trackId,
+          trackName: name,
+          options: getDefaultCounterOptions(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({
+          trace: ctx,
+          uri,
+          trackId: trackId,
+          trackName: name,
+          options: getDefaultCounterOptions(name),
+        }),
+      });
+      const group = getOrCreateGroupForThread(ctx.workspace, 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);
+  `);
+    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({
+          trace: ctx,
+          uri,
+          trackId: trackId,
+          trackName: name,
+          options: getDefaultCounterOptions(name),
+        }),
+      });
+      const group = getOrCreateGroupForProcess(ctx.workspace, 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({
+          trace: ctx,
+          uri,
+          trackId: it.id,
+          trackName: name,
+          options: getDefaultCounterOptions(name),
+        }),
+      });
+      const track = new TrackNode({uri, title: name, sortOrder: -20});
+      ctx.workspace.addChildInOrder(track);
+    }
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Counter/trace_processor_counter_track.ts b/ui/src/plugins/dev.perfetto.Counter/trace_processor_counter_track.ts
new file mode 100644
index 0000000..ddbdbcb
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Counter/trace_processor_counter_track.ts
@@ -0,0 +1,115 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {LONG, LONG_NULL, NUM} from '../../trace_processor/query_result';
+import {
+  BaseCounterTrack,
+  BaseCounterTrackArgs,
+} from '../../frontend/base_counter_track';
+
+import {TrackMouseEvent} from '../../public/track';
+import {TrackEventDetails} from '../../public/selection';
+import {Time} from '../../base/time';
+import {CounterDetailsPanel} from './counter_details_panel';
+
+interface TraceProcessorCounterTrackArgs extends BaseCounterTrackArgs {
+  readonly trackId: number;
+  readonly trackName: string;
+  readonly rootTable?: string;
+}
+
+export class TraceProcessorCounterTrack extends BaseCounterTrack {
+  private readonly trackId: number;
+  private readonly rootTable: string;
+  private readonly trackName: string;
+
+  constructor(args: TraceProcessorCounterTrackArgs) {
+    super(args);
+    this.trackId = args.trackId;
+    this.rootTable = args.rootTable ?? 'counter';
+    this.trackName = args.trackName;
+  }
+
+  getSqlSource() {
+    return `
+      select
+        id,
+        ts,
+        value
+      from ${this.rootTable}
+      where track_id = ${this.trackId}
+    `;
+  }
+
+  onMouseClick({x, timescale}: TrackMouseEvent): boolean {
+    const time = timescale.pxToHpTime(x).toTime('floor');
+
+    const query = `
+      select
+        id
+      from ${this.rootTable}
+      where
+        track_id = ${this.trackId}
+        and ts < ${time}
+      order by ts DESC
+      limit 1
+    `;
+
+    this.engine.query(query).then((result) => {
+      const it = result.iter({
+        id: NUM,
+      });
+      if (!it.valid()) {
+        return;
+      }
+      const id = it.id;
+      this.trace.selection.selectTrackEvent(this.uri, id);
+    });
+
+    return true;
+  }
+
+  // We must define this here instead of in `BaseCounterTrack` because
+  // `BaseCounterTrack` does not require the query to have an id column. Here,
+  // however, we make the assumption that `rootTable` has an id column, as we
+  // need it ot make selections in `onMouseClick` above. Whether or not we
+  // SHOULD assume `rootTable` has an id column is another matter...
+  async getSelectionDetails(id: number): Promise<TrackEventDetails> {
+    const query = `
+      WITH 
+        CTE AS (
+          SELECT
+            id,
+            ts as leftTs,
+            LEAD(ts) OVER (ORDER BY ts) AS rightTs
+          FROM ${this.rootTable}
+        )
+      SELECT * FROM CTE WHERE id = ${id}
+    `;
+
+    const counter = await this.engine.query(query);
+    const row = counter.iter({
+      leftTs: LONG,
+      rightTs: LONG_NULL,
+    });
+    const leftTs = Time.fromRaw(row.leftTs);
+    const rightTs = row.rightTs !== null ? Time.fromRaw(row.rightTs) : leftTs;
+    const duration = rightTs - leftTs;
+    return {ts: leftTs, dur: duration};
+  }
+
+  detailsPanel() {
+    return new CounterDetailsPanel(this.trace, this.trackId, this.trackName);
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.CpuFreq/cpu_freq_track.ts b/ui/src/plugins/dev.perfetto.CpuFreq/cpu_freq_track.ts
new file mode 100644
index 0000000..9e59cf2
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.CpuFreq/cpu_freq_track.ts
@@ -0,0 +1,426 @@
+// 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 {BigintMath as BIMath} from '../../base/bigint_math';
+import {searchSegment} from '../../base/binary_search';
+import {assertTrue} from '../../base/logging';
+import {duration, time, Time} from '../../base/time';
+import {drawTrackHoverTooltip} from '../../base/canvas_utils';
+import {colorForCpu} from '../../public/lib/colorizer';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {checkerboardExcept} from '../../frontend/checkerboard';
+import {Track} from '../../public/track';
+import {LONG, NUM} from '../../trace_processor/query_result';
+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 {AsyncDisposableStack} from '../../base/disposable_stack';
+import {Trace} from '../../public/trace';
+
+export interface Data extends TrackData {
+  timestamps: BigInt64Array;
+  minFreqKHz: Uint32Array;
+  maxFreqKHz: Uint32Array;
+  lastFreqKHz: Uint32Array;
+  lastIdleValues: Int8Array;
+}
+
+interface Config {
+  cpu: number;
+  freqTrackId: number;
+  idleTrackId?: number;
+  maximumValue: number;
+}
+
+// 0.5 Makes the horizontal lines sharp.
+const MARGIN_TOP = 4.5;
+const RECT_HEIGHT = 20;
+
+export class CpuFreqTrack implements Track {
+  private mousePos: Point2D = {x: 0, y: 0};
+  private hoveredValue: number | undefined = undefined;
+  private hoveredTs: time | undefined = undefined;
+  private hoveredTsEnd: time | undefined = undefined;
+  private hoveredIdle: number | undefined = undefined;
+  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+
+  private trackUuid = uuidv4Sql();
+
+  private trash!: AsyncDisposableStack;
+
+  constructor(
+    private readonly config: Config,
+    private readonly trace: Trace,
+  ) {}
+
+  async onCreate() {
+    this.trash = new AsyncDisposableStack();
+    if (this.config.idleTrackId === undefined) {
+      this.trash.use(
+        await createView(
+          this.trace.engine,
+          `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}
+          `,
+        ),
+      );
+    } else {
+      this.trash.use(
+        await createView(
+          this.trace.engine,
+          `raw_freq_${this.trackUuid}`,
+          `
+            select ts, dur, value as freqValue
+            from experimental_counter_dur c
+            where track_id = ${this.config.freqTrackId}
+          `,
+        ),
+      );
+
+      this.trash.use(
+        await createView(
+          this.trace.engine,
+          `raw_idle_${this.trackUuid}`,
+          `
+            select
+              ts,
+              dur,
+              iif(value = 4294967295, -1, cast(value as int)) as idleValue
+            from experimental_counter_dur c
+            where track_id = ${this.config.idleTrackId}
+          `,
+        ),
+      );
+
+      this.trash.use(
+        await createVirtualTable(
+          this.trace.engine,
+          `raw_freq_idle_${this.trackUuid}`,
+          `span_join(raw_freq_${this.trackUuid}, raw_idle_${this.trackUuid})`,
+        ),
+      );
+    }
+
+    this.trash.use(
+      await createVirtualTable(
+        this.trace.engine,
+        `cpu_freq_${this.trackUuid}`,
+        `
+          __intrinsic_counter_mipmap((
+            select ts, freqValue as value
+            from raw_freq_idle_${this.trackUuid}
+          ))
+        `,
+      ),
+    );
+
+    this.trash.use(
+      await createVirtualTable(
+        this.trace.engine,
+        `cpu_idle_${this.trackUuid}`,
+        `
+          __intrinsic_counter_mipmap((
+            select ts, idleValue as value
+            from raw_freq_idle_${this.trackUuid}
+          ))
+        `,
+      ),
+    );
+  }
+
+  async onUpdate({
+    visibleWindow,
+    resolution,
+  }: TrackRenderContext): Promise<void> {
+    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
+  }
+
+  async onDestroy(): Promise<void> {
+    await this.trash.asyncDispose();
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    // The resolution should always be a power of two for the logic of this
+    // function to make sense.
+    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
+
+    const freqResult = await this.trace.engine.query(`
+      SELECT
+        min_value as minFreq,
+        max_value as maxFreq,
+        last_ts as ts,
+        last_value as lastFreq
+      FROM cpu_freq_${this.trackUuid}(
+        ${start},
+        ${end},
+        ${resolution}
+      );
+    `);
+    const idleResult = await this.trace.engine.query(`
+      SELECT last_value as lastIdle
+      FROM cpu_idle_${this.trackUuid}(
+        ${start},
+        ${end},
+        ${resolution}
+      );
+    `);
+
+    const freqRows = freqResult.numRows();
+    const idleRows = idleResult.numRows();
+    assertTrue(freqRows == idleRows);
+
+    const data: Data = {
+      start,
+      end,
+      resolution,
+      length: freqRows,
+      timestamps: new BigInt64Array(freqRows),
+      minFreqKHz: new Uint32Array(freqRows),
+      maxFreqKHz: new Uint32Array(freqRows),
+      lastFreqKHz: new Uint32Array(freqRows),
+      lastIdleValues: new Int8Array(freqRows),
+    };
+
+    const freqIt = freqResult.iter({
+      ts: LONG,
+      minFreq: NUM,
+      maxFreq: NUM,
+      lastFreq: NUM,
+    });
+    const idleIt = idleResult.iter({
+      lastIdle: NUM,
+    });
+    for (let i = 0; freqIt.valid(); ++i, freqIt.next(), idleIt.next()) {
+      data.timestamps[i] = freqIt.ts;
+      data.minFreqKHz[i] = freqIt.minFreq;
+      data.maxFreqKHz[i] = freqIt.maxFreq;
+      data.lastFreqKHz[i] = freqIt.lastFreq;
+      data.lastIdleValues[i] = idleIt.lastIdle;
+    }
+    return data;
+  }
+
+  getHeight() {
+    return MARGIN_TOP + RECT_HEIGHT;
+  }
+
+  render({ctx, size, timescale, visibleWindow}: TrackRenderContext): void {
+    // TODO: fonts and colors should come from the CSS and not hardcoded here.
+    const data = this.fetcher.data;
+
+    if (data === undefined || data.timestamps.length === 0) {
+      // Can't possibly draw anything.
+      return;
+    }
+
+    assertTrue(data.timestamps.length === data.lastFreqKHz.length);
+    assertTrue(data.timestamps.length === data.minFreqKHz.length);
+    assertTrue(data.timestamps.length === data.maxFreqKHz.length);
+    assertTrue(data.timestamps.length === data.lastIdleValues.length);
+
+    const endPx = size.width;
+    const zeroY = MARGIN_TOP + RECT_HEIGHT;
+
+    // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
+    let yMax = this.config.maximumValue;
+    const kUnits = ['', 'K', 'M', 'G', 'T', 'E'];
+    const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
+    const pow10 = Math.pow(10, exp);
+    yMax = Math.ceil(yMax / (pow10 / 4)) * (pow10 / 4);
+    const unitGroup = Math.floor(exp / 3);
+    const num = yMax / Math.pow(10, unitGroup * 3);
+    // The values we have for cpufreq are in kHz so +1 to unitGroup.
+    const yLabel = `${num} ${kUnits[unitGroup + 1]}Hz`;
+
+    const color = colorForCpu(this.config.cpu);
+    let saturation = 45;
+    if (this.trace.timeline.hoveredUtid !== undefined) {
+      saturation = 0;
+    }
+
+    ctx.fillStyle = color.setHSL({s: saturation, l: 70}).cssString;
+    ctx.strokeStyle = color.setHSL({s: saturation, l: 55}).cssString;
+
+    const calculateX = (timestamp: time) => {
+      return Math.floor(timescale.timeToPx(timestamp));
+    };
+    const calculateY = (value: number) => {
+      return zeroY - Math.round((value / yMax) * RECT_HEIGHT);
+    };
+
+    const timespan = visibleWindow.toTimeSpan();
+    const start = timespan.start;
+    const end = timespan.end;
+
+    const [rawStartIdx] = searchSegment(data.timestamps, start);
+    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
+
+    const [, rawEndIdx] = searchSegment(data.timestamps, end);
+    const endIdx = rawEndIdx === -1 ? data.timestamps.length : rawEndIdx;
+
+    // Draw the CPU frequency graph.
+    {
+      ctx.beginPath();
+      const timestamp = Time.fromRaw(data.timestamps[startIdx]);
+      ctx.moveTo(Math.max(calculateX(timestamp), 0), zeroY);
+
+      let lastDrawnY = zeroY;
+      for (let i = startIdx; i < endIdx; i++) {
+        const timestamp = Time.fromRaw(data.timestamps[i]);
+        const x = Math.max(0, calculateX(timestamp));
+        const minY = calculateY(data.minFreqKHz[i]);
+        const maxY = calculateY(data.maxFreqKHz[i]);
+        const lastY = calculateY(data.lastFreqKHz[i]);
+
+        ctx.lineTo(x, lastDrawnY);
+        if (minY === maxY) {
+          assertTrue(lastY === minY);
+          ctx.lineTo(x, lastY);
+        } else {
+          ctx.lineTo(x, minY);
+          ctx.lineTo(x, maxY);
+          ctx.lineTo(x, lastY);
+        }
+        lastDrawnY = lastY;
+      }
+      ctx.lineTo(endPx, lastDrawnY);
+      ctx.lineTo(endPx, zeroY);
+      ctx.closePath();
+      ctx.fill();
+      ctx.stroke();
+    }
+
+    // Draw CPU idle rectangles that overlay the CPU freq graph.
+    ctx.fillStyle = `rgba(240, 240, 240, 1)`;
+    {
+      for (let i = startIdx; i < endIdx; i++) {
+        if (data.lastIdleValues[i] < 0) {
+          continue;
+        }
+
+        // We intentionally don't use the floor function here when computing x
+        // coordinates. Instead we use floating point which prevents flickering as
+        // we pan and zoom; this relies on the browser anti-aliasing pixels
+        // correctly.
+        const timestamp = Time.fromRaw(data.timestamps[i]);
+        const x = timescale.timeToPx(timestamp);
+        const xEnd =
+          i === data.lastIdleValues.length - 1
+            ? endPx
+            : timescale.timeToPx(Time.fromRaw(data.timestamps[i + 1]));
+
+        const width = xEnd - x;
+        const height = calculateY(data.lastFreqKHz[i]) - zeroY;
+
+        ctx.fillRect(x, zeroY, width, height);
+      }
+    }
+
+    ctx.font = '10px Roboto Condensed';
+
+    if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) {
+      let text = `${this.hoveredValue.toLocaleString()}kHz`;
+
+      ctx.fillStyle = color.setHSL({s: 45, l: 75}).cssString;
+      ctx.strokeStyle = color.setHSL({s: 45, l: 45}).cssString;
+
+      const xStart = Math.floor(timescale.timeToPx(this.hoveredTs));
+      const xEnd =
+        this.hoveredTsEnd === undefined
+          ? endPx
+          : Math.floor(timescale.timeToPx(this.hoveredTsEnd));
+      const y = zeroY - Math.round((this.hoveredValue / yMax) * RECT_HEIGHT);
+
+      // Highlight line.
+      ctx.beginPath();
+      ctx.moveTo(xStart, y);
+      ctx.lineTo(xEnd, y);
+      ctx.lineWidth = 3;
+      ctx.stroke();
+      ctx.lineWidth = 1;
+
+      // Draw change marker.
+      ctx.beginPath();
+      ctx.arc(
+        xStart,
+        y,
+        3 /* r*/,
+        0 /* start angle*/,
+        2 * Math.PI /* end angle*/,
+      );
+      ctx.fill();
+      ctx.stroke();
+
+      // Display idle value if current hover is idle.
+      if (this.hoveredIdle !== undefined && this.hoveredIdle !== -1) {
+        // Display the idle value +1 to be consistent with catapult.
+        text += ` (Idle: ${(this.hoveredIdle + 1).toLocaleString()})`;
+      }
+
+      // Draw the tooltip.
+      drawTrackHoverTooltip(ctx, this.mousePos, size, text);
+    }
+
+    // Write the Y scale on the top left corner.
+    ctx.textBaseline = 'alphabetic';
+    ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
+    ctx.fillRect(0, 0, 42, 18);
+    ctx.fillStyle = '#666';
+    ctx.textAlign = 'left';
+    ctx.fillText(`${yLabel}`, 4, 14);
+
+    // If the cached trace slices don't fully cover the visible time range,
+    // show a gray rectangle with a "Loading..." label.
+    checkerboardExcept(
+      ctx,
+      this.getHeight(),
+      0,
+      size.width,
+      timescale.timeToPx(data.start),
+      timescale.timeToPx(data.end),
+    );
+  }
+
+  onMouseMove({x, y, timescale}: TrackMouseEvent) {
+    const data = this.fetcher.data;
+    if (data === undefined) return;
+    this.mousePos = {x, y};
+    const time = timescale.pxToHpTime(x);
+
+    const [left, right] = searchSegment(data.timestamps, time.toTime());
+
+    this.hoveredTs =
+      left === -1 ? undefined : Time.fromRaw(data.timestamps[left]);
+    this.hoveredTsEnd =
+      right === -1 ? undefined : Time.fromRaw(data.timestamps[right]);
+    this.hoveredValue = left === -1 ? undefined : data.lastFreqKHz[left];
+    this.hoveredIdle = left === -1 ? undefined : data.lastIdleValues[left];
+  }
+
+  onMouseOut() {
+    this.hoveredValue = undefined;
+    this.hoveredTs = undefined;
+    this.hoveredTsEnd = undefined;
+    this.hoveredIdle = undefined;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.CpuFreq/index.ts b/ui/src/plugins/dev.perfetto.CpuFreq/index.ts
new file mode 100644
index 0000000..bd7b96c
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.CpuFreq/index.ts
@@ -0,0 +1,93 @@
+// 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 {CPU_FREQ_TRACK_KIND} from '../../public/track_kinds';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {NUM, NUM_NULL} from '../../trace_processor/query_result';
+import {CpuFreqTrack} from './cpu_freq_track';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.CpuFreq';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const {engine} = ctx;
+
+    const cpus = [];
+    const cpusResult = await engine.query(
+      'select distinct cpu from cpu_counter_track order by cpu;',
+    );
+    for (const it = cpusResult.iter({cpu: NUM}); it.valid(); it.next()) {
+      cpus.push(it.cpu);
+    }
+
+    const maxCpuFreqResult = await engine.query(`
+      select ifnull(max(value), 0) as freq
+      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';
+    `);
+    const maxCpuFreq = maxCpuFreqResult.firstRow({freq: NUM}).freq;
+
+    for (const cpu of cpus) {
+      // Only add a cpu freq track if we have cpu freq data.
+      const cpuFreqIdleResult = await engine.query(`
+        select
+          id as cpuFreqId,
+          (
+            select id
+            from cpu_counter_track
+            where name = 'cpuidle'
+            and cpu = ${cpu}
+            limit 1
+          ) as cpuIdleId
+        from cpu_counter_track
+        join _counter_track_summary using (id)
+        where name = 'cpufreq' and cpu = ${cpu}
+        limit 1;
+      `);
+
+      if (cpuFreqIdleResult.numRows() > 0) {
+        const row = cpuFreqIdleResult.firstRow({
+          cpuFreqId: NUM,
+          cpuIdleId: NUM_NULL,
+        });
+        const freqTrackId = row.cpuFreqId;
+        const idleTrackId = row.cpuIdleId === null ? undefined : row.cpuIdleId;
+
+        const config = {
+          cpu,
+          maximumValue: maxCpuFreq,
+          freqTrackId,
+          idleTrackId,
+        };
+
+        const uri = `/cpu_freq_cpu${cpu}`;
+        const title = `Cpu ${cpu} Frequency`;
+        ctx.tracks.registerTrack({
+          uri,
+          title,
+          tags: {
+            kind: CPU_FREQ_TRACK_KIND,
+            cpu,
+          },
+          track: new CpuFreqTrack(config, ctx),
+        });
+        const trackNode = new TrackNode({uri, title, sortOrder: -40});
+        ctx.workspace.addChildInOrder(trackNode);
+      }
+    }
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_details_panel.ts b/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_details_panel.ts
new file mode 100644
index 0000000..060746e
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_details_panel.ts
@@ -0,0 +1,107 @@
+// 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 {time} from '../../base/time';
+import {
+  metricsFromTableOrSubquery,
+  QueryFlamegraph,
+} from '../../public/lib/query_flamegraph';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {
+  TrackEventDetailsPanel,
+  TrackEventDetailsPanelSerializeArgs,
+} from '../../public/details_panel';
+import {DetailsShell} from '../../widgets/details_shell';
+import {Trace} from '../../public/trace';
+import {
+  Flamegraph,
+  FLAMEGRAPH_STATE_SCHEMA,
+  FlamegraphState,
+} from '../../widgets/flamegraph';
+
+export class CpuProfileSampleFlamegraphDetailsPanel
+  implements TrackEventDetailsPanel
+{
+  private readonly flamegraph: QueryFlamegraph;
+  readonly serialization: TrackEventDetailsPanelSerializeArgs<FlamegraphState>;
+
+  constructor(
+    trace: Trace,
+    private ts: time,
+    utid: number,
+  ) {
+    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 cpu_profile_stack_sample p
+            where p.ts = ${ts} and p.utid = ${utid}
+          ))
+        )
+      `,
+      [
+        {
+          name: 'CPU Profile Samples',
+          unit: '',
+          columnName: 'self_count',
+        },
+      ],
+      'include perfetto module callstacks.stack_profile',
+      [{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',
+        },
+      ],
+    );
+    this.serialization = {
+      schema: FLAMEGRAPH_STATE_SCHEMA,
+      state: Flamegraph.createDefaultState(metrics),
+    };
+    this.flamegraph = new QueryFlamegraph(trace, metrics, this.serialization);
+  }
+
+  render() {
+    return m(
+      '.flamegraph-profile',
+      m(
+        DetailsShell,
+        {
+          fillParent: true,
+          title: m('.title', 'CPU Profile Samples'),
+          description: [],
+          buttons: [m('div.time', `Timestamp: `, m(Timestamp, {ts: this.ts}))],
+        },
+        this.flamegraph.render(),
+      ),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_track.ts b/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_track.ts
new file mode 100644
index 0000000..4baeddf
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_track.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 {assertExists} from '../../base/logging';
+import {TrackEventDetails, TrackEventSelection} from '../../public/selection';
+import {getColorForSample} from '../../public/lib/colorizer';
+import {
+  BaseSliceTrack,
+  OnSliceClickArgs,
+} from '../../frontend/base_slice_track';
+import {NAMED_ROW, NamedRow} from '../../frontend/named_slice_track';
+import {NewTrackArgs} from '../../frontend/track';
+import {NUM} from '../../trace_processor/query_result';
+import {Slice} from '../../public/track';
+import {CpuProfileSampleFlamegraphDetailsPanel} from './cpu_profile_details_panel';
+
+interface CpuProfileRow extends NamedRow {
+  callsiteId: number;
+}
+
+export class CpuProfileTrack extends BaseSliceTrack<Slice, CpuProfileRow> {
+  constructor(
+    args: NewTrackArgs,
+    private utid: number,
+  ) {
+    super(args);
+  }
+
+  protected getRowSpec(): CpuProfileRow {
+    return {...NAMED_ROW, callsiteId: NUM};
+  }
+
+  protected rowToSlice(row: CpuProfileRow): 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;
+    }
+  }
+
+  getSqlSource(): string {
+    return `
+      select
+        p.id,
+        ts,
+        0 as dur,
+        0 as depth,
+        'CPU Sample' as name,
+        callsite_id as callsiteId
+      from cpu_profile_stack_sample p
+      where utid = ${this.utid}
+      order by ts
+    `;
+  }
+
+  onSliceClick({slice}: OnSliceClickArgs<Slice>) {
+    this.trace.selection.selectTrackEvent(this.uri, slice.id);
+  }
+
+  async getSelectionDetails(
+    id: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const baseDetails = await super.getSelectionDetails(id);
+    if (baseDetails === undefined) return undefined;
+    return {...baseDetails, utid: this.utid};
+  }
+
+  detailsPanel(selection: TrackEventSelection) {
+    const {ts, utid} = selection;
+    return new CpuProfileSampleFlamegraphDetailsPanel(
+      this.trace,
+      ts,
+      assertExists(utid),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.CpuProfile/index.ts b/ui/src/plugins/dev.perfetto.CpuProfile/index.ts
new file mode 100644
index 0000000..31e49fe
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.CpuProfile/index.ts
@@ -0,0 +1,76 @@
+// 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 {CPU_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 {CpuProfileTrack} from './cpu_profile_track';
+import {getThreadUriPrefix} from '../../public/utils';
+import {exists} from '../../base/utils';
+import {getOrCreateGroupForThread} from '../../public/standard_groups';
+import {TrackNode} from '../../public/workspace';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.CpuProfile';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const result = await ctx.engine.query(`
+      with thread_cpu_sample as (
+        select distinct utid
+        from cpu_profile_stack_sample
+        where utid != 0
+      )
+      select
+        utid,
+        tid,
+        upid,
+        thread.name as threadName
+      from thread_cpu_sample
+      join thread using(utid)
+    `);
+
+    const it = result.iter({
+      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 threadName = it.threadName;
+      const uri = `${getThreadUriPrefix(upid, utid)}_cpu_samples`;
+      const title = `${threadName} (CPU Stack Samples)`;
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        tags: {
+          kind: CPU_PROFILE_TRACK_KIND,
+          utid,
+          ...(exists(upid) && {upid}),
+        },
+        track: new CpuProfileTrack(
+          {
+            trace: ctx,
+            uri,
+          },
+          utid,
+        ),
+      });
+      const group = getOrCreateGroupForThread(ctx.workspace, utid);
+      const track = new TrackNode({uri, title, sortOrder: -40});
+      group.addChildInOrder(track);
+    }
+  }
+}
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
new file mode 100644
index 0000000..87d5555
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_by_process_selection_aggregator.ts
@@ -0,0 +1,103 @@
+// 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 {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';
+
+export class CpuSliceByProcessSelectionAggregator
+  implements AreaSelectionAggregator
+{
+  readonly id = 'cpu_by_process_aggregation';
+
+  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;
+
+    await engine.query(`
+      create or replace perfetto table ${this.id} as
+      select
+        process.name as process_name,
+        process.pid,
+        sum(dur) AS total_dur,
+        sum(dur) / count() as avg_dur,
+        count() as occurrences
+      from sched
+      join thread USING (utid)
+      join process USING (upid)
+      where
+        cpu in (${selectedCpus})
+        and ts + dur > ${area.start}
+        and ts < ${area.end}
+        and utid != 0
+      group by upid
+    `);
+    return true;
+  }
+
+  getTabName() {
+    return 'CPU by process';
+  }
+
+  async getExtra() {}
+
+  getDefaultSorting(): Sorting {
+    return {column: 'total_dur', direction: 'DESC'};
+  }
+
+  getColumnDefinitions(): ColumnDef[] {
+    return [
+      {
+        title: 'Process',
+        kind: 'STRING',
+        columnConstructor: Uint16Array,
+        columnId: 'process_name',
+      },
+      {
+        title: 'PID',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'pid',
+      },
+      {
+        title: 'Wall duration (ms)',
+        kind: 'TIMESTAMP_NS',
+        columnConstructor: Float64Array,
+        columnId: 'total_dur',
+        sum: true,
+      },
+      {
+        title: 'Avg Wall duration (ms)',
+        kind: 'TIMESTAMP_NS',
+        columnConstructor: Float64Array,
+        columnId: 'avg_dur',
+      },
+      {
+        title: 'Occurrences',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'occurrences',
+        sum: 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
new file mode 100644
index 0000000..064ba8a
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_selection_aggregator.ts
@@ -0,0 +1,114 @@
+// 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.
+
+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';
+
+export class CpuSliceSelectionAggregator implements AreaSelectionAggregator {
+  readonly id = 'cpu_aggregation';
+
+  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;
+
+    await engine.query(`
+      create or replace perfetto table ${this.id} as
+      select
+        process.name as process_name,
+        pid,
+        thread.name as thread_name,
+        tid,
+        sum(dur) AS total_dur,
+        sum(dur) / count() as avg_dur,
+        count() as occurrences
+      from process
+      join thread using (upid)
+      join sched using (utid)
+      where cpu in (${selectedCpus})
+        and sched.ts + sched.dur > ${area.start}
+        and sched.ts < ${area.end}
+        and utid != 0
+      group by utid
+    `);
+    return true;
+  }
+
+  getTabName() {
+    return 'CPU by thread';
+  }
+
+  async getExtra() {}
+
+  getDefaultSorting(): Sorting {
+    return {column: 'total_dur', direction: 'DESC'};
+  }
+
+  getColumnDefinitions(): ColumnDef[] {
+    return [
+      {
+        title: 'Process',
+        kind: 'STRING',
+        columnConstructor: Uint16Array,
+        columnId: 'process_name',
+      },
+      {
+        title: 'PID',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'pid',
+      },
+      {
+        title: 'Thread',
+        kind: 'STRING',
+        columnConstructor: Uint16Array,
+        columnId: 'thread_name',
+      },
+      {
+        title: 'TID',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'tid',
+      },
+      {
+        title: 'Wall duration (ms)',
+        kind: 'TIMESTAMP_NS',
+        columnConstructor: Float64Array,
+        columnId: 'total_dur',
+        sum: true,
+      },
+      {
+        title: 'Avg Wall duration (ms)',
+        kind: 'TIMESTAMP_NS',
+        columnConstructor: Float64Array,
+        columnId: 'avg_dur',
+      },
+      {
+        title: 'Occurrences',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'occurrences',
+        sum: 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
new file mode 100644
index 0000000..2c5b456
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_track.ts
@@ -0,0 +1,497 @@
+// 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 {BigintMath as BIMath} from '../../base/bigint_math';
+import {search, searchEq, searchSegment} from '../../base/binary_search';
+import {assertExists, assertTrue} from '../../base/logging';
+import {Duration, duration, Time, time} from '../../base/time';
+import {
+  drawDoubleHeadedArrow,
+  drawIncompleteSlice,
+  drawTrackHoverTooltip,
+} from '../../base/canvas_utils';
+import {cropText} from '../../base/string_utils';
+import {Color} from '../../public/color';
+import {colorForThread} from '../../public/lib/colorizer';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {checkerboardExcept} from '../../frontend/checkerboard';
+import {Point2D} from '../../base/geom';
+import {Track} from '../../public/track';
+import {LONG, NUM} from '../../trace_processor/query_result';
+import {uuidv4Sql} from '../../base/uuid';
+import {TrackMouseEvent, TrackRenderContext} from '../../public/track';
+import {TrackEventDetails} from '../../public/selection';
+import {asSchedSqlId} from '../../trace_processor/sql_utils/core_types';
+import {
+  getSched,
+  getSchedWakeupInfo,
+} from '../../trace_processor/sql_utils/sched';
+import {SchedSliceDetailsPanel} from './sched_details_tab';
+import {Trace} from '../../public/trace';
+import {exists} from '../../base/utils';
+import {ThreadMap} from '../dev.perfetto.Thread/threads';
+
+export interface Data extends TrackData {
+  // Slices are stored in a columnar fashion. All fields have the same length.
+  ids: Float64Array;
+  startQs: BigInt64Array;
+  endQs: BigInt64Array;
+  utids: Uint32Array;
+  flags: Uint8Array;
+  lastRowId: number;
+}
+
+const MARGIN_TOP = 3;
+const RECT_HEIGHT = 24;
+const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
+
+const CPU_SLICE_FLAGS_INCOMPLETE = 1;
+const CPU_SLICE_FLAGS_REALTIME = 2;
+
+export class CpuSliceTrack implements Track {
+  private mousePos?: Point2D;
+  private utidHoveredInThisTrack?: number;
+  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+
+  private lastRowId = -1;
+  private trackUuid = uuidv4Sql();
+
+  constructor(
+    private readonly trace: Trace,
+    private readonly uri: string,
+    private readonly cpu: number,
+    private readonly threads: ThreadMap,
+  ) {}
+
+  async onCreate() {
+    await this.trace.engine.query(`
+      create virtual table cpu_slice_${this.trackUuid}
+      using __intrinsic_slice_mipmap((
+        select
+          id,
+          ts,
+          iif(dur = -1, lead(ts, 1, trace_end()) over (order by ts) - ts, dur),
+          0 as depth
+        from sched
+        where cpu = ${this.cpu} and utid != 0
+      ));
+    `);
+    const it = await this.trace.engine.query(`
+      select coalesce(max(id), -1) as lastRowId
+      from sched
+      where cpu = ${this.cpu} and utid != 0
+    `);
+    this.lastRowId = it.firstRow({lastRowId: NUM}).lastRowId;
+  }
+
+  async onUpdate({
+    visibleWindow,
+    resolution,
+  }: TrackRenderContext): Promise<void> {
+    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
+
+    const queryRes = await this.trace.engine.query(`
+      select
+        (z.ts / ${resolution}) * ${resolution} as tsQ,
+        (((z.ts + z.dur) / ${resolution}) + 1) * ${resolution} as tsEndQ,
+        s.utid,
+        s.id,
+        s.dur = -1 as isIncomplete,
+        ifnull(s.priority < 100, 0) as isRealtime
+      from cpu_slice_${this.trackUuid}(${start}, ${end}, ${resolution}) z
+      cross join sched s using (id)
+    `);
+
+    const numRows = queryRes.numRows();
+    const slices: Data = {
+      start,
+      end,
+      resolution,
+      length: numRows,
+      lastRowId: this.lastRowId,
+      ids: new Float64Array(numRows),
+      startQs: new BigInt64Array(numRows),
+      endQs: new BigInt64Array(numRows),
+      utids: new Uint32Array(numRows),
+      flags: new Uint8Array(numRows),
+    };
+
+    const it = queryRes.iter({
+      tsQ: LONG,
+      tsEndQ: LONG,
+      utid: NUM,
+      id: NUM,
+      isIncomplete: NUM,
+      isRealtime: NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      slices.startQs[row] = it.tsQ;
+      slices.endQs[row] = it.tsEndQ;
+      slices.utids[row] = it.utid;
+      slices.ids[row] = it.id;
+
+      slices.flags[row] = 0;
+      if (it.isIncomplete) {
+        slices.flags[row] |= CPU_SLICE_FLAGS_INCOMPLETE;
+      }
+      if (it.isRealtime) {
+        slices.flags[row] |= CPU_SLICE_FLAGS_REALTIME;
+      }
+    }
+    return slices;
+  }
+
+  async onDestroy() {
+    await this.trace.engine.tryQuery(
+      `drop table if exists cpu_slice_${this.trackUuid}`,
+    );
+    this.fetcher[Symbol.dispose]();
+  }
+
+  getHeight(): number {
+    return TRACK_HEIGHT;
+  }
+
+  render(trackCtx: TrackRenderContext): void {
+    const {ctx, size, timescale} = trackCtx;
+
+    // TODO: fonts and colors should come from the CSS and not hardcoded here.
+    const data = this.fetcher.data;
+
+    if (data === undefined) return; // Can't possibly draw anything.
+
+    // If the cached trace slices don't fully cover the visible time range,
+    // show a gray rectangle with a "Loading..." label.
+    checkerboardExcept(
+      ctx,
+      this.getHeight(),
+      0,
+      size.width,
+      timescale.timeToPx(data.start),
+      timescale.timeToPx(data.end),
+    );
+
+    this.renderSlices(trackCtx, data);
+  }
+
+  renderSlices(
+    {ctx, timescale, size, visibleWindow}: TrackRenderContext,
+    data: Data,
+  ): void {
+    assertTrue(data.startQs.length === data.endQs.length);
+    assertTrue(data.startQs.length === data.utids.length);
+
+    const visWindowEndPx = size.width;
+
+    ctx.textAlign = 'center';
+    ctx.font = '12px Roboto Condensed';
+    const charWidth = ctx.measureText('dbpqaouk').width / 8;
+
+    const timespan = visibleWindow.toTimeSpan();
+
+    const startTime = timespan.start;
+    const endTime = timespan.end;
+
+    const rawStartIdx = data.endQs.findIndex((end) => end >= startTime);
+    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
+
+    const [, rawEndIdx] = searchSegment(data.startQs, endTime);
+    const endIdx = rawEndIdx === -1 ? data.startQs.length : rawEndIdx;
+
+    for (let i = startIdx; i < endIdx; i++) {
+      const tStart = Time.fromRaw(data.startQs[i]);
+      let tEnd = Time.fromRaw(data.endQs[i]);
+      const utid = data.utids[i];
+
+      // If the last slice is incomplete, it should end with the end of the
+      // window, else it might spill over the window and the end would not be
+      // visible as a zigzag line.
+      if (
+        data.ids[i] === data.lastRowId &&
+        data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE
+      ) {
+        tEnd = endTime;
+      }
+      const rectStart = timescale.timeToPx(tStart);
+      const rectEnd = timescale.timeToPx(tEnd);
+      const rectWidth = Math.max(1, rectEnd - rectStart);
+
+      const threadInfo = this.threads.get(utid);
+      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+      const pid = threadInfo && threadInfo.pid ? threadInfo.pid : -1;
+
+      const isHovering = this.trace.timeline.hoveredUtid !== undefined;
+      const isThreadHovered = this.trace.timeline.hoveredUtid === utid;
+      const isProcessHovered = this.trace.timeline.hoveredPid === pid;
+      const colorScheme = colorForThread(threadInfo);
+      let color: Color;
+      let textColor: Color;
+      if (isHovering && !isThreadHovered) {
+        if (!isProcessHovered) {
+          color = colorScheme.disabled;
+          textColor = colorScheme.textDisabled;
+        } else {
+          color = colorScheme.variant;
+          textColor = colorScheme.textVariant;
+        }
+      } else {
+        color = colorScheme.base;
+        textColor = colorScheme.textBase;
+      }
+      ctx.fillStyle = color.cssString;
+
+      if (data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE) {
+        drawIncompleteSlice(ctx, rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
+      } else {
+        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
+      }
+
+      // Don't render text when we have less than 5px to play with.
+      if (rectWidth < 5) continue;
+
+      // Stylize real-time threads. We don't do it when zoomed out as the
+      // fillRect is expensive.
+      if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
+        ctx.fillStyle = getHatchedPattern(ctx);
+        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
+      }
+
+      // TODO: consider de-duplicating this code with the copied one from
+      // chrome_slices/frontend.ts.
+      let title = `[utid:${utid}]`;
+      let subTitle = '';
+      if (threadInfo) {
+        if (threadInfo.pid !== undefined && threadInfo.pid !== 0) {
+          let procName = threadInfo.procName ?? '';
+          if (procName.startsWith('/')) {
+            // Remove folder paths from name
+            procName = procName.substring(procName.lastIndexOf('/') + 1);
+          }
+          title = `${procName} [${threadInfo.pid}]`;
+          subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`;
+        } else {
+          title = `${threadInfo.threadName} [${threadInfo.tid}]`;
+        }
+      }
+
+      if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
+        subTitle = subTitle + ' (RT)';
+      }
+
+      const right = Math.min(visWindowEndPx, rectEnd);
+      const left = Math.max(rectStart, 0);
+      const visibleWidth = Math.max(right - left, 1);
+      title = cropText(title, charWidth, visibleWidth);
+      subTitle = cropText(subTitle, charWidth, visibleWidth);
+      const rectXCenter = left + visibleWidth / 2;
+      ctx.fillStyle = textColor.cssString;
+      ctx.font = '12px Roboto Condensed';
+      ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 1);
+      ctx.fillStyle = textColor.setAlpha(0.6).cssString;
+      ctx.font = '10px Roboto Condensed';
+      ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 9);
+    }
+
+    const selection = this.trace.selection.selection;
+    if (selection.kind === 'track_event') {
+      if (selection.trackUri === this.uri) {
+        const [startIndex, endIndex] = searchEq(data.ids, selection.eventId);
+        if (startIndex !== endIndex) {
+          const tStart = Time.fromRaw(data.startQs[startIndex]);
+          const tEnd = Time.fromRaw(data.endQs[startIndex]);
+          const utid = data.utids[startIndex];
+          const color = colorForThread(this.threads.get(utid));
+          const rectStart = timescale.timeToPx(tStart);
+          const rectEnd = timescale.timeToPx(tEnd);
+          const rectWidth = Math.max(1, rectEnd - rectStart);
+
+          // Draw a rectangle around the slice that is currently selected.
+          ctx.strokeStyle = color.base.setHSL({l: 30}).cssString;
+          ctx.beginPath();
+          ctx.lineWidth = 3;
+          ctx.strokeRect(
+            rectStart,
+            MARGIN_TOP - 1.5,
+            rectWidth,
+            RECT_HEIGHT + 3,
+          );
+          ctx.closePath();
+          // Draw arrow from wakeup time of current slice.
+          if (selection.wakeupTs) {
+            const wakeupPos = timescale.timeToPx(selection.wakeupTs);
+            const latencyWidth = rectStart - wakeupPos;
+            drawDoubleHeadedArrow(
+              ctx,
+              wakeupPos,
+              MARGIN_TOP + RECT_HEIGHT,
+              latencyWidth,
+              latencyWidth >= 20,
+            );
+            // Latency time with a white semi-transparent background.
+            const latency = tStart - selection.wakeupTs;
+            const displayText = Duration.humanise(latency);
+            const measured = ctx.measureText(displayText);
+            if (latencyWidth >= measured.width + 2) {
+              ctx.fillStyle = 'rgba(255,255,255,0.7)';
+              ctx.fillRect(
+                wakeupPos + latencyWidth / 2 - measured.width / 2 - 1,
+                MARGIN_TOP + RECT_HEIGHT - 12,
+                measured.width + 2,
+                11,
+              );
+              ctx.textBaseline = 'bottom';
+              ctx.fillStyle = 'black';
+              ctx.fillText(
+                displayText,
+                wakeupPos + latencyWidth / 2,
+                MARGIN_TOP + RECT_HEIGHT - 1,
+              );
+            }
+          }
+        }
+      }
+
+      // Draw diamond if the track being drawn is the cpu of the waker.
+      if (this.cpu === selection.wakerCpu && selection.wakeupTs) {
+        const wakeupPos = Math.floor(timescale.timeToPx(selection.wakeupTs));
+        ctx.beginPath();
+        ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8);
+        ctx.fillStyle = 'black';
+        ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2);
+        ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8);
+        ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2);
+        ctx.fill();
+        ctx.closePath();
+      }
+
+      if (this.utidHoveredInThisTrack !== undefined) {
+        const hoveredThread = this.threads.get(this.utidHoveredInThisTrack);
+        if (hoveredThread && this.mousePos !== undefined) {
+          const tidText = `T: ${hoveredThread.threadName}
+          [${hoveredThread.tid}]`;
+          // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+          if (hoveredThread.pid) {
+            const pidText = `P: ${hoveredThread.procName}
+            [${hoveredThread.pid}]`;
+            drawTrackHoverTooltip(ctx, this.mousePos, size, pidText, tidText);
+          } else {
+            drawTrackHoverTooltip(ctx, this.mousePos, size, tidText);
+          }
+        }
+      }
+    }
+  }
+
+  onMouseMove({x, y, timescale}: TrackMouseEvent) {
+    const data = this.fetcher.data;
+    this.mousePos = {x, y};
+    if (data === undefined) return;
+    if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) {
+      this.utidHoveredInThisTrack = undefined;
+      this.trace.timeline.hoveredUtid = undefined;
+      this.trace.timeline.hoveredPid = undefined;
+      return;
+    }
+    const t = timescale.pxToHpTime(x);
+    let hoveredUtid = undefined;
+
+    for (let i = 0; i < data.startQs.length; i++) {
+      const tStart = Time.fromRaw(data.startQs[i]);
+      const tEnd = Time.fromRaw(data.endQs[i]);
+      const utid = data.utids[i];
+      if (t.gte(tStart) && t.lt(tEnd)) {
+        hoveredUtid = utid;
+        break;
+      }
+    }
+    this.utidHoveredInThisTrack = hoveredUtid;
+    const threadInfo = exists(hoveredUtid) && this.threads.get(hoveredUtid);
+    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+    const hoveredPid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
+    this.trace.timeline.hoveredUtid = hoveredUtid;
+    this.trace.timeline.hoveredPid = hoveredPid;
+  }
+
+  onMouseOut() {
+    this.utidHoveredInThisTrack = -1;
+    this.trace.timeline.hoveredUtid = undefined;
+    this.trace.timeline.hoveredPid = undefined;
+    this.mousePos = undefined;
+  }
+
+  onMouseClick({x, timescale}: TrackMouseEvent) {
+    const data = this.fetcher.data;
+    if (data === undefined) return false;
+    const time = timescale.pxToHpTime(x);
+    const index = search(data.startQs, time.toTime());
+    const id = index === -1 ? undefined : data.ids[index];
+    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+    if (!id || this.utidHoveredInThisTrack === -1) return false;
+
+    this.trace.selection.selectTrackEvent(this.uri, id);
+    return true;
+  }
+
+  async getSelectionDetails?(
+    eventId: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const sched = await getSched(this.trace.engine, asSchedSqlId(eventId));
+    if (sched === undefined) {
+      return undefined;
+    }
+    const wakeup = await getSchedWakeupInfo(this.trace.engine, sched);
+    return {
+      ts: sched.ts,
+      dur: sched.dur,
+      wakeupTs: wakeup?.wakeupTs,
+      wakerCpu: wakeup?.wakerCpu,
+    };
+  }
+
+  detailsPanel() {
+    return new SchedSliceDetailsPanel(this.trace, this.threads);
+  }
+}
+
+// Creates a diagonal hatched pattern to be used for distinguishing slices with
+// real-time priorities. The pattern is created once as an offscreen canvas and
+// is kept cached inside the Context2D of the main canvas, without making
+// assumptions on the lifetime of the main canvas.
+function getHatchedPattern(mainCtx: CanvasRenderingContext2D): CanvasPattern {
+  const mctx = mainCtx as CanvasRenderingContext2D & {
+    sliceHatchedPattern?: CanvasPattern;
+  };
+  if (mctx.sliceHatchedPattern !== undefined) return mctx.sliceHatchedPattern;
+  const canvas = document.createElement('canvas');
+  const SIZE = 8;
+  canvas.width = canvas.height = SIZE;
+  const ctx = assertExists(canvas.getContext('2d'));
+  ctx.strokeStyle = 'rgba(255,255,255,0.3)';
+  ctx.beginPath();
+  ctx.lineWidth = 1;
+  ctx.moveTo(0, SIZE);
+  ctx.lineTo(SIZE, 0);
+  ctx.stroke();
+  mctx.sliceHatchedPattern = assertExists(mctx.createPattern(canvas, 'repeat'));
+  return mctx.sliceHatchedPattern;
+}
diff --git a/ui/src/plugins/dev.perfetto.CpuSlices/index.ts b/ui/src/plugins/dev.perfetto.CpuSlices/index.ts
new file mode 100644
index 0000000..4e3f3d3
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/index.ts
@@ -0,0 +1,113 @@
+// 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 {CPU_SLICE_TRACK_KIND} from '../../public/track_kinds';
+import {Engine} from '../../trace_processor/engine';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {NUM, STR_NULL} from '../../trace_processor/query_result';
+import {CpuSliceTrack} from './cpu_slice_track';
+import {TrackNode} from '../../public/workspace';
+import {CpuSliceSelectionAggregator} from './cpu_slice_selection_aggregator';
+import {CpuSliceByProcessSelectionAggregator} from './cpu_slice_by_process_selection_aggregator';
+import ThreadPlugin from '../dev.perfetto.Thread';
+
+function uriForSchedTrack(cpu: number): string {
+  return `/sched_cpu${cpu}`;
+}
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.CpuSlices';
+  static readonly dependencies = [ThreadPlugin];
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    ctx.selection.registerAreaSelectionAggreagtor(
+      new CpuSliceSelectionAggregator(),
+    );
+    ctx.selection.registerAreaSelectionAggreagtor(
+      new CpuSliceByProcessSelectionAggregator(),
+    );
+
+    const cpus = ctx.traceInfo.cpus;
+    const cpuToClusterType = await this.getAndroidCpuClusterTypes(ctx.engine);
+
+    for (const cpu of cpus) {
+      const size = cpuToClusterType.get(cpu);
+      const uri = uriForSchedTrack(cpu);
+
+      const threads = ctx.plugins.getPlugin(ThreadPlugin).getThreadMap();
+
+      const name = size === undefined ? `Cpu ${cpu}` : `Cpu ${cpu} (${size})`;
+      ctx.tracks.registerTrack({
+        uri,
+        title: name,
+        tags: {
+          kind: CPU_SLICE_TRACK_KIND,
+          cpu,
+        },
+        track: new CpuSliceTrack(ctx, uri, cpu, threads),
+      });
+      const trackNode = new TrackNode({uri, title: name, sortOrder: -50});
+      ctx.workspace.addChildInOrder(trackNode);
+    }
+
+    ctx.selection.registerSqlSelectionResolver({
+      sqlTableName: 'sched_slice',
+      callback: async (id: number) => {
+        const result = await ctx.engine.query(`
+          select
+            cpu
+          from sched_slice
+          where id = ${id}
+        `);
+
+        const cpu = result.firstRow({
+          cpu: NUM,
+        }).cpu;
+
+        return {
+          eventId: id,
+          trackUri: uriForSchedTrack(cpu),
+        };
+      },
+    });
+  }
+
+  async getAndroidCpuClusterTypes(
+    engine: Engine,
+  ): Promise<Map<number, string>> {
+    const cpuToClusterType = new Map<number, string>();
+    await engine.query(`
+      include perfetto module android.cpu.cluster_type;
+    `);
+    const result = await engine.query(`
+      select cpu, cluster_type as clusterType
+      from android_cpu_cluster_mapping
+    `);
+
+    const it = result.iter({
+      cpu: NUM,
+      clusterType: STR_NULL,
+    });
+
+    for (; it.valid(); it.next()) {
+      const clusterType = it.clusterType;
+      if (clusterType !== null) {
+        cpuToClusterType.set(it.cpu, clusterType);
+      }
+    }
+
+    return cpuToClusterType;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts b/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts
new file mode 100644
index 0000000..f4b3b93
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts
@@ -0,0 +1,264 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use size 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 {DetailsShell} from '../../widgets/details_shell';
+import {GridLayout} from '../../widgets/grid_layout';
+import {Section} from '../../widgets/section';
+import {SqlRef} from '../../widgets/sql_ref';
+import {Tree, TreeNode} from '../../widgets/tree';
+import {DurationWidget} from '../../frontend/widgets/duration';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {asSchedSqlId} from '../../trace_processor/sql_utils/core_types';
+import {
+  getSched,
+  getSchedWakeupInfo,
+  Sched,
+  SchedWakeupInfo,
+} from '../../trace_processor/sql_utils/sched';
+import {exists} from '../../base/utils';
+import {translateState} from '../../trace_processor/sql_utils/thread_state';
+import {Trace} from '../../public/trace';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {TrackEventSelection} from '../../public/selection';
+import {ThreadDesc, ThreadMap} from '../dev.perfetto.Thread/threads';
+import {assetSrc} from '../../base/assets';
+
+const MIN_NORMAL_SCHED_PRIORITY = 100;
+
+function getDisplayName(
+  name: string | undefined,
+  id: number | undefined,
+): string | undefined {
+  if (name === undefined) {
+    return id === undefined ? undefined : `${id}`;
+  } else {
+    return id === undefined ? name : `${name} ${id}`;
+  }
+}
+
+interface Data {
+  sched: Sched;
+  wakeup?: SchedWakeupInfo;
+}
+
+export class SchedSliceDetailsPanel implements TrackEventDetailsPanel {
+  private details?: Data;
+
+  constructor(
+    private readonly trace: Trace,
+    private readonly threads: ThreadMap,
+  ) {}
+
+  async load({eventId}: TrackEventSelection) {
+    const sched = await getSched(this.trace.engine, asSchedSqlId(eventId));
+    if (sched === undefined) {
+      return;
+    }
+    const wakeup = await getSchedWakeupInfo(this.trace.engine, sched);
+    this.details = {sched, wakeup};
+    this.trace.scheduleFullRedraw();
+  }
+
+  render() {
+    if (this.details === undefined) {
+      return m(DetailsShell, {title: 'Sched', description: 'Loading...'});
+    }
+    const threadInfo = this.threads.get(this.details.sched.thread.utid);
+
+    return m(
+      DetailsShell,
+      {
+        title: 'CPU Sched Slice',
+        description: this.renderTitle(this.details),
+      },
+      m(
+        GridLayout,
+        this.renderDetails(this.details, threadInfo),
+        this.renderSchedLatencyInfo(this.details),
+      ),
+    );
+  }
+
+  private renderTitle(data: Data) {
+    const threadInfo = this.threads.get(data.sched.thread.utid);
+    if (!threadInfo) {
+      return null;
+    }
+    return `${threadInfo.procName} [${threadInfo.pid}]`;
+  }
+
+  private renderSchedLatencyInfo(data: Data): m.Children {
+    if (
+      data.wakeup?.wakeupTs === undefined ||
+      data.wakeup?.wakerUtid === undefined
+    ) {
+      return null;
+    }
+    return m(
+      Section,
+      {title: 'Scheduling Latency'},
+      m(
+        '.slice-details-latency-panel',
+        m('img.slice-details-image', {
+          src: assetSrc('assets/scheduling_latency.png'),
+        }),
+        this.renderWakeupText(data),
+        this.renderDisplayLatencyText(data),
+      ),
+    );
+  }
+
+  private renderWakeupText(data: Data): m.Children {
+    if (
+      data.wakeup?.wakerUtid === undefined ||
+      data.wakeup?.wakeupTs === undefined ||
+      data.wakeup?.wakerCpu === undefined
+    ) {
+      return null;
+    }
+    const threadInfo = this.threads.get(data.wakeup.wakerUtid);
+    if (!threadInfo) {
+      return null;
+    }
+    return m(
+      '.slice-details-wakeup-text',
+      m(
+        '',
+        `Wakeup @ `,
+        m(Timestamp, {ts: data.wakeup?.wakeupTs}),
+        ` on CPU ${data.wakeup.wakerCpu} by`,
+      ),
+      m('', `P: ${threadInfo.procName} [${threadInfo.pid}]`),
+      m('', `T: ${threadInfo.threadName} [${threadInfo.tid}]`),
+    );
+  }
+
+  private renderDisplayLatencyText(data: Data): m.Children {
+    if (data.wakeup?.wakeupTs === undefined) {
+      return null;
+    }
+
+    const latency = data.sched.ts - data.wakeup?.wakeupTs;
+    return m(
+      '.slice-details-latency-text',
+      m('', `Scheduling latency: `, m(DurationWidget, {dur: latency})),
+      m(
+        '.text-detail',
+        `This is the interval from when the task became eligible to run
+        (e.g. because of notifying a wait queue it was suspended on) to
+        when it started running.`,
+      ),
+    );
+  }
+
+  private renderPriorityText(priority?: number) {
+    if (priority === undefined) {
+      return undefined;
+    }
+    return priority < MIN_NORMAL_SCHED_PRIORITY
+      ? `${priority} (real-time)`
+      : `${priority}`;
+  }
+
+  protected getProcessThreadDetails(data: Data) {
+    const process = data.sched.thread.process;
+    return new Map<string, string | undefined>([
+      ['Thread', getDisplayName(data.sched.thread.name, data.sched.thread.tid)],
+      ['Process', getDisplayName(process?.name, process?.pid)],
+      ['User ID', exists(process?.uid) ? String(process?.uid) : undefined],
+      ['Package name', process?.packageName],
+      [
+        'Version code',
+        process?.versionCode !== undefined
+          ? String(process?.versionCode)
+          : undefined,
+      ],
+    ]);
+  }
+
+  private renderDetails(data: Data, threadInfo?: ThreadDesc): m.Children {
+    if (!threadInfo) {
+      return null;
+    }
+
+    const extras: m.Children = [];
+
+    for (const [key, value] of this.getProcessThreadDetails(data)) {
+      if (value !== undefined) {
+        extras.push(m(TreeNode, {left: key, right: value}));
+      }
+    }
+
+    const treeNodes = [
+      m(TreeNode, {
+        left: 'Process',
+        right: `${threadInfo.procName} [${threadInfo.pid}]`,
+      }),
+      m(TreeNode, {
+        left: 'Thread',
+        right: m(
+          Anchor,
+          {
+            icon: 'call_made',
+            onclick: () => {
+              this.goToThread(data);
+            },
+          },
+          `${threadInfo.threadName} [${threadInfo.tid}]`,
+        ),
+      }),
+      m(TreeNode, {
+        left: 'Cmdline',
+        right: threadInfo.cmdline,
+      }),
+      m(TreeNode, {
+        left: 'Start time',
+        right: m(Timestamp, {ts: data.sched.ts}),
+      }),
+      m(TreeNode, {
+        left: 'Duration',
+        right: m(DurationWidget, {dur: data.sched.dur}),
+      }),
+      m(TreeNode, {
+        left: 'Priority',
+        right: this.renderPriorityText(data.sched.priority),
+      }),
+      m(TreeNode, {
+        left: 'End State',
+        right: translateState(data.sched.endState),
+      }),
+      m(TreeNode, {
+        left: 'SQL ID',
+        right: m(SqlRef, {table: 'sched', id: data.sched.id}),
+      }),
+      ...extras,
+    ];
+
+    return m(Section, {title: 'Details'}, m(Tree, treeNodes));
+  }
+
+  goToThread(data: Data) {
+    if (data.sched.threadStateId) {
+      this.trace.selection.selectSqlEvent(
+        'thread_state',
+        data.sched.threadStateId,
+        {scrollToSelection: true},
+      );
+    }
+  }
+
+  renderCanvas() {}
+}
diff --git a/ui/src/plugins/dev.perfetto.CpuidleTimeInState/OWNERS b/ui/src/plugins/dev.perfetto.CpuidleTimeInState/OWNERS
new file mode 100644
index 0000000..4dd7964
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.CpuidleTimeInState/OWNERS
@@ -0,0 +1 @@
+zhaon@google.com
\ No newline at end of file
diff --git a/ui/src/plugins/dev.perfetto.CpuidleTimeInState/index.ts b/ui/src/plugins/dev.perfetto.CpuidleTimeInState/index.ts
new file mode 100644
index 0000000..3e4aaa1
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.CpuidleTimeInState/index.ts
@@ -0,0 +1,81 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {CounterOptions} from '../../frontend/base_counter_track';
+import {TrackNode} from '../../public/workspace';
+import {createQueryCounterTrack} from '../../public/lib/tracks/query_counter_track';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.CpuidleTimeInState';
+  private async addCounterTrack(
+    ctx: Trace,
+    name: string,
+    query: string,
+    group?: TrackNode,
+    options?: Partial<CounterOptions>,
+  ) {
+    const uri = `/cpuidle_time_in_state_${name}`;
+    const track = await createQueryCounterTrack({
+      trace: ctx,
+      uri,
+      data: {
+        sqlSource: query,
+        columns: ['ts', 'value'],
+      },
+      columns: {ts: 'ts', value: 'value'},
+      options,
+    });
+    ctx.tracks.registerTrack({
+      uri,
+      title: name,
+      track,
+    });
+    const trackNode = new TrackNode({uri, title: name});
+    if (group) {
+      group.addChildInOrder(trackNode);
+    }
+  }
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const group = new TrackNode({
+      title: 'Cpuidle Time In State',
+      isSummary: true,
+    });
+
+    const e = ctx.engine;
+    await e.query(`INCLUDE PERFETTO MODULE linux.cpu.idle_time_in_state;`);
+    const result = await e.query(
+      `select distinct state_name from cpu_idle_time_in_state_counters`,
+    );
+    const it = result.iter({state_name: 'str'});
+    for (; it.valid(); it.next()) {
+      this.addCounterTrack(
+        ctx,
+        it.state_name,
+        `select
+            ts,
+            idle_percentage as value
+        from cpu_idle_time_in_state_counters
+        where state_name='${it.state_name}'`,
+        group,
+        {unit: 'percent'},
+      );
+    }
+    if (group.hasChildren) {
+      ctx.workspace.addChildInOrder(group);
+    }
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.CriticalPath/OWNERS b/ui/src/plugins/dev.perfetto.CriticalPath/OWNERS
new file mode 100644
index 0000000..cd23fd9
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.CriticalPath/OWNERS
@@ -0,0 +1,2 @@
+zezeozue@google.com
+lalitm@google.com
\ No newline at end of file
diff --git a/ui/src/plugins/dev.perfetto.CriticalPath/index.ts b/ui/src/plugins/dev.perfetto.CriticalPath/index.ts
new file mode 100644
index 0000000..94acec3
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.CriticalPath/index.ts
@@ -0,0 +1,316 @@
+// 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 {
+  getThreadInfo,
+  ThreadInfo,
+} from '../../trace_processor/sql_utils/thread';
+import {addDebugSliceTrack} from '../../public/debug_tracks';
+import {Trace} from '../../public/trace';
+import {THREAD_STATE_TRACK_KIND} from '../../public/track_kinds';
+import {PerfettoPlugin} from '../../public/plugin';
+import {asUtid, Utid} from '../../trace_processor/sql_utils/core_types';
+import {addQueryResultsTab} from '../../public/lib/query_table/query_result_tab';
+import {showModal} from '../../widgets/modal';
+import {
+  CRITICAL_PATH_CMD,
+  CRITICAL_PATH_LITE_CMD,
+} from '../../public/exposed_commands';
+import {getTimeSpanOfSelectionOrVisibleWindow} from '../../public/utils';
+
+const criticalPathSliceColumns = {
+  ts: 'ts',
+  dur: 'dur',
+  name: 'name',
+};
+
+const criticalPathsliceColumnNames = [
+  'id',
+  'utid',
+  'ts',
+  'dur',
+  'name',
+  'table_name',
+];
+
+const criticalPathsliceLiteColumns = {
+  ts: 'ts',
+  dur: 'dur',
+  name: 'thread_name',
+};
+
+const criticalPathsliceLiteColumnNames = [
+  'id',
+  'utid',
+  'ts',
+  'dur',
+  'thread_name',
+  'process_name',
+  'table_name',
+];
+
+const sliceLiteColumns = {ts: 'ts', dur: 'dur', name: 'thread_name'};
+
+const sliceLiteColumnNames = [
+  'id',
+  'utid',
+  'ts',
+  'dur',
+  'thread_name',
+  'process_name',
+  'table_name',
+];
+
+const sliceColumns = {ts: 'ts', dur: 'dur', name: 'name'};
+
+const sliceColumnNames = ['id', 'utid', 'ts', 'dur', 'name', 'table_name'];
+
+function getFirstUtidOfSelectionOrVisibleWindow(trace: Trace): number {
+  const selection = trace.selection.selection;
+  if (selection.kind === 'area') {
+    for (const trackDesc of selection.tracks) {
+      if (
+        trackDesc?.tags?.kind === THREAD_STATE_TRACK_KIND &&
+        trackDesc?.tags?.utid !== undefined
+      ) {
+        return trackDesc.tags.utid;
+      }
+    }
+  }
+
+  return 0;
+}
+
+function showModalErrorAreaSelectionRequired() {
+  showModal({
+    title: 'Error: range selection required',
+    content:
+      'This command requires an area selection over a thread state track.',
+  });
+}
+
+function showModalErrorThreadStateRequired() {
+  showModal({
+    title: 'Error: thread state selection required',
+    content: 'This command requires a thread state slice to be selected.',
+  });
+}
+
+// If utid is undefined, returns the utid for the selected thread state track,
+// if any. If it's defined, looks up the info about that specific utid.
+async function getThreadInfoForUtidOrSelection(
+  trace: Trace,
+  utid?: Utid,
+): Promise<ThreadInfo | undefined> {
+  if (utid === undefined) {
+    const selection = trace.selection.selection;
+    if (selection.kind === 'track_event') {
+      if (selection.utid !== undefined) {
+        utid = asUtid(selection.utid);
+      }
+    }
+  }
+  if (utid === undefined) return undefined;
+  return getThreadInfo(trace.engine, utid);
+}
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.CriticalPath';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    // The 3 commands below are used in two contextes:
+    // 1. By clicking a slice and using the command palette. In this case the
+    //    utid argument is undefined and we need to look at the selection.
+    // 2. Invoked via runCommand(...) by thread_state_tab.ts when the user
+    //    clicks on the buttons in the details panel. In this case the details
+    //    panel passes the utid explicitly.
+    ctx.commands.registerCommand({
+      id: CRITICAL_PATH_LITE_CMD,
+      name: 'Critical path lite (selected thread state slice)',
+      callback: async (utid?: Utid) => {
+        const thdInfo = await getThreadInfoForUtidOrSelection(ctx, utid);
+        if (thdInfo === undefined) {
+          return showModalErrorThreadStateRequired();
+        }
+        ctx.engine
+          .query(`INCLUDE PERFETTO MODULE sched.thread_executing_span;`)
+          .then(() =>
+            addDebugSliceTrack({
+              trace: ctx,
+              data: {
+                sqlSource: `
+                SELECT
+                  cr.id,
+                  cr.utid,
+                  cr.ts,
+                  cr.dur,
+                  thread.name AS thread_name,
+                  process.name AS process_name,
+                  'thread_state' AS table_name
+                FROM
+                  _thread_executing_span_critical_path(
+                    ${thdInfo.utid},
+                    trace_bounds.start_ts,
+                    trace_bounds.end_ts - trace_bounds.start_ts) cr,
+                  trace_bounds
+                JOIN thread USING(utid)
+                JOIN process USING(upid)
+              `,
+                columns: sliceLiteColumnNames,
+              },
+              title: `${thdInfo.name}`,
+              columns: sliceLiteColumns,
+              argColumns: sliceLiteColumnNames,
+            }),
+          );
+      },
+    });
+
+    ctx.commands.registerCommand({
+      id: CRITICAL_PATH_CMD,
+      name: 'Critical path (selected thread state slice)',
+      callback: async (utid?: Utid) => {
+        const thdInfo = await getThreadInfoForUtidOrSelection(ctx, utid);
+        if (thdInfo === undefined) {
+          return showModalErrorThreadStateRequired();
+        }
+        ctx.engine
+          .query(
+            `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
+          )
+          .then(() =>
+            addDebugSliceTrack({
+              trace: ctx,
+              data: {
+                sqlSource: `
+                SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
+                  FROM
+                    _thread_executing_span_critical_path_stack(
+                      ${thdInfo.utid},
+                      trace_bounds.start_ts,
+                      trace_bounds.end_ts - trace_bounds.start_ts) cr,
+                    trace_bounds WHERE name IS NOT NULL
+              `,
+                columns: sliceColumnNames,
+              },
+              title: `${thdInfo.name}`,
+              columns: sliceColumns,
+              argColumns: sliceColumnNames,
+            }),
+          );
+      },
+    });
+
+    ctx.commands.registerCommand({
+      id: 'perfetto.CriticalPathLite_AreaSelection',
+      name: 'Critical path lite (over area selection)',
+      callback: async () => {
+        const trackUtid = getFirstUtidOfSelectionOrVisibleWindow(ctx);
+        const window = await getTimeSpanOfSelectionOrVisibleWindow(ctx);
+        if (trackUtid === 0) {
+          return showModalErrorAreaSelectionRequired();
+        }
+        await ctx.engine.query(
+          `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
+        );
+        await addDebugSliceTrack({
+          trace: ctx,
+          data: {
+            sqlSource: `
+                SELECT
+                  cr.id,
+                  cr.utid,
+                  cr.ts,
+                  cr.dur,
+                  thread.name AS thread_name,
+                  process.name AS process_name,
+                  'thread_state' AS table_name
+                FROM
+                  _thread_executing_span_critical_path(
+                      ${trackUtid},
+                      ${window.start},
+                      ${window.end} - ${window.start}) cr
+                JOIN thread USING(utid)
+                JOIN process USING(upid)
+                `,
+            columns: criticalPathsliceLiteColumnNames,
+          },
+          title:
+            (await getThreadInfo(ctx.engine, trackUtid as Utid)).name ??
+            '<thread name>',
+          columns: criticalPathsliceLiteColumns,
+          argColumns: criticalPathsliceLiteColumnNames,
+        });
+      },
+    });
+
+    ctx.commands.registerCommand({
+      id: 'perfetto.CriticalPath_AreaSelection',
+      name: 'Critical path  (over area selection)',
+      callback: async () => {
+        const trackUtid = getFirstUtidOfSelectionOrVisibleWindow(ctx);
+        const window = await getTimeSpanOfSelectionOrVisibleWindow(ctx);
+        if (trackUtid === 0) {
+          return showModalErrorAreaSelectionRequired();
+        }
+        await ctx.engine.query(
+          `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
+        );
+        await addDebugSliceTrack({
+          trace: ctx,
+          data: {
+            sqlSource: `
+                SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
+                FROM
+                _critical_path_stack(
+                  ${trackUtid},
+                  ${window.start},
+                  ${window.end} - ${window.start}, 1, 1, 1, 1) cr
+                WHERE name IS NOT NULL
+                `,
+            columns: criticalPathsliceColumnNames,
+          },
+          title:
+            (await getThreadInfo(ctx.engine, trackUtid as Utid)).name ??
+            '<thread name>',
+          columns: criticalPathSliceColumns,
+          argColumns: criticalPathsliceColumnNames,
+        });
+      },
+    });
+
+    ctx.commands.registerCommand({
+      id: 'perfetto.CriticalPathPprof_AreaSelection',
+      name: 'Critical path pprof (over area selection)',
+      callback: async () => {
+        const trackUtid = getFirstUtidOfSelectionOrVisibleWindow(ctx);
+        const window = await getTimeSpanOfSelectionOrVisibleWindow(ctx);
+        if (trackUtid === 0) {
+          return showModalErrorAreaSelectionRequired();
+        }
+        addQueryResultsTab(ctx, {
+          query: `
+              INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;
+              SELECT *
+                FROM
+                  _thread_executing_span_critical_path_graph(
+                  "criical_path",
+                    ${trackUtid},
+                    ${window.start},
+                    ${window.end} - ${window.start}) cr`,
+          title: 'Critical path',
+        });
+      },
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Debug/index.ts b/ui/src/plugins/dev.perfetto.Debug/index.ts
new file mode 100644
index 0000000..90d9ba2
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Debug/index.ts
@@ -0,0 +1,77 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+  addDebugCounterTrack,
+  addDebugSliceTrack,
+} from '../../public/lib/tracks/debug_tracks';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {exists} from '../../base/utils';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.DebugTracks';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    ctx.commands.registerCommand({
+      id: 'perfetto.DebugTracks#addDebugSliceTrack',
+      name: 'Add debug slice track',
+      callback: async (arg: unknown) => {
+        // This command takes a query and creates a debug track out of it The
+        // query can be passed in using the first arg, or if this is not defined
+        // or is the wrong type, we prompt the user for it.
+        const query = await getStringFromArgOrPrompt(ctx, arg);
+        if (exists(query)) {
+          await addDebugSliceTrack({
+            trace: ctx,
+            data: {
+              sqlSource: query,
+            },
+            title: 'Debug slice track',
+          });
+        }
+      },
+    });
+
+    ctx.commands.registerCommand({
+      id: 'perfetto.DebugTracks#addDebugCounterTrack',
+      name: 'Add debug counter track',
+      callback: async (arg: unknown) => {
+        const query = await getStringFromArgOrPrompt(ctx, arg);
+        if (exists(query)) {
+          await addDebugCounterTrack({
+            trace: ctx,
+            data: {
+              sqlSource: query,
+            },
+            title: 'Debug slice track',
+          });
+        }
+      },
+    });
+  }
+}
+
+// If arg is a string, return it, otherwise prompt the user for a string. An
+// exception is thrown if the prompt is cancelled, so this function handles this
+// and returns undefined in this case.
+async function getStringFromArgOrPrompt(
+  ctx: Trace,
+  arg: unknown,
+): Promise<string | undefined> {
+  if (typeof arg === 'string') {
+    return arg;
+  } else {
+    return await ctx.omnibox.prompt('Enter a query...');
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.DeeplinkQuerystring/index.ts b/ui/src/plugins/dev.perfetto.DeeplinkQuerystring/index.ts
new file mode 100644
index 0000000..f351854
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.DeeplinkQuerystring/index.ts
@@ -0,0 +1,125 @@
+// 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.
+
+// When deep linking into Perfetto UI it is possible to pass arguments in the
+// query string to automatically select a slice or run a query once the
+// trace is loaded. This plugin deals with kicking off the relevant logic
+// once the trace has loaded.
+
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {addQueryResultsTab} from '../../public/lib/query_table/query_result_tab';
+import {Time} from '../../base/time';
+import {RouteArgs} from '../../public/route_schema';
+import {App} from '../../public/app';
+import {exists} from '../../base/utils';
+import {NUM} from '../../trace_processor/query_result';
+
+let routeArgsForFirstTrace: RouteArgs | undefined;
+
+/**
+ * Uses URL args (table, ts, dur) to select events on trace load.
+ *
+ * E.g. ?table=thread_state&ts=39978672284068&dur=18995809
+ *
+ * Note: `ts` and `dur` are used rather than id as id is not stable over TP
+ * versions.
+ *
+ * The table passed must have `ts`, `dur` (if a dur value is supplied) and `id`
+ * columns, and SQL resolvers must be available for those tables (usually from
+ * plugins).
+ */
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.DeeplinkQuerystring';
+
+  static onActivate(app: App): void {
+    routeArgsForFirstTrace = app.initialRouteArgs;
+  }
+
+  async onTraceLoad(trace: Trace) {
+    trace.addEventListener('traceready', async () => {
+      const initialRouteArgs = routeArgsForFirstTrace;
+      routeArgsForFirstTrace = undefined;
+      if (initialRouteArgs === undefined) return;
+
+      await selectInitialRouteArgs(trace, initialRouteArgs);
+      if (
+        initialRouteArgs.visStart !== undefined &&
+        initialRouteArgs.visEnd !== undefined
+      ) {
+        zoomPendingDeeplink(
+          trace,
+          initialRouteArgs.visStart,
+          initialRouteArgs.visEnd,
+        );
+      }
+      if (initialRouteArgs.query !== undefined) {
+        addQueryResultsTab(trace, {
+          query: initialRouteArgs.query,
+          title: 'Deeplink Query',
+        });
+      }
+    });
+  }
+}
+
+function zoomPendingDeeplink(trace: Trace, visStart: string, visEnd: string) {
+  const visualStart = Time.fromRaw(BigInt(visStart));
+  const visualEnd = Time.fromRaw(BigInt(visEnd));
+  if (
+    !(
+      visualStart < visualEnd &&
+      trace.traceInfo.start <= visualStart &&
+      visualEnd <= trace.traceInfo.end
+    )
+  ) {
+    return;
+  }
+  trace.timeline.setViewportTime(visualStart, visualEnd);
+}
+
+async function selectInitialRouteArgs(trace: Trace, args: RouteArgs) {
+  const {table = 'slice', ts, dur} = args;
+
+  // We need at least a ts
+  if (!exists(ts)) {
+    return;
+  }
+
+  const conditions = [];
+  conditions.push(`ts = ${ts}`);
+  exists(dur) && conditions.push(`dur = ${dur}`);
+
+  // Find the id of the slice with this ts & dur in the given table
+  const result = await trace.engine.query(`
+    select
+      id
+    from
+      ${table}
+    where ${conditions.join(' AND ')}
+  `);
+
+  if (result.numRows() === 0) {
+    return;
+  }
+
+  const {id} = result.firstRow({
+    id: NUM,
+  });
+
+  trace.selection.selectSqlEvent(table, id, {
+    scrollToSelection: true,
+    switchToCurrentSelectionTab: false,
+  });
+}
diff --git a/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts b/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
deleted file mode 100644
index fef903c..0000000
--- a/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
-
-// This is just an example plugin, used to prove that the plugin system works.
-class ExampleSimpleCommand implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.registerCommand({
-      id: 'dev.perfetto.ExampleSimpleCommand#LogHelloWorld',
-      name: 'Log "Hello, world!"',
-      callback: () => console.log('Hello, world!'),
-    });
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.ExampleSimpleCommand',
-  plugin: ExampleSimpleCommand,
-};
diff --git a/ui/src/plugins/dev.perfetto.ExampleState/index.ts b/ui/src/plugins/dev.perfetto.ExampleState/index.ts
deleted file mode 100644
index 5b0aae1..0000000
--- a/ui/src/plugins/dev.perfetto.ExampleState/index.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {exists} from '../../base/utils';
-import {
-  createStore,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  Store,
-} from '../../public';
-
-interface State {
-  counter: number;
-}
-
-// This example plugin shows using state that is persisted in the
-// permalink.
-class ExampleState implements Plugin {
-  private store: Store<State> = createStore({counter: 0});
-
-  private migrate(initialState: unknown): State {
-    if (
-      exists(initialState) &&
-      typeof initialState === 'object' &&
-      'counter' in initialState &&
-      typeof initialState.counter === 'number'
-    ) {
-      return {counter: initialState.counter};
-    } else {
-      return {counter: 0};
-    }
-  }
-
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    this.store = ctx.mountStore((init: unknown) => this.migrate(init));
-
-    ctx.registerCommand({
-      id: 'dev.perfetto.ExampleState#ShowCounter',
-      name: 'Show ExampleState counter',
-      callback: () => {
-        const counter = this.store.state.counter;
-        ctx.tabs.openQuery(
-          `SELECT ${counter} as counter;`,
-          `Show counter ${counter}`,
-        );
-        this.store.edit((draft) => {
-          ++draft.counter;
-        });
-      },
-    });
-  }
-
-  async onTraceUnload(_: PluginContextTrace): Promise<void> {
-    this.store[Symbol.dispose]();
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.ExampleState',
-  plugin: ExampleState,
-};
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/OWNERS b/ui/src/plugins/dev.perfetto.ExplorePage/OWNERS
new file mode 100644
index 0000000..99ba254
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/OWNERS
@@ -0,0 +1 @@
+lydiatse@google.com
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts b/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
new file mode 100644
index 0000000..5b1d0d4
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.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 {PageWithTraceAttrs} from '../../public/page';
+import {Trace} from '../../public/trace';
+import {
+  DurationColumn,
+  ProcessColumnSet,
+  StandardColumn,
+  ThreadColumnSet,
+  TimestampColumn,
+} from '../../frontend/widgets/sql/table/well_known_columns';
+import {SqlTableState} from '../../frontend/widgets/sql/table/state';
+import {SqlTable} from '../../frontend/widgets/sql/table/table';
+import {exists} from '../../base/utils';
+import {Menu, MenuItem, MenuItemAttrs} from '../../widgets/menu';
+import {
+  TableColumn,
+  TableColumnSet,
+} from '../../frontend/widgets/sql/table/column';
+import {Button} from '../../widgets/button';
+import {Icons} from '../../base/semantic_icons';
+import {DetailsShell} from '../../widgets/details_shell';
+
+interface ExplorePageState {
+  sqlTableState?: SqlTableState;
+  selectedTable?: ExplorableTable;
+}
+
+interface ExplorableTable {
+  name: string;
+  module: string;
+  columns: (TableColumn | TableColumnSet)[];
+}
+
+export class ExplorePage implements m.ClassComponent<PageWithTraceAttrs> {
+  private readonly state: ExplorePageState;
+
+  constructor() {
+    this.state = {
+      sqlTableState: undefined,
+      selectedTable: undefined,
+    };
+  }
+
+  // Show menu with standard library tables
+  private renderSelectableTablesMenuItems(
+    trace: Trace,
+  ): m.Vnode<MenuItemAttrs, unknown>[] {
+    // TODO (lydiatse@): The following is purely for prototyping and
+    // should be derived from the actual stdlib itself rather than
+    // being hardcoded.
+    const explorableTables: ExplorableTable[] = [
+      {
+        name: 'android_binder_txns',
+        module: 'android.binder',
+        columns: [
+          new StandardColumn('aidl_name'),
+          new StandardColumn('aidl_ts'),
+          new StandardColumn('aidl_dur'),
+          new StandardColumn('binder_txn_id', {startsHidden: true}),
+          new ProcessColumnSet('client_upid', {title: 'client_upid'}),
+          new ThreadColumnSet('client_utid', {title: 'client_utid'}),
+          new StandardColumn('is_main_thread'),
+          new TimestampColumn('client_ts'),
+          new DurationColumn('client_dur'),
+          new StandardColumn('binder_reply_id', {startsHidden: true}),
+          new ProcessColumnSet('server_upid', {title: 'server_upid'}),
+          new ThreadColumnSet('server_utid', {title: 'server_utid'}),
+          new TimestampColumn('server_ts'),
+          new DurationColumn('server_dur'),
+          new StandardColumn('client_oom_score', {aggregationType: 'nominal'}),
+          new StandardColumn('server_oom_score', {aggregationType: 'nominal'}),
+          new StandardColumn('is_sync', {startsHidden: true}),
+          new StandardColumn('client_monotonic_dur', {startsHidden: true}),
+          new StandardColumn('server_monotonic_dur', {startsHidden: true}),
+          new StandardColumn('client_package_version_code', {
+            startsHidden: true,
+          }),
+          new StandardColumn('server_package_version_code', {
+            startsHidden: true,
+          }),
+          new StandardColumn('is_client_package_debuggable', {
+            startsHidden: true,
+          }),
+          new StandardColumn('is_server_package_debuggable', {
+            startsHidden: true,
+          }),
+        ],
+      },
+    ];
+
+    return explorableTables.map((table) => {
+      return m(MenuItem, {
+        label: table.name,
+        onclick: () => {
+          if (
+            this.state.selectedTable &&
+            table.name === this.state.selectedTable.name
+          ) {
+            return;
+          }
+
+          this.state.selectedTable = table;
+
+          const sqlTableState = new SqlTableState(
+            trace,
+            {
+              name: table.name,
+              columns: table.columns,
+            },
+            {imports: [table.module]},
+          );
+          this.state.sqlTableState = sqlTableState;
+        },
+      });
+    });
+  }
+
+  private renderSqlTable() {
+    const sqlTableState = this.state.sqlTableState;
+
+    if (sqlTableState === undefined) return;
+
+    const range = sqlTableState.getDisplayedRange();
+    const rowCount = sqlTableState.getTotalRowCount();
+
+    const navigation = [
+      exists(range) &&
+        exists(rowCount) &&
+        `Showing rows ${range.from}-${range.to} of ${rowCount}`,
+      m(Button, {
+        icon: Icons.GoBack,
+        disabled: !sqlTableState.canGoBack(),
+        onclick: () => sqlTableState!.goBack(),
+      }),
+      m(Button, {
+        icon: Icons.GoForward,
+        disabled: !sqlTableState.canGoForward(),
+        onclick: () => sqlTableState!.goForward(),
+      }),
+    ];
+
+    return m(
+      DetailsShell,
+      {
+        title: 'Explore Table',
+        buttons: navigation,
+        fillParent: false,
+      },
+      m(SqlTable, {
+        state: sqlTableState,
+      }),
+    );
+  }
+
+  view({attrs}: m.CVnode<PageWithTraceAttrs>) {
+    return m(
+      '.explore-page',
+      m(Menu, this.renderSelectableTablesMenuItems(attrs.trace)),
+      this.state.selectedTable && this.renderSqlTable(),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/index.ts b/ui/src/plugins/dev.perfetto.ExplorePage/index.ts
new file mode 100644
index 0000000..5c18701
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/index.ts
@@ -0,0 +1,31 @@
+// 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 {Trace} from '../../public/trace';
+import {ExplorePage} from './explore_page';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.ExplorePage';
+
+  async onTraceLoad(trace: Trace): Promise<void> {
+    trace.pages.registerPage({route: '/explore', page: ExplorePage});
+    trace.sidebar.addMenuItem({
+      section: 'current_trace',
+      text: 'Explore',
+      href: '#!/explore',
+      icon: 'data_exploration',
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts b/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
new file mode 100644
index 0000000..d75dd77
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
@@ -0,0 +1,144 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {HSLColor} from '../../public/color';
+import {makeColorScheme} from '../../public/lib/colorizer';
+import {ColorScheme} from '../../public/color_scheme';
+import {NAMED_ROW, NamedSliceTrack} from '../../frontend/named_slice_track';
+import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../frontend/slice_layout';
+import {STR_NULL} from '../../trace_processor/query_result';
+import {Slice} from '../../public/track';
+import {Trace} from '../../public/trace';
+import {TrackEventDetails} from '../../public/selection';
+
+// 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
+// full jank)
+const BLUE_500 = makeColorScheme(new HSLColor('#03A9F4'));
+const BLUE_200 = makeColorScheme(new HSLColor('#90CAF9'));
+const GREEN_500 = makeColorScheme(new HSLColor('#4CAF50'));
+const GREEN_200 = makeColorScheme(new HSLColor('#A5D6A7'));
+const YELLOW_500 = makeColorScheme(new HSLColor('#FFEB3B'));
+const YELLOW_100 = makeColorScheme(new HSLColor('#FFF9C4'));
+const RED_500 = makeColorScheme(new HSLColor('#FF5722'));
+const RED_200 = makeColorScheme(new HSLColor('#EF9A9A'));
+const LIGHT_GREEN_500 = makeColorScheme(new HSLColor('#C0D588'));
+const LIGHT_GREEN_100 = makeColorScheme(new HSLColor('#DCEDC8'));
+const PINK_500 = makeColorScheme(new HSLColor('#F515E0'));
+const PINK_200 = makeColorScheme(new HSLColor('#F48FB1'));
+
+export const ACTUAL_FRAME_ROW = {
+  // Base columns (tsq, ts, dur, id, depth).
+  ...NAMED_ROW,
+
+  // Jank-specific columns.
+  jankTag: STR_NULL,
+  jankSeverityType: STR_NULL,
+};
+export type ActualFrameRow = typeof ACTUAL_FRAME_ROW;
+
+export class ActualFramesTrack extends NamedSliceTrack<Slice, ActualFrameRow> {
+  constructor(
+    trace: Trace,
+    maxDepth: number,
+    uri: string,
+    private trackIds: number[],
+  ) {
+    super({trace, uri});
+    this.sliceLayout = {
+      ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
+      depthGuess: maxDepth,
+    };
+  }
+
+  // This is used by the base class to call iter().
+  protected getRowSpec() {
+    return ACTUAL_FRAME_ROW;
+  }
+
+  getSqlSource(): string {
+    return `
+      SELECT
+        s.ts as ts,
+        s.dur as dur,
+        s.layout_depth as depth,
+        s.name as name,
+        s.id as id,
+        afs.jank_tag as jankTag,
+        afs.jank_severity_type as jankSeverityType
+      from experimental_slice_layout s
+      join actual_frame_timeline_slice afs using(id)
+      where
+        filter_track_ids = '${this.trackIds.join(',')}'
+    `;
+  }
+
+  rowToSlice(row: ActualFrameRow): Slice {
+    const baseSlice = this.rowToSliceBase(row);
+    return {
+      ...baseSlice,
+      colorScheme: getColorSchemeForJank(row.jankTag, row.jankSeverityType),
+    };
+  }
+
+  override async getSelectionDetails(
+    id: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const baseDetails = await super.getSelectionDetails(id);
+    if (!baseDetails) return undefined;
+    return {
+      ...baseDetails,
+      tableName: 'slice',
+    };
+  }
+}
+
+function getColorSchemeForJank(
+  jankTag: string | null,
+  jankSeverityType: string | null,
+): ColorScheme {
+  if (jankSeverityType === 'Partial') {
+    switch (jankTag) {
+      case 'Self Jank':
+        return RED_200;
+      case 'Other Jank':
+        return YELLOW_100;
+      case 'Dropped Frame':
+        return BLUE_200;
+      case 'Buffer Stuffing':
+      case 'SurfaceFlinger Stuffing':
+        return LIGHT_GREEN_100;
+      case 'No Jank': // should not happen
+        return GREEN_200;
+      default:
+        return PINK_200;
+    }
+  } else {
+    switch (jankTag) {
+      case 'Self Jank':
+        return RED_500;
+      case 'Other Jank':
+        return YELLOW_500;
+      case 'Dropped Frame':
+        return BLUE_500;
+      case 'Buffer Stuffing':
+      case 'SurfaceFlinger Stuffing':
+        return LIGHT_GREEN_500;
+      case 'No Jank':
+        return GREEN_500;
+      default:
+        return PINK_500;
+    }
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts b/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts
new file mode 100644
index 0000000..ff04311
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts
@@ -0,0 +1,76 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {HSLColor} from '../../public/color';
+import {makeColorScheme} from '../../public/lib/colorizer';
+import {
+  NAMED_ROW,
+  NamedRow,
+  NamedSliceTrack,
+} from '../../frontend/named_slice_track';
+import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../frontend/slice_layout';
+import {Slice} from '../../public/track';
+import {Trace} from '../../public/trace';
+import {TrackEventDetails} from '../../public/selection';
+
+const GREEN = makeColorScheme(new HSLColor('#4CAF50')); // Green 500
+
+export class ExpectedFramesTrack extends NamedSliceTrack {
+  constructor(
+    trace: Trace,
+    maxDepth: number,
+    uri: string,
+    private trackIds: number[],
+  ) {
+    super({trace, uri});
+    this.sliceLayout = {
+      ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
+      depthGuess: maxDepth,
+    };
+  }
+
+  getSqlSource(): string {
+    return `
+      SELECT
+        ts,
+        dur,
+        layout_depth as depth,
+        name,
+        id
+      from experimental_slice_layout
+      where
+        filter_track_ids = '${this.trackIds.join(',')}'
+    `;
+  }
+
+  rowToSlice(row: NamedRow): Slice {
+    const baseSlice = this.rowToSliceBase(row);
+    return {...baseSlice, colorScheme: GREEN};
+  }
+
+  getRowSpec(): NamedRow {
+    return NAMED_ROW;
+  }
+
+  override async getSelectionDetails(
+    id: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const baseDetails = await super.getSelectionDetails(id);
+    if (!baseDetails) return undefined;
+    return {
+      ...baseDetails,
+      tableName: 'slice',
+    };
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Frames/frame_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.Frames/frame_selection_aggregator.ts
new file mode 100644
index 0000000..6bf4eba
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Frames/frame_selection_aggregator.ts
@@ -0,0 +1,96 @@
+// 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 {ColumnDef, Sorting} from '../../public/aggregation';
+import {AreaSelection} from '../../public/selection';
+import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../../public/track_kinds';
+import {Engine} from '../../trace_processor/engine';
+import {AreaSelectionAggregator} from '../../public/selection';
+
+export class FrameSelectionAggregator implements AreaSelectionAggregator {
+  readonly id = 'frame_aggregation';
+
+  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;
+
+    await engine.query(`
+      create or replace perfetto table ${this.id} as
+      select
+        jank_type,
+        count(1) as occurrences,
+        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}
+        AND ts < ${area.end}
+      group by jank_type
+    `);
+    return true;
+  }
+
+  getTabName() {
+    return 'Frames';
+  }
+
+  async getExtra() {}
+
+  getDefaultSorting(): Sorting {
+    return {column: 'occurrences', direction: 'DESC'};
+  }
+
+  getColumnDefinitions(): ColumnDef[] {
+    return [
+      {
+        title: 'Jank Type',
+        kind: 'STRING',
+        columnConstructor: Uint16Array,
+        columnId: 'jank_type',
+      },
+      {
+        title: 'Min duration',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'minDur',
+      },
+      {
+        title: 'Max duration',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'maxDur',
+      },
+      {
+        title: 'Mean duration',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'meanDur',
+      },
+      {
+        title: 'Occurrences',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'occurrences',
+        sum: true,
+      },
+    ];
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Frames/index.ts b/ui/src/plugins/dev.perfetto.Frames/index.ts
new file mode 100644
index 0000000..8d3abec
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Frames/index.ts
@@ -0,0 +1,159 @@
+// 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 {
+  ACTUAL_FRAMES_SLICE_TRACK_KIND,
+  EXPECTED_FRAMES_SLICE_TRACK_KIND,
+} from '../../public/track_kinds';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {getOrCreateGroupForProcess} from '../../public/standard_groups';
+import {getTrackName} from '../../public/utils';
+import {TrackNode} from '../../public/workspace';
+import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
+import {ActualFramesTrack} from './actual_frames_track';
+import {ExpectedFramesTrack} from './expected_frames_track';
+import {FrameSelectionAggregator} from './frame_selection_aggregator';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.Frames';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    this.addExpectedFrames(ctx);
+    this.addActualFrames(ctx);
+    ctx.selection.registerAreaSelectionAggreagtor(
+      new FrameSelectionAggregator(),
+    );
+  }
+
+  async addExpectedFrames(ctx: Trace): Promise<void> {
+    const {engine} = ctx;
+    const result = await engine.query(`
+      select
+        upid,
+        t.name as trackName,
+        t.track_ids as trackIds,
+        process.name as processName,
+        process.pid as pid,
+        __max_layout_depth(t.track_count, t.track_ids) as maxDepth
+      from _process_track_summary_by_upid_and_parent_id_and_name t
+      join process using(upid)
+      where t.name = "Expected Timeline"
+    `);
+
+    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`;
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        track: new ExpectedFramesTrack(ctx, maxDepth, uri, trackIds),
+        tags: {
+          trackIds,
+          upid,
+          kind: EXPECTED_FRAMES_SLICE_TRACK_KIND,
+        },
+      });
+      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
+      const track = new TrackNode({uri, title, sortOrder: -50});
+      group.addChildInOrder(track);
+    }
+  }
+
+  async addActualFrames(ctx: Trace): Promise<void> {
+    const {engine} = ctx;
+    const result = await engine.query(`
+      select
+        upid,
+        t.name as trackName,
+        t.track_ids as trackIds,
+        process.name as processName,
+        process.pid as pid,
+        __max_layout_depth(t.track_count, t.track_ids) as maxDepth
+      from _process_track_summary_by_upid_and_parent_id_and_name t
+      join process using(upid)
+      where t.name = "Actual Timeline"
+    `);
+
+    const it = result.iter({
+      upid: NUM,
+      trackName: STR_NULL,
+      trackIds: STR,
+      processName: STR_NULL,
+      pid: NUM_NULL,
+      maxDepth: NUM_NULL,
+    });
+    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`;
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        track: new ActualFramesTrack(ctx, maxDepth, uri, trackIds),
+        tags: {
+          upid,
+          trackIds,
+          kind: ACTUAL_FRAMES_SLICE_TRACK_KIND,
+        },
+      });
+      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
+      const track = new TrackNode({uri, title, sortOrder: -50});
+      group.addChildInOrder(track);
+    }
+  }
+}
diff --git a/ui/src/core_plugins/ftrace/common.ts b/ui/src/plugins/dev.perfetto.Ftrace/common.ts
similarity index 100%
rename from ui/src/core_plugins/ftrace/common.ts
rename to ui/src/plugins/dev.perfetto.Ftrace/common.ts
diff --git a/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts
new file mode 100644
index 0000000..b4036e5
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts
@@ -0,0 +1,324 @@
+// 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 {time, Time} from '../../base/time';
+import {colorForFtrace} from '../../public/lib/colorizer';
+import {DetailsShell} from '../../widgets/details_shell';
+import {
+  MultiSelectDiff,
+  Option as MultiSelectOption,
+  PopupMultiSelect,
+} from '../../widgets/multiselect';
+import {PopupPosition} from '../../widgets/popup';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {FtraceFilter, FtraceStat} from './common';
+import {Engine} from '../../trace_processor/engine';
+import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result';
+import {AsyncLimiter} from '../../base/async_limiter';
+import {Monitor} from '../../base/monitor';
+import {Button} from '../../widgets/button';
+import {VirtualTable, VirtualTableRow} from '../../widgets/virtual_table';
+import {Store} from '../../base/store';
+import {Trace} from '../../public/trace';
+
+const ROW_H = 20;
+
+interface FtraceExplorerAttrs {
+  cache: FtraceExplorerCache;
+  filterStore: Store<FtraceFilter>;
+  trace: Trace;
+}
+
+interface FtraceEvent {
+  id: number;
+  ts: time;
+  name: string;
+  cpu: number;
+  thread: string | null;
+  process: string | null;
+  args: string;
+}
+
+interface FtracePanelData {
+  events: FtraceEvent[];
+  offset: number;
+  numEvents: number; // Number of events in the visible window
+}
+
+interface Pagination {
+  offset: number;
+  count: number;
+}
+
+export interface FtraceExplorerCache {
+  state: 'blank' | 'loading' | 'valid';
+  counters: FtraceStat[];
+}
+
+async function getFtraceCounters(engine: Engine): Promise<FtraceStat[]> {
+  // TODO(stevegolton): this is an extraordinarily slow query on large traces
+  // as it goes through every ftrace event which can be a lot on big traces.
+  // Consider if we can have some different UX which avoids needing these
+  // counts
+  // TODO(mayzner): the +name below is an awful hack to workaround
+  // extraordinarily slow sorting of strings. However, even with this hack,
+  // this is just a slow query. There are various ways we can improve this
+  // (e.g. with using the vtab_distinct APIs of SQLite).
+  const result = await engine.query(`
+    select
+      name,
+      count(1) as cnt
+    from ftrace_event
+    group by name
+    order by cnt desc
+  `);
+  const counters: FtraceStat[] = [];
+  const it = result.iter({name: STR, cnt: NUM});
+  for (let row = 0; it.valid(); it.next(), row++) {
+    counters.push({name: it.name, count: it.cnt});
+  }
+  return counters;
+}
+
+export class FtraceExplorer implements m.ClassComponent<FtraceExplorerAttrs> {
+  private pagination: Pagination = {
+    offset: 0,
+    count: 0,
+  };
+  private readonly monitor: Monitor;
+  private readonly queryLimiter = new AsyncLimiter();
+
+  // A cache of the data we have most recently loaded from our store
+  private data?: FtracePanelData;
+
+  constructor({attrs}: m.CVnode<FtraceExplorerAttrs>) {
+    this.monitor = new Monitor([
+      () => attrs.trace.timeline.visibleWindow.toTimeSpan().start,
+      () => attrs.trace.timeline.visibleWindow.toTimeSpan().end,
+      () => attrs.filterStore.state,
+    ]);
+
+    if (attrs.cache.state === 'blank') {
+      getFtraceCounters(attrs.trace.engine)
+        .then((counters) => {
+          attrs.cache.counters = counters;
+          attrs.cache.state = 'valid';
+        })
+        .catch(() => {
+          attrs.cache.state = 'blank';
+        });
+      attrs.cache.state = 'loading';
+    }
+  }
+
+  view({attrs}: m.CVnode<FtraceExplorerAttrs>) {
+    this.monitor.ifStateChanged(() => {
+      this.reloadData(attrs);
+    });
+
+    return m(
+      DetailsShell,
+      {
+        title: this.renderTitle(),
+        buttons: this.renderFilterPanel(attrs),
+        fillParent: true,
+      },
+      m(VirtualTable, {
+        className: 'pf-ftrace-explorer',
+        columns: [
+          {header: 'ID', width: '5em'},
+          {header: 'Timestamp', width: '13em'},
+          {header: 'Name', width: '24em'},
+          {header: 'CPU', width: '3em'},
+          {header: 'Process', width: '24em'},
+          {header: 'Args', width: '200em'},
+        ],
+        firstRowOffset: this.data?.offset ?? 0,
+        numRows: this.data?.numEvents ?? 0,
+        rowHeight: ROW_H,
+        rows: this.renderData(),
+        onReload: (offset, count) => {
+          this.pagination = {offset, count};
+          this.reloadData(attrs);
+        },
+        onRowHover: (id) => {
+          const event = this.data?.events.find((event) => event.id === id);
+          if (event) {
+            attrs.trace.timeline.hoverCursorTimestamp = event.ts;
+          }
+        },
+        onRowOut: () => {
+          attrs.trace.timeline.hoverCursorTimestamp = undefined;
+        },
+      }),
+    );
+  }
+
+  private reloadData(attrs: FtraceExplorerAttrs): void {
+    this.queryLimiter.schedule(async () => {
+      this.data = await lookupFtraceEvents(
+        attrs.trace,
+        this.pagination.offset,
+        this.pagination.count,
+        attrs.filterStore.state,
+      );
+      attrs.trace.scheduleFullRedraw();
+    });
+  }
+
+  private renderData(): VirtualTableRow[] {
+    if (!this.data) {
+      return [];
+    }
+
+    return this.data.events.map((event) => {
+      const {ts, name, cpu, process, args, id} = event;
+      const timestamp = m(Timestamp, {ts});
+      const color = colorForFtrace(name).base.cssString;
+
+      return {
+        id,
+        cells: [
+          id,
+          timestamp,
+          m(
+            '.pf-ftrace-namebox',
+            m('.pf-ftrace-colorbox', {style: {background: color}}),
+            name,
+          ),
+          cpu,
+          process,
+          args,
+        ],
+      };
+    });
+  }
+
+  private renderTitle() {
+    if (this.data) {
+      const {numEvents} = this.data;
+      return `Ftrace Events (${numEvents})`;
+    } else {
+      return 'Ftrace Events';
+    }
+  }
+
+  private renderFilterPanel(attrs: FtraceExplorerAttrs) {
+    if (attrs.cache.state !== 'valid') {
+      return m(Button, {
+        label: 'Filter',
+        disabled: true,
+        loading: true,
+      });
+    }
+
+    const excludeList = attrs.filterStore.state.excludeList;
+    const options: MultiSelectOption[] = attrs.cache.counters.map(
+      ({name, count}) => {
+        return {
+          id: name,
+          name: `${name} (${count})`,
+          checked: !excludeList.some((excluded: string) => excluded === name),
+        };
+      },
+    );
+
+    return m(PopupMultiSelect, {
+      label: 'Filter',
+      icon: 'filter_list_alt',
+      popupPosition: PopupPosition.Top,
+      options,
+      onChange: (diffs: MultiSelectDiff[]) => {
+        const newList = new Set<string>(excludeList);
+        diffs.forEach(({checked, id}) => {
+          if (checked) {
+            newList.delete(id);
+          } else {
+            newList.add(id);
+          }
+        });
+        attrs.filterStore.edit((draft) => {
+          draft.excludeList = Array.from(newList);
+        });
+      },
+    });
+  }
+}
+
+async function lookupFtraceEvents(
+  trace: Trace,
+  offset: number,
+  count: number,
+  filter: FtraceFilter,
+): Promise<FtracePanelData> {
+  const {start, end} = trace.timeline.visibleWindow.toTimeSpan();
+
+  const excludeList = filter.excludeList;
+  const excludeListSql = excludeList.map((s) => `'${s}'`).join(',');
+
+  // TODO(stevegolton): This query can be slow when traces are huge.
+  // The number of events is only used for correctly sizing the panel's
+  // scroll container so that the scrollbar works as if the panel were fully
+  // populated.
+  // Perhaps we could work out some UX that doesn't need this.
+  let queryRes = await trace.engine.query(`
+    select count(id) as numEvents
+    from ftrace_event
+    where
+      ftrace_event.name not in (${excludeListSql}) and
+      ts >= ${start} and ts <= ${end}
+    `);
+  const {numEvents} = queryRes.firstRow({numEvents: NUM});
+
+  queryRes = await trace.engine.query(`
+    select
+      ftrace_event.id as id,
+      ftrace_event.ts as ts,
+      ftrace_event.name as name,
+      ftrace_event.cpu as cpu,
+      thread.name as thread,
+      process.name as process,
+      to_ftrace(ftrace_event.id) as args
+    from ftrace_event
+    join thread using (utid)
+    left join process on thread.upid = process.upid
+    where
+      ftrace_event.name not in (${excludeListSql}) and
+      ts >= ${start} and ts <= ${end}
+    order by id
+    limit ${count} offset ${offset};`);
+  const events: FtraceEvent[] = [];
+  const it = queryRes.iter({
+    id: NUM,
+    ts: LONG,
+    name: STR,
+    cpu: NUM,
+    thread: STR_NULL,
+    process: STR_NULL,
+    args: STR,
+  });
+  for (let row = 0; it.valid(); it.next(), row++) {
+    events.push({
+      id: it.id,
+      ts: Time.fromRaw(it.ts),
+      name: it.name,
+      cpu: it.cpu,
+      thread: it.thread,
+      process: it.process,
+      args: it.args,
+    });
+  }
+  return {events, offset, numEvents};
+}
diff --git a/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts
new file mode 100644
index 0000000..31ea7de
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts
@@ -0,0 +1,145 @@
+// 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 {duration, Time, time} from '../../base/time';
+import {colorForFtrace} from '../../public/lib/colorizer';
+import {LIMIT} from '../../common/track_data';
+import {Store, TimelineFetcher} from '../../common/track_helper';
+import {checkerboardExcept} from '../../frontend/checkerboard';
+import {TrackData} from '../../common/track_data';
+import {Engine} from '../../trace_processor/engine';
+import {Track} from '../../public/track';
+import {LONG, STR} from '../../trace_processor/query_result';
+import {FtraceFilter} from './common';
+import {Monitor} from '../../base/monitor';
+import {TrackRenderContext} from '../../public/track';
+
+const MARGIN = 2;
+const RECT_HEIGHT = 18;
+const TRACK_HEIGHT = RECT_HEIGHT + 2 * MARGIN;
+
+export interface Data extends TrackData {
+  timestamps: BigInt64Array;
+  names: string[];
+}
+
+export interface Config {
+  cpu?: number;
+}
+
+export class FtraceRawTrack implements Track {
+  private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
+  private engine: Engine;
+  private cpu: number;
+  private store: Store<FtraceFilter>;
+  private readonly monitor: Monitor;
+
+  constructor(engine: Engine, cpu: number, store: Store<FtraceFilter>) {
+    this.engine = engine;
+    this.cpu = cpu;
+    this.store = store;
+
+    this.monitor = new Monitor([() => store.state]);
+  }
+
+  async onUpdate({
+    visibleWindow,
+    resolution,
+  }: TrackRenderContext): Promise<void> {
+    this.monitor.ifStateChanged(() => {
+      this.fetcher.invalidate();
+    });
+    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
+  }
+
+  async onDestroy?(): Promise<void> {
+    this.fetcher[Symbol.dispose]();
+  }
+
+  getHeight(): number {
+    return TRACK_HEIGHT;
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    const excludeList = Array.from(this.store.state.excludeList);
+    const excludeListSql = excludeList.map((s) => `'${s}'`).join(',');
+    const cpuFilter = this.cpu === undefined ? '' : `and cpu = ${this.cpu}`;
+
+    const queryRes = await this.engine.query(`
+      select
+        cast(ts / ${resolution} as integer) * ${resolution} as tsQuant,
+        name
+      from ftrace_event
+      where
+        name not in (${excludeListSql}) and
+        ts >= ${start} and ts <= ${end} ${cpuFilter}
+      group by tsQuant
+      order by tsQuant limit ${LIMIT};`);
+
+    const rowCount = queryRes.numRows();
+    const result: Data = {
+      start,
+      end,
+      resolution,
+      length: rowCount,
+      timestamps: new BigInt64Array(rowCount),
+      names: [],
+    };
+
+    const it = queryRes.iter({tsQuant: LONG, name: STR});
+    for (let row = 0; it.valid(); it.next(), row++) {
+      result.timestamps[row] = it.tsQuant;
+      result.names[row] = it.name;
+    }
+    return result;
+  }
+
+  render({ctx, size, timescale}: TrackRenderContext): void {
+    const data = this.fetcher.data;
+
+    if (data === undefined) return; // Can't possibly draw anything.
+
+    const dataStartPx = timescale.timeToPx(data.start);
+    const dataEndPx = timescale.timeToPx(data.end);
+
+    checkerboardExcept(
+      ctx,
+      this.getHeight(),
+      0,
+      size.width,
+      dataStartPx,
+      dataEndPx,
+    );
+
+    const diamondSideLen = RECT_HEIGHT / Math.sqrt(2);
+
+    for (let i = 0; i < data.timestamps.length; i++) {
+      const name = data.names[i];
+      ctx.fillStyle = colorForFtrace(name).base.cssString;
+      const timestamp = Time.fromRaw(data.timestamps[i]);
+      const xPos = Math.floor(timescale.timeToPx(timestamp));
+
+      // Draw a diamond over the event
+      ctx.save();
+      ctx.translate(xPos, MARGIN);
+      ctx.rotate(Math.PI / 4);
+      ctx.fillRect(0, 0, diamondSideLen, diamondSideLen);
+      ctx.restore();
+    }
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Ftrace/index.ts b/ui/src/plugins/dev.perfetto.Ftrace/index.ts
new file mode 100644
index 0000000..7b0b6d4
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Ftrace/index.ts
@@ -0,0 +1,130 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {FtraceExplorer, FtraceExplorerCache} from './ftrace_explorer';
+import {Engine} from '../../trace_processor/engine';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {NUM} from '../../trace_processor/query_result';
+import {FtraceFilter, FtracePluginState} from './common';
+import {FtraceRawTrack} from './ftrace_track';
+import {TrackNode} from '../../public/workspace';
+
+const VERSION = 1;
+
+const DEFAULT_STATE: FtracePluginState = {
+  version: VERSION,
+  filter: {
+    excludeList: [],
+  },
+};
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.Ftrace';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const store = ctx.mountStore<FtracePluginState>((init: unknown) => {
+      if (
+        typeof init === 'object' &&
+        init !== null &&
+        'version' in init &&
+        init.version === VERSION
+      ) {
+        return init as {} as FtracePluginState;
+      } else {
+        return DEFAULT_STATE;
+      }
+    });
+    ctx.trash.use(store);
+
+    const filterStore = store.createSubStore(
+      ['filter'],
+      (x) => x as FtraceFilter,
+    );
+    ctx.trash.use(filterStore);
+
+    const cpus = await this.lookupCpuCores(ctx.engine);
+    const group = new TrackNode({
+      title: 'Ftrace Events',
+      sortOrder: -5,
+      isSummary: true,
+    });
+
+    for (const cpuNum of cpus) {
+      const uri = `/ftrace/cpu${cpuNum}`;
+      const title = `Ftrace Track for CPU ${cpuNum}`;
+
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        tags: {
+          cpu: cpuNum,
+          groupName: 'Ftrace Events',
+        },
+        track: new FtraceRawTrack(ctx.engine, cpuNum, filterStore),
+      });
+
+      const track = new TrackNode({uri, title});
+      group.addChildInOrder(track);
+    }
+
+    if (group.children.length) {
+      ctx.workspace.addChildInOrder(group);
+    }
+
+    const cache: FtraceExplorerCache = {
+      state: 'blank',
+      counters: [],
+    };
+
+    const ftraceTabUri = 'perfetto.FtraceRaw#FtraceEventsTab';
+
+    ctx.tabs.registerTab({
+      uri: ftraceTabUri,
+      isEphemeral: false,
+      content: {
+        render: () =>
+          m(FtraceExplorer, {
+            filterStore,
+            cache,
+            trace: ctx,
+          }),
+        getTitle: () => 'Ftrace Events',
+      },
+    });
+
+    ctx.commands.registerCommand({
+      id: 'perfetto.FtraceRaw#ShowFtraceTab',
+      name: 'Show ftrace tab',
+      callback: () => {
+        ctx.tabs.showTab(ftraceTabUri);
+      },
+    });
+  }
+
+  private async lookupCpuCores(engine: Engine): Promise<number[]> {
+    const query = 'select distinct cpu from ftrace_event order by cpu';
+
+    const result = await engine.query(query);
+    const it = result.iter({cpu: NUM});
+
+    const cpuCores: number[] = [];
+
+    for (; it.valid(); it.next()) {
+      cpuCores.push(it.cpu);
+    }
+
+    return cpuCores;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts b/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts
index ed0b403..9705156 100644
--- a/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts
+++ b/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts
@@ -12,21 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  NUM_NULL,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  STR_NULL,
-  Slice,
-} from '../../public';
+import {NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
+import {Trace} from '../../public/trace';
+import {Slice} from '../../public/track';
+import {PerfettoPlugin} from '../../public/plugin';
 import {
   NAMED_ROW,
   NamedRow,
   NamedSliceTrack,
 } from '../../frontend/named_slice_track';
 import {NewTrackArgs} from '../../frontend/track';
-
+import {TrackNode} from '../../public/workspace';
 class GpuPidTrack extends NamedSliceTrack {
   upid: number;
 
@@ -52,8 +48,9 @@
   }
 }
 
-class GpuByProcess implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.GpuByProcess';
+  async onTraceLoad(ctx: Trace): Promise<void> {
     // Find all unique upid values in gpu_slices and join with process table.
     const results = await ctx.engine.query(`
       WITH slice_upids AS (
@@ -82,20 +79,17 @@
         processName = `${it.pid}`;
       }
 
-      ctx.registerStaticTrack({
-        uri: `dev.perfetto.GpuByProcess#${upid}`,
-        title: `GPU ${processName}`,
-        trackFactory: ({trackKey}) => {
-          return new GpuPidTrack({engine: ctx.engine, trackKey}, upid);
-        },
+      const uri = `dev.perfetto.GpuByProcess#${upid}`;
+      const title = `GPU ${processName}`;
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        track: new GpuPidTrack({trace: ctx, uri}, upid),
       });
+      const track = new TrackNode({uri, title});
+      track.uri = uri;
+      track.title = title;
+      ctx.workspace.addChildInOrder(track);
     }
   }
-
-  async onTraceUnload(_: PluginContextTrace): Promise<void> {}
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.GpuByProcess',
-  plugin: GpuByProcess,
-};
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
new file mode 100644
index 0000000..c3e145e
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_details_panel.ts
@@ -0,0 +1,414 @@
+// 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 {assertExists, assertFalse} from '../../base/logging';
+import {time} from '../../base/time';
+import {
+  QueryFlamegraph,
+  QueryFlamegraphMetric,
+  metricsFromTableOrSubquery,
+} from '../../public/lib/query_flamegraph';
+import {convertTraceToPprofAndDownload} from '../../frontend/trace_converter';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {
+  TrackEventDetailsPanel,
+  TrackEventDetailsPanelSerializeArgs,
+} from '../../public/details_panel';
+import {ProfileType, TrackEventSelection} from '../../public/selection';
+import {Trace} from '../../public/trace';
+import {NUM} from '../../trace_processor/query_result';
+import {Button} from '../../widgets/button';
+import {Intent} from '../../widgets/common';
+import {DetailsShell} from '../../widgets/details_shell';
+import {Icon} from '../../widgets/icon';
+import {Modal, showModal} from '../../widgets/modal';
+import {Popup} from '../../widgets/popup';
+import {
+  Flamegraph,
+  FLAMEGRAPH_STATE_SCHEMA,
+  FlamegraphState,
+} from '../../widgets/flamegraph';
+
+interface Props {
+  ts: time;
+  type: ProfileType;
+}
+
+export class HeapProfileFlamegraphDetailsPanel
+  implements TrackEventDetailsPanel
+{
+  private readonly flamegraph: QueryFlamegraph;
+  private readonly props: Props;
+  private flamegraphModalDismissed = false;
+
+  readonly serialization: TrackEventDetailsPanelSerializeArgs<FlamegraphState>;
+
+  constructor(
+    private trace: Trace,
+    private heapGraphIncomplete: boolean,
+    private upid: number,
+    sel: TrackEventSelection,
+  ) {
+    const {profileType, ts} = sel;
+    const metrics = flamegraphMetrics(assertExists(profileType), ts, upid);
+    this.serialization = {
+      schema: FLAMEGRAPH_STATE_SCHEMA,
+      state: Flamegraph.createDefaultState(metrics),
+    };
+    this.flamegraph = new QueryFlamegraph(trace, metrics, this.serialization);
+    this.props = {ts, type: assertExists(profileType)};
+  }
+
+  render() {
+    const {type, ts} = this.props;
+    return m(
+      '.flamegraph-profile',
+      this.maybeShowModal(this.trace, type, this.heapGraphIncomplete),
+      m(
+        DetailsShell,
+        {
+          fillParent: true,
+          title: m(
+            '.title',
+            getFlamegraphTitle(type),
+            type === ProfileType.MIXED_HEAP_PROFILE &&
+              m(
+                Popup,
+                {
+                  trigger: m(Icon, {icon: 'warning'}),
+                },
+                m(
+                  '',
+                  {style: {width: '300px'}},
+                  'This is a mixed java/native heap profile, free()s are not visualized. To visualize free()s, remove "all_heaps: true" from the config.',
+                ),
+              ),
+          ),
+          description: [],
+          buttons: [
+            m('.time', `Snapshot time: `, m(Timestamp, {ts})),
+            (type === ProfileType.NATIVE_HEAP_PROFILE ||
+              type === ProfileType.JAVA_HEAP_SAMPLES) &&
+              m(Button, {
+                icon: 'file_download',
+                intent: Intent.Primary,
+                onclick: () => {
+                  downloadPprof(this.trace, this.upid, ts);
+                  this.trace.scheduleFullRedraw();
+                },
+              }),
+          ],
+        },
+        assertExists(this.flamegraph).render(),
+      ),
+    );
+  }
+
+  private maybeShowModal(
+    trace: Trace,
+    type: ProfileType,
+    heapGraphIncomplete: boolean,
+  ) {
+    if (type !== ProfileType.JAVA_HEAP_GRAPH || !heapGraphIncomplete) {
+      return undefined;
+    }
+    if (this.flamegraphModalDismissed) {
+      return undefined;
+    }
+    return m(Modal, {
+      title: 'The flamegraph is incomplete',
+      vAlign: 'TOP',
+      content: m(
+        'div',
+        'The current trace does not have a fully formed flamegraph',
+      ),
+      buttons: [
+        {
+          text: 'Show the errors',
+          primary: true,
+          action: () => trace.navigate('#!/info'),
+        },
+        {
+          text: 'Skip',
+          action: () => {
+            this.flamegraphModalDismissed = true;
+            trace.scheduleFullRedraw();
+          },
+        },
+      ],
+    });
+  }
+}
+
+function flamegraphMetrics(
+  type: ProfileType,
+  ts: time,
+  upid: number,
+): ReadonlyArray<QueryFlamegraphMetric> {
+  switch (type) {
+    case ProfileType.NATIVE_HEAP_PROFILE:
+      return flamegraphMetricsForHeapProfile(ts, upid, [
+        {
+          name: 'Unreleased Malloc Size',
+          unit: 'B',
+          columnName: 'self_size',
+        },
+        {
+          name: 'Unreleased Malloc Count',
+          unit: '',
+          columnName: 'self_count',
+        },
+        {
+          name: 'Total Malloc Size',
+          unit: 'B',
+          columnName: 'self_alloc_size',
+        },
+        {
+          name: 'Total Malloc Count',
+          unit: '',
+          columnName: 'self_alloc_count',
+        },
+      ]);
+    case ProfileType.HEAP_PROFILE:
+      return flamegraphMetricsForHeapProfile(ts, upid, [
+        {
+          name: 'Unreleased Size',
+          unit: 'B',
+          columnName: 'self_size',
+        },
+        {
+          name: 'Unreleased Count',
+          unit: '',
+          columnName: 'self_count',
+        },
+        {
+          name: 'Total Size',
+          unit: 'B',
+          columnName: 'self_alloc_size',
+        },
+        {
+          name: 'Total Count',
+          unit: '',
+          columnName: 'self_alloc_count',
+        },
+      ]);
+    case ProfileType.JAVA_HEAP_SAMPLES:
+      return flamegraphMetricsForHeapProfile(ts, upid, [
+        {
+          name: 'Unreleased Allocation Size',
+          unit: 'B',
+          columnName: 'self_size',
+        },
+        {
+          name: 'Unreleased Allocation Count',
+          unit: '',
+          columnName: 'self_count',
+        },
+      ]);
+    case ProfileType.MIXED_HEAP_PROFILE:
+      return flamegraphMetricsForHeapProfile(ts, upid, [
+        {
+          name: 'Unreleased Allocation Size (malloc + java)',
+          unit: 'B',
+          columnName: 'self_size',
+        },
+        {
+          name: 'Unreleased Allocation Count (malloc + java)',
+          unit: '',
+          columnName: 'self_count',
+        },
+      ]);
+    case ProfileType.JAVA_HEAP_GRAPH:
+      return [
+        {
+          name: 'Object Size',
+          unit: 'B',
+          dependencySql:
+            'include perfetto module android.memory.heap_graph.class_tree;',
+          statement: `
+            select
+              id,
+              parent_id as parentId,
+              ifnull(name, '[Unknown]') as name,
+              root_type,
+              self_size as value,
+              self_count
+            from _heap_graph_class_tree
+            where graph_sample_ts = ${ts} and upid = ${upid}
+          `,
+          unaggregatableProperties: [
+            {name: 'root_type', displayName: 'Root Type'},
+          ],
+          aggregatableProperties: [
+            {
+              name: 'self_count',
+              displayName: 'Self Count',
+              mergeAggregation: 'SUM',
+            },
+          ],
+        },
+        {
+          name: 'Object Count',
+          unit: '',
+          dependencySql:
+            'include perfetto module android.memory.heap_graph.class_tree;',
+          statement: `
+            select
+              id,
+              parent_id as parentId,
+              ifnull(name, '[Unknown]') as name,
+              root_type,
+              self_size,
+              self_count as value
+            from _heap_graph_class_tree
+            where graph_sample_ts = ${ts} and upid = ${upid}
+          `,
+          unaggregatableProperties: [
+            {name: 'root_type', displayName: 'Root Type'},
+          ],
+        },
+        {
+          name: 'Dominated Object Size',
+          unit: 'B',
+          dependencySql:
+            'include perfetto module android.memory.heap_graph.dominator_class_tree;',
+          statement: `
+            select
+              id,
+              parent_id as parentId,
+              ifnull(name, '[Unknown]') as name,
+              root_type,
+              self_size as value,
+              self_count
+            from _heap_graph_dominator_class_tree
+            where graph_sample_ts = ${ts} and upid = ${upid}
+          `,
+          unaggregatableProperties: [
+            {name: 'root_type', displayName: 'Root Type'},
+          ],
+          aggregatableProperties: [
+            {
+              name: 'self_count',
+              displayName: 'Self Count',
+              mergeAggregation: 'SUM',
+            },
+          ],
+        },
+        {
+          name: 'Dominated Object Count',
+          unit: '',
+          dependencySql:
+            'include perfetto module android.memory.heap_graph.dominator_class_tree;',
+          statement: `
+            select
+              id,
+              parent_id as parentId,
+              ifnull(name, '[Unknown]') as name,
+              root_type,
+              self_size,
+              self_count as value
+            from _heap_graph_class_tree
+            where graph_sample_ts = ${ts} and upid = ${upid}
+          `,
+          unaggregatableProperties: [
+            {name: 'root_type', displayName: 'Root Type'},
+          ],
+        },
+      ];
+    case ProfileType.PERF_SAMPLE:
+      throw new Error('Perf sample not supported');
+  }
+}
+
+function flamegraphMetricsForHeapProfile(
+  ts: time,
+  upid: number,
+  metrics: {name: string; unit: string; columnName: string}[],
+) {
+  return metricsFromTableOrSubquery(
+    `
+      (
+        select
+          id,
+          parent_id as parentId,
+          name,
+          mapping_name,
+          source_file,
+          cast(line_number AS text) as line_number,
+          self_size,
+          self_count,
+          self_alloc_size,
+          self_alloc_count
+        from _android_heap_profile_callstacks_for_allocations!((
+          select
+            callsite_id,
+            size,
+            count,
+            max(size, 0) as alloc_size,
+            max(count, 0) as alloc_count
+          from heap_profile_allocation a
+          where a.ts <= ${ts} and a.upid = ${upid}
+        ))
+      )
+    `,
+    metrics,
+    'include perfetto module android.memory.heap_profile.callstacks',
+    [{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',
+      },
+    ],
+  );
+}
+
+function getFlamegraphTitle(type: ProfileType) {
+  switch (type) {
+    case ProfileType.HEAP_PROFILE:
+      return 'Heap profile';
+    case ProfileType.JAVA_HEAP_GRAPH:
+      return 'Java heap graph';
+    case ProfileType.JAVA_HEAP_SAMPLES:
+      return 'Java heap samples';
+    case ProfileType.MIXED_HEAP_PROFILE:
+      return 'Mixed heap profile';
+    case ProfileType.NATIVE_HEAP_PROFILE:
+      return 'Native heap profile';
+    case ProfileType.PERF_SAMPLE:
+      assertFalse(false, 'Perf sample not supported');
+      return 'Impossible';
+  }
+}
+
+async function downloadPprof(trace: Trace, upid: number, ts: time) {
+  const pid = await trace.engine.query(
+    `select pid from process where upid = ${upid}`,
+  );
+  if (!trace.traceInfo.downloadable) {
+    showModal({
+      title: 'Download not supported',
+      content: m('div', 'This trace file does not support downloads'),
+    });
+  }
+  const blob = await trace.getTraceFile();
+  convertTraceToPprofAndDownload(blob, pid.firstRow({pid: NUM}).pid, ts);
+}
diff --git a/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_track.ts b/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_track.ts
new file mode 100644
index 0000000..d4effa4
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_track.ts
@@ -0,0 +1,117 @@
+// 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 {Duration, Time} from '../../base/time';
+import {
+  BASE_ROW,
+  BaseSliceTrack,
+  OnSliceClickArgs,
+  OnSliceOverArgs,
+} from '../../frontend/base_slice_track';
+import {NewTrackArgs} from '../../frontend/track';
+import {
+  ProfileType,
+  profileType,
+  TrackEventDetails,
+  TrackEventSelection,
+} from '../../public/selection';
+import {Slice} from '../../public/track';
+import {LONG, STR} from '../../trace_processor/query_result';
+import {HeapProfileFlamegraphDetailsPanel} from './heap_profile_details_panel';
+
+const HEAP_PROFILE_ROW = {
+  ...BASE_ROW,
+  type: STR,
+};
+type HeapProfileRow = typeof HEAP_PROFILE_ROW;
+interface HeapProfileSlice extends Slice {
+  type: ProfileType;
+}
+
+export class HeapProfileTrack extends BaseSliceTrack<
+  HeapProfileSlice,
+  HeapProfileRow
+> {
+  constructor(
+    args: NewTrackArgs,
+    private readonly tableName: string,
+    private readonly upid: number,
+    private readonly heapProfileIsIncomplete: boolean,
+  ) {
+    super(args);
+  }
+
+  getSqlSource(): string {
+    return this.tableName;
+  }
+
+  getRowSpec(): HeapProfileRow {
+    return HEAP_PROFILE_ROW;
+  }
+
+  rowToSlice(row: HeapProfileRow): HeapProfileSlice {
+    const slice = this.rowToSliceBase(row);
+    return {
+      ...slice,
+      type: profileType(row.type),
+    };
+  }
+
+  onSliceOver(args: OnSliceOverArgs<HeapProfileSlice>) {
+    args.tooltip = [args.slice.type];
+  }
+
+  onSliceClick(args: OnSliceClickArgs<HeapProfileSlice>) {
+    this.trace.selection.selectTrackEvent(this.uri, args.slice.id);
+  }
+
+  async getSelectionDetails(
+    id: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const query = `
+      SELECT
+        ts,
+        dur,
+        type
+      FROM (${this.getSqlSource()})
+      WHERE id = ${id}
+    `;
+
+    const result = await this.engine.query(query);
+    if (result.numRows() === 0) {
+      return undefined;
+    }
+
+    const row = result.iter({
+      ts: LONG,
+      dur: LONG,
+      type: STR,
+    });
+
+    return {
+      ts: Time.fromRaw(row.ts),
+      dur: Duration.fromRaw(row.dur),
+      profileType: profileType(row.type),
+    };
+  }
+
+  detailsPanel(sel: TrackEventSelection) {
+    return new HeapProfileFlamegraphDetailsPanel(
+      this.trace,
+      this.heapProfileIsIncomplete,
+      this.upid,
+      sel,
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.HeapProfile/index.ts b/ui/src/plugins/dev.perfetto.HeapProfile/index.ts
new file mode 100644
index 0000000..f4f9121
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.HeapProfile/index.ts
@@ -0,0 +1,138 @@
+// 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 {HEAP_PROFILE_TRACK_KIND} from '../../public/track_kinds';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {LONG, NUM, STR} from '../../trace_processor/query_result';
+import {HeapProfileTrack} from './heap_profile_track';
+import {getOrCreateGroupForProcess} from '../../public/standard_groups';
+import {TrackNode} from '../../public/workspace';
+import {createPerfettoTable} from '../../trace_processor/sql_utils';
+
+function getUriForTrack(upid: number): string {
+  return `/process_${upid}/heap_profile`;
+}
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.HeapProfile';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const it = await ctx.engine.query(`
+      select value from stats
+      where name = 'heap_graph_non_finalized_graph'
+    `);
+    const incomplete = it.firstRow({value: NUM}).value > 0;
+
+    const result = await ctx.engine.query(`
+      select distinct upid from heap_profile_allocation
+      union
+      select distinct upid from heap_graph_object
+    `);
+    for (const it = result.iter({upid: NUM}); it.valid(); it.next()) {
+      const upid = it.upid;
+      const uri = getUriForTrack(upid);
+      const title = 'Heap Profile';
+      const tableName = `_heap_profile_${upid}`;
+
+      createPerfettoTable(
+        ctx.engine,
+        tableName,
+        `
+          with
+            heaps as (select group_concat(distinct heap_name) h from heap_profile_allocation where upid = ${upid}),
+            allocation_tses as (select distinct ts from heap_profile_allocation where upid = ${upid}),
+            graph_tses as (select distinct graph_sample_ts from heap_graph_object where upid = ${upid})
+          select
+            *,
+            0 AS dur,
+            0 AS depth
+          from (
+            select
+              (
+                select a.id
+                from heap_profile_allocation a
+                where a.ts = t.ts
+                order by a.id
+                limit 1
+              ) as id,
+              ts,
+              'heap_profile:' || (select h from heaps) AS type
+            from allocation_tses t
+            union all
+            select
+              (
+                select o.id
+                from heap_graph_object o
+                where o.graph_sample_ts = g.graph_sample_ts
+                order by o.id
+                limit 1
+              ) as id,
+              graph_sample_ts AS ts,
+              'graph' AS type
+            from graph_tses g
+          )
+        `,
+      );
+
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        tags: {
+          kind: HEAP_PROFILE_TRACK_KIND,
+          upid,
+        },
+        track: new HeapProfileTrack(
+          {
+            trace: ctx,
+            uri,
+          },
+          tableName,
+          upid,
+          incomplete,
+        ),
+      });
+      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
+      const track = new TrackNode({uri, title, sortOrder: -30});
+      group.addChildInOrder(track);
+    }
+
+    ctx.addEventListener('traceready', async () => {
+      await selectFirstHeapProfile(ctx);
+    });
+  }
+}
+
+async function selectFirstHeapProfile(ctx: Trace) {
+  const query = `
+    select * from (
+      select
+        min(ts) AS ts,
+        'heap_profile:' || group_concat(distinct heap_name) AS type,
+        upid
+      from heap_profile_allocation
+      group by upid
+      union
+      select distinct graph_sample_ts as ts, 'graph' as type, upid
+      from heap_graph_object
+    )
+    order by ts
+    limit 1
+  `;
+  const profile = await ctx.engine.query(query);
+  if (profile.numRows() !== 1) return;
+  const row = profile.firstRow({ts: LONG, type: STR, upid: NUM});
+  const upid = row.upid;
+
+  ctx.selection.selectTrackEvent(getUriForTrack(upid), 0);
+}
diff --git a/ui/src/plugins/dev.perfetto.InsightsPage/index.ts b/ui/src/plugins/dev.perfetto.InsightsPage/index.ts
new file mode 100644
index 0000000..fdd84a1
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.InsightsPage/index.ts
@@ -0,0 +1,31 @@
+// 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 {Trace} from '../../public/trace';
+import {InsightsPage} from './insights_page';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.InsightsPage';
+
+  async onTraceLoad(trace: Trace): Promise<void> {
+    trace.pages.registerPage({route: '/insights', page: InsightsPage});
+    trace.sidebar.addMenuItem({
+      section: 'current_trace',
+      text: 'Insights',
+      href: '#!/insights',
+      icon: 'insights',
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.InsightsPage/insights_page.ts b/ui/src/plugins/dev.perfetto.InsightsPage/insights_page.ts
new file mode 100644
index 0000000..5b0b742
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.InsightsPage/insights_page.ts
@@ -0,0 +1,22 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {PageWithTraceAttrs} from '../../public/page';
+
+export class InsightsPage implements m.ClassComponent<PageWithTraceAttrs> {
+  view() {
+    return m('.insights-page');
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts b/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
index 218d59f..ffbbd7c 100644
--- a/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
@@ -12,16 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
 
-class LargeScreensPerf implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerCommand({
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.LargeScreensPerf';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.LargeScreensPerf#PinUnfoldLatencyTracks',
       name: 'Pin: Unfold latency tracks',
       callback: () => {
-        ctx.timeline.pinTracksByPredicate((track) => {
-          return (
+        ctx.workspace.flatTracks.forEach((track) => {
+          if (
             !!track.title.includes('UnfoldTransition') ||
             track.title.includes('Screen on blocked') ||
             track.title.includes('hingeAngle') ||
@@ -32,14 +34,11 @@
             track.title == 'Waiting for KeyguardDrawnCallback#onDrawn' ||
             track.title == 'FoldedState' ||
             track.title == 'FoldUpdate'
-          );
+          ) {
+            track.pin();
+          }
         });
       },
     });
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.LargeScreensPerf',
-  plugin: LargeScreensPerf,
-};
diff --git a/ui/src/plugins/dev.perfetto.MetricsPage/index.ts b/ui/src/plugins/dev.perfetto.MetricsPage/index.ts
new file mode 100644
index 0000000..55cda40
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.MetricsPage/index.ts
@@ -0,0 +1,32 @@
+// 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 {Trace} from '../../public/trace';
+import {MetricsPage} from './metrics_page';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.MetricsPage';
+
+  async onTraceLoad(trace: Trace): Promise<void> {
+    trace.pages.registerPage({route: '/metrics', page: MetricsPage});
+    trace.sidebar.addMenuItem({
+      section: 'current_trace',
+      text: 'Metrics',
+      href: '#!/metrics',
+      icon: 'speed',
+      sortOrder: 9,
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.MetricsPage/metrics_page.ts b/ui/src/plugins/dev.perfetto.MetricsPage/metrics_page.ts
new file mode 100644
index 0000000..8f1f0f3
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.MetricsPage/metrics_page.ts
@@ -0,0 +1,270 @@
+// 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.
+
+import m from 'mithril';
+import {
+  error,
+  isError,
+  isPending,
+  pending,
+  Result,
+  success,
+} from '../../base/result';
+import {MetricVisualisation} from '../../public/plugin';
+import {Engine} from '../../trace_processor/engine';
+import {STR} from '../../trace_processor/query_result';
+import {Select} from '../../widgets/select';
+import {Spinner} from '../../widgets/spinner';
+import {VegaView} from '../../widgets/vega_view';
+import {PageWithTraceAttrs} from '../../public/page';
+import {assertExists} from '../../base/logging';
+import {Trace} from '../../public/trace';
+
+type Format = 'json' | 'prototext' | 'proto';
+const FORMATS: Format[] = ['json', 'prototext', 'proto'];
+
+async function getMetrics(engine: Engine): Promise<string[]> {
+  const metrics: string[] = [];
+  const metricsResult = await engine.query('select name from trace_metrics');
+  for (const it = metricsResult.iter({name: STR}); it.valid(); it.next()) {
+    metrics.push(it.name);
+  }
+  return metrics;
+}
+
+async function getMetric(
+  engine: Engine,
+  metric: string,
+  format: Format,
+): Promise<string> {
+  const result = await engine.computeMetric([metric], format);
+  if (result instanceof Uint8Array) {
+    return `Uint8Array<len=${result.length}>`;
+  } else {
+    return result;
+  }
+}
+
+class MetricsController {
+  private readonly trace: Trace;
+  private readonly engine: Engine;
+  private _metrics: string[];
+  private _selected?: string;
+  private _result: Result<string>;
+  private _format: Format;
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  private _json: any;
+
+  constructor(trace: Trace) {
+    this.trace = trace;
+    this.engine = trace.engine.getProxy('MetricsPage');
+    this._metrics = [];
+    this._result = success('');
+    this._json = {};
+    this._format = 'json';
+    getMetrics(this.engine).then((metrics) => {
+      this._metrics = metrics;
+    });
+  }
+
+  get metrics(): string[] {
+    return this._metrics;
+  }
+
+  get visualisations(): MetricVisualisation[] {
+    return this.trace.plugins
+      .metricVisualisations()
+      .filter((v) => v.metric === this.selected);
+  }
+
+  set selected(metric: string | undefined) {
+    if (this._selected === metric) {
+      return;
+    }
+    this._selected = metric;
+    this.update();
+  }
+
+  get selected(): string | undefined {
+    return this._selected;
+  }
+
+  set format(format: Format) {
+    if (this._format === format) {
+      return;
+    }
+    this._format = format;
+    this.update();
+  }
+
+  get format(): Format {
+    return this._format;
+  }
+
+  get result(): Result<string> {
+    return this._result;
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  get resultAsJson(): any {
+    return this._json;
+  }
+
+  private update() {
+    const selected = this._selected;
+    const format = this._format;
+    if (selected === undefined) {
+      this._result = success('');
+      this._json = {};
+    } else {
+      this._result = pending();
+      this._json = {};
+      getMetric(this.engine, selected, format)
+        .then((result) => {
+          if (this._selected === selected && this._format === format) {
+            this._result = success(result);
+            if (format === 'json') {
+              this._json = JSON.parse(result);
+            }
+          }
+        })
+        .catch((e) => {
+          if (this._selected === selected && this._format === format) {
+            this._result = error(e);
+            this._json = {};
+          }
+        })
+        .finally(() => {
+          this.trace.scheduleFullRedraw();
+        });
+    }
+    this.trace.scheduleFullRedraw();
+  }
+}
+
+interface MetricResultAttrs {
+  result: Result<string>;
+}
+
+class MetricResultView implements m.ClassComponent<MetricResultAttrs> {
+  view({attrs}: m.CVnode<MetricResultAttrs>) {
+    const result = attrs.result;
+    if (isPending(result)) {
+      return m(Spinner);
+    }
+
+    if (isError(result)) {
+      return m('pre.metric-error', result.error);
+    }
+
+    return m('pre', result.data);
+  }
+}
+
+interface MetricPickerAttrs {
+  controller: MetricsController;
+}
+
+class MetricPicker implements m.ClassComponent<MetricPickerAttrs> {
+  view({attrs}: m.CVnode<MetricPickerAttrs>) {
+    const {controller} = attrs;
+    return m(
+      '.metrics-page-picker',
+      m(
+        Select,
+        {
+          value: controller.selected,
+          oninput: (e: Event) => {
+            if (!e.target) return;
+            controller.selected = (e.target as HTMLSelectElement).value;
+          },
+        },
+        controller.metrics.map((metric) =>
+          m(
+            'option',
+            {
+              value: metric,
+              key: metric,
+            },
+            metric,
+          ),
+        ),
+      ),
+      m(
+        Select,
+        {
+          oninput: (e: Event) => {
+            if (!e.target) return;
+            controller.format = (e.target as HTMLSelectElement).value as Format;
+          },
+        },
+        FORMATS.map((f) => {
+          return m('option', {
+            selected: controller.format === f,
+            key: f,
+            value: f,
+            label: f,
+          });
+        }),
+      ),
+    );
+  }
+}
+
+interface MetricVizViewAttrs {
+  visualisation: MetricVisualisation;
+  data: unknown;
+}
+
+class MetricVizView implements m.ClassComponent<MetricVizViewAttrs> {
+  view({attrs}: m.CVnode<MetricVizViewAttrs>) {
+    return m(
+      '',
+      m(VegaView, {
+        spec: attrs.visualisation.spec,
+        data: {
+          metric: attrs.data,
+        },
+      }),
+    );
+  }
+}
+
+export class MetricsPage implements m.ClassComponent<PageWithTraceAttrs> {
+  private controller?: MetricsController;
+
+  oninit({attrs}: m.Vnode<PageWithTraceAttrs>) {
+    this.controller = new MetricsController(attrs.trace);
+  }
+
+  view() {
+    const controller = assertExists(this.controller);
+    const json = controller.resultAsJson;
+    return m(
+      '.metrics-page',
+      m(MetricPicker, {
+        controller,
+      }),
+      controller.format === 'json' &&
+        controller.visualisations.map((visualisation) => {
+          let data = json;
+          for (const p of visualisation.path) {
+            data = data[p] ?? [];
+          }
+          return m(MetricVizView, {visualisation, data});
+        }),
+      m(MetricResultView, {result: controller.result}),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.PerfSamplesProfile/index.ts b/ui/src/plugins/dev.perfetto.PerfSamplesProfile/index.ts
new file mode 100644
index 0000000..26ac305
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.PerfSamplesProfile/index.ts
@@ -0,0 +1,144 @@
+// 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 {TrackData} from '../../common/track_data';
+import {PERF_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 {
+  ProcessPerfSamplesProfileTrack,
+  ThreadPerfSamplesProfileTrack,
+} from './perf_samples_profile_track';
+import {getThreadUriPrefix} from '../../public/utils';
+import {
+  getOrCreateGroupForProcess,
+  getOrCreateGroupForThread,
+} from '../../public/standard_groups';
+import {TrackNode} from '../../public/workspace';
+
+export interface Data extends TrackData {
+  tsStarts: BigInt64Array;
+}
+
+function makeUriForProc(upid: number) {
+  return `/process_${upid}/perf_samples_profile`;
+}
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.PerfSamplesProfile';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const pResult = await ctx.engine.query(`
+      select distinct upid
+      from perf_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: PERF_SAMPLES_PROFILE_TRACK_KIND,
+          upid,
+        },
+        track: new ProcessPerfSamplesProfileTrack(
+          {
+            trace: ctx,
+            uri,
+          },
+          upid,
+        ),
+      });
+      const group = getOrCreateGroupForProcess(ctx.workspace, 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 perf_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)}_perf_samples_profile`;
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        tags: {
+          kind: PERF_SAMPLES_PROFILE_TRACK_KIND,
+          utid,
+          upid: upid ?? undefined,
+        },
+        track: new ThreadPerfSamplesProfileTrack(
+          {
+            trace: ctx,
+            uri,
+          },
+          utid,
+        ),
+      });
+      const group = getOrCreateGroupForThread(ctx.workspace, utid);
+      const track = new TrackNode({uri, title, sortOrder: -50});
+      group.addChildInOrder(track);
+    }
+
+    ctx.addEventListener('traceready', async () => {
+      await selectPerfSample(ctx);
+    });
+  }
+}
+
+async function selectPerfSample(ctx: Trace) {
+  const profile = await assertExists(ctx.engine).query(`
+    select upid
+    from perf_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 perf samples track
+  ctx.selection.selectArea({
+    start: ctx.traceInfo.start,
+    end: ctx.traceInfo.end,
+    trackUris: [makeUriForProc(upid)],
+  });
+}
diff --git a/ui/src/plugins/dev.perfetto.PerfSamplesProfile/perf_samples_profile_track.ts b/ui/src/plugins/dev.perfetto.PerfSamplesProfile/perf_samples_profile_track.ts
new file mode 100644
index 0000000..839d0d1
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.PerfSamplesProfile/perf_samples_profile_track.ts
@@ -0,0 +1,293 @@
+// 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 {NUM} from '../../trace_processor/query_result';
+import {Slice} from '../../public/track';
+import {
+  BaseSliceTrack,
+  OnSliceClickArgs,
+} from '../../frontend/base_slice_track';
+import {NewTrackArgs} from '../../frontend/track';
+import {NAMED_ROW, NamedRow} from '../../frontend/named_slice_track';
+import {getColorForSample} from '../../public/lib/colorizer';
+import {
+  ProfileType,
+  TrackEventDetails,
+  TrackEventSelection,
+} from '../../public/selection';
+import {assertExists} from '../../base/logging';
+import {
+  metricsFromTableOrSubquery,
+  QueryFlamegraph,
+} from '../../public/lib/query_flamegraph';
+import {DetailsShell} from '../../widgets/details_shell';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {time} from '../../base/time';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Flamegraph, FLAMEGRAPH_STATE_SCHEMA} from '../../widgets/flamegraph';
+
+interface PerfSampleRow extends NamedRow {
+  callsiteId: number;
+}
+
+abstract class BasePerfSamplesProfileTrack extends BaseSliceTrack<
+  Slice,
+  PerfSampleRow
+> {
+  constructor(args: NewTrackArgs) {
+    super(args);
+  }
+
+  protected getRowSpec(): PerfSampleRow {
+    return {...NAMED_ROW, callsiteId: NUM};
+  }
+
+  protected rowToSlice(row: PerfSampleRow): 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;
+    }
+  }
+
+  onSliceClick(args: OnSliceClickArgs<Slice>): void {
+    // TODO(stevegolton): Perhaps we could just move this to BaseSliceTrack?
+    this.trace.selection.selectTrackEvent(this.uri, args.slice.id);
+  }
+}
+
+export class ProcessPerfSamplesProfileTrack extends BasePerfSamplesProfileTrack {
+  constructor(
+    args: NewTrackArgs,
+    private upid: number,
+  ) {
+    super(args);
+  }
+
+  getSqlSource(): string {
+    return `
+      select
+        p.id,
+        ts,
+        0 as dur,
+        0 as depth,
+        'Perf Sample' as name,
+        callsite_id as callsiteId
+      from perf_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.PERF_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 perf_sample p
+            join thread t using (utid)
+            where p.ts >= ${ts}
+              and p.ts <= ${ts}
+              and t.upid = ${upid}
+          ))
+        )
+      `,
+      [
+        {
+          name: 'Perf Samples',
+          unit: '',
+          columnName: 'self_count',
+        },
+      ],
+      'include perfetto module linux.perf.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 ThreadPerfSamplesProfileTrack extends BasePerfSamplesProfileTrack {
+  constructor(
+    args: NewTrackArgs,
+    private utid: number,
+  ) {
+    super(args);
+  }
+
+  getSqlSource(): string {
+    return `
+      select
+        p.id,
+        ts,
+        0 as dur,
+        0 as depth,
+        'Perf Sample' as name,
+        callsite_id as callsiteId
+      from perf_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.PERF_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 perf_sample p
+            where p.ts >= ${ts}
+              and p.ts <= ${ts}
+              and p.utid = ${utid}
+          ))
+        )
+      `,
+      [
+        {
+          name: 'Perf Samples',
+          unit: '',
+          columnName: 'self_count',
+        },
+      ],
+      'include perfetto module linux.perf.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', 'Perf 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.PinAndroidPerfMetrics/OWNERS b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/OWNERS
index b655639..de52817 100644
--- a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/OWNERS
+++ b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/OWNERS
@@ -1,4 +1,3 @@
-paulsoumyadeep@google.com
 nishantpanwar@google.com
 bvineeth@google.com
 nicomazz@google.com
\ No newline at end of file
diff --git a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/fullTraceJankMetricHandler.ts b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/fullTraceJankMetricHandler.ts
index afe75a9..f3704ac 100644
--- a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/fullTraceJankMetricHandler.ts
+++ b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/fullTraceJankMetricHandler.ts
@@ -18,13 +18,8 @@
   JankType,
   MetricHandler,
 } from './metricUtils';
-import {PluginContextTrace} from '../../../public';
-import {
-  addAndPinSliceTrack,
-  TrackType,
-} from '../../dev.perfetto.AndroidCujs/trackUtils';
-import {SimpleSliceTrackConfig} from '../../../frontend/simple_slice_track';
-import {PLUGIN_ID} from '../pluginId';
+import {Trace} from '../../../public/trace';
+import {addDebugSliceTrack} from '../../../public/debug_tracks';
 
 class FullTraceJankMetricHandler implements MetricHandler {
   /**
@@ -49,43 +44,33 @@
 
   /**
    * Adds the debug track for full trace jank metrics
-   * The track contains missed sf/app frames for the process
-   * registerStaticTrack used when plugin adds tracks onTraceload()
-   * addDebugSliceTrack used for adding tracks using the command
    *
    * @param {FullTraceMetricData} metricData Parsed metric data for the full trace jank
-   * @param {PluginContextTrace} ctx PluginContextTrace for trace related properties and methods
-   * @param {TrackType} type 'static' for onTraceload and 'debug' for command
+   * @param {Trace} ctx PluginContextTrace for trace related properties and methods
    * @returns {void} Adds one track for Jank slice
    */
-  public async addMetricTrack(
-    metricData: FullTraceMetricData,
-    ctx: PluginContextTrace,
-    type: TrackType,
-  ) {
+  public async addMetricTrack(metricData: FullTraceMetricData, ctx: Trace) {
     const INCLUDE_PREQUERY = `
     INCLUDE PERFETTO MODULE android.frames.jank_type;
     INCLUDE PERFETTO MODULE slices.slices;
     `;
-    const uri = `${PLUGIN_ID}#FullTraceJank#${metricData}`;
-    const {config: fullTraceJankConfig, trackName: trackName} =
-      this.fullTraceJankConfig(metricData);
+    const config = this.fullTraceJankConfig(metricData);
     await ctx.engine.query(INCLUDE_PREQUERY);
-    addAndPinSliceTrack(ctx, fullTraceJankConfig, trackName, type, uri);
+    addDebugSliceTrack({trace: ctx, ...config});
   }
 
-  private fullTraceJankConfig(metricData: FullTraceMetricData): {
-    config: SimpleSliceTrackConfig;
-    trackName: string;
-  } {
+  private fullTraceJankConfig(metricData: FullTraceMetricData) {
     let jankTypeFilter;
-    let jankTypeDisplayName = 'all';
+    let jankTypeDisplayName;
     if (metricData.jankType?.includes('app')) {
       jankTypeFilter = ' android_is_app_jank_type(display_value)';
       jankTypeDisplayName = 'app';
     } else if (metricData.jankType?.includes('sf')) {
       jankTypeFilter = ' android_is_sf_jank_type(display_value)';
       jankTypeDisplayName = 'sf';
+    } else {
+      jankTypeFilter = " display_value != 'None'";
+      jankTypeDisplayName = 'all';
     }
     const processName = metricData.process;
 
@@ -125,18 +110,18 @@
       'process_name',
       'pid',
     ];
-    const fullTraceJankConfig: SimpleSliceTrackConfig = {
+
+    const trackName = jankTypeDisplayName + ' missed frames in ' + processName;
+
+    return {
       data: {
         sqlSource: fullTraceJankQuery,
         columns: fullTraceJankColumns,
       },
       columns: {ts: 'ts', dur: 'dur', name: 'name'},
       argColumns: fullTraceJankColumns,
+      tableName: trackName,
     };
-
-    const trackName = jankTypeDisplayName + ' missed frames in ' + processName;
-
-    return {config: fullTraceJankConfig, trackName: trackName};
   }
 }
 
diff --git a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/handlerRegistry.ts b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/handlerRegistry.ts
index b353576..575bf8e 100644
--- a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/handlerRegistry.ts
+++ b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/handlerRegistry.ts
@@ -16,9 +16,11 @@
 import {pinBlockingCallHandlerInstance} from './pinBlockingCall';
 import {pinCujScopedJankInstance} from './pinCujScoped';
 import {pinFullTraceJankInstance} from './fullTraceJankMetricHandler';
+import {pinCujInstance} from './pinCujMetricHandler';
 
 // TODO: b/337774166 - Add handlers for the metric name categories here
 export const METRIC_HANDLERS: MetricHandler[] = [
+  pinCujInstance,
   pinCujScopedJankInstance,
   pinBlockingCallHandlerInstance,
   pinFullTraceJankInstance,
diff --git a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/metricUtils.ts b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/metricUtils.ts
index a9b99fd..9bb7201 100644
--- a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/metricUtils.ts
+++ b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/metricUtils.ts
@@ -11,9 +11,7 @@
 // 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 {TrackType} from '../../dev.perfetto.AndroidCujs/trackUtils';
-import {PluginContextTrace} from '../../../public';
+import {Trace} from '../../../public/trace';
 
 /**
  * Represents data for a Full trace metric
@@ -61,11 +59,17 @@
   aggregation: string;
 }
 
+/** Represents a cuj to be pinned. */
+export interface CujMetricData {
+  cujName: string;
+}
+
 // Common MetricData for all handler. If new needed then add here.
 export type MetricData =
   | FullTraceMetricData
   | CujScopedMetricData
-  | BlockingCallMetricData;
+  | BlockingCallMetricData
+  | CujMetricData;
 
 // Common JankType for cujScoped and fullTrace metrics
 export type JankType = 'sf_frames' | 'app_frames' | 'frames';
@@ -86,16 +90,10 @@
    * Add debug track for parsed metric data.
    *
    * @param {MetricData} metricData The parsed metric data.
-   * @param {PluginContextTrace} ctx context for trace methods and properties
-   * @param {TrackType} type 'static' onTraceload, 'debug' on command.
-   * TODO: b/349502258 - Refactor to single API
+   * @param {Trace} ctx context for trace methods and properties
    * @returns {void}
    */
-  addMetricTrack(
-    metricData: MetricData,
-    ctx: PluginContextTrace,
-    type: TrackType,
-  ): void;
+  addMetricTrack(metricData: MetricData, ctx: Trace): void;
 }
 
 // Pair for matching metric and its handler
@@ -115,6 +113,8 @@
     return 'com.android.systemui';
   } else if (metricProcessName.includes('launcher')) {
     return 'com.google.android.apps.nexuslauncher';
+  } else if (metricProcessName.includes('surfaceflinger')) {
+    return '/system/bin/surfaceflinger';
   } else {
     return metricProcessName;
   }
diff --git a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/pinBlockingCall.ts b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/pinBlockingCall.ts
index f445915..1ae9ac6 100644
--- a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/pinBlockingCall.ts
+++ b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/pinBlockingCall.ts
@@ -17,14 +17,9 @@
   BlockingCallMetricData,
   MetricHandler,
 } from './metricUtils';
-import {PluginContextTrace} from '../../../public';
-import {PLUGIN_ID} from '../pluginId';
-import {SimpleSliceTrackConfig} from '../../../frontend/simple_slice_track';
+import {Trace} from '../../../public/trace';
 import {addJankCUJDebugTrack} from '../../dev.perfetto.AndroidCujs';
-import {
-  addAndPinSliceTrack,
-  TrackType,
-} from '../../dev.perfetto.AndroidCujs/trackUtils';
+import {addDebugSliceTrack} from '../../../public/debug_tracks';
 
 class BlockingCallMetricHandler implements MetricHandler {
   /**
@@ -51,41 +46,23 @@
 
   /**
    * Adds the debug tracks for Blocking Call metrics
-   * registerStaticTrack used when plugin adds tracks onTraceload()
-   * addDebugSliceTrack used for adding tracks using the command
    *
    * @param {BlockingCallMetricData} metricData Parsed metric data for the cuj scoped jank
-   * @param {PluginContextTrace} ctx PluginContextTrace for trace related properties and methods
-   * @param {TrackType} type 'static' when called onTraceload and 'debug' when called through command
+   * @param {Trace} ctx PluginContextTrace for trace related properties and methods
    * @returns {void} Adds one track for Jank CUJ slice and one for Janky CUJ frames
    */
-  public addMetricTrack(
-    metricData: BlockingCallMetricData,
-    ctx: PluginContextTrace,
-    type: TrackType,
-  ): void {
-    this.pinSingleCuj(ctx, metricData, type);
-    const uri = `${PLUGIN_ID}#BlockingCallSlices#${metricData}`;
-    // TODO: b/349502258 - Refactor to single API
-    const {config: blockingCallMetricConfig, trackName: trackName} =
-      this.blockingCallTrackConfig(metricData);
-    addAndPinSliceTrack(ctx, blockingCallMetricConfig, trackName, type, uri);
+  public addMetricTrack(metricData: BlockingCallMetricData, ctx: Trace): void {
+    this.pinSingleCuj(ctx, metricData);
+    const config = this.blockingCallTrackConfig(metricData);
+    addDebugSliceTrack({trace: ctx, ...config});
   }
 
-  private pinSingleCuj(
-    ctx: PluginContextTrace,
-    metricData: BlockingCallMetricData,
-    type: TrackType,
-  ) {
-    const uri = `${PLUGIN_ID}#BlockingCallCUJ#${metricData}`;
+  private pinSingleCuj(ctx: Trace, metricData: BlockingCallMetricData) {
     const trackName = `Jank CUJ: ${metricData.cujName}`;
-    addJankCUJDebugTrack(ctx, trackName, type, metricData.cujName, uri);
+    addJankCUJDebugTrack(ctx, trackName, metricData.cujName);
   }
 
-  private blockingCallTrackConfig(metricData: BlockingCallMetricData): {
-    config: SimpleSliceTrackConfig;
-    trackName: string;
-  } {
+  private blockingCallTrackConfig(metricData: BlockingCallMetricData) {
     const cuj = metricData.cujName;
     const processName = metricData.process;
     const blockingCallName = metricData.blockingCallName;
@@ -99,18 +76,16 @@
       AND name = "${blockingCallName}"
   `;
 
-    const blockingCallMetricConfig: SimpleSliceTrackConfig = {
+    const trackName = 'Blocking calls in ' + processName;
+    return {
       data: {
         sqlSource: blockingCallDuringCujQuery,
         columns: ['name', 'ts', 'dur'],
       },
       columns: {ts: 'ts', dur: 'dur', name: 'name'},
       argColumns: ['name', 'ts', 'dur'],
+      trackName,
     };
-
-    const trackName = 'Blocking calls in ' + processName;
-
-    return {config: blockingCallMetricConfig, trackName: trackName};
   }
 }
 
diff --git a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/pinCujMetricHandler.ts b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/pinCujMetricHandler.ts
new file mode 100644
index 0000000..05234a4
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/pinCujMetricHandler.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 {CujMetricData, MetricHandler} from './metricUtils';
+import {Trace} from '../../../public/trace';
+import {addJankCUJDebugTrack} from '../../dev.perfetto.AndroidCujs';
+
+/** Pins a single CUJ from CUJ scoped metrics. */
+class PinCujMetricHandler implements MetricHandler {
+  /**
+   * Matches metric key & return parsed data if successful.
+   *
+   * @param {string} metricKey The metric key to match.
+   * @returns {CujMetricData | undefined} Parsed data or undefined if no match.
+   */
+  public match(metricKey: string): CujMetricData | undefined {
+    const matcher = /perfetto_cuj_(?<process>.*)-(?<cujName>.*)-.*-missed_.*/;
+    const match = matcher.exec(metricKey);
+    if (!match?.groups) {
+      return undefined;
+    }
+    return {
+      cujName: match.groups.cujName,
+    };
+  }
+
+  /**
+   * Adds the debug tracks for cuj Scoped jank metrics
+   *
+   * @param {CujMetricData} metricData Parsed metric data for the cuj scoped jank
+   * @param {Trace} ctx PluginContextTrace for trace related properties and methods
+   * @returns {void} Adds one track for Jank CUJ slice and one for Janky CUJ frames
+   */
+  public async addMetricTrack(metricData: CujMetricData, ctx: Trace) {
+    this.pinSingleCuj(ctx, metricData.cujName);
+  }
+
+  private pinSingleCuj(ctx: Trace, cujName: string) {
+    const trackName = `Jank CUJ: ${cujName}`;
+    addJankCUJDebugTrack(ctx, trackName, cujName);
+  }
+}
+
+export const pinCujInstance = new PinCujMetricHandler();
diff --git a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/pinCujScoped.ts b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/pinCujScoped.ts
index f1c2b8b..5d61843 100644
--- a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/pinCujScoped.ts
+++ b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/handlers/pinCujScoped.ts
@@ -18,18 +18,13 @@
   MetricHandler,
   JankType,
 } from './metricUtils';
-import {LONG, NUM} from '../../../trace_processor/query_result';
-import {PluginContextTrace} from '../../../public';
-import {SimpleSliceTrackConfig} from '../../../frontend/simple_slice_track';
-import {addJankCUJDebugTrack} from '../../dev.perfetto.AndroidCujs';
-import {
-  addAndPinSliceTrack,
-  focusOnSlice,
-  SliceIdentifier,
-  TrackType,
-} from '../../dev.perfetto.AndroidCujs/trackUtils';
-import {PLUGIN_ID} from '../pluginId';
-import {Time} from '../../../base/time';
+import {NUM} from '../../../trace_processor/query_result';
+import {Trace} from '../../../public/trace';
+
+// TODO(primiano): make deps check stricter, we shouldn't allow plugins to
+// depend on each other.
+import {focusOnSlice} from '../../dev.perfetto.AndroidCujs/trackUtils';
+import {addDebugSliceTrack} from '../../../public/debug_tracks';
 
 const ENABLE_FOCUS_ON_FIRST_JANK = true;
 
@@ -53,52 +48,32 @@
       jankType: match.groups.jankType as JankType,
     };
     return metricData;
+    1;
   }
 
   /**
-   * Adds the debug tracks for cuj Scoped jank metrics
-   * registerStaticTrack used when plugin adds tracks onTraceload()
-   * addDebugSliceTrack used for adding tracks using the command
+   * Adds the debug tracks for cuj Scoped jank metrics.
    *
    * @param {CujScopedMetricData} metricData Parsed metric data for the cuj scoped jank
-   * @param {PluginContextTrace} ctx PluginContextTrace for trace related properties and methods
-   * @param {TrackType} type 'static' for onTraceload and 'debug' for command
+   * @param {Trace} ctx PluginContextTrace for trace related properties and methods
    * @returns {void} Adds one track for Jank CUJ slice and one for Janky CUJ frames
    */
-  public async addMetricTrack(
-    metricData: CujScopedMetricData,
-    ctx: PluginContextTrace,
-    type: TrackType,
-  ) {
+  public async addMetricTrack(metricData: CujScopedMetricData, ctx: Trace) {
     // TODO: b/349502258 - Refactor to single API
-    const {config: cujScopedJankSlice, trackName: trackName} =
-      await this.cujScopedTrackConfig(metricData, ctx);
-    this.pinSingleCuj(ctx, metricData, type);
-    const uri = `${PLUGIN_ID}#CUJScopedJankSlice#${metricData}`;
-
-    addAndPinSliceTrack(ctx, cujScopedJankSlice, trackName, type, uri);
+    const {tableName, ...config} = await this.cujScopedTrackConfig(
+      metricData,
+      ctx,
+    );
+    addDebugSliceTrack({trace: ctx, ...config});
     if (ENABLE_FOCUS_ON_FIRST_JANK) {
-      await this.focusOnFirstJank(ctx);
+      await this.focusOnFirstJank(ctx, tableName);
     }
   }
 
-  private pinSingleCuj(
-    ctx: PluginContextTrace,
-    metricData: CujScopedMetricData,
-    type: TrackType,
-  ) {
-    const uri = `${PLUGIN_ID}#CUJScopedBoundaryTimes#${metricData}`;
-    const trackName = `Jank CUJ: ${metricData.cujName}`;
-    addJankCUJDebugTrack(ctx, trackName, type, metricData.cujName, uri);
-  }
-
   private async cujScopedTrackConfig(
     metricData: CujScopedMetricData,
-    ctx: PluginContextTrace,
-  ): Promise<{
-    config: SimpleSliceTrackConfig;
-    trackName: string;
-  }> {
+    ctx: Trace,
+  ) {
     let jankTypeFilter;
     let jankTypeDisplayName = 'all';
     if (metricData.jankType?.includes('app')) {
@@ -111,73 +86,63 @@
     const cuj = metricData.cujName;
     const processName = metricData.process;
 
+    const tableWithJankyFramesName = `_janky_frames_during_cuj_from_metric_key_${Math.floor(Math.random() * 1_000_000)}`;
+
     const createJankyCujFrameTable = `
-    CREATE PERFETTO TABLE _janky_frames_during_cuj_from_metric_key AS
+    CREATE OR REPLACE PERFETTO TABLE ${tableWithJankyFramesName} AS
     SELECT
       f.vsync as id,
       f.ts AS ts,
       f.dur as dur
     FROM android_jank_cuj_frame f LEFT JOIN android_jank_cuj cuj USING (cuj_id)
-    WHERE cuj.process_name = "${processName}" 
+    WHERE cuj.process_name = "${processName}"
     AND cuj_name = "${cuj}" ${jankTypeFilter}
     `;
 
     await ctx.engine.query(createJankyCujFrameTable);
 
     const jankyFramesDuringCujQuery = `
-    SELECT id, ts, dur 
-    FROM _janky_frames_during_cuj_from_metric_key
+        SELECT id, ts, dur
+        FROM ${tableWithJankyFramesName}
     `;
 
-    const cujScopedJankSlice: SimpleSliceTrackConfig = {
+    const trackName = jankTypeDisplayName + ' missed frames in ' + processName;
+
+    const cujScopedJankSlice = {
       data: {
         sqlSource: jankyFramesDuringCujQuery,
         columns: ['id', 'ts', 'dur'],
       },
       columns: {ts: 'ts', dur: 'dur', name: 'id'},
       argColumns: ['id', 'ts', 'dur'],
+      trackName,
     };
 
-    const trackName = jankTypeDisplayName + ' missed frames in ' + processName;
-
-    return {config: cujScopedJankSlice, trackName: trackName};
+    return {
+      ...cujScopedJankSlice,
+      tableName: tableWithJankyFramesName,
+    };
   }
 
-  private async findFirstJank(
-    ctx: PluginContextTrace,
-  ): Promise<SliceIdentifier | undefined> {
+  private async focusOnFirstJank(ctx: Trace, tableWithJankyFramesName: string) {
     const queryForFirstJankyFrame = `
-      SELECT slice_id, track_id, ts, dur FROM slice
+        SELECT slice_id, track_id
+        FROM slice
         WHERE type = "actual_frame_timeline_slice"
-        AND name =
-        CAST(
-        (SELECT id FROM _janky_frames_during_cuj_from_metric_key LIMIT 1)
-        AS VARCHAR(20) );
+          AND name =
+              CAST(
+                      (SELECT id FROM ${tableWithJankyFramesName} LIMIT 1)
+                  AS VARCHAR(20));
     `;
     const queryResult = await ctx.engine.query(queryForFirstJankyFrame);
     if (queryResult.numRows() === 0) {
-      return undefined;
+      return;
     }
     const row = queryResult.firstRow({
       slice_id: NUM,
       track_id: NUM,
-      ts: LONG,
-      dur: LONG,
     });
-    const slice: SliceIdentifier = {
-      sliceId: row.slice_id,
-      trackId: row.track_id,
-      ts: Time.fromRaw(row.ts),
-      dur: row.dur,
-    };
-    return slice;
-  }
-
-  private async focusOnFirstJank(ctx: PluginContextTrace) {
-    const slice = await this.findFirstJank(ctx);
-    if (slice) {
-      focusOnSlice(slice);
-    }
+    focusOnSlice(ctx, row.slice_id);
   }
 }
 
diff --git a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/index.ts b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/index.ts
index cbac918..430acd8 100644
--- a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/index.ts
+++ b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/index.ts
@@ -12,16 +12,36 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {TrackType} from '../dev.perfetto.AndroidCujs/trackUtils';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
 import {METRIC_HANDLERS} from './handlers/handlerRegistry';
-import {MetricHandlerMatch} from './handlers/metricUtils';
+import {MetricData, MetricHandlerMatch} from './handlers/metricUtils';
 import {PLUGIN_ID} from './pluginId';
+import AndroidCujsPlugin from '../dev.perfetto.AndroidCujs';
 
 const JANK_CUJ_QUERY_PRECONDITIONS = `
   SELECT RUN_METRIC('android/android_blocking_calls_cuj_metric.sql');
 `;
 
+function getMetricsFromHash(): string[] {
+  const metricVal = location.hash;
+  const regex = new RegExp(`${PLUGIN_ID}:metrics=(.*)`);
+  const match = metricVal.match(regex);
+  if (match === null) {
+    return [];
+  }
+  const capturedString = match[1];
+  let metricList: string[] = [];
+  if (capturedString.includes('--')) {
+    metricList = capturedString.split('--');
+  } else {
+    metricList = [capturedString];
+  }
+  return metricList.map((metric) => decodeURIComponent(metric));
+}
+
+let metrics: string[];
+
 /**
  * Plugin that adds and pins the debug track for the metric passed
  * For more context -
@@ -32,34 +52,31 @@
  * the regression, the user will not have to manually search for the
  * slices related to the regressed metric
  */
-class PinAndroidPerfMetrics implements Plugin {
-  private metrics: string[] = [];
+export default class implements PerfettoPlugin {
+  static readonly id = PLUGIN_ID;
+  static readonly dependencies = [AndroidCujsPlugin];
 
-  onActivate(): void {
-    this.metrics = this.getMetricsFromHash();
+  static onActivate(): void {
+    metrics = getMetricsFromHash();
   }
 
-  async onTraceReady(ctx: PluginContextTrace) {
-    ctx.registerCommand({
+  async onTraceLoad(ctx: Trace) {
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.PinAndroidPerfMetrics#PinAndroidPerfMetrics',
       name: 'Add and Pin: Jank Metric Slice',
       callback: async (metric) => {
         metric = prompt('Metrics names (separated by comma)', '');
         if (metric === null) return;
         const metricList = metric.split(',');
-        this.callHandlers(metricList, ctx, 'debug');
+        this.callHandlers(metricList, ctx);
       },
     });
-    if (this.metrics.length !== 0) {
-      this.callHandlers(this.metrics, ctx, 'debug');
+    if (metrics.length !== 0) {
+      this.callHandlers(metrics, ctx);
     }
   }
 
-  private async callHandlers(
-    metricsList: string[],
-    ctx: PluginContextTrace,
-    type: TrackType,
-  ) {
+  private async callHandlers(metricsList: string[], ctx: Trace) {
     // List of metrics that actually match some handler
     const metricsToShow: MetricHandlerMatch[] =
       this.getMetricsToShow(metricsList);
@@ -70,53 +87,33 @@
 
     await ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS);
     for (const {metricData, metricHandler} of metricsToShow) {
-      metricHandler.addMetricTrack(metricData, ctx, type);
+      metricHandler.addMetricTrack(metricData, ctx);
     }
   }
 
-  private getMetricsFromHash(): string[] {
-    const metricVal = location.hash;
-    const regex = new RegExp(`${PLUGIN_ID}:metrics=(.*)`);
-    const match = metricVal.match(regex);
-    if (match === null) {
-      return [];
-    }
-    const capturedString = match[1];
-    let metricList: string[] = [];
-    if (capturedString.includes('--')) {
-      metricList = capturedString.split('--');
-    } else {
-      metricList = [capturedString];
-    }
-    return metricList.map((metric) => decodeURIComponent(metric));
-  }
-
-  private getMetricsToShow(metricList: string[]) {
+  private getMetricsToShow(metricList: string[]): MetricHandlerMatch[] {
+    const sortedMetricList = [...metricList].sort();
     const validMetrics: MetricHandlerMatch[] = [];
-    metricList.forEach((metric) => {
-      const matchedHandler = this.matchMetricToHandler(metric);
-      if (matchedHandler) {
-        validMetrics.push(matchedHandler);
+    const alreadyMatchedMetricData: Set<string> = new Set();
+    for (const metric of sortedMetricList) {
+      for (const metricHandler of METRIC_HANDLERS) {
+        const metricData = metricHandler.match(metric);
+        if (!metricData) continue;
+        const jsonMetricData = this.metricDataToJson(metricData);
+        if (!alreadyMatchedMetricData.has(jsonMetricData)) {
+          alreadyMatchedMetricData.add(jsonMetricData);
+          validMetrics.push({
+            metricData: metricData,
+            metricHandler: metricHandler,
+          });
+        }
       }
-    });
+    }
     return validMetrics;
   }
 
-  private matchMetricToHandler(metric: string): MetricHandlerMatch | null {
-    for (const metricHandler of METRIC_HANDLERS) {
-      const match = metricHandler.match(metric);
-      if (match) {
-        return {
-          metricData: match,
-          metricHandler: metricHandler,
-        };
-      }
-    }
-    return null;
+  private metricDataToJson(metricData: MetricData): string {
+    // Used to have a deterministic keys order.
+    return JSON.stringify(metricData, Object.keys(metricData).sort());
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: PLUGIN_ID,
-  plugin: PinAndroidPerfMetrics,
-};
diff --git a/ui/src/plugins/dev.perfetto.PinSysUITracks/OWNERS b/ui/src/plugins/dev.perfetto.PinSysUITracks/OWNERS
index b655639..de52817 100644
--- a/ui/src/plugins/dev.perfetto.PinSysUITracks/OWNERS
+++ b/ui/src/plugins/dev.perfetto.PinSysUITracks/OWNERS
@@ -1,4 +1,3 @@
-paulsoumyadeep@google.com
 nishantpanwar@google.com
 bvineeth@google.com
 nicomazz@google.com
\ No newline at end of file
diff --git a/ui/src/plugins/dev.perfetto.PinSysUITracks/index.ts b/ui/src/plugins/dev.perfetto.PinSysUITracks/index.ts
index bcbb2b2..eb53737 100644
--- a/ui/src/plugins/dev.perfetto.PinSysUITracks/index.ts
+++ b/ui/src/plugins/dev.perfetto.PinSysUITracks/index.ts
@@ -12,7 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {NUM, Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
+import {NUM} from '../../trace_processor/query_result';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
 
 // List of tracks to pin
 const TRACKS_TO_PIN: string[] = [
@@ -27,8 +29,9 @@
 const SYSTEM_UI_PROCESS: string = 'com.android.systemui';
 
 // Plugin that pins the tracks relevant to System UI
-class PinSysUITracks implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.PinSysUITracks';
+  async onTraceLoad(ctx: Trace): Promise<void> {
     // Find the upid for the sysui process
     const result = await ctx.engine.query(`
       INCLUDE PERFETTO MODULE android.process_metadata;
@@ -45,32 +48,31 @@
       upid: NUM,
     }).upid;
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.PinSysUITracks#PinSysUITracks',
       name: 'Pin: System UI Related Tracks',
       callback: () => {
-        ctx.timeline.pinTracksByPredicate((track) => {
-          if (!track.uri.startsWith(`/process_${sysuiUpid}`)) return false;
+        ctx.workspace.flatTracks.forEach((track) => {
+          if (!track.uri) return;
+          // Ensure we only grab tracks that are in the SysUI process group
+          if (!track.uri.startsWith(`/process_${sysuiUpid}`)) return;
           if (
             !TRACKS_TO_PIN.some((trackName) =>
               track.title.startsWith(trackName),
             )
           ) {
-            return false;
+            return;
           }
-          return true;
+          track.pin();
         });
 
         // expand the sysui process tracks group
-        ctx.timeline.expandGroupsByPredicate((groupRef) => {
-          return groupRef.displayName?.startsWith(SYSTEM_UI_PROCESS) ?? false;
+        ctx.workspace.flatTracks.forEach((track) => {
+          if (track.hasChildren && track.title.startsWith(SYSTEM_UI_PROCESS)) {
+            track.expand();
+          }
         });
       },
     });
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.PinSysUITracks',
-  plugin: PinSysUITracks,
-};
diff --git a/ui/src/plugins/dev.perfetto.Process/index.ts b/ui/src/plugins/dev.perfetto.Process/index.ts
new file mode 100644
index 0000000..52ce021
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Process/index.ts
@@ -0,0 +1,35 @@
+// 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 {sqlTableRegistry} from '../../frontend/widgets/sql/table/sql_table_registry';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {getProcessTable} from './table';
+import {extensions} from '../../public/lib/extensions';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.Process';
+  async onTraceLoad(ctx: Trace) {
+    sqlTableRegistry['process'] = getProcessTable();
+    ctx.commands.registerCommand({
+      id: 'perfetto.ShowTable.process',
+      name: 'Open table: process',
+      callback: () => {
+        extensions.addSqlTableTab(ctx, {
+          table: getProcessTable(),
+        });
+      },
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Process/table.ts b/ui/src/plugins/dev.perfetto.Process/table.ts
new file mode 100644
index 0000000..2bce0cd
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Process/table.ts
@@ -0,0 +1,41 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {SqlTableDescription} from '../../frontend/widgets/sql/table/table_description';
+import {
+  ArgSetColumnSet,
+  ProcessColumn,
+  ProcessIdColumn,
+  StandardColumn,
+  TimestampColumn,
+} from '../../frontend/widgets/sql/table/well_known_columns';
+
+export function getProcessTable(): SqlTableDescription {
+  return {
+    name: 'process',
+    columns: [
+      new ProcessIdColumn('upid'),
+      new StandardColumn('pid', {aggregationType: 'nominal'}),
+      new StandardColumn('name'),
+      new TimestampColumn('start_ts'),
+      new TimestampColumn('end_ts'),
+      new ProcessColumn('parent_upid'),
+      new StandardColumn('uid', {aggregationType: 'nominal'}),
+      new StandardColumn('android_appid', {aggregationType: 'nominal'}),
+      new StandardColumn('cmdline', {startsHidden: true}),
+      new StandardColumn('machine_id', {aggregationType: 'nominal'}),
+      new ArgSetColumnSet('arg_set_id'),
+    ],
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts b/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts
new file mode 100644
index 0000000..20e1803
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts
@@ -0,0 +1,209 @@
+// 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 {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {getThreadOrProcUri} from '../../public/utils';
+import {NUM, NUM_NULL, STR} from '../../trace_processor/query_result';
+import {
+  Config as ProcessSchedulingTrackConfig,
+  PROCESS_SCHEDULING_TRACK_KIND,
+  ProcessSchedulingTrack,
+} from './process_scheduling_track';
+import {
+  Config as ProcessSummaryTrackConfig,
+  PROCESS_SUMMARY_TRACK,
+  ProcessSummaryTrack,
+} from './process_summary_track';
+import ThreadPlugin from '../dev.perfetto.Thread';
+
+// This plugin is responsible for adding summary tracks for process and thread
+// groups.
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.ProcessSummary';
+  static readonly dependencies = [ThreadPlugin];
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    await this.addProcessTrackGroups(ctx);
+    await this.addKernelThreadSummary(ctx);
+  }
+
+  private async addProcessTrackGroups(ctx: Trace): Promise<void> {
+    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;
+
+      select *
+      from (
+        select
+          _process_available_info_summary.upid,
+          null as utid,
+          process.pid,
+          null as tid,
+          process.name as processName,
+          null as threadName,
+          sum_running_dur > 0 as hasSched,
+          android_process_metadata.debuggable as isDebuggable,
+          ifnull((
+            select group_concat(string_value)
+            from args
+            where
+              process.arg_set_id is not null and
+              arg_set_id = process.arg_set_id and
+              flat_key = 'chrome.process_label'
+          ), '') as chromeProcessLabels
+        from _process_available_info_summary
+        join process using(upid)
+        left join android_process_metadata using(upid)
+      )
+      union all
+      select *
+      from (
+        select
+          null,
+          utid,
+          null as pid,
+          tid,
+          null as processName,
+          thread.name threadName,
+          sum_running_dur > 0 as hasSched,
+          0 as isDebuggable,
+          '' as chromeProcessLabels
+        from _thread_available_info_summary
+        join thread using (utid)
+        where upid is null
+      )
+  `);
+
+    const it = result.iter({
+      upid: NUM_NULL,
+      utid: NUM_NULL,
+      pid: NUM_NULL,
+      tid: NUM_NULL,
+      hasSched: NUM_NULL,
+      isDebuggable: NUM_NULL,
+      chromeProcessLabels: STR,
+    });
+    for (; it.valid(); it.next()) {
+      const upid = it.upid;
+      const utid = it.utid;
+      const pid = it.pid;
+      const tid = it.tid;
+      const hasSched = Boolean(it.hasSched);
+      const isDebuggable = Boolean(it.isDebuggable);
+      const subtitle = it.chromeProcessLabels;
+
+      // Group by upid if present else by utid.
+      const pidForColor = pid ?? tid ?? upid ?? utid ?? 0;
+      const uri = getThreadOrProcUri(upid, utid);
+
+      const chips: string[] = [];
+      isDebuggable && chips.push('debuggable');
+
+      if (hasSched) {
+        const config: ProcessSchedulingTrackConfig = {
+          pidForColor,
+          upid,
+          utid,
+        };
+
+        ctx.tracks.registerTrack({
+          uri,
+          title: `${upid === null ? tid : pid} schedule`,
+          tags: {
+            kind: PROCESS_SCHEDULING_TRACK_KIND,
+          },
+          chips,
+          track: new ProcessSchedulingTrack(ctx, config, cpuCount, threads),
+          subtitle,
+        });
+      } else {
+        const config: ProcessSummaryTrackConfig = {
+          pidForColor,
+          upid,
+          utid,
+        };
+
+        ctx.tracks.registerTrack({
+          uri,
+          title: `${upid === null ? tid : pid} summary`,
+          tags: {
+            kind: PROCESS_SUMMARY_TRACK,
+          },
+          chips,
+          track: new ProcessSummaryTrack(ctx.engine, config),
+          subtitle,
+        });
+      }
+    }
+  }
+
+  private async addKernelThreadSummary(ctx: Trace): Promise<void> {
+    const {engine} = ctx;
+
+    // Identify kernel threads if this is a linux system trace, and sufficient
+    // process information is available. Kernel threads are identified by being
+    // children of kthreadd (always pid 2).
+    // The query will return the kthreadd process row first, which must exist
+    // for any other kthreads to be returned by the query.
+    // TODO(rsavitski): figure out how to handle the idle process (swapper),
+    // which has pid 0 but appears as a distinct process (with its own comm) on
+    // each cpu. It'd make sense to exclude its thread state track, but still
+    // put process-scoped tracks in this group.
+    const result = await engine.query(`
+      select
+        t.utid, p.upid, (case p.pid when 2 then 1 else 0 end) isKthreadd
+      from
+        thread t
+        join process p using (upid)
+        left join process parent on (p.parent_upid = parent.upid)
+        join
+          (select true from metadata m
+             where (m.name = 'system_name' and m.str_value = 'Linux')
+           union
+           select 1 from (select true from sched limit 1))
+      where
+        p.pid = 2 or parent.pid = 2
+      order by isKthreadd desc
+    `);
+
+    const it = result.iter({
+      utid: NUM,
+      upid: NUM,
+    });
+
+    // Not applying kernel thread grouping.
+    if (!it.valid()) {
+      return;
+    }
+
+    const config: ProcessSummaryTrackConfig = {
+      pidForColor: 2,
+      upid: it.upid,
+      utid: it.utid,
+    };
+
+    ctx.tracks.registerTrack({
+      uri: '/kernel',
+      title: `Kernel thread summary`,
+      tags: {
+        kind: PROCESS_SUMMARY_TRACK,
+      },
+      track: new ProcessSummaryTrack(ctx.engine, config),
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.ProcessSummary/process_scheduling_track.ts b/ui/src/plugins/dev.perfetto.ProcessSummary/process_scheduling_track.ts
new file mode 100644
index 0000000..91f3646
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ProcessSummary/process_scheduling_track.ts
@@ -0,0 +1,301 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {BigintMath as BIMath} from '../../base/bigint_math';
+import {searchEq, searchRange} from '../../base/binary_search';
+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 {colorForThread} from '../../public/lib/colorizer';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {checkerboardExcept} from '../../frontend/checkerboard';
+import {Track} from '../../public/track';
+import {LONG, NUM, QueryResult} from '../../trace_processor/query_result';
+import {uuidv4Sql} from '../../base/uuid';
+import {TrackMouseEvent, TrackRenderContext} from '../../public/track';
+import {Point2D} from '../../base/geom';
+import {Trace} from '../../public/trace';
+import {ThreadMap} from '../dev.perfetto.Thread/threads';
+
+export const PROCESS_SCHEDULING_TRACK_KIND = 'ProcessSchedulingTrack';
+
+const MARGIN_TOP = 5;
+const RECT_HEIGHT = 30;
+const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
+
+interface Data extends TrackData {
+  kind: 'slice';
+  maxCpu: number;
+
+  // Slices are stored in a columnar fashion. All fields have the same length.
+  starts: BigInt64Array;
+  ends: BigInt64Array;
+  utids: Uint32Array;
+  cpus: Uint32Array;
+}
+
+export interface Config {
+  pidForColor: number;
+  upid: number | null;
+  utid: number | null;
+}
+
+export class ProcessSchedulingTrack implements Track {
+  private mousePos?: Point2D;
+  private utidHoveredInThisTrack = -1;
+  private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
+  private trackUuid = uuidv4Sql();
+
+  constructor(
+    private readonly trace: Trace,
+    private readonly config: Config,
+    private readonly cpuCount: number,
+    private readonly threads: ThreadMap,
+  ) {}
+
+  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((
+          select
+            id,
+            ts,
+            iif(
+              dur = -1,
+              lead(ts, 1, trace_end()) over (partition by cpu order by ts) - ts,
+              dur
+            ) as dur,
+            cpu as depth
+          from experimental_sched_upid
+          where
+            utid != 0 and
+            upid = ${this.config.upid}
+        ));
+      `);
+    } else {
+      assertExists(this.config.utid);
+      await this.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}
+        ));
+      `);
+    }
+  }
+
+  async onUpdate({
+    visibleWindow,
+    resolution,
+  }: TrackRenderContext): Promise<void> {
+    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
+  }
+
+  async onDestroy(): Promise<void> {
+    this.fetcher[Symbol.dispose]();
+    await this.trace.engine.tryQuery(`
+      drop table process_scheduling_${this.trackUuid}
+    `);
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    // Resolution must always be a power of 2 for this logic to work
+    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
+
+    const queryRes = await this.queryData(start, end, resolution);
+    const numRows = queryRes.numRows();
+    const slices: Data = {
+      kind: 'slice',
+      start,
+      end,
+      resolution,
+      length: numRows,
+      maxCpu: this.cpuCount,
+      starts: new BigInt64Array(numRows),
+      ends: new BigInt64Array(numRows),
+      cpus: new Uint32Array(numRows),
+      utids: new Uint32Array(numRows),
+    };
+
+    const it = queryRes.iter({
+      ts: LONG,
+      dur: LONG,
+      cpu: NUM,
+      utid: NUM,
+    });
+
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const start = Time.fromRaw(it.ts);
+      const dur = it.dur;
+      const end = Time.add(start, dur);
+
+      slices.starts[row] = start;
+      slices.ends[row] = end;
+      slices.cpus[row] = it.cpu;
+      slices.utids[row] = it.utid;
+      slices.end = Time.max(end, slices.end);
+    }
+    return slices;
+  }
+
+  private async queryData(
+    start: time,
+    end: time,
+    bucketSize: duration,
+  ): Promise<QueryResult> {
+    return this.trace.engine.query(`
+      select
+        (z.ts / ${bucketSize}) * ${bucketSize} as ts,
+        iif(s.dur = -1, s.dur, max(z.dur, ${bucketSize})) as dur,
+        s.id,
+        z.depth as cpu,
+        utid
+      from process_scheduling_${this.trackUuid}(
+        ${start}, ${end}, ${bucketSize}
+      ) z
+      cross join sched s using (id)
+    `);
+  }
+
+  getHeight(): number {
+    return TRACK_HEIGHT;
+  }
+
+  render({ctx, size, timescale, visibleWindow}: TrackRenderContext): void {
+    // TODO: fonts and colors should come from the CSS and not hardcoded here.
+    const data = this.fetcher.data;
+
+    if (data === undefined) return; // Can't possibly draw anything.
+
+    // If the cached trace slices don't fully cover the visible time range,
+    // show a gray rectangle with a "Loading..." label.
+    checkerboardExcept(
+      ctx,
+      this.getHeight(),
+      0,
+      size.width,
+      timescale.timeToPx(data.start),
+      timescale.timeToPx(data.end),
+    );
+
+    assertTrue(data.starts.length === data.ends.length);
+    assertTrue(data.starts.length === data.utids.length);
+
+    const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu);
+
+    for (let i = 0; i < data.ends.length; i++) {
+      const tStart = Time.fromRaw(data.starts[i]);
+      const tEnd = Time.fromRaw(data.ends[i]);
+
+      // Cull slices that lie completely outside the visible window
+      if (!visibleWindow.overlaps(tStart, tEnd)) continue;
+
+      const utid = data.utids[i];
+      const cpu = data.cpus[i];
+
+      const rectStart = Math.floor(timescale.timeToPx(tStart));
+      const rectEnd = Math.floor(timescale.timeToPx(tEnd));
+      const rectWidth = Math.max(1, rectEnd - rectStart);
+
+      const threadInfo = this.threads.get(utid);
+      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+      const pid = (threadInfo ? threadInfo.pid : -1) || -1;
+
+      const isHovering = this.trace.timeline.hoveredUtid !== undefined;
+      const isThreadHovered = this.trace.timeline.hoveredUtid === utid;
+      const isProcessHovered = this.trace.timeline.hoveredPid === pid;
+      const colorScheme = colorForThread(threadInfo);
+      let color: Color;
+      if (isHovering && !isThreadHovered) {
+        if (!isProcessHovered) {
+          color = colorScheme.disabled;
+        } else {
+          color = colorScheme.variant;
+        }
+      } else {
+        color = colorScheme.base;
+      }
+      ctx.fillStyle = color.cssString;
+      const y = MARGIN_TOP + cpuTrackHeight * cpu + cpu;
+      ctx.fillRect(rectStart, y, rectWidth, cpuTrackHeight);
+    }
+
+    const hoveredThread = this.threads.get(this.utidHoveredInThisTrack);
+    if (hoveredThread !== undefined && this.mousePos !== undefined) {
+      const tidText = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`;
+      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+      if (hoveredThread.pid) {
+        const pidText = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`;
+        drawTrackHoverTooltip(ctx, this.mousePos, size, pidText, tidText);
+      } else {
+        drawTrackHoverTooltip(ctx, this.mousePos, size, tidText);
+      }
+    }
+  }
+
+  onMouseMove({x, y, timescale}: TrackMouseEvent) {
+    const data = this.fetcher.data;
+    this.mousePos = {x, y};
+    if (data === undefined) return;
+    if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) {
+      this.utidHoveredInThisTrack = -1;
+      this.trace.timeline.hoveredUtid = undefined;
+      this.trace.timeline.hoveredPid = undefined;
+      return;
+    }
+
+    const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu);
+    const cpu = Math.floor((y - MARGIN_TOP) / (cpuTrackHeight + 1));
+    const t = timescale.pxToHpTime(x).toTime('floor');
+
+    const [i, j] = searchRange(data.starts, t, searchEq(data.cpus, cpu));
+    if (i === j || i >= data.starts.length || t > data.ends[i]) {
+      this.utidHoveredInThisTrack = -1;
+      this.trace.timeline.hoveredUtid = undefined;
+      this.trace.timeline.hoveredPid = undefined;
+      return;
+    }
+
+    const utid = data.utids[i];
+    this.utidHoveredInThisTrack = utid;
+    const threadInfo = this.threads.get(utid);
+    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+    const pid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
+    this.trace.timeline.hoveredUtid = utid;
+    this.trace.timeline.hoveredPid = pid;
+  }
+
+  onMouseOut() {
+    this.utidHoveredInThisTrack = -1;
+    this.trace.timeline.hoveredUtid = undefined;
+    this.trace.timeline.hoveredPid = undefined;
+    this.mousePos = undefined;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.ProcessSummary/process_summary_track.ts b/ui/src/plugins/dev.perfetto.ProcessSummary/process_summary_track.ts
new file mode 100644
index 0000000..778c068
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ProcessSummary/process_summary_track.ts
@@ -0,0 +1,205 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {BigintMath} from '../../base/bigint_math';
+import {assertExists, assertTrue} from '../../base/logging';
+import {duration, Time, time} from '../../base/time';
+import {colorForTid} from '../../public/lib/colorizer';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {checkerboardExcept} from '../../frontend/checkerboard';
+import {Engine} from '../../trace_processor/engine';
+import {Track} from '../../public/track';
+import {LONG, NUM} from '../../trace_processor/query_result';
+import {uuidv4Sql} from '../../base/uuid';
+import {TrackRenderContext} from '../../public/track';
+
+export const PROCESS_SUMMARY_TRACK = 'ProcessSummaryTrack';
+
+interface Data extends TrackData {
+  starts: BigInt64Array;
+  utilizations: Float64Array;
+}
+
+export interface Config {
+  pidForColor: number;
+  upid: number | null;
+  utid: number | null;
+}
+
+const MARGIN_TOP = 5;
+const RECT_HEIGHT = 30;
+const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
+const SUMMARY_HEIGHT = TRACK_HEIGHT - MARGIN_TOP;
+
+export class ProcessSummaryTrack implements Track {
+  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+  private engine: Engine;
+  private config: Config;
+  private uuid = uuidv4Sql();
+
+  constructor(engine: Engine, config: Config) {
+    this.engine = engine;
+    this.config = config;
+  }
+
+  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 = `
+        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
+          )
+        select
+          ts,
+          sum(value) over (order by ts) / (select count() from tt) as value
+        from ss
+        order by ts
+      ));
+    `);
+  }
+
+  async onUpdate({
+    visibleWindow,
+    resolution,
+  }: TrackRenderContext): Promise<void> {
+    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    // Resolution must always be a power of 2 for this logic to work
+    assertTrue(
+      BigintMath.popcount(resolution) === 1,
+      `${resolution} not pow of 2`,
+    );
+
+    const queryRes = await this.engine.query(`
+      select last_ts as ts, last_value as utilization
+      from process_summary_${this.uuid}(${start}, ${end}, ${resolution});
+    `);
+    const numRows = queryRes.numRows();
+    const slices: Data = {
+      start,
+      end,
+      resolution,
+      length: numRows,
+      starts: new BigInt64Array(numRows),
+      utilizations: new Float64Array(numRows),
+    };
+    const it = queryRes.iter({
+      ts: LONG,
+      utilization: NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      slices.starts[row] = it.ts;
+      slices.utilizations[row] = it.utilization;
+    }
+    return slices;
+  }
+
+  async onDestroy(): Promise<void> {
+    await this.engine.tryQuery(
+      `drop table if exists process_summary_${this.uuid};`,
+    );
+    this.fetcher[Symbol.dispose]();
+  }
+
+  getHeight(): number {
+    return TRACK_HEIGHT;
+  }
+
+  render(trackCtx: TrackRenderContext): void {
+    const {ctx, size, timescale} = trackCtx;
+
+    const data = this.fetcher.data;
+    if (data === undefined) {
+      return;
+    }
+
+    // If the cached trace slices don't fully cover the visible time range,
+    // show a gray rectangle with a "Loading..." label.
+    checkerboardExcept(
+      ctx,
+      this.getHeight(),
+      0,
+      size.width,
+      timescale.timeToPx(data.start),
+      timescale.timeToPx(data.end),
+    );
+
+    this.renderSummary(trackCtx, data);
+  }
+
+  private renderSummary(
+    {ctx, timescale}: TrackRenderContext,
+    data: Data,
+  ): void {
+    const startPx = 0;
+    const bottomY = TRACK_HEIGHT;
+
+    let lastX = startPx;
+    let lastY = bottomY;
+
+    const color = colorForTid(this.config.pidForColor);
+    ctx.fillStyle = color.base.cssString;
+    ctx.beginPath();
+    ctx.moveTo(lastX, lastY);
+    for (let i = 0; i < data.utilizations.length; i++) {
+      const startTime = Time.fromRaw(data.starts[i]);
+      const utilization = data.utilizations[i];
+      lastX = Math.floor(timescale.timeToPx(startTime));
+      ctx.lineTo(lastX, lastY);
+      lastY = MARGIN_TOP + Math.round(SUMMARY_HEIGHT * (1 - utilization));
+      ctx.lineTo(lastX, lastY);
+    }
+    ctx.lineTo(lastX, bottomY);
+    ctx.closePath();
+    ctx.fill();
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.ProcessThreadGroups/index.ts b/ui/src/plugins/dev.perfetto.ProcessThreadGroups/index.ts
new file mode 100644
index 0000000..60aa11c
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ProcessThreadGroups/index.ts
@@ -0,0 +1,343 @@
+// 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 {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {
+  getOrCreateGroupForProcess,
+  getOrCreateGroupForThread,
+} from '../../public/standard_groups';
+import {TrackNode} from '../../public/workspace';
+import {NUM, STR, STR_NULL} from '../../trace_processor/query_result';
+
+function stripPathFromExecutable(path: string) {
+  if (path[0] === '/') {
+    return path.split('/').slice(-1)[0];
+  } else {
+    return path;
+  }
+}
+
+function getThreadDisplayName(threadName: string | undefined, tid: number) {
+  if (threadName) {
+    return `${stripPathFromExecutable(threadName)} ${tid}`;
+  } else {
+    return `Thread ${tid}`;
+  }
+}
+
+// This plugin is responsible for organizing all process and thread groups
+// including the kernel groups, sorting, and adding summary tracks.
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.ProcessThreadGroups';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const processGroups = new Map<number, TrackNode>();
+    const threadGroups = new Map<number, TrackNode>();
+
+    // Pre-group all kernel "threads" (actually processes) if this is a linux
+    // system trace. Below, addProcessTrackGroups will skip them due to an
+    // existing group uuid, and addThreadStateTracks will fill in the
+    // per-thread tracks. Quirk: since all threads will appear to be
+    // TrackKindPriority.MAIN_THREAD, any process-level tracks will end up
+    // pushed to the bottom of the group in the UI.
+    await this.addKernelThreadGrouping(ctx, threadGroups);
+
+    // Create the per-process track groups. Note that this won't necessarily
+    // create a track per process. If a process has been completely idle and has
+    // no sched events, no track group will be emitted.
+    // Will populate this.addTrackGroupActions
+    await this.addProcessGroups(ctx, processGroups, threadGroups);
+    await this.addThreadGroups(ctx, processGroups, threadGroups);
+
+    ctx.addEventListener('traceready', () => {
+      // If, by the time the trace has finished loading, some of the process or
+      // thread group tracks nodes have no children, just remove them.
+      const removeIfEmpty = (g: TrackNode) => {
+        if (!g.hasChildren) {
+          g.remove();
+        }
+      };
+      processGroups.forEach(removeIfEmpty);
+      threadGroups.forEach(removeIfEmpty);
+    });
+  }
+
+  private async addKernelThreadGrouping(
+    ctx: Trace,
+    threadGroups: Map<number, TrackNode>,
+  ): Promise<void> {
+    // Identify kernel threads if this is a linux system trace, and sufficient
+    // process information is available. Kernel threads are identified by being
+    // children of kthreadd (always pid 2).
+    // The query will return the kthreadd process row first, which must exist
+    // for any other kthreads to be returned by the query.
+    // TODO(rsavitski): figure out how to handle the idle process (swapper),
+    // which has pid 0 but appears as a distinct process (with its own comm) on
+    // each cpu. It'd make sense to exclude its thread state track, but still
+    // put process-scoped tracks in this group.
+    const result = await ctx.engine.query(`
+      select
+        t.utid, p.upid, (case p.pid when 2 then 1 else 0 end) isKthreadd
+      from
+        thread t
+        join process p using (upid)
+        left join process parent on (p.parent_upid = parent.upid)
+        join
+          (select true from metadata m
+             where (m.name = 'system_name' and m.str_value = 'Linux')
+           union
+           select 1 from (select true from sched limit 1))
+      where
+        p.pid = 2 or parent.pid = 2
+      order by isKthreadd desc
+    `);
+
+    const it = result.iter({
+      utid: NUM,
+      upid: NUM,
+    });
+
+    // Not applying kernel thread grouping.
+    if (!it.valid()) {
+      return;
+    }
+
+    // Create the track group. Use kthreadd's PROCESS_SUMMARY_TRACK for the
+    // main track. It doesn't summarise the kernel threads within the group,
+    // but creating a dedicated track type is out of scope at the time of
+    // writing.
+    const kernelThreadsGroup = new TrackNode({
+      title: 'Kernel threads',
+      uri: '/kernel',
+      sortOrder: 50,
+      isSummary: true,
+    });
+    ctx.workspace.addChildInOrder(kernelThreadsGroup);
+
+    // Set the group for all kernel threads (including kthreadd itself).
+    for (; it.valid(); it.next()) {
+      const {utid} = it;
+
+      const threadGroup = getOrCreateGroupForThread(ctx.workspace, utid);
+      threadGroup.headless = true;
+      kernelThreadsGroup.addChildInOrder(threadGroup);
+
+      threadGroups.set(utid, threadGroup);
+    }
+  }
+
+  // Adds top level groups for processes and thread that don't belong to a
+  // process.
+  private async addProcessGroups(
+    ctx: Trace,
+    processGroups: Map<number, TrackNode>,
+    threadGroups: Map<number, TrackNode>,
+  ): Promise<void> {
+    const result = await ctx.engine.query(`
+      with processGroups as (
+        select
+          upid,
+          process.pid as pid,
+          process.name as processName,
+          sum_running_dur as sumRunningDur,
+          thread_slice_count + process_slice_count as sliceCount,
+          perf_sample_count as perfSampleCount,
+          allocation_count as heapProfileAllocationCount,
+          graph_object_count as heapGraphObjectCount,
+          (
+            select group_concat(string_value)
+            from args
+            where
+              process.arg_set_id is not null and
+              arg_set_id = process.arg_set_id and
+              flat_key = 'chrome.process_label'
+          ) chromeProcessLabels,
+          case process.name
+            when 'Browser' then 3
+            when 'Gpu' then 2
+            when 'Renderer' then 1
+            else 0
+          end as chromeProcessRank
+        from _process_available_info_summary
+        join process using(upid)
+      ),
+      threadGroups as (
+        select
+          utid,
+          tid,
+          thread.name as threadName,
+          sum_running_dur as sumRunningDur,
+          slice_count as sliceCount,
+          perf_sample_count as perfSampleCount
+        from _thread_available_info_summary
+        join thread using (utid)
+        where upid is null
+      )
+      select *
+      from (
+        select
+          'process' as kind,
+          upid as uid,
+          pid as id,
+          processName as name
+        from processGroups
+        order by
+          chromeProcessRank desc,
+          heapProfileAllocationCount desc,
+          heapGraphObjectCount desc,
+          perfSampleCount desc,
+          sumRunningDur desc,
+          sliceCount desc,
+          processName asc,
+          upid asc
+      )
+      union all
+      select *
+      from (
+        select
+          'thread' as kind,
+          utid as uid,
+          tid as id,
+          threadName as name
+        from threadGroups
+        order by
+          perfSampleCount desc,
+          sumRunningDur desc,
+          sliceCount desc,
+          threadName asc,
+          utid asc
+      )
+  `);
+
+    const it = result.iter({
+      kind: STR,
+      uid: NUM,
+      id: NUM,
+      name: STR_NULL,
+    });
+    for (; it.valid(); it.next()) {
+      const {kind, uid, id, name} = it;
+
+      if (kind === 'process') {
+        // Ignore kernel process groups
+        if (processGroups.has(uid)) {
+          continue;
+        }
+
+        function getProcessDisplayName(
+          processName: string | undefined,
+          pid: number,
+        ) {
+          if (processName) {
+            return `${stripPathFromExecutable(processName)} ${pid}`;
+          } else {
+            return `Process ${pid}`;
+          }
+        }
+
+        const displayName = getProcessDisplayName(name ?? undefined, id);
+        const group = getOrCreateGroupForProcess(ctx.workspace, uid);
+        group.title = displayName;
+        group.uri = `/process_${uid}`; // Summary track URI
+        group.sortOrder = 50;
+
+        // Re-insert the child node to sort it
+        ctx.workspace.addChildInOrder(group);
+        processGroups.set(uid, group);
+      } else {
+        // Ignore kernel process groups
+        if (threadGroups.has(uid)) {
+          continue;
+        }
+
+        const displayName = getThreadDisplayName(name ?? undefined, id);
+        const group = getOrCreateGroupForThread(ctx.workspace, uid);
+        group.title = displayName;
+        group.uri = `/thread_${uid}`; // Summary track URI
+        group.sortOrder = 50;
+
+        // Re-insert the child node to sort it
+        ctx.workspace.addChildInOrder(group);
+        threadGroups.set(uid, group);
+      }
+    }
+  }
+
+  // Create all the nested & headless thread groups that live inside existing
+  // process groups.
+  private async addThreadGroups(
+    ctx: Trace,
+    processGroups: Map<number, TrackNode>,
+    threadGroups: Map<number, TrackNode>,
+  ): Promise<void> {
+    const result = await ctx.engine.query(`
+      with threadGroups as (
+        select
+          utid,
+          upid,
+          tid,
+          thread.name as threadName,
+          CASE
+            WHEN thread.is_main_thread = 1 THEN 10
+            WHEN thread.name = 'CrBrowserMain' THEN 10
+            WHEN thread.name = 'CrRendererMain' THEN 10
+            WHEN thread.name = 'CrGpuMain' THEN 10
+            WHEN thread.name glob '*RenderThread*' THEN 9
+            WHEN thread.name glob '*GPU completion*' THEN 8
+            WHEN thread.name = 'Chrome_ChildIOThread' THEN 7
+            WHEN thread.name = 'Chrome_IOThread' THEN 7
+            WHEN thread.name = 'Compositor' THEN 6
+            WHEN thread.name = 'VizCompositorThread' THEN 6
+            ELSE 5
+          END as priority
+        from _thread_available_info_summary
+        join thread using (utid)
+        where upid is not null
+      )
+      select *
+      from (
+        select
+          utid,
+          upid,
+          tid,
+          threadName
+        from threadGroups
+        order by
+          priority desc,
+          tid asc
+      )
+  `);
+
+    const it = result.iter({
+      utid: NUM,
+      tid: NUM,
+      upid: NUM,
+      threadName: STR_NULL,
+    });
+    for (; it.valid(); it.next()) {
+      const {utid, tid, upid, threadName} = it;
+
+      // Ignore kernel thread groups
+      if (threadGroups.has(utid)) {
+        continue;
+      }
+
+      const group = getOrCreateGroupForThread(ctx.workspace, utid);
+      group.title = getThreadDisplayName(threadName ?? undefined, tid);
+      threadGroups.set(utid, group);
+      group.headless = true;
+      processGroups.get(upid)?.addChildInOrder(group);
+    }
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.QueryPage/index.ts b/ui/src/plugins/dev.perfetto.QueryPage/index.ts
new file mode 100644
index 0000000..2fd4b77
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.QueryPage/index.ts
@@ -0,0 +1,32 @@
+// 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 {Trace} from '../../public/trace';
+import {QueryPage} from './query_page';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.QueryPage';
+
+  async onTraceLoad(trace: Trace): Promise<void> {
+    trace.pages.registerPage({route: '/query', page: QueryPage});
+    trace.sidebar.addMenuItem({
+      section: 'current_trace',
+      text: 'Query (SQL)',
+      href: '#!/query',
+      icon: 'database',
+      sortOrder: 1,
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.QueryPage/query_history.ts b/ui/src/plugins/dev.perfetto.QueryPage/query_history.ts
new file mode 100644
index 0000000..a98c987
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.QueryPage/query_history.ts
@@ -0,0 +1,194 @@
+// 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 {Icons} from '../../base/semantic_icons';
+import {assertTrue} from '../../base/logging';
+import {Icon} from '../../widgets/icon';
+import {z} from 'zod';
+import {Trace} from '../../public/trace';
+
+const QUERY_HISTORY_KEY = 'queryHistory';
+
+export interface QueryHistoryComponentAttrs {
+  trace: Trace;
+  runQuery: (query: string) => void;
+  setQuery: (query: string) => void;
+}
+
+export class QueryHistoryComponent
+  implements m.ClassComponent<QueryHistoryComponentAttrs>
+{
+  view({attrs}: m.CVnode<QueryHistoryComponentAttrs>): m.Child {
+    const runQuery = attrs.runQuery;
+    const setQuery = attrs.setQuery;
+    const unstarred: HistoryItemComponentAttrs[] = [];
+    const starred: HistoryItemComponentAttrs[] = [];
+    for (let i = queryHistoryStorage.data.length - 1; i >= 0; i--) {
+      const entry = queryHistoryStorage.data[i];
+      const arr = entry.starred ? starred : unstarred;
+      arr.push({trace: attrs.trace, index: i, entry, runQuery, setQuery});
+    }
+    return m(
+      '.query-history',
+      m(
+        'header.overview',
+        `Query history (${queryHistoryStorage.data.length} queries)`,
+      ),
+      starred.map((attrs) => m(HistoryItemComponent, attrs)),
+      unstarred.map((attrs) => m(HistoryItemComponent, attrs)),
+    );
+  }
+}
+
+export interface HistoryItemComponentAttrs {
+  trace: Trace;
+  index: number;
+  entry: QueryHistoryEntry;
+  runQuery: (query: string) => void;
+  setQuery: (query: string) => void;
+}
+
+export class HistoryItemComponent
+  implements m.ClassComponent<HistoryItemComponentAttrs>
+{
+  view(vnode: m.Vnode<HistoryItemComponentAttrs>): m.Child {
+    const query = vnode.attrs.entry.query;
+    return m(
+      '.history-item',
+      m(
+        '.history-item-buttons',
+        m(
+          'button',
+          {
+            onclick: () => {
+              queryHistoryStorage.setStarred(
+                vnode.attrs.index,
+                !vnode.attrs.entry.starred,
+              );
+              vnode.attrs.trace.scheduleFullRedraw();
+            },
+          },
+          m(Icon, {icon: Icons.Star, filled: vnode.attrs.entry.starred}),
+        ),
+        m(
+          'button',
+          {
+            onclick: () => vnode.attrs.setQuery(query),
+          },
+          m(Icon, {icon: 'edit'}),
+        ),
+        m(
+          'button',
+          {
+            onclick: () => vnode.attrs.runQuery(query),
+          },
+          m(Icon, {icon: 'play_arrow'}),
+        ),
+        m(
+          'button',
+          {
+            onclick: () => {
+              queryHistoryStorage.remove(vnode.attrs.index);
+              vnode.attrs.trace.scheduleFullRedraw();
+            },
+          },
+          m(Icon, {icon: 'delete'}),
+        ),
+      ),
+      m(
+        'pre',
+        {
+          onclick: () => vnode.attrs.setQuery(query),
+          ondblclick: () => vnode.attrs.runQuery(query),
+        },
+        query,
+      ),
+    );
+  }
+}
+
+class HistoryStorage {
+  data: QueryHistory;
+  maxItems = 50;
+
+  constructor() {
+    this.data = this.load();
+  }
+
+  saveQuery(query: string) {
+    const items = this.data;
+    let firstUnstarred = -1;
+    let countUnstarred = 0;
+    for (let i = 0; i < items.length; i++) {
+      if (!items[i].starred) {
+        countUnstarred++;
+        if (firstUnstarred === -1) {
+          firstUnstarred = i;
+        }
+      }
+
+      if (items[i].query === query) {
+        // Query is already in the history, no need to save
+        return;
+      }
+    }
+
+    if (countUnstarred >= this.maxItems) {
+      assertTrue(firstUnstarred !== -1);
+      items.splice(firstUnstarred, 1);
+    }
+
+    items.push({query, starred: false});
+    this.save();
+  }
+
+  setStarred(index: number, starred: boolean) {
+    assertTrue(index >= 0 && index < this.data.length);
+    this.data[index].starred = starred;
+    this.save();
+  }
+
+  remove(index: number) {
+    assertTrue(index >= 0 && index < this.data.length);
+    this.data.splice(index, 1);
+    this.save();
+  }
+
+  private load(): QueryHistory {
+    const value = window.localStorage.getItem(QUERY_HISTORY_KEY);
+    if (value === null) {
+      return [];
+    }
+    const res = QUERY_HISTORY_SCHEMA.safeParse(JSON.parse(value));
+    return res.success ? res.data : [];
+  }
+
+  private save() {
+    window.localStorage.setItem(QUERY_HISTORY_KEY, JSON.stringify(this.data));
+  }
+}
+
+const QUERY_HISTORY_ENTRY_SCHEMA = z.object({
+  query: z.string(),
+  starred: z.boolean().default(false),
+});
+
+type QueryHistoryEntry = z.infer<typeof QUERY_HISTORY_ENTRY_SCHEMA>;
+
+const QUERY_HISTORY_SCHEMA = z.array(QUERY_HISTORY_ENTRY_SCHEMA);
+
+type QueryHistory = z.infer<typeof QUERY_HISTORY_SCHEMA>;
+
+export const queryHistoryStorage = new HistoryStorage();
diff --git a/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts b/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts
new file mode 100644
index 0000000..9ceaefc
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts
@@ -0,0 +1,139 @@
+// 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.
+
+import m from 'mithril';
+import {SimpleResizeObserver} from '../../base/resize_observer';
+import {undoCommonChatAppReplacements} from '../../base/string_utils';
+import {QueryResponse, runQuery} from '../../public/lib/query_table/queries';
+import {Callout} from '../../widgets/callout';
+import {Editor} from '../../widgets/editor';
+import {PageWithTraceAttrs} from '../../public/page';
+import {QueryHistoryComponent, queryHistoryStorage} from './query_history';
+import {Trace, TraceAttrs} from '../../public/trace';
+import {addQueryResultsTab} from '../../public/lib/query_table/query_result_tab';
+import {QueryTable} from '../../public/lib/query_table/query_table';
+
+interface QueryPageState {
+  enteredText: string;
+  executedQuery?: string;
+  queryResult?: QueryResponse;
+  heightPx: string;
+  generation: number;
+}
+
+const state: QueryPageState = {
+  enteredText: '',
+  heightPx: '100px',
+  generation: 0,
+};
+
+function runManualQuery(trace: Trace, query: string) {
+  state.executedQuery = query;
+  state.queryResult = undefined;
+  runQuery(undoCommonChatAppReplacements(query), trace.engine).then(
+    (resp: QueryResponse) => {
+      addQueryResultsTab(
+        trace,
+        {
+          query: query,
+          title: 'Standalone Query',
+          prefetchedResponse: resp,
+        },
+        'analyze_page_query',
+      );
+      // We might have started to execute another query. Ignore it in that
+      // case.
+      if (state.executedQuery !== query) {
+        return;
+      }
+      state.queryResult = resp;
+      trace.scheduleFullRedraw();
+    },
+  );
+}
+
+export type QueryInputAttrs = TraceAttrs;
+
+class QueryInput implements m.ClassComponent<QueryInputAttrs> {
+  private resize?: Disposable;
+
+  oncreate({dom}: m.CVnodeDOM<QueryInputAttrs>): void {
+    this.resize = new SimpleResizeObserver(dom, () => {
+      state.heightPx = (dom as HTMLElement).style.height;
+    });
+    (dom as HTMLElement).style.height = state.heightPx;
+  }
+
+  onremove(): void {
+    if (this.resize) {
+      this.resize[Symbol.dispose]();
+      this.resize = undefined;
+    }
+  }
+
+  view({attrs}: m.CVnode<QueryInputAttrs>) {
+    return m(Editor, {
+      generation: state.generation,
+      initialText: state.enteredText,
+
+      onExecute: (text: string) => {
+        if (!text) {
+          return;
+        }
+        queryHistoryStorage.saveQuery(text);
+        runManualQuery(attrs.trace, text);
+      },
+
+      onUpdate: (text: string) => {
+        state.enteredText = text;
+        attrs.trace.scheduleFullRedraw();
+      },
+    });
+  }
+}
+
+export class QueryPage implements m.ClassComponent<PageWithTraceAttrs> {
+  view({attrs}: m.CVnode<PageWithTraceAttrs>) {
+    return m(
+      '.query-page',
+      m(Callout, 'Enter query and press Cmd/Ctrl + Enter'),
+      state.enteredText.includes('"') &&
+        m(
+          Callout,
+          {icon: 'warning'},
+          `" (double quote) character observed in query; if this is being used to ` +
+            `define a string, please use ' (single quote) instead. Using double quotes ` +
+            `can cause subtle problems which are very hard to debug.`,
+        ),
+      m(QueryInput, attrs),
+      state.executedQuery === undefined
+        ? null
+        : m(QueryTable, {
+            trace: attrs.trace,
+            query: state.executedQuery,
+            resp: state.queryResult,
+            fillParent: false,
+          }),
+      m(QueryHistoryComponent, {
+        trace: attrs.trace,
+        runQuery: (q: string) => runManualQuery(attrs.trace, q),
+        setQuery: (q: string) => {
+          state.enteredText = q;
+          state.generation++;
+          attrs.trace.scheduleFullRedraw();
+        },
+      }),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/adb.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb.ts
new file mode 100644
index 0000000..5197d23
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb.ts
@@ -0,0 +1,701 @@
+// Copyright (C) 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
+//
+//      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 {isString} from '../../base/object_utils';
+import {utf8Decode, utf8Encode} from '../../base/string_utils';
+import {Adb, AdbMsg, AdbStream, CmdType} from './adb_interfaces';
+
+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 AdbOverWebUsb's handleAuthentication().
+  AUTH_STEP1 = 1,
+  AUTH_STEP2 = 2,
+  AUTH_STEP3 = 3,
+
+  CONNECTED = 2,
+}
+
+enum AuthCmd {
+  TOKEN = 1,
+  SIGNATURE = 2,
+  RSAPUBLICKEY = 3,
+}
+
+const DEVICE_NOT_SET_ERROR = 'Device not set.';
+
+// This class is a basic TypeScript implementation of adb that only supports
+// shell commands. It is used to send the start tracing command to the connected
+// android device, and to automatically pull the trace after the end of the
+// recording. It works through the webUSB API. A brief description of how it
+// works is the following:
+// - The connection with the device is initiated by findAndConnect, which shows
+//   a dialog with a list of connected devices. Once one is selected the
+//   authentication begins. The authentication has to pass different steps, as
+//   described in the "handeAuthentication" method.
+// - AdbOverWebUsb tracks the state of the authentication via a state machine
+//   (see AdbState).
+// - A Message handler loop is executed to keep receiving the messages.
+// - All the messages received from the device are passed to "onMessage" that is
+//   implemented as a state machine.
+// - When a new shell is established, it becomes an AdbStream, and is kept in
+//   the "streams" map. Each time a message from the device is for a specific
+//   previously opened stream, the "onMessage" function will forward it to the
+//   stream (identified by a number).
+export class AdbOverWebUsb implements Adb {
+  state: AdbState = AdbState.DISCONNECTED;
+  streams = new Map<number, AdbStream>();
+  devProps = '';
+  maxPayload = DEFAULT_MAX_PAYLOAD_BYTES;
+  key?: CryptoKeyPair;
+  onConnected = () => {};
+
+  // Devices after Dec 2017 don't use checksum. This will be auto-detected
+  // during the connection.
+  useChecksum = true;
+
+  private lastStreamId = 0;
+  private dev?: USBDevice;
+  private usbInterfaceNumber?: number;
+  private usbReadEndpoint = -1;
+  private usbWriteEpEndpoint = -1;
+  private filter = {
+    classCode: 255, // USB vendor specific code
+    subclassCode: 66, // Android vendor specific subclass
+    protocolCode: 1, // Adb protocol
+  };
+
+  async findDevice() {
+    if (!('usb' in navigator)) {
+      throw new Error('WebUSB not supported by the browser (requires HTTPS)');
+    }
+    return navigator.usb.requestDevice({filters: [this.filter]});
+  }
+
+  async getPairedDevices() {
+    try {
+      return await navigator.usb.getDevices();
+    } catch (e) {
+      // WebUSB not available.
+      return Promise.resolve([]);
+    }
+  }
+
+  async connect(device: USBDevice): Promise<void> {
+    // If we are already connected, we are also already authenticated, so we can
+    // skip doing the authentication again.
+    if (this.state === AdbState.CONNECTED) {
+      if (this.dev === device && device.opened) {
+        this.onConnected();
+        this.onConnected = () => {};
+        return;
+      }
+      // Another device was connected.
+      await this.disconnect();
+    }
+
+    this.dev = device;
+    this.useChecksum = true;
+    this.key = await AdbOverWebUsb.initKey();
+
+    await this.dev.open();
+
+    const {configValue, usbInterfaceNumber, endpoints} =
+      this.findInterfaceAndEndpoint();
+    this.usbInterfaceNumber = usbInterfaceNumber;
+
+    this.usbReadEndpoint = this.findEndpointNumber(endpoints, 'in');
+    this.usbWriteEpEndpoint = this.findEndpointNumber(endpoints, 'out');
+
+    console.assert(this.usbReadEndpoint >= 0 && this.usbWriteEpEndpoint >= 0);
+
+    await this.dev.selectConfiguration(configValue);
+    await this.dev.claimInterface(usbInterfaceNumber);
+
+    await this.startAuthentication();
+
+    // This will start a message handler loop.
+    this.receiveDeviceMessages();
+    // The promise will be resolved after the handshake.
+    return new Promise<void>((resolve, _) => (this.onConnected = resolve));
+  }
+
+  async disconnect(): Promise<void> {
+    if (this.state === AdbState.DISCONNECTED) {
+      return;
+    }
+    this.state = AdbState.DISCONNECTED;
+
+    if (!this.dev) return;
+
+    new Map(this.streams).forEach((stream, _id) => stream.setClosed());
+    console.assert(this.streams.size === 0);
+
+    await this.dev.releaseInterface(assertExists(this.usbInterfaceNumber));
+    this.dev = undefined;
+    this.usbInterfaceNumber = undefined;
+  }
+
+  async startAuthentication() {
+    // USB connected, now let's authenticate.
+    const VERSION = this.useChecksum
+      ? VERSION_WITH_CHECKSUM
+      : VERSION_NO_CHECKSUM;
+    this.state = AdbState.AUTH_STEP1;
+    await this.send('CNXN', VERSION, this.maxPayload, 'host:1:UsbADB');
+  }
+
+  findInterfaceAndEndpoint() {
+    if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
+    for (const config of this.dev.configurations) {
+      for (const interface_ of config.interfaces) {
+        for (const alt of interface_.alternates) {
+          if (
+            alt.interfaceClass === this.filter.classCode &&
+            alt.interfaceSubclass === this.filter.subclassCode &&
+            alt.interfaceProtocol === this.filter.protocolCode
+          ) {
+            return {
+              configValue: config.configurationValue,
+              usbInterfaceNumber: interface_.interfaceNumber,
+              endpoints: alt.endpoints,
+            };
+          } // if (alternate)
+        } // for (interface.alternates)
+      } // for (configuration.interfaces)
+    } // for (configurations)
+
+    throw Error('Cannot find interfaces and endpoints');
+  }
+
+  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 Error(`Cannot find ${direction} endpoint`);
+  }
+
+  receiveDeviceMessages() {
+    this.recv()
+      .then((msg) => {
+        this.onMessage(msg);
+        this.receiveDeviceMessages();
+      })
+      .catch((e) => {
+        // Ignore error with "DEVICE_NOT_SET_ERROR" message since it is always
+        // thrown after the device disconnects.
+        if (e.message !== DEVICE_NOT_SET_ERROR) {
+          console.error(`Exception in recv: ${e.name}. error: ${e.message}`);
+        }
+        this.disconnect();
+      });
+  }
+
+  async onMessage(msg: AdbMsg) {
+    if (!this.key) throw Error('ADB key not initialized');
+
+    if (msg.cmd === 'AUTH' && msg.arg0 === AuthCmd.TOKEN) {
+      this.handleAuthentication(msg);
+    } else if (msg.cmd === 'CNXN') {
+      console.assert(
+        [AdbState.AUTH_STEP2, AdbState.AUTH_STEP3].includes(this.state),
+      );
+      this.state = AdbState.CONNECTED;
+      this.handleConnectedMessage(msg);
+    } else if (
+      this.state === AdbState.CONNECTED &&
+      ['OKAY', 'WRTE', 'CLSE'].indexOf(msg.cmd) >= 0
+    ) {
+      const stream = this.streams.get(msg.arg1);
+      if (!stream) {
+        console.warn(`Received message ${msg} for unknown stream ${msg.arg1}`);
+        return;
+      }
+      stream.onMessage(msg);
+    } else {
+      console.error(`Unexpected message `, msg, ` in state ${this.state}`);
+    }
+  }
+
+  async handleAuthentication(msg: AdbMsg) {
+    if (!this.key) throw Error('ADB key not initialized');
+
+    console.assert(msg.cmd === 'AUTH' && msg.arg0 === AuthCmd.TOKEN);
+    const token = msg.data;
+
+    if (this.state === AdbState.AUTH_STEP1) {
+      // 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 will not be displayed. Otherwise we will receive another message
+      // ending up in AUTH_STEP3.
+      this.state = AdbState.AUTH_STEP2;
+
+      const signedToken = await signAdbTokenWithPrivateKey(
+        this.key.privateKey,
+        token,
+      );
+      this.send('AUTH', AuthCmd.SIGNATURE, 0, new Uint8Array(signedToken));
+      return;
+    }
+
+    console.assert(this.state === AdbState.AUTH_STEP2);
+
+    // During this step, we send our public key. The dialog will appear, and
+    // if the user chooses to remember our public key, it will be
+    // saved, so that the next time we will only pass through AUTH_STEP1.
+    this.state = AdbState.AUTH_STEP3;
+    const encodedPubKey = await encodePubKey(this.key.publicKey);
+    this.send('AUTH', AuthCmd.RSAPUBLICKEY, 0, encodedPubKey);
+  }
+
+  private handleConnectedMessage(msg: AdbMsg) {
+    console.assert(msg.cmd === 'CNXN');
+
+    this.maxPayload = msg.arg1;
+    this.devProps = utf8Decode(msg.data);
+
+    const deviceVersion = msg.arg0;
+
+    if (![VERSION_WITH_CHECKSUM, VERSION_NO_CHECKSUM].includes(deviceVersion)) {
+      console.error('Version ', msg.arg0, ' not really supported!');
+    }
+    this.useChecksum = deviceVersion === VERSION_WITH_CHECKSUM;
+    this.state = AdbState.CONNECTED;
+
+    // This will resolve the promise returned by "onConnect"
+    this.onConnected();
+    this.onConnected = () => {};
+  }
+
+  shell(cmd: string): Promise<AdbStream> {
+    return this.openStream('shell:' + cmd);
+  }
+
+  socket(path: string): Promise<AdbStream> {
+    return this.openStream('localfilesystem:' + path);
+  }
+
+  openStream(svc: string): Promise<AdbStream> {
+    const stream = new AdbStreamImpl(this, ++this.lastStreamId);
+    this.streams.set(stream.localStreamId, stream);
+    this.send('OPEN', stream.localStreamId, 0, svc);
+
+    //  The stream will resolve this promise once it receives the
+    //  acknowledgement message from the device.
+    return new Promise<AdbStream>((resolve, reject) => {
+      stream.onConnect = () => {
+        stream.onClose = () => {};
+        resolve(stream);
+      };
+      stream.onClose = () =>
+        reject(new Error(`Failed to openStream svc=${svc}`));
+    });
+  }
+
+  async shellOutputAsString(cmd: string): Promise<string> {
+    const shell = await this.shell(cmd);
+
+    return new Promise<string>((resolve, _) => {
+      const output: string[] = [];
+      shell.onData = (raw) => output.push(utf8Decode(raw));
+      shell.onClose = () => resolve(output.join());
+    });
+  }
+
+  async send(
+    cmd: CmdType,
+    arg0: number,
+    arg1: number,
+    data?: Uint8Array | string,
+  ) {
+    await this.sendMsg(
+      AdbMsgImpl.create({cmd, arg0, arg1, data, useChecksum: this.useChecksum}),
+    );
+  }
+
+  //  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.
+  async sendMsg(msg: AdbMsgImpl) {
+    const sendPromises = [this.sendRaw(msg.encodeHeader())];
+    if (msg.data.length > 0) sendPromises.push(this.sendRaw(msg.data));
+    await Promise.all(sendPromises);
+  }
+
+  async recv(): Promise<AdbMsg> {
+    const res = await this.recvRaw(ADB_MSG_SIZE);
+    console.assert(res.status === 'ok');
+    const msg = AdbMsgImpl.decodeHeader(res.data!);
+
+    if (msg.dataLen > 0) {
+      const resp = await this.recvRaw(msg.dataLen);
+      msg.data = new Uint8Array(
+        resp.data!.buffer,
+        resp.data!.byteOffset,
+        resp.data!.byteLength,
+      );
+    }
+    if (this.useChecksum) {
+      console.assert(AdbOverWebUsb.checksum(msg.data) === msg.dataChecksum);
+    }
+    return msg;
+  }
+
+  static async initKey(): Promise<CryptoKeyPair> {
+    const KEY_SIZE = 2048;
+
+    const keySpec = {
+      name: 'RSASSA-PKCS1-v1_5',
+      modulusLength: KEY_SIZE,
+      publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
+      hash: {name: 'SHA-1'},
+    };
+
+    const key = await crypto.subtle.generateKey(
+      keySpec,
+      /* extractable=*/ true,
+      ['sign', 'verify'],
+    );
+    return key;
+  }
+
+  static checksum(data: Uint8Array): number {
+    let res = 0;
+    for (let i = 0; i < data.byteLength; i++) res += data[i];
+    return res & 0xffffffff;
+  }
+
+  sendRaw(buf: Uint8Array): Promise<USBOutTransferResult> {
+    console.assert(buf.length <= this.maxPayload);
+    if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
+    return this.dev.transferOut(this.usbWriteEpEndpoint, buf.buffer);
+  }
+
+  recvRaw(dataLen: number): Promise<USBInTransferResult> {
+    if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
+    return this.dev.transferIn(this.usbReadEndpoint, dataLen);
+  }
+}
+
+enum AdbStreamState {
+  WAITING_INITIAL_OKAY = 0,
+  CONNECTED = 1,
+  CLOSED = 2,
+}
+
+// An AdbStream is instantiated after the creation of a shell 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. Also streams have an initialization flow:
+//   1. WAITING_INITIAL_OKAY: waiting for first "OKAY" message. Once received,
+//      the next state will be "CONNECTED".
+//   2. CONNECTED: ready to receive or send messages.
+//   3. WRITING: this is needed because we must receive an ack after sending
+//      each message (so, before sending the next one). For this reason, many
+//      subsequent "write" calls will result in different messages in the
+//      writeQueue. After each new acknowledgement ('OKAY') a new one will be
+//      sent. When the queue is empty, the state will return to CONNECTED.
+//   4. CLOSED: entered when the device closes the stream or close() is called.
+//      For shell commands, the stream is closed after the command completed.
+export class AdbStreamImpl implements AdbStream {
+  private adb: AdbOverWebUsb;
+  localStreamId: number;
+  private remoteStreamId = -1;
+  private state: AdbStreamState = AdbStreamState.WAITING_INITIAL_OKAY;
+  private writeQueue: Uint8Array[] = [];
+  private sendInProgress = false;
+
+  onData: AdbStreamReadCallback = (_) => {};
+  onConnect = () => {};
+  onClose = () => {};
+
+  constructor(adb: AdbOverWebUsb, localStreamId: number) {
+    this.adb = adb;
+    this.localStreamId = localStreamId;
+  }
+
+  close() {
+    console.assert(this.state === AdbStreamState.CONNECTED);
+
+    if (this.writeQueue.length > 0) {
+      console.error(
+        `Dropping ${this.writeQueue.length} queued messages due to stream closing.`,
+      );
+      this.writeQueue = [];
+    }
+
+    this.adb.send('CLSE', this.localStreamId, this.remoteStreamId);
+  }
+
+  async write(msg: string | Uint8Array) {
+    const raw = isString(msg) ? utf8Encode(msg) : msg;
+    if (
+      this.sendInProgress ||
+      this.state === AdbStreamState.WAITING_INITIAL_OKAY
+    ) {
+      this.writeQueue.push(raw);
+      return;
+    }
+    console.assert(this.state === AdbStreamState.CONNECTED);
+    this.sendInProgress = true;
+    await this.adb.send('WRTE', this.localStreamId, this.remoteStreamId, raw);
+  }
+
+  setClosed() {
+    this.state = AdbStreamState.CLOSED;
+    this.adb.streams.delete(this.localStreamId);
+    this.onClose();
+  }
+
+  onMessage(msg: AdbMsgImpl) {
+    console.assert(msg.arg1 === this.localStreamId);
+
+    if (
+      this.state === AdbStreamState.WAITING_INITIAL_OKAY &&
+      msg.cmd === 'OKAY'
+    ) {
+      this.remoteStreamId = msg.arg0;
+      this.state = AdbStreamState.CONNECTED;
+      this.onConnect();
+      return;
+    }
+
+    if (msg.cmd === 'WRTE') {
+      this.adb.send('OKAY', this.localStreamId, this.remoteStreamId);
+      this.onData(msg.data);
+      return;
+    }
+
+    if (msg.cmd === 'OKAY') {
+      console.assert(this.sendInProgress);
+      this.sendInProgress = false;
+      const queuedMsg = this.writeQueue.shift();
+      if (queuedMsg !== undefined) this.write(queuedMsg);
+      return;
+    }
+
+    if (msg.cmd === 'CLSE') {
+      this.setClosed();
+      return;
+    }
+    console.error(
+      `Unexpected stream msg ${msg.toString()} in state ${this.state}`,
+    );
+  }
+}
+
+interface AdbStreamReadCallback {
+  (raw: Uint8Array): void;
+}
+
+const ADB_MSG_SIZE = 6 * 4; // 6 * int32.
+
+export class AdbMsgImpl implements AdbMsg {
+  cmd: CmdType;
+  arg0: number;
+  arg1: number;
+  data: Uint8Array;
+  dataLen: number;
+  dataChecksum: number;
+
+  useChecksum: boolean;
+
+  constructor(
+    cmd: CmdType,
+    arg0: number,
+    arg1: number,
+    dataLen: number,
+    dataChecksum: number,
+    useChecksum = false,
+  ) {
+    console.assert(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;
+  }): AdbMsgImpl {
+    const encodedData = this.encodeData(data);
+    const msg = new AdbMsgImpl(
+      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): AdbMsgImpl {
+    console.assert(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);
+    console.assert(cmdNum === (cmdChecksum ^ 0xffffffff));
+    return new AdbMsgImpl(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 = AdbMsgImpl.encodeData(this.data);
+    const checksum = this.useChecksum ? AdbOverWebUsb.checksum(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;
+  }
+}
+
+function base64StringToArray(s: string) {
+  const decoded = atob(s.replaceAll('-', '+').replaceAll('_', '/'));
+  return [...decoded].map((char) => char.charCodeAt(0));
+}
+
+const ANDROID_PUBKEY_MODULUS_SIZE = 2048;
+const MODULUS_SIZE_BYTES = ANDROID_PUBKEY_MODULUS_SIZE / 8;
+
+// RSA Public keys are encoded in a rather unique way. It's a base64 encoded
+// struct of 524 bytes in total as follows (see
+// libcrypto_utils/android_pubkey.c):
+//
+// typedef struct RSAPublicKey {
+//   // Modulus length. This must be ANDROID_PUBKEY_MODULUS_SIZE.
+//   uint32_t modulus_size_words;
+//
+//   // Precomputed montgomery parameter: -1 / n[0] mod 2^32
+//   uint32_t n0inv;
+//
+//   // RSA modulus as a little-endian array.
+//   uint8_t modulus[ANDROID_PUBKEY_MODULUS_SIZE];
+//
+//   // Montgomery parameter R^2 as a little-endian array of little-endian
+//   words. uint8_t rr[ANDROID_PUBKEY_MODULUS_SIZE];
+//
+//   // RSA modulus: 3 or 65537
+//   uint32_t exponent;
+// } RSAPublicKey;
+//
+// However, the Montgomery params (n0inv and rr) are not really used, see
+// comment in android_pubkey_decode() ("Note that we don't extract the
+// montgomery parameters...")
+async function encodePubKey(key: CryptoKey) {
+  const expPubKey = await crypto.subtle.exportKey('jwk', key);
+  const nArr = base64StringToArray(expPubKey.n as string).reverse();
+  const eArr = base64StringToArray(expPubKey.e as string).reverse();
+
+  const arr = new Uint8Array(3 * 4 + 2 * MODULUS_SIZE_BYTES);
+  const dv = new DataView(arr.buffer);
+  dv.setUint32(0, MODULUS_SIZE_BYTES / 4, true);
+
+  // The Mongomery params (n0inv and rr) are not computed.
+  dv.setUint32(4, 0 /* n0inv*/, true);
+  // Modulus
+  for (let i = 0; i < MODULUS_SIZE_BYTES; i++) dv.setUint8(8 + i, nArr[i]);
+
+  // rr:
+  for (let i = 0; i < MODULUS_SIZE_BYTES; i++) {
+    dv.setUint8(8 + MODULUS_SIZE_BYTES + i, 0 /* rr*/);
+  }
+  // Exponent
+  for (let i = 0; i < 4; i++) {
+    dv.setUint8(8 + 2 * MODULUS_SIZE_BYTES + i, eArr[i]);
+  }
+  return (
+    btoa(String.fromCharCode(...new Uint8Array(dv.buffer))) + ' perfetto@webusb'
+  );
+}
+
+// TODO(nicomazz): This token signature will be useful only when we save the
+// generated keys. So far, we are not doing so. As a consequence, a dialog is
+// displayed every time a tracing session is started.
+// The reason why it has not already been implemented is that the standard
+// crypto.subtle.sign function assumes that the input needs hashing, which is
+// not the case for ADB, where the 20 bytes token is already hashed.
+// A solution to this is implementing a custom private key signature with a js
+// implementation of big integers. Maybe, wrapping the key like in the following
+// CL can work:
+// https://android-review.googlesource.com/c/platform/external/perfetto/+/1105354/18
+async function signAdbTokenWithPrivateKey(
+  _privateKey: CryptoKey,
+  token: Uint8Array,
+): Promise<ArrayBuffer> {
+  // This function is not implemented.
+  return token.buffer;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/adb_base_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_base_controller.ts
new file mode 100644
index 0000000..c447df5
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_base_controller.ts
@@ -0,0 +1,156 @@
+// Copyright (C) 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
+//
+//      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 {RecordingState, RecordingTarget, isAdbTarget} from './state';
+import {
+  extractDurationFromTraceConfig,
+  extractTraceConfig,
+} from './trace_config_utils';
+import {Adb} from './adb_interfaces';
+import {ReadBuffersResponse} from './consumer_port_types';
+import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
+
+export enum AdbConnectionState {
+  READY_TO_CONNECT,
+  AUTH_IN_PROGRESS,
+  CONNECTED,
+  CLOSED,
+}
+
+interface Command {
+  method: string;
+  params: Uint8Array;
+}
+
+export abstract class AdbBaseConsumerPort extends RpcConsumerPort {
+  // Contains the commands sent while the authentication is in progress. They
+  // will all be executed afterwards. If the device disconnects, they are
+  // removed.
+  private commandQueue: Command[] = [];
+
+  protected adb: Adb;
+  protected state = AdbConnectionState.READY_TO_CONNECT;
+  protected device?: USBDevice;
+  protected recState: RecordingState;
+
+  protected constructor(
+    adb: Adb,
+    consumer: Consumer,
+    recState: RecordingState,
+  ) {
+    super(consumer);
+    this.adb = adb;
+    this.recState = recState;
+  }
+
+  async handleCommand(method: string, params: Uint8Array) {
+    try {
+      if (method === 'FreeBuffers') {
+        // When we finish tracing, we disconnect the adb device interface.
+        // Otherwise, we will keep holding the device interface and won't allow
+        // adb to access it. https://wicg.github.io/webusb/#abusing-a-device
+        // "Lastly, since USB devices are unable to distinguish requests from
+        // multiple sources, operating systems only allow a USB interface to
+        // have a single owning user-space or kernel-space driver."
+        this.state = AdbConnectionState.CLOSED;
+        await this.adb.disconnect();
+      } else if (method === 'EnableTracing') {
+        this.state = AdbConnectionState.READY_TO_CONNECT;
+      }
+
+      if (this.state === AdbConnectionState.CLOSED) return;
+
+      this.commandQueue.push({method, params});
+
+      if (
+        this.state === AdbConnectionState.READY_TO_CONNECT ||
+        this.deviceDisconnected()
+      ) {
+        this.state = AdbConnectionState.AUTH_IN_PROGRESS;
+        this.device = await this.findDevice(this.recState.recordingTarget);
+        if (!this.device) {
+          this.state = AdbConnectionState.READY_TO_CONNECT;
+          const target = this.recState.recordingTarget;
+          throw Error(
+            `Device with serial ${
+              isAdbTarget(target) ? target.serial : 'n/a'
+            } not found.`,
+          );
+        }
+
+        this.sendStatus(`Please allow USB debugging on device.
+          If you press cancel, reload the page.`);
+
+        await this.adb.connect(this.device);
+
+        // During the authentication the device may have been disconnected.
+        if (!this.recState.recordingInProgress || this.deviceDisconnected()) {
+          throw Error('Recording not in progress after adb authorization.');
+        }
+
+        this.state = AdbConnectionState.CONNECTED;
+        this.sendStatus('Device connected.');
+      }
+
+      if (this.state === AdbConnectionState.AUTH_IN_PROGRESS) return;
+
+      console.assert(this.state === AdbConnectionState.CONNECTED);
+
+      for (const cmd of this.commandQueue) this.invoke(cmd.method, cmd.params);
+
+      this.commandQueue = [];
+    } catch (e) {
+      this.commandQueue = [];
+      this.state = AdbConnectionState.READY_TO_CONNECT;
+      this.sendErrorMessage(e.message);
+    }
+  }
+
+  private deviceDisconnected() {
+    return !this.device || !this.device.opened;
+  }
+
+  setDurationStatus(enableTracingProto: Uint8Array) {
+    const traceConfigProto = extractTraceConfig(enableTracingProto);
+    if (!traceConfigProto) return;
+    const duration = extractDurationFromTraceConfig(traceConfigProto);
+    this.sendStatus(
+      `Recording in progress${
+        exists(duration) ? ' for ' + duration.toString() + ' ms' : ''
+      }...`,
+    );
+  }
+
+  abstract invoke(method: string, argsProto: Uint8Array): void;
+
+  generateChunkReadResponse(
+    data: Uint8Array,
+    last = false,
+  ): ReadBuffersResponse {
+    return {
+      type: 'ReadBuffersResponse',
+      slices: [{data, lastSliceForPacket: last}],
+    };
+  }
+
+  async findDevice(
+    connectedDevice: RecordingTarget,
+  ): Promise<USBDevice | undefined> {
+    if (!('usb' in navigator)) return undefined;
+    if (!isAdbTarget(connectedDevice)) return undefined;
+    const devices = await navigator.usb.getDevices();
+    return devices.find((d) => d.serialNumber === connectedDevice.serial);
+  }
+}
diff --git a/ui/src/controller/adb_interfaces.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_interfaces.ts
similarity index 100%
rename from ui/src/controller/adb_interfaces.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_interfaces.ts
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/adb_jsdomtest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_jsdomtest.ts
new file mode 100644
index 0000000..1d228a5
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_jsdomtest.ts
@@ -0,0 +1,78 @@
+// Copyright (C) 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
+//
+//      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 {
+  AdbMsgImpl,
+  AdbOverWebUsb,
+  AdbState,
+  DEFAULT_MAX_PAYLOAD_BYTES,
+  VERSION_WITH_CHECKSUM,
+} from './adb';
+import {utf8Encode} from '../../base/string_utils';
+
+test('startAuthentication', async () => {
+  const adb = new AdbOverWebUsb();
+
+  const sendRaw = jest.fn();
+  adb.sendRaw = sendRaw;
+  const recvRaw = jest.fn();
+  adb.recvRaw = recvRaw;
+
+  const expectedAuthMessage = AdbMsgImpl.create({
+    cmd: 'CNXN',
+    arg0: VERSION_WITH_CHECKSUM,
+    arg1: DEFAULT_MAX_PAYLOAD_BYTES,
+    data: 'host:1:UsbADB',
+    useChecksum: true,
+  });
+  await adb.startAuthentication();
+
+  expect(sendRaw).toHaveBeenCalledTimes(2);
+  expect(sendRaw).toBeCalledWith(expectedAuthMessage.encodeHeader());
+  expect(sendRaw).toBeCalledWith(expectedAuthMessage.data);
+});
+
+test('connectedMessage', async () => {
+  const adb = new AdbOverWebUsb();
+  adb.key = {} as unknown as CryptoKeyPair;
+  adb.state = AdbState.AUTH_STEP2;
+
+  const onConnected = jest.fn();
+  adb.onConnected = onConnected;
+
+  const expectedMaxPayload = 42;
+  const connectedMsg = AdbMsgImpl.create({
+    cmd: 'CNXN',
+    arg0: VERSION_WITH_CHECKSUM,
+    arg1: expectedMaxPayload,
+    data: utf8Encode('device'),
+    useChecksum: true,
+  });
+  await adb.onMessage(connectedMsg);
+
+  expect(adb.state).toBe(AdbState.CONNECTED);
+  expect(adb.maxPayload).toBe(expectedMaxPayload);
+  expect(adb.devProps).toBe('device');
+  expect(adb.useChecksum).toBe(true);
+  expect(onConnected).toHaveBeenCalledTimes(1);
+});
+
+test('shellOpening', () => {
+  const adb = new AdbOverWebUsb();
+  const openStream = jest.fn();
+  adb.openStream = openStream;
+
+  adb.shell('test');
+  expect(openStream).toBeCalledWith('shell:test');
+});
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/adb_record_controller_jsdomtest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_record_controller_jsdomtest.ts
new file mode 100644
index 0000000..6078a59
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_record_controller_jsdomtest.ts
@@ -0,0 +1,136 @@
+// Copyright (C) 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
+//
+//      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 {dingus} from 'dingusjs';
+import {utf8Encode} from '../../base/string_utils';
+import {EnableTracingRequest, TraceConfig} from '../../protos';
+import {AdbStream, MockAdb, MockAdbStream} from './adb_interfaces';
+import {AdbConsumerPort} from './adb_shell_controller';
+import {Consumer} from './record_controller_interfaces';
+import {createEmptyState} from './empty_state';
+
+function generateMockConsumer(): Consumer {
+  return {
+    onConsumerPortResponse: jest.fn(),
+    onError: jest.fn(),
+    onStatus: jest.fn(),
+  };
+}
+const mainCallback = generateMockConsumer();
+const adbMock = new MockAdb();
+const adbController = new AdbConsumerPort(
+  adbMock,
+  mainCallback,
+  createEmptyState(),
+);
+const mockIntArray = new Uint8Array();
+
+const enableTracingRequest = new EnableTracingRequest();
+enableTracingRequest.traceConfig = new TraceConfig();
+const enableTracingRequestProto =
+  EnableTracingRequest.encode(enableTracingRequest).finish();
+
+test('handleCommand', async () => {
+  adbController.findDevice = () => {
+    return Promise.resolve(dingus<USBDevice>());
+  };
+
+  const enableTracing = jest.fn();
+  adbController.enableTracing = enableTracing;
+  await adbController.invoke('EnableTracing', mockIntArray);
+  expect(enableTracing).toHaveBeenCalledTimes(1);
+
+  const readBuffers = jest.fn();
+  adbController.readBuffers = readBuffers;
+  adbController.invoke('ReadBuffers', mockIntArray);
+  expect(readBuffers).toHaveBeenCalledTimes(1);
+
+  const sendErrorMessage = jest.fn();
+  adbController.sendErrorMessage = sendErrorMessage;
+  adbController.invoke('unknown', mockIntArray);
+  expect(sendErrorMessage).toBeCalledWith('Method not recognized: unknown');
+});
+
+test('enableTracing', async () => {
+  const mainCallback = generateMockConsumer();
+  const adbMock = new MockAdb();
+  const adbController = new AdbConsumerPort(
+    adbMock,
+    mainCallback,
+    createEmptyState(),
+  );
+
+  adbController.sendErrorMessage = jest
+    .fn()
+    .mockImplementation((s) => console.error(s));
+
+  const findDevice = jest.fn().mockImplementation(() => {
+    return Promise.resolve({} as unknown as USBDevice);
+  });
+  adbController.findDevice = findDevice;
+
+  const connectToDevice = jest
+    .fn()
+    .mockImplementation((_: USBDevice) => Promise.resolve());
+  adbMock.connect = connectToDevice;
+
+  const stream: AdbStream = new MockAdbStream();
+  const adbShell = jest
+    .fn()
+    .mockImplementation((_: string) => Promise.resolve(stream));
+  adbMock.shell = adbShell;
+
+  const sendMessage = jest.fn();
+  adbController.sendMessage = sendMessage;
+
+  adbController.generateStartTracingCommand = (_) => 'CMD';
+
+  await adbController.enableTracing(enableTracingRequestProto);
+  expect(adbShell).toBeCalledWith('CMD');
+  expect(sendMessage).toHaveBeenCalledTimes(0);
+
+  stream.onData(utf8Encode('starting tracing Wrote 123 bytes'));
+  stream.onClose();
+
+  expect(adbController.sendErrorMessage).toHaveBeenCalledTimes(0);
+  expect(sendMessage).toBeCalledWith({type: 'EnableTracingResponse'});
+});
+
+test('generateStartTracing', () => {
+  adbController.traceDestFile = 'DEST';
+  const testArray = new Uint8Array(1);
+  testArray[0] = 65;
+  const generatedCmd = adbController.generateStartTracingCommand(testArray);
+  expect(generatedCmd).toBe(
+    `echo '${btoa('A')}' | base64 -d | perfetto -c - -o DEST`,
+  );
+});
+
+test('tracingEndedSuccessfully', () => {
+  expect(
+    adbController.tracingEndedSuccessfully(
+      'Connected to the Perfetto traced service, starting tracing for 10000 ms\nWrote 564 bytes into /data/misc/perfetto-traces/trace',
+    ),
+  ).toBe(true);
+  expect(
+    adbController.tracingEndedSuccessfully(
+      'Connected to the Perfetto traced service, starting tracing for 10000 ms',
+    ),
+  ).toBe(false);
+  expect(
+    adbController.tracingEndedSuccessfully(
+      'Connected to the Perfetto traced service, starting tracing for 0 ms',
+    ),
+  ).toBe(false);
+});
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/adb_shell_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_shell_controller.ts
new file mode 100644
index 0000000..623dc5d
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_shell_controller.ts
@@ -0,0 +1,191 @@
+// Copyright (C) 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
+//
+//      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 {base64Encode, utf8Decode} from '../../base/string_utils';
+import {RecordingState} from './state';
+import {extractTraceConfig} from './trace_config_utils';
+import {AdbBaseConsumerPort, AdbConnectionState} from './adb_base_controller';
+import {Adb, AdbStream} from './adb_interfaces';
+import {ReadBuffersResponse} from './consumer_port_types';
+import {Consumer} from './record_controller_interfaces';
+
+enum AdbShellState {
+  READY,
+  RECORDING,
+  FETCHING,
+}
+const DEFAULT_DESTINATION_FILE = '/data/misc/perfetto-traces/trace-by-ui';
+
+export class AdbConsumerPort extends AdbBaseConsumerPort {
+  traceDestFile = DEFAULT_DESTINATION_FILE;
+  shellState: AdbShellState = AdbShellState.READY;
+  private recordShell?: AdbStream;
+
+  constructor(adb: Adb, consumer: Consumer, recState: RecordingState) {
+    super(adb, consumer, recState);
+    this.adb = adb;
+  }
+
+  async invoke(method: string, params: Uint8Array) {
+    // ADB connection & authentication is handled by the superclass.
+    console.assert(this.state === AdbConnectionState.CONNECTED);
+
+    switch (method) {
+      case 'EnableTracing':
+        this.enableTracing(params);
+        break;
+      case 'ReadBuffers':
+        this.readBuffers();
+        break;
+      case 'DisableTracing':
+        this.disableTracing();
+        break;
+      case 'FreeBuffers':
+        this.freeBuffers();
+        break;
+      case 'GetTraceStats':
+        break;
+      default:
+        this.sendErrorMessage(`Method not recognized: ${method}`);
+        break;
+    }
+  }
+
+  async enableTracing(enableTracingProto: Uint8Array) {
+    try {
+      const traceConfigProto = extractTraceConfig(enableTracingProto);
+      if (!traceConfigProto) {
+        this.sendErrorMessage('Invalid config.');
+        return;
+      }
+
+      await this.startRecording(traceConfigProto);
+      this.setDurationStatus(enableTracingProto);
+    } catch (e) {
+      this.sendErrorMessage(e.message);
+    }
+  }
+
+  async startRecording(configProto: Uint8Array) {
+    this.shellState = AdbShellState.RECORDING;
+    const recordCommand = this.generateStartTracingCommand(configProto);
+    this.recordShell = await this.adb.shell(recordCommand);
+    const output: string[] = [];
+    this.recordShell.onData = (raw) => output.push(utf8Decode(raw));
+    this.recordShell.onClose = () => {
+      const response = output.join();
+      if (!this.tracingEndedSuccessfully(response)) {
+        this.sendErrorMessage(response);
+        this.shellState = AdbShellState.READY;
+        return;
+      }
+      this.sendStatus('Recording ended successfully. Fetching the trace..');
+      this.sendMessage({type: 'EnableTracingResponse'});
+      this.recordShell = undefined;
+    };
+  }
+
+  tracingEndedSuccessfully(response: string): boolean {
+    return !response.includes(' 0 ms') && response.includes('Wrote ');
+  }
+
+  async readBuffers() {
+    console.assert(this.shellState === AdbShellState.RECORDING);
+    this.shellState = AdbShellState.FETCHING;
+
+    const readTraceShell = await this.adb.shell(
+      this.generateReadTraceCommand(),
+    );
+    readTraceShell.onData = (raw) =>
+      this.sendMessage(this.generateChunkReadResponse(raw));
+
+    readTraceShell.onClose = () => {
+      this.sendMessage(
+        this.generateChunkReadResponse(new Uint8Array(), /* last */ true),
+      );
+    };
+  }
+
+  async getPidFromShellAsString() {
+    const pidStr = await this.adb.shellOutputAsString(
+      `ps -u shell | grep perfetto`,
+    );
+    // We used to use awk '{print $2}' but older phones/Go phones don't have
+    // awk installed. Instead we implement similar functionality here.
+    const awk = pidStr.split(' ').filter((str) => str !== '');
+    if (awk.length < 1) {
+      throw Error(`Unabled to find perfetto pid in string "${pidStr}"`);
+    }
+    return awk[1];
+  }
+
+  async disableTracing() {
+    if (!this.recordShell) return;
+    try {
+      // We are not using 'pidof perfetto' so that we can use more filters. 'ps
+      // -u shell' is meant to catch processes started from shell, so if there
+      // are other ongoing tracing sessions started by others, we are not
+      // killing them.
+      const pid = await this.getPidFromShellAsString();
+
+      if (pid.length === 0 || isNaN(Number(pid))) {
+        throw Error(`Perfetto pid not found. Impossible to stop/cancel the
+     recording. Command output: ${pid}`);
+      }
+      // Perfetto stops and finalizes the tracing session on SIGINT.
+      const killOutput = await this.adb.shellOutputAsString(
+        `kill -SIGINT ${pid}`,
+      );
+
+      if (killOutput.length !== 0) {
+        throw Error(`Unable to kill perfetto: ${killOutput}`);
+      }
+    } catch (e) {
+      this.sendErrorMessage(e.message);
+    }
+  }
+
+  freeBuffers() {
+    this.shellState = AdbShellState.READY;
+    if (this.recordShell) {
+      this.recordShell.close();
+      this.recordShell = undefined;
+    }
+  }
+
+  generateChunkReadResponse(
+    data: Uint8Array,
+    last = false,
+  ): ReadBuffersResponse {
+    return {
+      type: 'ReadBuffersResponse',
+      slices: [{data, lastSliceForPacket: last}],
+    };
+  }
+
+  generateReadTraceCommand(): string {
+    // We attempt to delete the trace file after tracing. On a non-root shell,
+    // this will fail (due to selinux denial), but perfetto cmd will be able to
+    // override the file later. However, on a root shell, we need to clean up
+    // the file since perfetto cmd might otherwise fail to override it in a
+    // future session.
+    return `gzip -c ${this.traceDestFile} && rm -f ${this.traceDestFile}`;
+  }
+
+  generateStartTracingCommand(tracingConfig: Uint8Array) {
+    const configBase64 = base64Encode(tracingConfig);
+    const perfettoCmd = `perfetto -c - -o ${this.traceDestFile}`;
+    return `echo '${configBase64}' | base64 -d | ${perfettoCmd}`;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/adb_socket_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_socket_controller.ts
new file mode 100644
index 0000000..a676747
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_socket_controller.ts
@@ -0,0 +1,394 @@
+// Copyright (C) 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
+//
+//      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 {
+  DisableTracingResponse,
+  EnableTracingResponse,
+  FreeBuffersResponse,
+  GetTraceStatsResponse,
+  IPCFrame,
+  ReadBuffersResponse,
+} from '../../protos';
+import {AdbBaseConsumerPort, AdbConnectionState} from './adb_base_controller';
+import {Adb, AdbStream} from './adb_interfaces';
+import {isReadBuffersResponse} from './consumer_port_types';
+import {Consumer} from './record_controller_interfaces';
+import {exists} from '../../base/utils';
+import {assertTrue} from '../../base/logging';
+import {RecordingState} from './state';
+
+enum SocketState {
+  DISCONNECTED,
+  BINDING_IN_PROGRESS,
+  BOUND,
+}
+
+// See wire_protocol.proto for more details.
+const WIRE_PROTOCOL_HEADER_SIZE = 4;
+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;
+
+declare type Frame = IPCFrame;
+declare type IMethodInfo = IPCFrame.BindServiceReply.IMethodInfo;
+declare type ISlice = ReadBuffersResponse.ISlice;
+
+interface Command {
+  method: string;
+  params: Uint8Array;
+}
+
+const TRACED_SOCKET = '/dev/socket/traced_consumer';
+
+export class AdbSocketConsumerPort extends AdbBaseConsumerPort {
+  private socketState = SocketState.DISCONNECTED;
+
+  private socket?: AdbStream;
+  // 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;
+
+  // Buffers received wire protocol data.
+  private incomingBuffer = new Uint8Array(MAX_IPC_BUFFER_SIZE);
+  private incomingBufferLen = 0;
+  private frameToParseLen = 0;
+
+  private availableMethods: IMethodInfo[] = [];
+  private serviceId = -1;
+
+  private resolveBindingPromise!: VoidFunction;
+  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();
+
+  private socketCommandQueue: Command[] = [];
+
+  constructor(adb: Adb, consumer: Consumer, recState: RecordingState) {
+    super(adb, consumer, recState);
+  }
+
+  async invoke(method: string, params: Uint8Array) {
+    // ADB connection & authentication is handled by the superclass.
+    console.assert(this.state === AdbConnectionState.CONNECTED);
+    this.socketCommandQueue.push({method, params});
+
+    if (this.socketState === SocketState.BINDING_IN_PROGRESS) return;
+    if (this.socketState === SocketState.DISCONNECTED) {
+      this.socketState = SocketState.BINDING_IN_PROGRESS;
+      await this.listenForMessages();
+      await this.bind();
+      this.traceProtoWriter = protobuf.Writer.create();
+      this.socketState = SocketState.BOUND;
+    }
+
+    console.assert(this.socketState === SocketState.BOUND);
+
+    for (const cmd of this.socketCommandQueue) {
+      this.invokeInternal(cmd.method, cmd.params);
+    }
+    this.socketCommandQueue = [];
+  }
+
+  private invokeInternal(method: string, argsProto: Uint8Array) {
+    // Socket is bound in invoke().
+    console.assert(this.socketState === SocketState.BOUND);
+    const requestId = this.requestId++;
+    const methodId = this.findMethodId(method);
+    if (methodId === undefined) {
+      // This can happen with 'GetTraceStats': it seems that not all the Android
+      // <= 9 devices support it.
+      console.error(`Method ${method} not supported by the target`);
+      return;
+    }
+    const frame = new IPCFrame({
+      requestId,
+      msgInvokeMethod: new IPCFrame.InvokeMethod({
+        serviceId: this.serviceId,
+        methodId,
+        argsProto,
+      }),
+    });
+    this.requestMethods.set(requestId, method);
+    this.sendFrame(frame);
+
+    if (method === 'EnableTracing') this.setDurationStatus(argsProto);
+  }
+
+  static generateFrameBufferToSend(frame: Frame): Uint8Array {
+    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]);
+    }
+    return buf;
+  }
+
+  async sendFrame(frame: Frame) {
+    console.assert(this.socket !== undefined);
+    if (!this.socket) return;
+    const buf = AdbSocketConsumerPort.generateFrameBufferToSend(frame);
+    await this.socket.write(buf);
+  }
+
+  async listenForMessages() {
+    this.socket = await this.adb.socket(TRACED_SOCKET);
+    this.socket.onData = (raw) => this.handleReceivedData(raw);
+    this.socket.onClose = () => {
+      this.socketState = SocketState.DISCONNECTED;
+      this.socketCommandQueue = [];
+    };
+  }
+
+  private parseMessageSize(buffer: Uint8Array) {
+    const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.length);
+    return dv.getUint32(0, true);
+  }
+
+  private parseMessage(frameBuffer: Uint8Array) {
+    // Copy message to new array:
+    const buf = new ArrayBuffer(frameBuffer.byteLength);
+    const arr = new Uint8Array(buf);
+    arr.set(frameBuffer);
+    const frame = IPCFrame.decode(arr);
+    this.handleIncomingFrame(frame);
+  }
+
+  private incompleteSizeHeader() {
+    if (!this.frameToParseLen) {
+      console.assert(this.incomingBufferLen < WIRE_PROTOCOL_HEADER_SIZE);
+      return true;
+    }
+    return false;
+  }
+
+  private canCompleteSizeHeader(newData: Uint8Array) {
+    return newData.length + this.incomingBufferLen > WIRE_PROTOCOL_HEADER_SIZE;
+  }
+
+  private canParseFullMessage(newData: Uint8Array) {
+    return (
+      this.frameToParseLen &&
+      this.incomingBufferLen + newData.length >= this.frameToParseLen
+    );
+  }
+
+  private appendToIncomingBuffer(array: Uint8Array) {
+    this.incomingBuffer.set(array, this.incomingBufferLen);
+    this.incomingBufferLen += array.length;
+  }
+
+  handleReceivedData(newData: Uint8Array) {
+    if (this.incompleteSizeHeader() && this.canCompleteSizeHeader(newData)) {
+      const newDataBytesToRead =
+        WIRE_PROTOCOL_HEADER_SIZE - this.incomingBufferLen;
+      // Add to the incoming buffer the remaining bytes to arrive at
+      // WIRE_PROTOCOL_HEADER_SIZE
+      this.appendToIncomingBuffer(newData.subarray(0, newDataBytesToRead));
+      newData = newData.subarray(newDataBytesToRead);
+
+      this.frameToParseLen = this.parseMessageSize(this.incomingBuffer);
+      this.incomingBufferLen = 0;
+    }
+
+    // Parse all complete messages in incomingBuffer and newData.
+    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+    while (this.canParseFullMessage(newData)) {
+      // All the message is in the newData buffer.
+      if (this.incomingBufferLen === 0) {
+        this.parseMessage(newData.subarray(0, this.frameToParseLen));
+        newData = newData.subarray(this.frameToParseLen);
+      } else {
+        // We need to complete the local buffer.
+        // Read the remaining part of this message.
+        const bytesToCompleteMessage =
+          this.frameToParseLen - this.incomingBufferLen;
+        this.appendToIncomingBuffer(
+          newData.subarray(0, bytesToCompleteMessage),
+        );
+        this.parseMessage(
+          this.incomingBuffer.subarray(0, this.frameToParseLen),
+        );
+        this.incomingBufferLen = 0;
+        // Remove the data just parsed.
+        newData = newData.subarray(bytesToCompleteMessage);
+      }
+      this.frameToParseLen = 0;
+      if (!this.canCompleteSizeHeader(newData)) break;
+
+      this.frameToParseLen = this.parseMessageSize(
+        newData.subarray(0, WIRE_PROTOCOL_HEADER_SIZE),
+      );
+      newData = newData.subarray(WIRE_PROTOCOL_HEADER_SIZE);
+    }
+    // Buffer the remaining data (part of the next header + message).
+    this.appendToIncomingBuffer(newData);
+  }
+
+  decodeResponse(
+    requestId: number,
+    responseProto: Uint8Array,
+    hasMore = false,
+  ) {
+    const method = this.requestMethods.get(requestId);
+    if (!method) {
+      console.error(`Unknown request id: ${requestId}`);
+      this.sendErrorMessage(`Wire protocol error.`);
+      return;
+    }
+    const decoder = decoders.get(method);
+    if (decoder === undefined) {
+      console.error(`Unable to decode method: ${method}`);
+      return;
+    }
+    const decodedResponse = decoder(responseProto);
+    const response = {type: `${method}Response`, ...decodedResponse};
+
+    // TODO(nicomazz): Fix this.
+    // We assemble all the trace and then send it back to the main controller.
+    // This is a temporary solution, that will be changed in a following CL,
+    // because now both the chrome consumer port and the other adb consumer port
+    // send back the entire trace, while the correct behavior should be to send
+    // back the slices, that are assembled by the main record controller.
+    if (isReadBuffersResponse(response)) {
+      if (response.slices) this.handleSlices(response.slices);
+      if (!hasMore) this.sendReadBufferResponse();
+      return;
+    }
+    this.sendMessage(response);
+  }
+
+  handleSlices(slices: ISlice[]) {
+    for (const slice of slices) {
+      this.partialPacket.push(slice);
+      if (slice.lastSliceForPacket) {
+        const tracePacket = this.generateTracePacket(this.partialPacket);
+        this.traceProtoWriter.uint32(TRACE_PACKET_PROTO_TAG);
+        this.traceProtoWriter.bytes(tracePacket);
+        this.partialPacket = [];
+      }
+    }
+  }
+
+  generateTracePacket(slices: ISlice[]): Uint8Array {
+    let bufferSize = 0;
+    for (const slice of slices) bufferSize += slice.data!.length;
+    const fullBuffer = new Uint8Array(bufferSize);
+    let written = 0;
+    for (const slice of slices) {
+      const data = slice.data!;
+      fullBuffer.set(data, written);
+      written += data.length;
+    }
+    return fullBuffer;
+  }
+
+  sendReadBufferResponse() {
+    this.sendMessage(
+      this.generateChunkReadResponse(
+        this.traceProtoWriter.finish(),
+        /* last */ true,
+      ),
+    );
+    this.traceProtoWriter = protobuf.Writer.create();
+  }
+
+  bind() {
+    console.assert(this.socket !== undefined);
+    const requestId = this.requestId++;
+    const frame = new IPCFrame({
+      requestId,
+      msgBindService: new IPCFrame.BindService({serviceName: 'ConsumerPort'}),
+    });
+    return new Promise<void>((resolve, _) => {
+      this.resolveBindingPromise = resolve;
+      this.sendFrame(frame);
+    });
+  }
+
+  findMethodId(method: string): number | undefined {
+    const methodObject = this.availableMethods.find((m) => m.name === method);
+    return methodObject?.id ?? undefined;
+  }
+
+  static async hasSocketAccess(device: USBDevice, adb: Adb): Promise<boolean> {
+    await adb.connect(device);
+    try {
+      const socket = await adb.socket(TRACED_SOCKET);
+      socket.close();
+      return true;
+    } catch (e) {
+      return false;
+    }
+  }
+
+  handleIncomingFrame(frame: IPCFrame) {
+    const requestId = frame.requestId;
+    switch (frame.msg) {
+      case '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();
+          this.resolveBindingPromise = () => {};
+        }
+        return;
+      }
+      case 'msgInvokeMethodReply': {
+        const msgInvokeMethodReply = frame.msgInvokeMethodReply;
+        if (msgInvokeMethodReply && msgInvokeMethodReply.replyProto) {
+          if (!msgInvokeMethodReply.success) {
+            console.error(
+              'Unsuccessful method invocation: ',
+              msgInvokeMethodReply,
+            );
+            return;
+          }
+          this.decodeResponse(
+            requestId,
+            msgInvokeMethodReply.replyProto,
+            msgInvokeMethodReply.hasMore === true,
+          );
+        }
+        return;
+      }
+      default:
+        console.error(`not recognized frame message: ${frame.msg}`);
+    }
+  }
+}
+
+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);
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/advanced_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/advanced_settings.ts
new file mode 100644
index 0000000..35e6fe2
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/advanced_settings.ts
@@ -0,0 +1,109 @@
+// 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 {Dropdown, Probe, Slider, Textarea, Toggle} from './record_widgets';
+import {RecordingSectionAttrs} from './recording_sections';
+
+const FTRACE_CATEGORIES = new Map<string, string>();
+FTRACE_CATEGORIES.set('binder/*', 'binder');
+FTRACE_CATEGORIES.set('block/*', 'block');
+FTRACE_CATEGORIES.set('clk/*', 'clk');
+FTRACE_CATEGORIES.set('ext4/*', 'ext4');
+FTRACE_CATEGORIES.set('f2fs/*', 'f2fs');
+FTRACE_CATEGORIES.set('i2c/*', 'i2c');
+FTRACE_CATEGORIES.set('irq/*', 'irq');
+FTRACE_CATEGORIES.set('kmem/*', 'kmem');
+FTRACE_CATEGORIES.set('memory_bus/*', 'memory_bus');
+FTRACE_CATEGORIES.set('mmc/*', 'mmc');
+FTRACE_CATEGORIES.set('oom/*', 'oom');
+FTRACE_CATEGORIES.set('power/*', 'power');
+FTRACE_CATEGORIES.set('regulator/*', 'regulator');
+FTRACE_CATEGORIES.set('sched/*', 'sched');
+FTRACE_CATEGORIES.set('sync/*', 'sync');
+FTRACE_CATEGORIES.set('task/*', 'task');
+FTRACE_CATEGORIES.set('task/*', 'task');
+FTRACE_CATEGORIES.set('vmscan/*', 'vmscan');
+FTRACE_CATEGORIES.set('fastrpc/*', 'fastrpc');
+
+export class AdvancedSettings
+  implements m.ClassComponent<RecordingSectionAttrs>
+{
+  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+    const recCfg = attrs.recState.recordConfig;
+    return m(
+      `.record-section${attrs.cssClass}`,
+      m(
+        Probe,
+        {
+          title: 'Advanced ftrace config',
+          img: 'rec_ftrace.png',
+          descr: `Enable individual events and tune the kernel-tracing (ftrace)
+                  module. The events enabled here are in addition to those from
+                  enabled by other probes.`,
+          setEnabled: (cfg, val) => (cfg.ftrace = val),
+          isEnabled: (cfg) => cfg.ftrace,
+          recCfg,
+        },
+        m(Toggle, {
+          title: 'Resolve kernel symbols',
+          cssClass: '.thin',
+          descr: `Enables lookup via /proc/kallsyms for workqueue,
+              sched_blocked_reason and other events
+              (userdebug/eng builds only).`,
+          setEnabled: (cfg, val) => (cfg.symbolizeKsyms = val),
+          isEnabled: (cfg) => cfg.symbolizeKsyms,
+          recCfg,
+        }),
+        m(Slider, {
+          title: 'Buf size',
+          cssClass: '.thin',
+          values: [0, 512, 1024, 2 * 1024, 4 * 1024, 16 * 1024, 32 * 1024],
+          unit: 'KB',
+          zeroIsDefault: true,
+          set: (cfg, val) => (cfg.ftraceBufferSizeKb = val),
+          get: (cfg) => cfg.ftraceBufferSizeKb,
+          recCfg,
+        }),
+        m(Slider, {
+          title: 'Drain rate',
+          cssClass: '.thin',
+          values: [0, 100, 250, 500, 1000, 2500, 5000],
+          unit: 'ms',
+          zeroIsDefault: true,
+          set: (cfg, val) => (cfg.ftraceDrainPeriodMs = val),
+          get: (cfg) => cfg.ftraceDrainPeriodMs,
+          recCfg,
+        }),
+        m(Dropdown, {
+          title: 'Event groups',
+          cssClass: '.multicolumn.ftrace-events',
+          options: FTRACE_CATEGORIES,
+          set: (cfg, val) => (cfg.ftraceEvents = val),
+          get: (cfg) => cfg.ftraceEvents,
+          recCfg,
+        }),
+        m(Textarea, {
+          placeholder:
+            'Add extra events, one per line, e.g.:\n' +
+            'sched/sched_switch\n' +
+            'kmem/*',
+          set: (cfg, val) => (cfg.ftraceExtraEvents = val),
+          get: (cfg) => cfg.ftraceExtraEvents,
+          recCfg,
+        }),
+      ),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/android_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/android_settings.ts
new file mode 100644
index 0000000..7c0d741
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/android_settings.ts
@@ -0,0 +1,285 @@
+// 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 {AtomId, DataSourceDescriptor} from '../../protos';
+import {Dropdown, Probe, Slider, Textarea, Toggle} from './record_widgets';
+import {RecordingSectionAttrs} from './recording_sections';
+import {RecordConfig} from './record_config_types';
+
+const PUSH_ATOM_IDS = new Map<string, string>();
+const PULL_ATOM_IDS = new Map<string, string>();
+for (const key in AtomId) {
+  if (!Object.hasOwn(AtomId, key)) continue;
+  const value = Number(AtomId[key]);
+  if (!isNaN(value)) {
+    if (value > 2 && value < 9999) {
+      PUSH_ATOM_IDS.set(String(value), key);
+    } else if (value >= 10000 && value < 99999) {
+      PULL_ATOM_IDS.set(String(value), key);
+    }
+  }
+}
+
+const LOG_BUFFERS = new Map<string, string>();
+LOG_BUFFERS.set('LID_CRASH', 'Crash');
+LOG_BUFFERS.set('LID_DEFAULT', 'Main');
+LOG_BUFFERS.set('LID_EVENTS', 'Binary events');
+LOG_BUFFERS.set('LID_KERNEL', 'Kernel');
+LOG_BUFFERS.set('LID_RADIO', 'Radio');
+LOG_BUFFERS.set('LID_SECURITY', 'Security');
+LOG_BUFFERS.set('LID_STATS', 'Stats');
+LOG_BUFFERS.set('LID_SYSTEM', 'System');
+
+const DEFAULT_ATRACE_CATEGORIES = new Map<string, string>();
+DEFAULT_ATRACE_CATEGORIES.set('adb', 'ADB');
+DEFAULT_ATRACE_CATEGORIES.set('aidl', 'AIDL calls');
+DEFAULT_ATRACE_CATEGORIES.set('am', 'Activity Manager');
+DEFAULT_ATRACE_CATEGORIES.set('audio', 'Audio');
+DEFAULT_ATRACE_CATEGORIES.set('binder_driver', 'Binder Kernel driver');
+DEFAULT_ATRACE_CATEGORIES.set('binder_lock', 'Binder global lock trace');
+DEFAULT_ATRACE_CATEGORIES.set('bionic', 'Bionic C library');
+DEFAULT_ATRACE_CATEGORIES.set('camera', 'Camera');
+DEFAULT_ATRACE_CATEGORIES.set('dalvik', 'ART & Dalvik');
+DEFAULT_ATRACE_CATEGORIES.set('database', 'Database');
+DEFAULT_ATRACE_CATEGORIES.set('gfx', 'Graphics');
+DEFAULT_ATRACE_CATEGORIES.set('hal', 'Hardware Modules');
+DEFAULT_ATRACE_CATEGORIES.set('input', 'Input');
+DEFAULT_ATRACE_CATEGORIES.set('network', 'Network');
+DEFAULT_ATRACE_CATEGORIES.set('nnapi', 'Neural Network API');
+DEFAULT_ATRACE_CATEGORIES.set('pm', 'Package Manager');
+DEFAULT_ATRACE_CATEGORIES.set('power', 'Power Management');
+DEFAULT_ATRACE_CATEGORIES.set('res', 'Resource Loading');
+DEFAULT_ATRACE_CATEGORIES.set('rro', 'Resource Overlay');
+DEFAULT_ATRACE_CATEGORIES.set('rs', 'RenderScript');
+DEFAULT_ATRACE_CATEGORIES.set('sm', 'Sync Manager');
+DEFAULT_ATRACE_CATEGORIES.set('ss', 'System Server');
+DEFAULT_ATRACE_CATEGORIES.set('vibrator', 'Vibrator');
+DEFAULT_ATRACE_CATEGORIES.set('video', 'Video');
+DEFAULT_ATRACE_CATEGORIES.set('view', 'View System');
+DEFAULT_ATRACE_CATEGORIES.set('webview', 'WebView');
+DEFAULT_ATRACE_CATEGORIES.set('wm', 'Window Manager');
+
+function isDataSourceDescriptor(
+  descriptor: unknown,
+): descriptor is DataSourceDescriptor {
+  if (descriptor instanceof Object) {
+    return (descriptor as DataSourceDescriptor).name !== undefined;
+  }
+  return false;
+}
+
+interface AtraceAppsListAttrs {
+  recCfg: RecordConfig;
+}
+
+class AtraceAppsList implements m.ClassComponent<AtraceAppsListAttrs> {
+  view({attrs}: m.CVnode<AtraceAppsListAttrs>) {
+    if (attrs.recCfg.allAtraceApps) {
+      return m('div');
+    }
+
+    return m(Textarea, {
+      placeholder:
+        'Apps to profile, one per line, e.g.:\n' +
+        'com.android.phone\n' +
+        'lmkd\n' +
+        'com.android.nfc',
+      cssClass: '.record-apps-list',
+      set: (cfg, val) => (cfg.atraceApps = val),
+      get: (cfg) => cfg.atraceApps,
+      recCfg: attrs.recCfg,
+    });
+  }
+}
+
+export class AndroidSettings
+  implements m.ClassComponent<RecordingSectionAttrs>
+{
+  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+    const recCfg = attrs.recState.recordConfig;
+    let atraceCategories = DEFAULT_ATRACE_CATEGORIES;
+    for (const dataSource of attrs.dataSources) {
+      if (
+        dataSource.name !== 'linux.ftrace' ||
+        !isDataSourceDescriptor(dataSource.descriptor)
+      ) {
+        continue;
+      }
+      const atraces = dataSource.descriptor.ftraceDescriptor?.atraceCategories;
+      if (!atraces || atraces.length === 0) {
+        break;
+      }
+
+      atraceCategories = new Map<string, string>();
+      for (const atrace of atraces) {
+        if (atrace.name) {
+          atraceCategories.set(atrace.name, atrace.description ?? '');
+        }
+      }
+    }
+
+    return m(
+      `.record-section${attrs.cssClass}`,
+      m(
+        Probe,
+        {
+          title: 'Atrace userspace annotations',
+          img: 'rec_atrace.png',
+          descr: `Enables C++ / Java codebase annotations (ATRACE_BEGIN() /
+                      os.Trace())`,
+          setEnabled: (cfg, val) => (cfg.atrace = val),
+          isEnabled: (cfg) => cfg.atrace,
+          recCfg,
+        },
+        m(Dropdown, {
+          title: 'Categories',
+          cssClass: '.multicolumn.atrace-categories',
+          options: atraceCategories,
+          set: (cfg, val) => (cfg.atraceCats = val),
+          get: (cfg) => cfg.atraceCats,
+          recCfg,
+        }),
+        m(Toggle, {
+          title: 'Record events from all Android apps and services',
+          descr: '',
+          setEnabled: (cfg, val) => (cfg.allAtraceApps = val),
+          isEnabled: (cfg) => cfg.allAtraceApps,
+          recCfg,
+        }),
+        m(AtraceAppsList, {recCfg}),
+      ),
+      m(
+        Probe,
+        {
+          title: 'Event log (logcat)',
+          img: 'rec_logcat.png',
+          descr: `Streams the event log into the trace. If no buffer filter is
+                      specified, all buffers are selected.`,
+          setEnabled: (cfg, val) => (cfg.androidLogs = val),
+          isEnabled: (cfg) => cfg.androidLogs,
+          recCfg,
+        },
+        m(Dropdown, {
+          title: 'Buffers',
+          cssClass: '.multicolumn',
+          options: LOG_BUFFERS,
+          set: (cfg, val) => (cfg.androidLogBuffers = val),
+          get: (cfg) => cfg.androidLogBuffers,
+          recCfg,
+        }),
+      ),
+      m(Probe, {
+        title: 'Frame timeline',
+        img: 'rec_frame_timeline.png',
+        descr: `Records expected/actual frame timings from surface_flinger.
+                      Requires Android 12 (S) or above.`,
+        setEnabled: (cfg, val) => (cfg.androidFrameTimeline = val),
+        isEnabled: (cfg) => cfg.androidFrameTimeline,
+        recCfg,
+      }),
+      m(Probe, {
+        title: 'Game intervention list',
+        img: '',
+        descr: `List game modes and interventions.
+                    Requires Android 13 (T) or above.`,
+        setEnabled: (cfg, val) => (cfg.androidGameInterventionList = val),
+        isEnabled: (cfg) => cfg.androidGameInterventionList,
+        recCfg,
+      }),
+      m(
+        Probe,
+        {
+          title: 'Network Tracing',
+          img: '',
+          descr: `Records detailed information on network packets.
+                      Requires Android 14 (U) or above.`,
+          setEnabled: (cfg, val) => (cfg.androidNetworkTracing = val),
+          isEnabled: (cfg) => cfg.androidNetworkTracing,
+          recCfg,
+        },
+        m(Slider, {
+          title: 'Poll interval',
+          cssClass: '.thin',
+          values: [100, 250, 500, 1000, 2500],
+          unit: 'ms',
+          set: (cfg, val) => (cfg.androidNetworkTracingPollMs = val),
+          get: (cfg) => cfg.androidNetworkTracingPollMs,
+          recCfg,
+        }),
+      ),
+      m(
+        Probe,
+        {
+          title: 'Statsd Atoms',
+          img: '',
+          descr:
+            "Record instances of statsd atoms to the 'Statsd Atoms' track.",
+          setEnabled: (cfg, val) => (cfg.androidStatsd = val),
+          isEnabled: (cfg) => cfg.androidStatsd,
+          recCfg,
+        },
+        m(Dropdown, {
+          title: 'Pushed Atoms',
+          cssClass: '.singlecolumn',
+          options: PUSH_ATOM_IDS,
+          set: (cfg, val) => (cfg.androidStatsdPushedAtoms = val),
+          get: (cfg) => cfg.androidStatsdPushedAtoms,
+          recCfg,
+        }),
+        m(Textarea, {
+          placeholder:
+            'Add raw pushed atoms IDs, one per line, e.g.:\n' + '818\n' + '819',
+          set: (cfg, val) => (cfg.androidStatsdRawPushedAtoms = val),
+          get: (cfg) => cfg.androidStatsdRawPushedAtoms,
+          recCfg,
+        }),
+        m(Dropdown, {
+          title: 'Pulled Atoms',
+          cssClass: '.singlecolumn',
+          options: PULL_ATOM_IDS,
+          set: (cfg, val) => (cfg.androidStatsdPulledAtoms = val),
+          get: (cfg) => cfg.androidStatsdPulledAtoms,
+          recCfg,
+        }),
+        m(Textarea, {
+          placeholder:
+            'Add raw pulled atom IDs, one per line, e.g.:\n' +
+            '10063\n' +
+            '10064\n',
+          set: (cfg, val) => (cfg.androidStatsdRawPulledAtoms = val),
+          get: (cfg) => cfg.androidStatsdRawPulledAtoms,
+          recCfg,
+        }),
+        m(Slider, {
+          title: 'Pulled atom pull frequency (ms)',
+          cssClass: '.thin',
+          values: [500, 1000, 5000, 30000, 60000],
+          unit: 'ms',
+          set: (cfg, val) => (cfg.androidStatsdPulledAtomPullFrequencyMs = val),
+          get: (cfg) => cfg.androidStatsdPulledAtomPullFrequencyMs,
+          recCfg,
+        }),
+        m(Textarea, {
+          placeholder:
+            'Add pulled atom packages, one per line, e.g.:\n' +
+            'com.android.providers.telephony',
+          set: (cfg, val) => (cfg.androidStatsdPulledAtomPackages = val),
+          get: (cfg) => cfg.androidStatsdPulledAtomPackages,
+          recCfg,
+        }),
+      ),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/chrome_proxy_record_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_proxy_record_controller.ts
new file mode 100644
index 0000000..ef0b999
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_proxy_record_controller.ts
@@ -0,0 +1,120 @@
+// Copyright (C) 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
+//
+//      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 {binaryDecode, binaryEncode} from '../../base/string_utils';
+import {TRACE_SUFFIX} from '../../public/trace';
+import {
+  ConsumerPortResponse,
+  hasProperty,
+  isReadBuffersResponse,
+  Typed,
+} from './consumer_port_types';
+import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
+
+export interface ChromeExtensionError extends Typed {
+  error: string;
+}
+
+export interface ChromeExtensionStatus extends Typed {
+  status: string;
+}
+
+export interface GetCategoriesResponse extends Typed {
+  categories: string[];
+}
+
+export type ChromeExtensionMessage =
+  | ChromeExtensionError
+  | ChromeExtensionStatus
+  | ConsumerPortResponse
+  | GetCategoriesResponse;
+
+export function isChromeExtensionError(
+  obj: Typed,
+): obj is ChromeExtensionError {
+  return obj.type === 'ChromeExtensionError';
+}
+
+export function isChromeExtensionStatus(
+  obj: Typed,
+): obj is ChromeExtensionStatus {
+  return obj.type === 'ChromeExtensionStatus';
+}
+
+function isObject(obj: unknown): obj is object {
+  return typeof obj === 'object' && obj !== null;
+}
+
+export function isGetCategoriesResponse(
+  obj: unknown,
+): obj is GetCategoriesResponse {
+  if (
+    !(
+      isObject(obj) &&
+      hasProperty(obj, 'type') &&
+      obj.type === 'GetCategoriesResponse'
+    )
+  ) {
+    return false;
+  }
+
+  return hasProperty(obj, 'categories') && Array.isArray(obj.categories);
+}
+
+// This class acts as a proxy from the record controller (running in a worker),
+// to the frontend. This is needed because we can't directly talk with the
+// extension from a web-worker, so we use a MessagePort to communicate with the
+// frontend, that will consecutively forward it to the extension.
+
+// Rationale for the binaryEncode / binaryDecode calls below:
+// Messages to/from extensions need to be JSON serializable. ArrayBuffers are
+// not supported. For this reason here we use binaryEncode/Decode.
+// See https://developer.chrome.com/extensions/messaging#simple
+
+export class ChromeExtensionConsumerPort extends RpcConsumerPort {
+  private extensionPort: MessagePort;
+
+  constructor(extensionPort: MessagePort, consumer: Consumer) {
+    super(consumer);
+    this.extensionPort = extensionPort;
+    this.extensionPort.onmessage = this.onExtensionMessage.bind(this);
+  }
+
+  onExtensionMessage(message: {data: ChromeExtensionMessage}) {
+    if (isChromeExtensionError(message.data)) {
+      this.sendErrorMessage(message.data.error);
+      return;
+    }
+    if (isChromeExtensionStatus(message.data)) {
+      this.sendStatus(message.data.status);
+      return;
+    }
+
+    // In this else branch message.data will be a ConsumerPortResponse.
+    if (isReadBuffersResponse(message.data) && message.data.slices) {
+      const slice = message.data.slices[0].data as unknown as string;
+      message.data.slices[0].data = binaryDecode(slice);
+    }
+    this.sendMessage(message.data);
+  }
+
+  handleCommand(method: string, requestData: Uint8Array): void {
+    const reqEncoded = binaryEncode(requestData);
+    this.extensionPort.postMessage({method, requestData: reqEncoded});
+  }
+
+  getRecordedTraceSuffix(): string {
+    return `${TRACE_SUFFIX}.gz`;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/chrome_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_settings.ts
new file mode 100644
index 0000000..fd09d82
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_settings.ts
@@ -0,0 +1,247 @@
+// 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 {DataSource} from './recordingV2/recording_interfaces_v2';
+import {
+  RecordingState,
+  getBuiltinChromeCategoryList,
+  isChromeTarget,
+} from './state';
+import {
+  MultiSelect,
+  MultiSelectDiff,
+  Option as MultiSelectOption,
+} from '../../widgets/multiselect';
+import {Section} from '../../widgets/section';
+import {CategoryGetter, CompactProbe, Toggle} from './record_widgets';
+import {RecordingSectionAttrs} from './recording_sections';
+
+function extractChromeCategories(
+  dataSources: DataSource[],
+): string[] | undefined {
+  for (const dataSource of dataSources) {
+    if (dataSource.name === 'chromeCategories') {
+      return dataSource.descriptor as string[];
+    }
+  }
+  return undefined;
+}
+
+class ChromeCategoriesSelection
+  implements m.ClassComponent<RecordingSectionAttrs>
+{
+  private recState: RecordingState;
+  private defaultCategoryOptions: MultiSelectOption[] | undefined = undefined;
+  private disabledByDefaultCategoryOptions: MultiSelectOption[] | undefined =
+    undefined;
+
+  constructor({attrs}: m.CVnode<RecordingSectionAttrs>) {
+    this.recState = attrs.recState;
+  }
+
+  private updateValue(attrs: CategoryGetter, diffs: MultiSelectDiff[]) {
+    const values = attrs.get(this.recState.recordConfig);
+    for (const diff of diffs) {
+      const value = diff.id;
+      const index = values.indexOf(value);
+      const enabled = diff.checked;
+      if (enabled && index === -1) {
+        values.push(value);
+      }
+      if (!enabled && index !== -1) {
+        values.splice(index, 1);
+      }
+    }
+  }
+
+  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+    const categoryConfigGetter: CategoryGetter = {
+      get: (cfg) => cfg.chromeCategoriesSelected,
+      set: (cfg, val) => (cfg.chromeCategoriesSelected = val),
+    };
+
+    if (
+      this.defaultCategoryOptions === undefined ||
+      this.disabledByDefaultCategoryOptions === undefined
+    ) {
+      // If we are attempting to record via the Chrome extension, we receive the
+      // list of actually supported categories via DevTools. Otherwise, we fall
+      // back to an integrated list of categories from a recent version of
+      // Chrome.
+      const enabled = new Set(
+        categoryConfigGetter.get(this.recState.recordConfig),
+      );
+      let categories =
+        attrs.recState.chromeCategories ||
+        extractChromeCategories(attrs.dataSources);
+      if (!categories || !isChromeTarget(attrs.recState.recordingTarget)) {
+        categories = getBuiltinChromeCategoryList();
+      }
+      this.defaultCategoryOptions = [];
+      this.disabledByDefaultCategoryOptions = [];
+      const disabledPrefix = 'disabled-by-default-';
+      categories.forEach((cat) => {
+        const checked = enabled.has(cat);
+
+        if (
+          cat.startsWith(disabledPrefix) &&
+          this.disabledByDefaultCategoryOptions !== undefined
+        ) {
+          this.disabledByDefaultCategoryOptions.push({
+            id: cat,
+            name: cat.replace(disabledPrefix, ''),
+            checked: checked,
+          });
+        } else if (
+          !cat.startsWith(disabledPrefix) &&
+          this.defaultCategoryOptions !== undefined
+        ) {
+          this.defaultCategoryOptions.push({
+            id: cat,
+            name: cat,
+            checked: checked,
+          });
+        }
+      });
+    }
+
+    return m(
+      'div.chrome-categories',
+      m(
+        Section,
+        {title: 'Additional Categories'},
+        m(MultiSelect, {
+          options: this.defaultCategoryOptions,
+          repeatCheckedItemsAtTop: false,
+          fixedSize: false,
+          onChange: (diffs: MultiSelectDiff[]) => {
+            diffs.forEach(({id, checked}) => {
+              if (this.defaultCategoryOptions === undefined) {
+                return;
+              }
+              for (const option of this.defaultCategoryOptions) {
+                if (option.id == id) {
+                  option.checked = checked;
+                }
+              }
+            });
+            this.updateValue(categoryConfigGetter, diffs);
+          },
+        }),
+      ),
+      m(
+        Section,
+        {title: 'High Overhead Categories'},
+        m(MultiSelect, {
+          options: this.disabledByDefaultCategoryOptions,
+          repeatCheckedItemsAtTop: false,
+          fixedSize: false,
+          onChange: (diffs: MultiSelectDiff[]) => {
+            diffs.forEach(({id, checked}) => {
+              if (this.disabledByDefaultCategoryOptions === undefined) {
+                return;
+              }
+              for (const option of this.disabledByDefaultCategoryOptions) {
+                if (option.id == id) {
+                  option.checked = checked;
+                }
+              }
+            });
+            this.updateValue(categoryConfigGetter, diffs);
+          },
+        }),
+      ),
+    );
+  }
+}
+
+export class ChromeSettings implements m.ClassComponent<RecordingSectionAttrs> {
+  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+    const recCfg = attrs.recState.recordConfig;
+    return m(
+      `.record-section${attrs.cssClass}`,
+      CompactProbe({
+        title: 'Task scheduling',
+        setEnabled: (cfg, val) => (cfg.taskScheduling = val),
+        isEnabled: (cfg) => cfg.taskScheduling,
+        recCfg,
+      }),
+      CompactProbe({
+        title: 'IPC flows',
+        setEnabled: (cfg, val) => (cfg.ipcFlows = val),
+        isEnabled: (cfg) => cfg.ipcFlows,
+        recCfg,
+      }),
+      CompactProbe({
+        title: 'Javascript execution',
+        setEnabled: (cfg, val) => (cfg.jsExecution = val),
+        isEnabled: (cfg) => cfg.jsExecution,
+        recCfg,
+      }),
+      CompactProbe({
+        title: 'Web content rendering, layout and compositing',
+        setEnabled: (cfg, val) => (cfg.webContentRendering = val),
+        isEnabled: (cfg) => cfg.webContentRendering,
+        recCfg,
+      }),
+      CompactProbe({
+        title: 'UI rendering & surface compositing',
+        setEnabled: (cfg, val) => (cfg.uiRendering = val),
+        isEnabled: (cfg) => cfg.uiRendering,
+        recCfg,
+      }),
+      CompactProbe({
+        title: 'Input events',
+        setEnabled: (cfg, val) => (cfg.inputEvents = val),
+        isEnabled: (cfg) => cfg.inputEvents,
+        recCfg,
+      }),
+      CompactProbe({
+        title: 'Navigation & Loading',
+        setEnabled: (cfg, val) => (cfg.navigationAndLoading = val),
+        isEnabled: (cfg) => cfg.navigationAndLoading,
+        recCfg,
+      }),
+      CompactProbe({
+        title: 'Chrome Logs',
+        setEnabled: (cfg, val) => (cfg.chromeLogs = val),
+        isEnabled: (cfg) => cfg.chromeLogs,
+        recCfg,
+      }),
+      CompactProbe({
+        title: 'Audio',
+        setEnabled: (cfg, val) => (cfg.audio = val),
+        isEnabled: (cfg) => cfg.audio,
+        recCfg,
+      }),
+      CompactProbe({
+        title: 'Video',
+        setEnabled: (cfg, val) => (cfg.video = val),
+        isEnabled: (cfg) => cfg.video,
+        recCfg,
+      }),
+      m(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.',
+        setEnabled: (cfg, val) => (cfg.chromePrivacyFiltering = val),
+        isEnabled: (cfg) => cfg.chromePrivacyFiltering,
+        recCfg,
+      }),
+      m(ChromeCategoriesSelection, attrs),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/consumer_port_types.ts b/ui/src/plugins/dev.perfetto.RecordTrace/consumer_port_types.ts
new file mode 100644
index 0000000..732e9e8
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/consumer_port_types.ts
@@ -0,0 +1,81 @@
+// Copyright (C) 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
+//
+//      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 {
+  IDisableTracingResponse,
+  IEnableTracingResponse,
+  IFreeBuffersResponse,
+  IGetTraceStatsResponse,
+  IReadBuffersResponse,
+} from '../../protos';
+
+export interface Typed {
+  type: string;
+}
+
+// A type guard that can be used in order to be able to access the property of
+// an object in a checked manner.
+export function hasProperty<T extends object, P extends string>(
+  obj: T,
+  prop: P,
+): obj is T & {[prop in P]: unknown} {
+  return obj.hasOwnProperty(prop);
+}
+
+export function isTyped(obj: object): obj is Typed {
+  return obj.hasOwnProperty('type');
+}
+
+export interface ReadBuffersResponse extends Typed, IReadBuffersResponse {}
+export interface EnableTracingResponse extends Typed, IEnableTracingResponse {}
+export interface GetTraceStatsResponse extends Typed, IGetTraceStatsResponse {}
+export interface FreeBuffersResponse extends Typed, IFreeBuffersResponse {}
+export interface GetCategoriesResponse extends Typed {}
+export interface DisableTracingResponse
+  extends Typed,
+    IDisableTracingResponse {}
+
+export type ConsumerPortResponse =
+  | EnableTracingResponse
+  | ReadBuffersResponse
+  | GetTraceStatsResponse
+  | GetCategoriesResponse
+  | FreeBuffersResponse
+  | DisableTracingResponse;
+
+export function isReadBuffersResponse(obj: Typed): obj is ReadBuffersResponse {
+  return obj.type === 'ReadBuffersResponse';
+}
+
+export function isEnableTracingResponse(
+  obj: Typed,
+): obj is EnableTracingResponse {
+  return obj.type === 'EnableTracingResponse';
+}
+
+export function isGetTraceStatsResponse(
+  obj: Typed,
+): obj is GetTraceStatsResponse {
+  return obj.type === 'GetTraceStatsResponse';
+}
+
+export function isFreeBuffersResponse(obj: Typed): obj is FreeBuffersResponse {
+  return obj.type === 'FreeBuffersResponse';
+}
+
+export function isDisableTracingResponse(
+  obj: Typed,
+): obj is DisableTracingResponse {
+  return obj.type === 'DisableTracingResponse';
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/cpu_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/cpu_settings.ts
new file mode 100644
index 0000000..06b2713
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/cpu_settings.ts
@@ -0,0 +1,85 @@
+// 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 {Probe, Slider} from './record_widgets';
+import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
+
+export class CpuSettings implements m.ClassComponent<RecordingSectionAttrs> {
+  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+    const recCfg = attrs.recState.recordConfig;
+    return m(
+      `.record-section${attrs.cssClass}`,
+      m(
+        Probe,
+        {
+          title: 'Coarse CPU usage counter',
+          img: 'rec_cpu_coarse.png',
+          descr: `Lightweight polling of CPU usage counters via /proc/stat.
+                    Allows to periodically monitor CPU usage.`,
+          setEnabled: (cfg, val) => (cfg.cpuCoarse = val),
+          isEnabled: (cfg) => cfg.cpuCoarse,
+          recCfg,
+        },
+        m(Slider, {
+          title: 'Poll interval',
+          cssClass: '.thin',
+          values: POLL_INTERVAL_MS,
+          unit: 'ms',
+          set: (cfg, val) => (cfg.cpuCoarsePollMs = val),
+          get: (cfg) => cfg.cpuCoarsePollMs,
+          recCfg,
+        }),
+      ),
+      m(Probe, {
+        title: 'Scheduling details',
+        img: 'rec_cpu_fine.png',
+        descr: 'Enables high-detailed tracking of scheduling events',
+        setEnabled: (cfg, val) => (cfg.cpuSched = val),
+        isEnabled: (cfg) => cfg.cpuSched,
+        recCfg,
+      }),
+      m(
+        Probe,
+        {
+          title: 'CPU frequency and idle states',
+          img: 'rec_cpu_freq.png',
+          descr:
+            'Records cpu frequency and idle state changes via ftrace and sysfs',
+          setEnabled: (cfg, val) => (cfg.cpuFreq = val),
+          isEnabled: (cfg) => cfg.cpuFreq,
+          recCfg,
+        },
+        m(Slider, {
+          title: 'Sysfs poll interval',
+          cssClass: '.thin',
+          values: POLL_INTERVAL_MS,
+          unit: 'ms',
+          set: (cfg, val) => (cfg.cpuFreqPollMs = val),
+          get: (cfg) => cfg.cpuFreqPollMs,
+          recCfg,
+        }),
+      ),
+      m(Probe, {
+        title: 'Syscalls',
+        img: 'rec_syscalls.png',
+        descr: `Tracks the enter and exit of all syscalls. On Android
+                requires a userdebug or eng build.`,
+        setEnabled: (cfg, val) => (cfg.cpuSyscall = val),
+        isEnabled: (cfg) => cfg.cpuSyscall,
+        recCfg,
+      }),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/empty_state.ts b/ui/src/plugins/dev.perfetto.RecordTrace/empty_state.ts
new file mode 100644
index 0000000..bfafe3f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/empty_state.ts
@@ -0,0 +1,34 @@
+// 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 {autosaveConfigStore, recordTargetStore} from './record_config';
+import {RecordingState} from './state';
+
+export function createEmptyState(): RecordingState {
+  return {
+    recordConfig: autosaveConfigStore.get(),
+    lastLoadedConfig: {type: 'NONE'},
+
+    recordingInProgress: false,
+    recordingCancelled: false,
+    extensionInstalled: false,
+    recordingTarget: recordTargetStore.getValidTarget(),
+    availableAdbDevices: [],
+
+    fetchChromeCategories: false,
+    chromeCategories: undefined,
+    bufferUsage: 0,
+    recordingLog: '',
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/etw_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/etw_settings.ts
new file mode 100644
index 0000000..eefb8ac
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/etw_settings.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.
+
+import m from 'mithril';
+import {Probe} from './record_widgets';
+import {RecordingSectionAttrs} from './recording_sections';
+
+export class EtwSettings implements m.ClassComponent<RecordingSectionAttrs> {
+  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+    const recCfg = attrs.recState.recordConfig;
+    return m(
+      `.record-section${attrs.cssClass}`,
+      m(Probe, {
+        title: 'CSwitch',
+        img: null,
+        descr: `Enables to recording of context switches.`,
+        setEnabled: (cfg, val) => (cfg.etwCSwitch = val),
+        isEnabled: (cfg) => cfg.etwCSwitch,
+        recCfg,
+      }),
+      m(Probe, {
+        title: 'Dispatcher',
+        img: null,
+        descr: 'Enables to get thread state.',
+        setEnabled: (cfg, val) => (cfg.etwThreadState = val),
+        isEnabled: (cfg) => cfg.etwThreadState,
+        recCfg,
+      }),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/gpu_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/gpu_settings.ts
new file mode 100644
index 0000000..1040f75
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/gpu_settings.ts
@@ -0,0 +1,52 @@
+// 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 {Probe} from './record_widgets';
+import {RecordingSectionAttrs} from './recording_sections';
+
+export class GpuSettings implements m.ClassComponent<RecordingSectionAttrs> {
+  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+    const recCfg = attrs.recState.recordConfig;
+    return m(
+      `.record-section${attrs.cssClass}`,
+      m(Probe, {
+        title: 'GPU frequency',
+        img: 'rec_cpu_freq.png',
+        descr: 'Records gpu frequency via ftrace',
+        setEnabled: (cfg, val) => (cfg.gpuFreq = val),
+        isEnabled: (cfg) => cfg.gpuFreq,
+        recCfg,
+      }),
+      m(Probe, {
+        title: 'GPU memory',
+        img: 'rec_gpu_mem_total.png',
+        descr: `Allows to track per process and global total GPU memory usages.
+                (Available on recent Android 12+ kernels)`,
+        setEnabled: (cfg, val) => (cfg.gpuMemTotal = val),
+        isEnabled: (cfg) => cfg.gpuMemTotal,
+        recCfg,
+      }),
+      m(Probe, {
+        title: 'GPU work period',
+        img: 'rec_cpu_voltage.png',
+        descr: `Allows to track per package GPU work.
+                (Available on recent Android 14+ kernels)`,
+        setEnabled: (cfg, val) => (cfg.gpuWorkPeriod = val),
+        isEnabled: (cfg) => cfg.gpuWorkPeriod,
+        recCfg,
+      }),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/index.ts b/ui/src/plugins/dev.perfetto.RecordTrace/index.ts
new file mode 100644
index 0000000..e0c5a1f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/index.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 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 onActivate(app: App) {
+    app.sidebar.addMenuItem({
+      section: 'navigation',
+      text: 'Record new trace',
+      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 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/linux_perf_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/linux_perf_settings.ts
new file mode 100644
index 0000000..a0fcf9f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/linux_perf_settings.ts
@@ -0,0 +1,68 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {Probe, Slider, Textarea} from './record_widgets';
+import {RecordingSectionAttrs} from './recording_sections';
+
+const PLACEHOLDER_TEXT = `Filters for processes to profile, one per line e.g.:
+com.android.phone
+lmkd
+com.android.webview:sandboxed_process*`;
+
+export interface LinuxPerfConfiguration {
+  targets: string[];
+}
+
+export class LinuxPerfSettings
+  implements m.ClassComponent<RecordingSectionAttrs>
+{
+  config = {targets: []} as LinuxPerfConfiguration;
+  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+    const recCfg = attrs.recState.recordConfig;
+    return m(
+      `.record-section${attrs.cssClass}`,
+      m(
+        Probe,
+        {
+          title: 'Callstack sampling',
+          img: 'rec_profiling.png',
+          descr: `Periodically records the current callstack (chain of
+              function calls) of processes.`,
+          setEnabled: (cfg, val) => (cfg.tracePerf = val),
+          isEnabled: (cfg) => cfg.tracePerf,
+          recCfg,
+        },
+        m(Slider, {
+          title: 'Sampling Frequency',
+          cssClass: '.thin',
+          values: [20, 40, 60, 80, 100, 120, 140, 160, 180, 200],
+          unit: 'hz',
+          set: (cfg, val) => (cfg.timebaseFrequency = val),
+          get: (cfg) => cfg.timebaseFrequency,
+          recCfg,
+        }),
+        m(Textarea, {
+          placeholder: PLACEHOLDER_TEXT,
+          cssClass: '.record-apps-list',
+          set: (cfg, val) => {
+            cfg.targetCmdLine = val.split('\n');
+          },
+          get: (cfg) => cfg.targetCmdLine.join('\n'),
+          recCfg,
+        }),
+      ),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/memory_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/memory_settings.ts
new file mode 100644
index 0000000..231306f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/memory_settings.ts
@@ -0,0 +1,351 @@
+// 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 {MeminfoCounters, VmstatCounters} from '../../protos';
+import {Dropdown, Probe, Slider, Textarea, Toggle} from './record_widgets';
+import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
+
+class HeapSettings implements m.ClassComponent<RecordingSectionAttrs> {
+  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+    const valuesForMS = [
+      0,
+      1000,
+      10 * 1000,
+      30 * 1000,
+      60 * 1000,
+      5 * 60 * 1000,
+      10 * 60 * 1000,
+      30 * 60 * 1000,
+      60 * 60 * 1000,
+    ];
+    const valuesForShMemBuff = [
+      0,
+      512,
+      1024,
+      2 * 1024,
+      4 * 1024,
+      8 * 1024,
+      16 * 1024,
+      32 * 1024,
+      64 * 1024,
+      128 * 1024,
+      256 * 1024,
+      512 * 1024,
+      1024 * 1024,
+      64 * 1024 * 1024,
+      128 * 1024 * 1024,
+      256 * 1024 * 1024,
+      512 * 1024 * 1024,
+    ];
+    const recCfg = attrs.recState.recordConfig;
+    return m(
+      `.${attrs.cssClass}`,
+      m(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',
+        set: (cfg, val) => (cfg.hpProcesses = val),
+        get: (cfg) => cfg.hpProcesses,
+        recCfg,
+      }),
+      m(Slider, {
+        title: 'Sampling interval',
+        cssClass: '.thin',
+        values: [
+          0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192,
+          16384, 32768, 65536, 131072, 262144, 524288, 1048576,
+        ],
+        unit: 'B',
+        min: 0,
+        set: (cfg, val) => (cfg.hpSamplingIntervalBytes = val),
+        get: (cfg) => cfg.hpSamplingIntervalBytes,
+        recCfg,
+      }),
+      m(Slider, {
+        title: 'Continuous dumps interval ',
+        description: 'Time between following dumps (0 = disabled)',
+        cssClass: '.thin',
+        values: valuesForMS,
+        unit: 'ms',
+        min: 0,
+        set: (cfg, val) => {
+          cfg.hpContinuousDumpsInterval = val;
+        },
+        get: (cfg) => cfg.hpContinuousDumpsInterval,
+        recCfg,
+      }),
+      m(Slider, {
+        title: 'Continuous dumps phase',
+        description: 'Time before first dump',
+        cssClass: `.thin${
+          attrs.recState.recordConfig.hpContinuousDumpsInterval === 0
+            ? '.greyed-out'
+            : ''
+        }`,
+        values: valuesForMS,
+        unit: 'ms',
+        min: 0,
+        disabled: attrs.recState.recordConfig.hpContinuousDumpsInterval === 0,
+        set: (cfg, val) => (cfg.hpContinuousDumpsPhase = val),
+        get: (cfg) => cfg.hpContinuousDumpsPhase,
+        recCfg,
+      }),
+      m(Slider, {
+        title: `Shared memory buffer`,
+        cssClass: '.thin',
+        values: valuesForShMemBuff.filter(
+          (value) => value === 0 || (value >= 8192 && value % 4096 === 0),
+        ),
+        unit: 'B',
+        min: 0,
+        set: (cfg, val) => (cfg.hpSharedMemoryBuffer = val),
+        get: (cfg) => cfg.hpSharedMemoryBuffer,
+        recCfg,
+      }),
+      m(Toggle, {
+        title: 'Block client',
+        cssClass: '.thin',
+        descr: `Slow down target application if profiler cannot keep up.`,
+        setEnabled: (cfg, val) => (cfg.hpBlockClient = val),
+        isEnabled: (cfg) => cfg.hpBlockClient,
+        recCfg,
+      }),
+      m(Toggle, {
+        title: 'All custom allocators (Q+)',
+        cssClass: '.thin',
+        descr: `If the target application exposes custom allocators, also
+sample from those.`,
+        setEnabled: (cfg, val) => (cfg.hpAllHeaps = val),
+        isEnabled: (cfg) => cfg.hpAllHeaps,
+        recCfg,
+      }),
+      // TODO(hjd): Add advanced options.
+    );
+  }
+}
+
+class JavaHeapDumpSettings implements m.ClassComponent<RecordingSectionAttrs> {
+  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+    const valuesForMS = [
+      0,
+      1000,
+      10 * 1000,
+      30 * 1000,
+      60 * 1000,
+      5 * 60 * 1000,
+      10 * 60 * 1000,
+      30 * 60 * 1000,
+      60 * 60 * 1000,
+    ];
+    const recCfg = attrs.recState.recordConfig;
+    return m(
+      `.${attrs.cssClass}`,
+      m(Textarea, {
+        title: 'Names or pids of the processes to track (required)',
+        placeholder: 'One per line, e.g.:\n' + 'com.android.vending\n' + '1503',
+        set: (cfg, val) => (cfg.jpProcesses = val),
+        get: (cfg) => cfg.jpProcesses,
+        recCfg,
+      }),
+      m(Slider, {
+        title: 'Continuous dumps interval ',
+        description: 'Time between following dumps (0 = disabled)',
+        cssClass: '.thin',
+        values: valuesForMS,
+        unit: 'ms',
+        min: 0,
+        set: (cfg, val) => {
+          cfg.jpContinuousDumpsInterval = val;
+        },
+        get: (cfg) => cfg.jpContinuousDumpsInterval,
+        recCfg,
+      }),
+      m(Slider, {
+        title: 'Continuous dumps phase',
+        description: 'Time before first dump',
+        cssClass: `.thin${
+          attrs.recState.recordConfig.jpContinuousDumpsInterval === 0
+            ? '.greyed-out'
+            : ''
+        }`,
+        values: valuesForMS,
+        unit: 'ms',
+        min: 0,
+        disabled: attrs.recState.recordConfig.jpContinuousDumpsInterval === 0,
+        set: (cfg, val) => (cfg.jpContinuousDumpsPhase = val),
+        get: (cfg) => cfg.jpContinuousDumpsPhase,
+        recCfg,
+      }),
+    );
+  }
+}
+
+export class MemorySettings implements m.ClassComponent<RecordingSectionAttrs> {
+  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+    const recCfg = attrs.recState.recordConfig;
+    const meminfoOpts = new Map<string, string>();
+    for (const x in MeminfoCounters) {
+      if (
+        typeof MeminfoCounters[x] === 'number' &&
+        !`${x}`.endsWith('_UNSPECIFIED')
+      ) {
+        meminfoOpts.set(x, x.replace('MEMINFO_', '').toLowerCase());
+      }
+    }
+    const vmstatOpts = new Map<string, string>();
+    for (const x in VmstatCounters) {
+      if (
+        typeof VmstatCounters[x] === 'number' &&
+        !`${x}`.endsWith('_UNSPECIFIED')
+      ) {
+        vmstatOpts.set(x, x.replace('VMSTAT_', '').toLowerCase());
+      }
+    }
+    return m(
+      `.record-section${attrs.cssClass}`,
+      m(
+        Probe,
+        {
+          title: 'Native heap profiling',
+          img: 'rec_native_heap_profiler.png',
+          descr: `Track native heap allocations & deallocations of an Android
+               process. (Available on Android 10+)`,
+          setEnabled: (cfg, val) => (cfg.heapProfiling = val),
+          isEnabled: (cfg) => cfg.heapProfiling,
+          recCfg,
+        },
+        m(HeapSettings, attrs),
+      ),
+      m(
+        Probe,
+        {
+          title: 'Java heap dumps',
+          img: 'rec_java_heap_dump.png',
+          descr: `Dump information about the Java object graph of an
+          Android app. (Available on Android 11+)`,
+          setEnabled: (cfg, val) => (cfg.javaHeapDump = val),
+          isEnabled: (cfg) => cfg.javaHeapDump,
+          recCfg,
+        },
+        m(JavaHeapDumpSettings, attrs),
+      ),
+      m(
+        Probe,
+        {
+          title: 'Kernel meminfo',
+          img: 'rec_meminfo.png',
+          descr: 'Polling of /proc/meminfo',
+          setEnabled: (cfg, val) => (cfg.meminfo = val),
+          isEnabled: (cfg) => cfg.meminfo,
+          recCfg,
+        },
+        m(Slider, {
+          title: 'Poll interval',
+          cssClass: '.thin',
+          values: POLL_INTERVAL_MS,
+          unit: 'ms',
+          set: (cfg, val) => (cfg.meminfoPeriodMs = val),
+          get: (cfg) => cfg.meminfoPeriodMs,
+          recCfg,
+        }),
+        m(Dropdown, {
+          title: 'Select counters',
+          cssClass: '.multicolumn',
+          options: meminfoOpts,
+          set: (cfg, val) => (cfg.meminfoCounters = val),
+          get: (cfg) => cfg.meminfoCounters,
+          recCfg,
+        }),
+      ),
+      m(Probe, {
+        title: 'High-frequency memory events',
+        img: 'rec_mem_hifreq.png',
+        descr: `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`,
+        setEnabled: (cfg, val) => (cfg.memHiFreq = val),
+        isEnabled: (cfg) => cfg.memHiFreq,
+        recCfg,
+      }),
+      m(Probe, {
+        title: 'Low memory killer',
+        img: 'rec_lmk.png',
+        descr: `Record LMK events. Works both with the old in-kernel LMK
+                and the newer userspace lmkd. It also tracks OOM score
+                adjustments.`,
+        setEnabled: (cfg, val) => (cfg.memLmk = val),
+        isEnabled: (cfg) => cfg.memLmk,
+        recCfg,
+      }),
+      m(
+        Probe,
+        {
+          title: 'Per process stats',
+          img: 'rec_ps_stats.png',
+          descr: `Periodically samples all processes in the system tracking:
+                    their thread list, memory counters (RSS, swap and other
+                    /proc/status counters) and oom_score_adj.`,
+          setEnabled: (cfg, val) => (cfg.procStats = val),
+          isEnabled: (cfg) => cfg.procStats,
+          recCfg,
+        },
+        m(Slider, {
+          title: 'Poll interval',
+          cssClass: '.thin',
+          values: POLL_INTERVAL_MS,
+          unit: 'ms',
+          set: (cfg, val) => (cfg.procStatsPeriodMs = val),
+          get: (cfg) => cfg.procStatsPeriodMs,
+          recCfg,
+        }),
+      ),
+      m(
+        Probe,
+        {
+          title: 'Virtual memory stats',
+          img: 'rec_vmstat.png',
+          descr: `Periodically polls virtual memory stats from /proc/vmstat.
+                    Allows to gather statistics about swap, eviction,
+                    compression and pagecache efficiency`,
+          setEnabled: (cfg, val) => (cfg.vmstat = val),
+          isEnabled: (cfg) => cfg.vmstat,
+          recCfg,
+        },
+        m(Slider, {
+          title: 'Poll interval',
+          cssClass: '.thin',
+          values: POLL_INTERVAL_MS,
+          unit: 'ms',
+          set: (cfg, val) => (cfg.vmstatPeriodMs = val),
+          get: (cfg) => cfg.vmstatPeriodMs,
+          recCfg,
+        }),
+        m(Dropdown, {
+          title: 'Select counters',
+          cssClass: '.multicolumn',
+          options: vmstatOpts,
+          set: (cfg, val) => (cfg.vmstatCounters = val),
+          get: (cfg) => cfg.vmstatCounters,
+          recCfg,
+        }),
+      ),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/power_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/power_settings.ts
new file mode 100644
index 0000000..bf88217
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/power_settings.ts
@@ -0,0 +1,88 @@
+// 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 {globals} from '../../frontend/globals';
+import {Probe, Slider} from './record_widgets';
+import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
+
+export class PowerSettings implements m.ClassComponent<RecordingSectionAttrs> {
+  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+    const recCfg = attrs.recState.recordConfig;
+    const DOC_URL = 'https://perfetto.dev/docs/data-sources/battery-counters';
+    const descr = [
+      m(
+        'div',
+        m(
+          'span',
+          `Polls charge counters and instantaneous power draw from
+                    the battery power management IC and the power rails from
+                    the PowerStats HAL (`,
+        ),
+        m('a', {href: DOC_URL, target: '_blank'}, 'see docs for more'),
+        m('span', ')'),
+      ),
+    ];
+    // TODO(primiano): figure out a better story for isInternalUser.
+    if (globals.isInternalUser) {
+      descr.push(
+        m(
+          'div',
+          m('span', 'Googlers: See '),
+          m(
+            'a',
+            {href: 'http://go/power-rails-internal-doc', target: '_blank'},
+            'this doc',
+          ),
+          m(
+            'span',
+            ` for instructions on how to change the default rail selection
+                  on internal devices.`,
+          ),
+        ),
+      );
+    }
+    return m(
+      `.record-section${attrs.cssClass}`,
+      m(
+        Probe,
+        {
+          title: 'Battery drain & power rails',
+          img: 'rec_battery_counters.png',
+          descr,
+          setEnabled: (cfg, val) => (cfg.batteryDrain = val),
+          isEnabled: (cfg) => cfg.batteryDrain,
+          recCfg,
+        },
+        m(Slider, {
+          title: 'Poll interval',
+          cssClass: '.thin',
+          values: POLL_INTERVAL_MS,
+          unit: 'ms',
+          set: (cfg, val) => (cfg.batteryDrainPollMs = val),
+          get: (cfg) => cfg.batteryDrainPollMs,
+          recCfg,
+        }),
+      ),
+      m(Probe, {
+        title: 'Board voltages & frequencies',
+        img: 'rec_board_voltage.png',
+        descr: 'Tracks voltage and frequency changes from board sensors',
+        setEnabled: (cfg, val) => (cfg.boardSensors = val),
+        isEnabled: (cfg) => cfg.boardSensors,
+        recCfg,
+      }),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/record_config.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_config.ts
new file mode 100644
index 0000000..ae41d9c
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_config.ts
@@ -0,0 +1,231 @@
+// 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.
+
+import {exists} from '../../base/utils';
+import {getDefaultRecordingTargets, RecordingTarget} from './state';
+import {
+  createEmptyRecordConfig,
+  NamedRecordConfig,
+  NAMED_RECORD_CONFIG_SCHEMA,
+  RecordConfig,
+  RECORD_CONFIG_SCHEMA,
+} from './record_config_types';
+
+const LOCAL_STORAGE_RECORD_CONFIGS_KEY = 'recordConfigs';
+const LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY = 'autosaveConfig';
+const LOCAL_STORAGE_RECORD_TARGET_OS_KEY = 'recordTargetOS';
+
+export class RecordConfigStore {
+  recordConfigs: Array<NamedRecordConfig>;
+  recordConfigNames: Set<string>;
+
+  constructor() {
+    this.recordConfigs = [];
+    this.recordConfigNames = new Set();
+    this.reloadFromLocalStorage();
+  }
+
+  private _save() {
+    window.localStorage.setItem(
+      LOCAL_STORAGE_RECORD_CONFIGS_KEY,
+      JSON.stringify(this.recordConfigs),
+    );
+  }
+
+  save(recordConfig: RecordConfig, title?: string): void {
+    // We reload from local storage in case of concurrent
+    // modifications of local storage from a different tab.
+    this.reloadFromLocalStorage();
+
+    const savedTitle = title ?? new Date().toJSON();
+    const config: NamedRecordConfig = {
+      title: savedTitle,
+      config: recordConfig,
+      key: new Date().toJSON(),
+    };
+
+    this.recordConfigs.push(config);
+    this.recordConfigNames.add(savedTitle);
+
+    this._save();
+  }
+
+  overwrite(recordConfig: RecordConfig, key: string) {
+    // We reload from local storage in case of concurrent
+    // modifications of local storage from a different tab.
+    this.reloadFromLocalStorage();
+
+    const found = this.recordConfigs.find((e) => e.key === key);
+    if (found === undefined) {
+      throw new Error('trying to overwrite non-existing config');
+    }
+
+    found.config = recordConfig;
+
+    this._save();
+  }
+
+  delete(key: string): void {
+    // We reload from local storage in case of concurrent
+    // modifications of local storage from a different tab.
+    this.reloadFromLocalStorage();
+
+    let idx = -1;
+    for (let i = 0; i < this.recordConfigs.length; ++i) {
+      if (this.recordConfigs[i].key === key) {
+        idx = i;
+        break;
+      }
+    }
+
+    if (idx !== -1) {
+      this.recordConfigNames.delete(this.recordConfigs[idx].title);
+      this.recordConfigs.splice(idx, 1);
+      this._save();
+    } else {
+      // TODO(bsebastien): Show a warning message to the user in the UI.
+      console.warn("The config selected doesn't exist any more");
+    }
+  }
+
+  private clearRecordConfigs(): void {
+    this.recordConfigs = [];
+    this.recordConfigNames.clear();
+    this._save();
+  }
+
+  reloadFromLocalStorage(): void {
+    const configsLocalStorage = window.localStorage.getItem(
+      LOCAL_STORAGE_RECORD_CONFIGS_KEY,
+    );
+
+    if (exists(configsLocalStorage)) {
+      this.recordConfigNames.clear();
+
+      try {
+        const validConfigLocalStorage: Array<NamedRecordConfig> = [];
+        const parsedConfigsLocalStorage = JSON.parse(configsLocalStorage);
+
+        // Check if it's an array.
+        if (!Array.isArray(parsedConfigsLocalStorage)) {
+          this.clearRecordConfigs();
+          return;
+        }
+
+        for (let i = 0; i < parsedConfigsLocalStorage.length; ++i) {
+          const serConfig = parsedConfigsLocalStorage[i];
+          const res = NAMED_RECORD_CONFIG_SCHEMA.safeParse(serConfig);
+          if (res.success) {
+            validConfigLocalStorage.push(res.data);
+          } else {
+            console.log(
+              'Validation of saved record config has failed: ',
+              res.error.toString(),
+            );
+          }
+        }
+
+        this.recordConfigs = validConfigLocalStorage;
+        this._save();
+      } catch (e) {
+        this.clearRecordConfigs();
+      }
+    } else {
+      this.clearRecordConfigs();
+    }
+  }
+
+  canSave(title: string) {
+    return !this.recordConfigNames.has(title);
+  }
+}
+
+// This class is a singleton to avoid many instances
+// conflicting as they attempt to edit localStorage.
+export const recordConfigStore = new RecordConfigStore();
+
+export class AutosaveConfigStore {
+  private config: RecordConfig;
+
+  // Whether the current config is a default one or has been saved before.
+  // Used to determine whether the button to load "last started config" should
+  // be present in the recording profiles list.
+  hasSavedConfig: boolean;
+
+  constructor() {
+    this.hasSavedConfig = false;
+    this.config = createEmptyRecordConfig();
+    const savedItem = window.localStorage.getItem(
+      LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY,
+    );
+    if (savedItem === null) {
+      return;
+    }
+    const parsed = JSON.parse(savedItem);
+    if (parsed !== null && typeof parsed === 'object') {
+      const res = RECORD_CONFIG_SCHEMA.safeParse(parsed);
+      if (res.success) {
+        this.config = res.data;
+        this.hasSavedConfig = true;
+      }
+    }
+  }
+
+  get(): RecordConfig {
+    return this.config;
+  }
+
+  save(newConfig: RecordConfig) {
+    window.localStorage.setItem(
+      LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY,
+      JSON.stringify(newConfig),
+    );
+    this.config = newConfig;
+    this.hasSavedConfig = true;
+  }
+}
+
+export const autosaveConfigStore = new AutosaveConfigStore();
+
+export class RecordTargetStore {
+  recordTargetOS: string | null;
+
+  constructor() {
+    this.recordTargetOS = window.localStorage.getItem(
+      LOCAL_STORAGE_RECORD_TARGET_OS_KEY,
+    );
+  }
+
+  get(): string | null {
+    return this.recordTargetOS;
+  }
+
+  getValidTarget(): RecordingTarget {
+    const validTargets = getDefaultRecordingTargets();
+    const savedOS = this.get();
+
+    const validSavedTarget = validTargets.find((el) => el.os === savedOS);
+    return validSavedTarget || validTargets[0];
+  }
+
+  save(newTargetOS: string) {
+    window.localStorage.setItem(
+      LOCAL_STORAGE_RECORD_TARGET_OS_KEY,
+      newTargetOS,
+    );
+    this.recordTargetOS = newTargetOS;
+  }
+}
+
+export const recordTargetStore = new RecordTargetStore();
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/record_config_types.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_config_types.ts
new file mode 100644
index 0000000..199158a
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_config_types.ts
@@ -0,0 +1,134 @@
+// 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 {z} from 'zod';
+
+const recordModes = ['STOP_WHEN_FULL', 'RING_BUFFER', 'LONG_TRACE'] as const;
+export const RECORD_CONFIG_SCHEMA = z
+  .object({
+    mode: z.enum(recordModes).default('STOP_WHEN_FULL'),
+    durationMs: z.number().default(10000.0),
+    maxFileSizeMb: z.number().default(100),
+    fileWritePeriodMs: z.number().default(2500),
+    bufferSizeMb: z.number().default(64.0),
+
+    cpuSched: z.boolean().default(false),
+    cpuFreq: z.boolean().default(false),
+    cpuFreqPollMs: z.number().default(1000),
+    cpuSyscall: z.boolean().default(false),
+
+    gpuFreq: z.boolean().default(false),
+    gpuMemTotal: z.boolean().default(false),
+    gpuWorkPeriod: z.boolean().default(false),
+
+    ftrace: z.boolean().default(false),
+    atrace: z.boolean().default(false),
+    ftraceEvents: z.array(z.string()).default([]),
+    ftraceExtraEvents: z.string().default(''),
+    atraceCats: z.array(z.string()).default([]),
+    allAtraceApps: z.boolean().default(true),
+    atraceApps: z.string().default(''),
+    ftraceBufferSizeKb: z.number().default(0),
+    ftraceDrainPeriodMs: z.number().default(0),
+    androidLogs: z.boolean().default(false),
+    androidLogBuffers: z.array(z.string()).default([]),
+    androidFrameTimeline: z.boolean().default(false),
+    androidGameInterventionList: z.boolean().default(false),
+    androidNetworkTracing: z.boolean().default(false),
+    androidNetworkTracingPollMs: z.number().default(250),
+    androidStatsd: z.boolean().default(false),
+    androidStatsdRawPushedAtoms: z.string().default(''),
+    androidStatsdRawPulledAtoms: z.string().default(''),
+    androidStatsdPushedAtoms: z.array(z.string()).default([]),
+    androidStatsdPulledAtoms: z.array(z.string()).default([]),
+    androidStatsdPulledAtomPackages: z.string().default(''),
+    androidStatsdPulledAtomPullFrequencyMs: z.number().default(5000),
+
+    cpuCoarse: z.boolean().default(false),
+    cpuCoarsePollMs: z.number().default(1000),
+
+    batteryDrain: z.boolean().default(false),
+    batteryDrainPollMs: z.number().default(1000),
+
+    boardSensors: z.boolean().default(false),
+
+    memHiFreq: z.boolean().default(false),
+    meminfo: z.boolean().default(false),
+    meminfoPeriodMs: z.number().default(1000),
+    meminfoCounters: z.array(z.string()).default([]),
+
+    vmstat: z.boolean().default(false),
+    vmstatPeriodMs: z.number().default(1000),
+    vmstatCounters: z.array(z.string()).default([]),
+
+    heapProfiling: z.boolean().default(false),
+    hpSamplingIntervalBytes: z.number().default(4096),
+    hpProcesses: z.string().default(''),
+    hpContinuousDumpsPhase: z.number().default(0),
+    hpContinuousDumpsInterval: z.number().default(0),
+    hpSharedMemoryBuffer: z.number().default(8 * 1048576),
+    hpBlockClient: z.boolean().default(true),
+    hpAllHeaps: z.boolean().default(false),
+
+    javaHeapDump: z.boolean().default(false),
+    jpProcesses: z.string().default(''),
+    jpContinuousDumpsPhase: z.number().default(0),
+    jpContinuousDumpsInterval: z.number().default(0),
+
+    memLmk: z.boolean().default(false),
+    procStats: z.boolean().default(false),
+    procStatsPeriodMs: z.number().default(1000),
+
+    chromeCategoriesSelected: z.array(z.string()).default([]),
+    chromeHighOverheadCategoriesSelected: z.array(z.string()).default([]),
+    chromePrivacyFiltering: z.boolean().default(false),
+
+    chromeLogs: z.boolean().default(false),
+    taskScheduling: z.boolean().default(false),
+    ipcFlows: z.boolean().default(false),
+    jsExecution: z.boolean().default(false),
+    webContentRendering: z.boolean().default(false),
+    uiRendering: z.boolean().default(false),
+    inputEvents: z.boolean().default(false),
+    navigationAndLoading: z.boolean().default(false),
+    audio: z.boolean().default(false),
+    video: z.boolean().default(false),
+
+    etwCSwitch: z.boolean().default(false),
+    etwThreadState: z.boolean().default(false),
+
+    symbolizeKsyms: z.boolean().default(false),
+
+    // Enabling stack sampling
+    tracePerf: z.boolean().default(false),
+    timebaseFrequency: z.number().default(100),
+    targetCmdLine: z.array(z.string()).default([]),
+
+    linuxDeviceRpm: z.boolean().default(false),
+  })
+  // .default({}) ensures that we can always default-construct a config and
+  // spots accidental missing .default(...)
+  .default({});
+
+export const NAMED_RECORD_CONFIG_SCHEMA = z.object({
+  title: z.string(),
+  key: z.string(),
+  config: RECORD_CONFIG_SCHEMA,
+});
+export type NamedRecordConfig = z.infer<typeof NAMED_RECORD_CONFIG_SCHEMA>;
+export type RecordConfig = z.infer<typeof RECORD_CONFIG_SCHEMA>;
+
+export function createEmptyRecordConfig(): RecordConfig {
+  return RECORD_CONFIG_SCHEMA.parse({});
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
new file mode 100644
index 0000000..8062650
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
@@ -0,0 +1,447 @@
+// 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 {Message, Method, rpc, RPCImplCallback} from 'protobufjs';
+import {isString} from '../../base/object_utils';
+import {base64Encode} from '../../base/string_utils';
+import {TRACE_SUFFIX} from '../../public/trace';
+import {genTraceConfig} from './recordingV2/recording_config_utils';
+import {TargetInfo} from './recordingV2/recording_interfaces_v2';
+import {
+  AdbRecordingTarget,
+  isAdbTarget,
+  isChromeTarget,
+  isWindowsTarget,
+  RecordingTarget,
+} from './state';
+import {ConsumerPort, TraceConfig} from '../../protos';
+import {AdbOverWebUsb} from './adb';
+import {AdbConsumerPort} from './adb_shell_controller';
+import {AdbSocketConsumerPort} from './adb_socket_controller';
+import {ChromeExtensionConsumerPort} from './chrome_proxy_record_controller';
+import {
+  ConsumerPortResponse,
+  GetTraceStatsResponse,
+  isDisableTracingResponse,
+  isEnableTracingResponse,
+  isFreeBuffersResponse,
+  isGetTraceStatsResponse,
+  isReadBuffersResponse,
+} from './consumer_port_types';
+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<{}>>;
+
+export function genConfigProto(
+  uiCfg: RecordConfig,
+  target: RecordingTarget,
+): Uint8Array {
+  return TraceConfig.encode(convertToRecordingV2Input(uiCfg, target)).finish();
+}
+
+// This method converts the 'RecordingTarget' to the 'TargetInfo' used by V2 of
+// the recording code. It is used so the logic is not duplicated and does not
+// diverge.
+// TODO(octaviant) delete this once we switch to RecordingV2.
+function convertToRecordingV2Input(
+  uiCfg: RecordConfig,
+  target: RecordingTarget,
+): TraceConfig {
+  let targetType: 'ANDROID' | 'CHROME' | 'CHROME_OS' | 'LINUX' | 'WINDOWS';
+  let androidApiLevel!: number;
+  switch (target.os) {
+    case 'L':
+      targetType = 'LINUX';
+      break;
+    case 'C':
+      targetType = 'CHROME';
+      break;
+    case 'CrOS':
+      targetType = 'CHROME_OS';
+      break;
+    case 'Win':
+      targetType = 'WINDOWS';
+      break;
+    case 'S':
+      androidApiLevel = 31;
+      targetType = 'ANDROID';
+      break;
+    case 'R':
+      androidApiLevel = 30;
+      targetType = 'ANDROID';
+      break;
+    case 'Q':
+      androidApiLevel = 29;
+      targetType = 'ANDROID';
+      break;
+    case 'P':
+      androidApiLevel = 28;
+      targetType = 'ANDROID';
+      break;
+    default:
+      androidApiLevel = 26;
+      targetType = 'ANDROID';
+  }
+
+  let targetInfo: TargetInfo;
+  if (targetType === 'ANDROID') {
+    targetInfo = {
+      targetType,
+      androidApiLevel,
+      dataSources: [],
+      name: '',
+    };
+  } else {
+    targetInfo = {
+      targetType,
+      dataSources: [],
+      name: '',
+    };
+  }
+
+  return genTraceConfig(uiCfg, targetInfo);
+}
+
+export function toPbtxt(configBuffer: Uint8Array): string {
+  const msg = TraceConfig.decode(configBuffer);
+  const json = msg.toJSON();
+  function snakeCase(s: string): string {
+    return s.replace(/[A-Z]/g, (c) => '_' + c.toLowerCase());
+  }
+  // With the ahead of time compiled protos we can't seem to tell which
+  // fields are enums.
+  function isEnum(value: string): boolean {
+    return (
+      value.startsWith('MEMINFO_') ||
+      value.startsWith('VMSTAT_') ||
+      value.startsWith('STAT_') ||
+      value.startsWith('LID_') ||
+      value.startsWith('BATTERY_COUNTER_') ||
+      value.startsWith('ATOM_') ||
+      value === 'DISCARD' ||
+      value === 'RING_BUFFER' ||
+      value === 'BACKGROUND' ||
+      value === 'USER_INITIATED' ||
+      value.startsWith('PERF_CLOCK_')
+    );
+  }
+  // Since javascript doesn't have 64 bit numbers when converting protos to
+  // json the proto library encodes them as strings. This is lossy since
+  // we can't tell which strings that look like numbers are actually strings
+  // and which are actually numbers. Ideally we would reflect on the proto
+  // definition somehow but for now we just hard code keys which have this
+  // problem in the config.
+  function is64BitNumber(key: string): boolean {
+    return [
+      'maxFileSizeBytes',
+      'pid',
+      'samplingIntervalBytes',
+      'shmemSizeBytes',
+      'timestampUnitMultiplier',
+      'frequency',
+    ].includes(key);
+  }
+  function* message(msg: {}, indent: number): IterableIterator<string> {
+    for (const [key, value] of Object.entries(msg)) {
+      const isRepeated = Array.isArray(value);
+      const isNested = typeof value === 'object' && !isRepeated;
+      for (const entry of isRepeated ? (value as Array<{}>) : [value]) {
+        yield ' '.repeat(indent) + `${snakeCase(key)}${isNested ? '' : ':'} `;
+        if (isString(entry)) {
+          if (isEnum(entry) || is64BitNumber(key)) {
+            yield entry;
+          } else {
+            yield `"${entry.replace(new RegExp('"', 'g'), '\\"')}"`;
+          }
+        } else if (typeof entry === 'number') {
+          yield entry.toString();
+        } else if (typeof entry === 'boolean') {
+          yield entry.toString();
+        } else if (typeof entry === 'object' && entry !== null) {
+          yield '{\n';
+          yield* message(entry, indent + 4);
+          yield ' '.repeat(indent) + '}';
+        } else {
+          throw new Error(
+            `Record proto entry "${entry}" with unexpected type ${typeof entry}`,
+          );
+        }
+        yield '\n';
+      }
+    }
+  }
+  return [...message(json, 0)].join('');
+}
+
+export class RecordController implements Consumer {
+  private app: App;
+  private recMgr: RecordingManager;
+  private config: RecordConfig | null = null;
+  private readonly extensionPort: MessagePort;
+  private recordingInProgress = false;
+  private consumerPort: ConsumerPort;
+  private traceBuffer: Uint8Array[] = [];
+  private bufferUpdateInterval: ReturnType<typeof setTimeout> | undefined;
+  private adb = new AdbOverWebUsb();
+  private recordedTraceSuffix = TRACE_SUFFIX;
+  private fetchedCategories = false;
+
+  // We have a different controller for each targetOS. The correct one will be
+  // created when needed, and stored here. When the key is a string, it is the
+  // serial of the target (used for android devices). When the key is a single
+  // char, it is the 'targetOS'
+  private controllerPromises = new Map<string, Promise<RpcConsumerPort>>();
+
+  constructor(app: App, recMgr: RecordingManager, extensionPort: MessagePort) {
+    this.app = app;
+    this.recMgr = recMgr;
+    this.consumerPort = ConsumerPort.create(this.rpcImpl.bind(this));
+    this.extensionPort = extensionPort;
+  }
+
+  private get state() {
+    return this.recMgr.state;
+  }
+
+  refreshOnStateChange() {
+    // TODO(eseckler): Use ConsumerPort's QueryServiceState instead
+    // of posting a custom extension message to retrieve the category list.
+    scheduleFullRedraw();
+    if (this.state.fetchChromeCategories && !this.fetchedCategories) {
+      this.fetchedCategories = true;
+      if (this.state.extensionInstalled) {
+        this.extensionPort.postMessage({method: 'GetCategories'});
+      }
+      this.recMgr.setFetchChromeCategories(false);
+    }
+
+    this.config = this.state.recordConfig;
+
+    const configProto = genConfigProto(this.config, this.state.recordingTarget);
+    const configProtoText = toPbtxt(configProto);
+    const configProtoBase64 = base64Encode(configProto);
+    const commandline = `
+      echo '${configProtoBase64}' |
+      base64 --decode |
+      adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace" &&
+      adb pull /data/misc/perfetto-traces/trace /tmp/trace
+    `;
+    const traceConfig = convertToRecordingV2Input(
+      this.config,
+      this.state.recordingTarget,
+    );
+    this.state.recordCmd = {
+      commandline,
+      pbBase64: configProtoBase64,
+      pbtxt: configProtoText,
+    };
+
+    // If the recordingInProgress boolean state is different, it means that we
+    // have to start or stop recording a trace.
+    if (this.state.recordingInProgress === this.recordingInProgress) return;
+    this.recordingInProgress = this.state.recordingInProgress;
+
+    if (this.recordingInProgress) {
+      this.startRecordTrace(traceConfig);
+    } else {
+      this.stopRecordTrace();
+    }
+  }
+
+  startRecordTrace(traceConfig: TraceConfig) {
+    this.scheduleBufferUpdateRequests();
+    this.traceBuffer = [];
+    this.consumerPort.enableTracing({traceConfig});
+  }
+
+  stopRecordTrace() {
+    if (this.bufferUpdateInterval) clearInterval(this.bufferUpdateInterval);
+    this.consumerPort.flush({});
+    this.consumerPort.disableTracing({});
+  }
+
+  scheduleBufferUpdateRequests() {
+    if (this.bufferUpdateInterval) clearInterval(this.bufferUpdateInterval);
+    this.bufferUpdateInterval = setInterval(() => {
+      this.consumerPort.getTraceStats({});
+    }, 200);
+  }
+
+  readBuffers() {
+    this.consumerPort.readBuffers({});
+  }
+
+  onConsumerPortResponse(data: ConsumerPortResponse) {
+    if (data === undefined) return;
+    if (isReadBuffersResponse(data)) {
+      if (!data.slices || data.slices.length === 0) return;
+      // TODO(nicomazz): handle this as intended by consumer_port.proto.
+      console.assert(data.slices.length === 1);
+      if (data.slices[0].data) this.traceBuffer.push(data.slices[0].data);
+      // The line underneath is 'misusing' the format ReadBuffersResponse.
+      // The boolean field 'lastSliceForPacket' is used as 'lastPacketInTrace'.
+      // See http://shortn/_53WB8A1aIr.
+      if (data.slices[0].lastSliceForPacket) this.onTraceComplete();
+    } else if (isEnableTracingResponse(data)) {
+      this.readBuffers();
+    } else if (isGetTraceStatsResponse(data)) {
+      const percentage = this.getBufferUsagePercentage(data);
+      if (percentage) {
+        this.recMgr.state.bufferUsage = percentage;
+      }
+    } else if (isFreeBuffersResponse(data)) {
+      // No action required.
+    } else if (isDisableTracingResponse(data)) {
+      // No action required.
+    } else {
+      console.error('Unrecognized consumer port response:', data);
+    }
+  }
+
+  onTraceComplete() {
+    this.consumerPort.freeBuffers({});
+    this.recMgr.setRecordingStatus(undefined);
+    if (this.state.recordingCancelled) {
+      this.recMgr.setLastRecordingError('Recording cancelled.');
+      this.traceBuffer = [];
+      return;
+    }
+    const trace = this.generateTrace();
+    this.app.openTraceFromBuffer({
+      title: 'Recorded trace',
+      buffer: trace.buffer,
+      fileName: `recorded_trace${this.recordedTraceSuffix}`,
+    });
+    this.traceBuffer = [];
+  }
+
+  // TODO(nicomazz): stream each chunk into the trace processor, instead of
+  // creating a big long trace.
+  generateTrace() {
+    let traceLen = 0;
+    for (const chunk of this.traceBuffer) traceLen += chunk.length;
+    const completeTrace = new Uint8Array(traceLen);
+    let written = 0;
+    for (const chunk of this.traceBuffer) {
+      completeTrace.set(chunk, written);
+      written += chunk.length;
+    }
+    return completeTrace;
+  }
+
+  getBufferUsagePercentage(data: GetTraceStatsResponse): number {
+    if (!data.traceStats || !data.traceStats.bufferStats) return 0.0;
+    let maximumUsage = 0;
+    for (const buffer of data.traceStats.bufferStats) {
+      const used = buffer.bytesWritten as number;
+      const total = buffer.bufferSize as number;
+      maximumUsage = Math.max(maximumUsage, used / total);
+    }
+    return maximumUsage;
+  }
+
+  onError(message: string) {
+    // TODO(octaviant): b/204998302
+    console.error('Error in record controller: ', message);
+    this.recMgr.setLastRecordingError(message.substring(0, 150));
+    this.recMgr.stopRecording();
+  }
+
+  onStatus(message: string) {
+    this.recMgr.setRecordingStatus(message);
+  }
+
+  // Depending on the recording target, different implementation of the
+  // consumer_port will be used.
+  // - Chrome target: This forwards the messages that have to be sent
+  // to the extension to the frontend. This is necessary because this
+  // controller is running in a separate worker, that can't directly send
+  // messages to the extension.
+  // - Android device target: WebUSB is used to communicate using the adb
+  // protocol. Actually, there is no full consumer_port implementation, but
+  // only the support to start tracing and fetch the file.
+  async getTargetController(target: RecordingTarget): Promise<RpcConsumerPort> {
+    const identifier = RecordController.getTargetIdentifier(target);
+
+    // The reason why caching the target 'record controller' Promise is that
+    // multiple rcp calls can happen while we are trying to understand if an
+    // android device has a socket connection available or not.
+    const precedentPromise = this.controllerPromises.get(identifier);
+    if (precedentPromise) return precedentPromise;
+
+    const controllerPromise = new Promise<RpcConsumerPort>(
+      async (resolve, _) => {
+        let controller: RpcConsumerPort | undefined = undefined;
+        if (isChromeTarget(target) || isWindowsTarget(target)) {
+          controller = new ChromeExtensionConsumerPort(
+            this.extensionPort,
+            this,
+          );
+        } else if (isAdbTarget(target)) {
+          this.onStatus(`Please allow USB debugging on device.
+                 If you press cancel, reload the page.`);
+          const socketAccess = await this.hasSocketAccess(target);
+
+          controller = socketAccess
+            ? new AdbSocketConsumerPort(this.adb, this, this.recMgr.state)
+            : new AdbConsumerPort(this.adb, this, this.recMgr.state);
+        } else {
+          throw Error(`No device connected`);
+        }
+
+        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
+        if (!controller) throw Error(`Unknown target: ${target}`);
+        /* eslint-enable */
+        resolve(controller);
+      },
+    );
+
+    this.controllerPromises.set(identifier, controllerPromise);
+    return controllerPromise;
+  }
+
+  private static getTargetIdentifier(target: RecordingTarget): string {
+    return isAdbTarget(target) ? target.serial : target.os;
+  }
+
+  private async hasSocketAccess(target: AdbRecordingTarget) {
+    const devices = await navigator.usb.getDevices();
+    const device = devices.find((d) => d.serialNumber === target.serial);
+    console.assert(device);
+    if (!device) return Promise.resolve(false);
+    return AdbSocketConsumerPort.hasSocketAccess(device, this.adb);
+  }
+
+  private async rpcImpl(
+    method: RPCImplMethod,
+    requestData: Uint8Array,
+    _callback: RPCImplCallback,
+  ) {
+    try {
+      const state = this.state;
+      // TODO(hjd): This is a bit weird. We implicitly send each RPC message to
+      // whichever target is currently selected (creating that target if needed)
+      // it would be nicer if the setup/teardown was more explicit.
+      const target = await this.getTargetController(state.recordingTarget);
+      this.recordedTraceSuffix = target.getRecordedTraceSuffix();
+      target.handleCommand(method.name, requestData);
+    } catch (e) {
+      console.error(`error invoking ${method}: ${e.message}`);
+    }
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_interfaces.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_interfaces.ts
new file mode 100644
index 0000000..f29940a
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_interfaces.ts
@@ -0,0 +1,58 @@
+// Copyright (C) 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
+//
+//      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_SUFFIX} from '../../public/trace';
+import {ConsumerPortResponse} from './consumer_port_types';
+
+export type ErrorCallback = (_: string) => void;
+export type StatusCallback = (_: string) => void;
+
+export abstract class RpcConsumerPort {
+  // The responses of the call invocations should be sent through this listener.
+  // This is done by the 3 "send" methods in this abstract class.
+  private consumerPortListener: Consumer;
+
+  protected constructor(consumerPortListener: Consumer) {
+    this.consumerPortListener = consumerPortListener;
+  }
+
+  // RequestData is the proto representing the arguments of the function call.
+  abstract handleCommand(methodName: string, requestData: Uint8Array): void;
+
+  sendMessage(data: ConsumerPortResponse) {
+    this.consumerPortListener.onConsumerPortResponse(data);
+  }
+
+  sendErrorMessage(message: string) {
+    this.consumerPortListener.onError(message);
+  }
+
+  sendStatus(status: string) {
+    this.consumerPortListener.onStatus(status);
+  }
+
+  // Allows the recording controller to customise the suffix added to recorded
+  // traces when they are downloaded. In the general case this will be
+  // .perfetto-trace however if the trace is recorded compressed if could be
+  // .perfetto-trace.gz etc.
+  getRecordedTraceSuffix(): string {
+    return TRACE_SUFFIX;
+  }
+}
+
+export interface Consumer {
+  onConsumerPortResponse(data: ConsumerPortResponse): void;
+  onError: ErrorCallback;
+  onStatus: StatusCallback;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_jsdomtest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_jsdomtest.ts
new file mode 100644
index 0000000..1035369
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_jsdomtest.ts
@@ -0,0 +1,463 @@
+// 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 {assertExists} from '../../base/logging';
+import {TraceConfig} from '../../protos';
+import {createEmptyRecordConfig} from './record_config_types';
+import {genConfigProto, toPbtxt} from './record_controller';
+
+test('encodeConfig', () => {
+  const config = createEmptyRecordConfig();
+  config.durationMs = 20000;
+  const result = TraceConfig.decode(
+    genConfigProto(config, {os: 'Q', name: 'Android Q'}),
+  );
+  expect(result.durationMs).toBe(20000);
+});
+
+test('SysConfig', () => {
+  const config = createEmptyRecordConfig();
+  config.cpuSyscall = true;
+  const result = TraceConfig.decode(
+    genConfigProto(config, {os: 'Q', name: 'Android Q'}),
+  );
+  const sources = assertExists(result.dataSources);
+  // TODO(hjd): This is all bad. Should just match the whole config.
+  const srcConfig = assertExists(sources[2].config);
+  const ftraceConfig = assertExists(srcConfig.ftraceConfig);
+  const ftraceEvents = assertExists(ftraceConfig.ftraceEvents);
+  expect(ftraceEvents.includes('raw_syscalls/sys_enter')).toBe(true);
+  expect(ftraceEvents.includes('raw_syscalls/sys_exit')).toBe(true);
+});
+
+test('LinuxSystemInfo present', () => {
+  const config = createEmptyRecordConfig();
+  const result = TraceConfig.decode(
+    genConfigProto(config, {os: 'Q', name: 'Android Q'}),
+  );
+  const sources = assertExists(result.dataSources);
+  const sysInfoConfig = assertExists(sources[1].config);
+  expect(sysInfoConfig.name).toBe('linux.system_info');
+});
+
+test('cpu scheduling includes kSyms if OS >= S', () => {
+  const config = createEmptyRecordConfig();
+  config.cpuSched = true;
+  const result = TraceConfig.decode(
+    genConfigProto(config, {os: 'S', name: 'Android S'}),
+  );
+  const sources = assertExists(result.dataSources);
+  const srcConfig = assertExists(sources[3].config);
+  const ftraceConfig = assertExists(srcConfig.ftraceConfig);
+  const ftraceEvents = assertExists(ftraceConfig.ftraceEvents);
+  expect(ftraceConfig.symbolizeKsyms).toBe(true);
+  expect(ftraceEvents.includes('sched/sched_blocked_reason')).toBe(true);
+});
+
+test('cpu scheduling does not include kSyms if OS <= S', () => {
+  const config = createEmptyRecordConfig();
+  config.cpuSched = true;
+  const result = TraceConfig.decode(
+    genConfigProto(config, {os: 'Q', name: 'Android Q'}),
+  );
+  const sources = assertExists(result.dataSources);
+  const srcConfig = assertExists(sources[3].config);
+  const ftraceConfig = assertExists(srcConfig.ftraceConfig);
+  const ftraceEvents = assertExists(ftraceConfig.ftraceEvents);
+  expect(ftraceConfig.symbolizeKsyms).toBe(false);
+  expect(ftraceEvents.includes('sched/sched_blocked_reason')).toBe(false);
+});
+
+test('kSyms can be enabled individually', () => {
+  const config = createEmptyRecordConfig();
+  config.ftrace = true;
+  config.symbolizeKsyms = true;
+  const result = TraceConfig.decode(
+    genConfigProto(config, {os: 'Q', name: 'Android Q'}),
+  );
+  const sources = assertExists(result.dataSources);
+  const srcConfig = assertExists(sources[2].config);
+  const ftraceConfig = assertExists(srcConfig.ftraceConfig);
+  const ftraceEvents = assertExists(ftraceConfig.ftraceEvents);
+  expect(ftraceConfig.symbolizeKsyms).toBe(true);
+  expect(ftraceEvents.includes('sched/sched_blocked_reason')).toBe(true);
+});
+
+test('kSyms can be disabled individually', () => {
+  const config = createEmptyRecordConfig();
+  config.ftrace = true;
+  config.symbolizeKsyms = false;
+  const result = TraceConfig.decode(
+    genConfigProto(config, {os: 'Q', name: 'Android Q'}),
+  );
+  const sources = assertExists(result.dataSources);
+  const srcConfig = assertExists(sources[2].config);
+  const ftraceConfig = assertExists(srcConfig.ftraceConfig);
+  const ftraceEvents = assertExists(ftraceConfig.ftraceEvents);
+  expect(ftraceConfig.symbolizeKsyms).toBe(false);
+  expect(ftraceEvents.includes('sched/sched_blocked_reason')).toBe(false);
+});
+
+test('toPbtxt', () => {
+  const config = {
+    durationMs: 1000,
+    maxFileSizeBytes: 43,
+    buffers: [
+      {
+        sizeKb: 42,
+      },
+    ],
+    dataSources: [
+      {
+        config: {
+          name: 'linux.ftrace',
+          targetBuffer: 1,
+          ftraceConfig: {
+            ftraceEvents: ['sched_switch', 'print'],
+          },
+        },
+      },
+    ],
+    producers: [
+      {
+        producerName: 'perfetto.traced_probes',
+      },
+    ],
+  };
+
+  const text = toPbtxt(TraceConfig.encode(config).finish());
+
+  expect(text).toEqual(`buffers: {
+    size_kb: 42
+}
+data_sources: {
+    config {
+        name: "linux.ftrace"
+        target_buffer: 1
+        ftrace_config {
+            ftrace_events: "sched_switch"
+            ftrace_events: "print"
+        }
+    }
+}
+duration_ms: 1000
+producers: {
+    producer_name: "perfetto.traced_probes"
+}
+max_file_size_bytes: 43
+`);
+});
+
+test('ChromeConfig', () => {
+  const config = createEmptyRecordConfig();
+  config.ipcFlows = true;
+  config.jsExecution = true;
+  config.mode = 'STOP_WHEN_FULL';
+  const result = TraceConfig.decode(
+    genConfigProto(config, {os: 'C', name: 'Chrome'}),
+  );
+  const sources = assertExists(result.dataSources);
+
+  const traceConfigSource = assertExists(sources[0].config);
+  expect(traceConfigSource.name).toBe('org.chromium.trace_event');
+  const chromeConfig = assertExists(traceConfigSource.chromeConfig);
+  expect(chromeConfig.privacyFilteringEnabled).toBe(false);
+  const traceConfig = assertExists(chromeConfig.traceConfig);
+
+  const trackEventConfigSource = assertExists(sources[1].config);
+  expect(trackEventConfigSource.name).toBe('track_event');
+  const trackEventConfig = assertExists(
+    trackEventConfigSource.trackEventConfig,
+  );
+  expect(trackEventConfig.filterDynamicEventNames).toBe(false);
+  expect(trackEventConfig.filterDebugAnnotations).toBe(false);
+  const chromeConfigT = assertExists(trackEventConfigSource.chromeConfig);
+  const traceConfigT = assertExists(chromeConfigT.traceConfig);
+
+  const metadataConfigSource = assertExists(sources[2].config);
+  expect(metadataConfigSource.name).toBe('org.chromium.trace_metadata');
+  const chromeConfigM = assertExists(metadataConfigSource.chromeConfig);
+  const traceConfigM = assertExists(chromeConfigM.traceConfig);
+
+  const expectedTraceConfig =
+    '{"record_mode":"record-until-full",' +
+    '"included_categories":' +
+    '["toplevel","toplevel.flow","disabled-by-default-ipc.flow",' +
+    '"mojom","v8"],' +
+    '"excluded_categories":["*"],' +
+    '"memory_dump_config":{}}';
+  expect(traceConfig).toEqual(expectedTraceConfig);
+  expect(traceConfigT).toEqual(expectedTraceConfig);
+  expect(traceConfigM).toEqual(expectedTraceConfig);
+});
+
+test('ChromeConfig with privacy filtering', () => {
+  const config = createEmptyRecordConfig();
+  config.ipcFlows = true;
+  config.jsExecution = true;
+  config.mode = 'STOP_WHEN_FULL';
+  config.chromePrivacyFiltering = true;
+  const result = TraceConfig.decode(
+    genConfigProto(config, {os: 'C', name: 'Chrome'}),
+  );
+  const sources = assertExists(result.dataSources);
+
+  const traceConfigSource = assertExists(sources[0].config);
+  expect(traceConfigSource.name).toBe('org.chromium.trace_event');
+  const chromeConfig = assertExists(traceConfigSource.chromeConfig);
+  expect(chromeConfig.privacyFilteringEnabled).toBe(true);
+
+  const trackEventConfigSource = assertExists(sources[1].config);
+  expect(trackEventConfigSource.name).toBe('track_event');
+  const trackEventConfig = assertExists(
+    trackEventConfigSource.trackEventConfig,
+  );
+  expect(trackEventConfig.filterDynamicEventNames).toBe(true);
+  expect(trackEventConfig.filterDebugAnnotations).toBe(true);
+});
+
+test('ChromeMemoryConfig', () => {
+  const config = createEmptyRecordConfig();
+  config.chromeHighOverheadCategoriesSelected = [
+    'disabled-by-default-memory-infra',
+  ];
+  const result = TraceConfig.decode(
+    genConfigProto(config, {os: 'C', name: 'Chrome'}),
+  );
+  const sources = assertExists(result.dataSources);
+
+  const traceConfigSource = assertExists(sources[0].config);
+  expect(traceConfigSource.name).toBe('org.chromium.trace_event');
+  const chromeConfig = assertExists(traceConfigSource.chromeConfig);
+  const traceConfig = assertExists(chromeConfig.traceConfig);
+
+  const trackEventConfigSource = assertExists(sources[1].config);
+  expect(trackEventConfigSource.name).toBe('track_event');
+  const chromeConfigT = assertExists(trackEventConfigSource.chromeConfig);
+  const traceConfigT = assertExists(chromeConfigT.traceConfig);
+
+  const metadataConfigSource = assertExists(sources[2].config);
+  expect(metadataConfigSource.name).toBe('org.chromium.trace_metadata');
+  const chromeConfigM = assertExists(metadataConfigSource.chromeConfig);
+  const traceConfigM = assertExists(chromeConfigM.traceConfig);
+
+  const miConfigSource = assertExists(sources[3].config);
+  expect(miConfigSource.name).toBe('org.chromium.memory_instrumentation');
+  const chromeConfigI = assertExists(miConfigSource.chromeConfig);
+  const traceConfigI = assertExists(chromeConfigI.traceConfig);
+
+  const hpConfigSource = assertExists(sources[4].config);
+  expect(hpConfigSource.name).toBe('org.chromium.native_heap_profiler');
+  const chromeConfigH = assertExists(hpConfigSource.chromeConfig);
+  const traceConfigH = assertExists(chromeConfigH.traceConfig);
+
+  const expectedTraceConfig =
+    '{"record_mode":"record-until-full",' +
+    '"included_categories":["disabled-by-default-memory-infra"],' +
+    '"excluded_categories":["*"],' +
+    '"memory_dump_config":{"allowed_dump_modes":["background",' +
+    '"light","detailed"],"triggers":[{"min_time_between_dumps_ms":' +
+    '10000,"mode":"detailed","type":"periodic_interval"}]}}';
+  expect(traceConfig).toEqual(expectedTraceConfig);
+  expect(traceConfigT).toEqual(expectedTraceConfig);
+  expect(traceConfigM).toEqual(expectedTraceConfig);
+  expect(traceConfigI).toEqual(expectedTraceConfig);
+  expect(traceConfigH).toEqual(expectedTraceConfig);
+});
+
+test('ChromeCpuProfilerConfig', () => {
+  const config = createEmptyRecordConfig();
+  config.chromeHighOverheadCategoriesSelected = [
+    'disabled-by-default-cpu_profiler',
+  ];
+  const decoded = TraceConfig.decode(
+    genConfigProto(config, {os: 'C', name: 'Chrome'}),
+  );
+  const sources = assertExists(decoded.dataSources);
+
+  const traceConfigSource = assertExists(sources[0].config);
+  expect(traceConfigSource.name).toBe('org.chromium.trace_event');
+  const traceEventChromeConfig = assertExists(traceConfigSource.chromeConfig);
+  const traceEventConfig = assertExists(traceEventChromeConfig.traceConfig);
+
+  const trackEventConfigSource = assertExists(sources[1].config);
+  expect(trackEventConfigSource.name).toBe('track_event');
+  const chromeConfigT = assertExists(trackEventConfigSource.chromeConfig);
+  const traceConfigT = assertExists(chromeConfigT.traceConfig);
+
+  const metadataConfigSource = assertExists(sources[2].config);
+  expect(metadataConfigSource.name).toBe('org.chromium.trace_metadata');
+  const traceMetadataChromeConfig = assertExists(
+    metadataConfigSource.chromeConfig,
+  );
+  const traceMetadataConfig = assertExists(
+    traceMetadataChromeConfig.traceConfig,
+  );
+
+  const profilerConfigSource = assertExists(sources[3].config);
+  expect(profilerConfigSource.name).toBe('org.chromium.sampler_profiler');
+  const profilerChromeConfig = assertExists(profilerConfigSource.chromeConfig);
+  const profilerConfig = assertExists(profilerChromeConfig.traceConfig);
+
+  const expectedTraceConfig =
+    '{"record_mode":"record-until-full",' +
+    '"included_categories":["disabled-by-default-cpu_profiler"],' +
+    '"excluded_categories":["*"],"memory_dump_config":{}}';
+  expect(traceEventConfig).toEqual(expectedTraceConfig);
+  expect(traceConfigT).toEqual(expectedTraceConfig);
+  expect(traceMetadataConfig).toEqual(expectedTraceConfig);
+  expect(profilerConfig).toEqual(expectedTraceConfig);
+});
+
+test('ChromeCpuProfilerDebugConfig', () => {
+  const config = createEmptyRecordConfig();
+  config.chromeHighOverheadCategoriesSelected = [
+    'disabled-by-default-cpu_profiler.debug',
+  ];
+  const decoded = TraceConfig.decode(
+    genConfigProto(config, {os: 'C', name: 'Chrome'}),
+  );
+  const sources = assertExists(decoded.dataSources);
+
+  const traceConfigSource = assertExists(sources[0].config);
+  expect(traceConfigSource.name).toBe('org.chromium.trace_event');
+  const traceEventChromeConfig = assertExists(traceConfigSource.chromeConfig);
+  const traceEventConfig = assertExists(traceEventChromeConfig.traceConfig);
+
+  const trackEventConfigSource = assertExists(sources[1].config);
+  expect(trackEventConfigSource.name).toBe('track_event');
+  const chromeConfigT = assertExists(trackEventConfigSource.chromeConfig);
+  const traceConfigT = assertExists(chromeConfigT.traceConfig);
+
+  const metadataConfigSource = assertExists(sources[2].config);
+  expect(metadataConfigSource.name).toBe('org.chromium.trace_metadata');
+  const traceMetadataChromeConfig = assertExists(
+    metadataConfigSource.chromeConfig,
+  );
+  const traceMetadataConfig = assertExists(
+    traceMetadataChromeConfig.traceConfig,
+  );
+
+  const profilerConfigSource = assertExists(sources[3].config);
+  expect(profilerConfigSource.name).toBe('org.chromium.sampler_profiler');
+  const profilerChromeConfig = assertExists(profilerConfigSource.chromeConfig);
+  const profilerConfig = assertExists(profilerChromeConfig.traceConfig);
+
+  const expectedTraceConfig =
+    '{"record_mode":"record-until-full",' +
+    '"included_categories":["disabled-by-default-cpu_profiler.debug"],' +
+    '"excluded_categories":["*"],"memory_dump_config":{}}';
+  expect(traceConfigT).toEqual(expectedTraceConfig);
+  expect(traceEventConfig).toEqual(expectedTraceConfig);
+  expect(traceMetadataConfig).toEqual(expectedTraceConfig);
+  expect(profilerConfig).toEqual(expectedTraceConfig);
+});
+
+test('ChromeConfigRingBuffer', () => {
+  const config = createEmptyRecordConfig();
+  config.ipcFlows = true;
+  config.jsExecution = true;
+  config.mode = 'RING_BUFFER';
+  const result = TraceConfig.decode(
+    genConfigProto(config, {os: 'C', name: 'Chrome'}),
+  );
+  const sources = assertExists(result.dataSources);
+
+  const traceConfigSource = assertExists(sources[0].config);
+  expect(traceConfigSource.name).toBe('org.chromium.trace_event');
+  const chromeConfig = assertExists(traceConfigSource.chromeConfig);
+  const traceConfig = assertExists(chromeConfig.traceConfig);
+
+  const trackEventConfigSource = assertExists(sources[1].config);
+  expect(trackEventConfigSource.name).toBe('track_event');
+  const chromeConfigT = assertExists(trackEventConfigSource.chromeConfig);
+  const traceConfigT = assertExists(chromeConfigT.traceConfig);
+
+  const metadataConfigSource = assertExists(sources[2].config);
+  expect(metadataConfigSource.name).toBe('org.chromium.trace_metadata');
+  const chromeConfigM = assertExists(metadataConfigSource.chromeConfig);
+  const traceConfigM = assertExists(chromeConfigM.traceConfig);
+
+  const expectedTraceConfig =
+    '{"record_mode":"record-continuously",' +
+    '"included_categories":' +
+    '["toplevel","toplevel.flow","disabled-by-default-ipc.flow",' +
+    '"mojom","v8"],' +
+    '"excluded_categories":["*"],"memory_dump_config":{}}';
+  expect(traceConfig).toEqual(expectedTraceConfig);
+  expect(traceConfigT).toEqual(expectedTraceConfig);
+  expect(traceConfigM).toEqual(expectedTraceConfig);
+});
+
+test('ChromeConfigLongTrace', () => {
+  const config = createEmptyRecordConfig();
+  config.ipcFlows = true;
+  config.jsExecution = true;
+  config.mode = 'RING_BUFFER';
+  const result = TraceConfig.decode(
+    genConfigProto(config, {os: 'C', name: 'Chrome'}),
+  );
+  const sources = assertExists(result.dataSources);
+
+  const traceConfigSource = assertExists(sources[0].config);
+  expect(traceConfigSource.name).toBe('org.chromium.trace_event');
+  const chromeConfig = assertExists(traceConfigSource.chromeConfig);
+  const traceConfig = assertExists(chromeConfig.traceConfig);
+
+  const trackEventConfigSource = assertExists(sources[1].config);
+  expect(trackEventConfigSource.name).toBe('track_event');
+  const chromeConfigT = assertExists(trackEventConfigSource.chromeConfig);
+  const traceConfigT = assertExists(chromeConfigT.traceConfig);
+
+  const metadataConfigSource = assertExists(sources[2].config);
+  expect(metadataConfigSource.name).toBe('org.chromium.trace_metadata');
+  const chromeConfigM = assertExists(metadataConfigSource.chromeConfig);
+  const traceConfigM = assertExists(chromeConfigM.traceConfig);
+
+  const expectedTraceConfig =
+    '{"record_mode":"record-continuously",' +
+    '"included_categories":' +
+    '["toplevel","toplevel.flow","disabled-by-default-ipc.flow",' +
+    '"mojom","v8"],' +
+    '"excluded_categories":["*"],"memory_dump_config":{}}';
+  expect(traceConfig).toEqual(expectedTraceConfig);
+  expect(traceConfigT).toEqual(expectedTraceConfig);
+  expect(traceConfigM).toEqual(expectedTraceConfig);
+});
+
+test('ChromeConfigToPbtxt', () => {
+  const config = {
+    dataSources: [
+      {
+        config: {
+          name: 'org.chromium.trace_event',
+          chromeConfig: {
+            traceConfig: JSON.stringify({included_categories: ['v8']}),
+          },
+        },
+      },
+    ],
+  };
+  const text = toPbtxt(TraceConfig.encode(config).finish());
+
+  expect(text).toEqual(`data_sources: {
+    config {
+        name: "org.chromium.trace_event"
+        chrome_config {
+            trace_config: "{\\"included_categories\\":[\\"v8\\"]}"
+        }
+    }
+}
+`);
+});
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
new file mode 100644
index 0000000..021a5db
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
@@ -0,0 +1,912 @@
+// 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 {
+  getDefaultRecordingTargets,
+  hasActiveProbes,
+  isAdbTarget,
+  isAndroidP,
+  isAndroidTarget,
+  isChromeTarget,
+  isCrOSTarget,
+  isLinuxTarget,
+  isWindowsTarget,
+  LoadedConfig,
+  MAX_TIME,
+  RecordingTarget,
+} from './state';
+import {AdbOverWebUsb} from './adb';
+import {RECORD_CONFIG_SCHEMA, RecordConfig} from './record_config_types';
+import {PageAttrs} from '../../public/page';
+import {
+  autosaveConfigStore,
+  recordConfigStore,
+  recordTargetStore,
+} from './record_config';
+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 {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 {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';
+import {CopyableLink} from '../../widgets/copyable_link';
+
+export const RECORDING_SECTIONS = [
+  'buffers',
+  'instructions',
+  'config',
+  'cpu',
+  'etw',
+  'gpu',
+  'power',
+  'memory',
+  'android',
+  'chrome',
+  'tracePerf',
+  'advanced',
+];
+
+function RecordHeader(recMgr: RecordingManager) {
+  return m(
+    '.record-header',
+    m(
+      '.top-part',
+      m(
+        '.target-and-status',
+        RecordingPlatformSelection(recMgr),
+        RecordingStatusLabel(recMgr),
+        ErrorLabel(recMgr),
+      ),
+      recordingButtons(recMgr),
+    ),
+    RecordingNotes(recMgr),
+  );
+}
+
+function RecordingPlatformSelection(recMgr: RecordingManager) {
+  if (recMgr.state.recordingInProgress) return [];
+
+  const availableAndroidDevices = recMgr.state.availableAdbDevices;
+  const recordingTarget = recMgr.state.recordingTarget;
+
+  const targets = [];
+  for (const {os, name} of getDefaultRecordingTargets()) {
+    targets.push(m('option', {value: os}, name));
+  }
+  for (const d of availableAndroidDevices) {
+    targets.push(m('option', {value: d.serial}, d.name));
+  }
+
+  const selectedIndex = isAdbTarget(recordingTarget)
+    ? targets.findIndex((node) => node.attrs.value === recordingTarget.serial)
+    : targets.findIndex((node) => node.attrs.value === recordingTarget.os);
+
+  return m(
+    '.target',
+    m(
+      'label',
+      'Target platform:',
+      m(
+        'select',
+        {
+          selectedIndex,
+          onchange: (e: Event) => {
+            onTargetChange(recMgr, (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;
+          },
+        },
+        ...targets,
+      ),
+    ),
+    m(
+      '.chip',
+      {onclick: () => addAndroidDevice(recMgr)},
+      m('button', 'Add ADB Device'),
+      m('i.material-icons', 'add'),
+    ),
+  );
+}
+
+// |target| can be the TargetOs or the android serial.
+function onTargetChange(recMgr: RecordingManager, target: string) {
+  const recordingTarget: RecordingTarget =
+    recMgr.state.availableAdbDevices.find((d) => d.serial === target) ||
+    getDefaultRecordingTargets().find((t) => t.os === target) ||
+    getDefaultRecordingTargets()[0];
+
+  if (isChromeTarget(recordingTarget)) {
+    recMgr.setFetchChromeCategories(true);
+  }
+
+  recMgr.setRecordingTarget(recordingTarget);
+  recordTargetStore.save(target);
+  scheduleFullRedraw();
+}
+
+function Instructions(recMgr: RecordingManager, cssClass: string) {
+  return m(
+    `.record-section.instructions${cssClass}`,
+    m('header', 'Recording command'),
+    m(
+      'button.permalinkconfig',
+      {
+        onclick: () => uploadRecordingConfig(recMgr.state.recordConfig),
+      },
+      'Share recording settings',
+    ),
+    RecordingSnippet(recMgr),
+    BufferUsageProgressBar(recMgr),
+    m('.buttons', StopCancelButtons(recMgr)),
+    recordingLog(recMgr),
+  );
+}
+
+export function loadedConfigEqual(
+  cfg1: LoadedConfig,
+  cfg2: LoadedConfig,
+): boolean {
+  return cfg1.type === 'NAMED' && cfg2.type === 'NAMED'
+    ? cfg1.name === cfg2.name
+    : cfg1.type === cfg2.type;
+}
+
+export function loadConfigButton(
+  recMgr: RecordingManager,
+  config: RecordConfig,
+  configType: LoadedConfig,
+): m.Vnode {
+  return m(
+    'button',
+    {
+      class: 'config-button',
+      title: 'Apply configuration settings',
+      disabled: loadedConfigEqual(configType, recMgr.state.lastLoadedConfig),
+      onclick: () => {
+        recMgr.setRecordConfig(config, configType);
+        scheduleFullRedraw();
+      },
+    },
+    m('i.material-icons', 'file_upload'),
+  );
+}
+
+export function displayRecordConfigs(recMgr: RecordingManager) {
+  const configs = [];
+  if (autosaveConfigStore.hasSavedConfig) {
+    configs.push(
+      m('.config', [
+        m('span.title-config', m('strong', 'Latest started recording')),
+        loadConfigButton(recMgr, autosaveConfigStore.get(), {
+          type: 'AUTOMATIC',
+        }),
+      ]),
+    );
+  }
+  for (const item of recordConfigStore.recordConfigs) {
+    configs.push(
+      m('.config', [
+        m('span.title-config', item.title),
+        loadConfigButton(recMgr, item.config, {
+          type: 'NAMED',
+          name: item.title,
+        }),
+        m(
+          'button',
+          {
+            class: 'config-button',
+            title: 'Overwrite configuration with current settings',
+            onclick: () => {
+              if (
+                confirm(
+                  `Overwrite config "${item.title}" with current settings?`,
+                )
+              ) {
+                recordConfigStore.overwrite(
+                  recMgr.state.recordConfig,
+                  item.key,
+                );
+                recMgr.setRecordConfig(item.config, {
+                  type: 'NAMED',
+                  name: item.title,
+                });
+                scheduleFullRedraw();
+              }
+            },
+          },
+          m('i.material-icons', 'save'),
+        ),
+        m(
+          'button',
+          {
+            class: 'config-button',
+            title: 'Remove configuration',
+            onclick: () => {
+              recordConfigStore.delete(item.key);
+              scheduleFullRedraw();
+            },
+          },
+          m('i.material-icons', 'delete'),
+        ),
+      ]),
+    );
+  }
+  return configs;
+}
+
+export const ConfigTitleState = {
+  title: '',
+  getTitle: () => {
+    return ConfigTitleState.title;
+  },
+  setTitle: (value: string) => {
+    ConfigTitleState.title = value;
+  },
+  clearTitle: () => {
+    ConfigTitleState.title = '';
+  },
+};
+
+export function Configurations(recMgr: RecordingManager, cssClass: string) {
+  const canSave = recordConfigStore.canSave(ConfigTitleState.getTitle());
+  return m(
+    `.record-section${cssClass}`,
+    m('header', 'Save and load configurations'),
+    m('.input-config', [
+      m('input', {
+        value: ConfigTitleState.title,
+        placeholder: 'Title for config',
+        oninput() {
+          ConfigTitleState.setTitle(this.value);
+          scheduleFullRedraw();
+        },
+      }),
+      m(
+        'button',
+        {
+          class: 'config-button',
+          disabled: !canSave,
+          title: canSave
+            ? 'Save current config'
+            : 'Duplicate name, saving disabled',
+          onclick: () => {
+            recordConfigStore.save(
+              recMgr.state.recordConfig,
+              ConfigTitleState.getTitle(),
+            );
+            scheduleFullRedraw();
+            ConfigTitleState.clearTitle();
+          },
+        },
+        m('i.material-icons', 'save'),
+      ),
+      m(
+        'button',
+        {
+          class: 'config-button',
+          title: 'Clear current configuration',
+          onclick: () => {
+            if (
+              confirm(
+                'Current configuration will be cleared. ' + 'Are you sure?',
+              )
+            ) {
+              recMgr.clearRecordConfig();
+              scheduleFullRedraw();
+            }
+          },
+        },
+        m('i.material-icons', 'delete_forever'),
+      ),
+    ]),
+    displayRecordConfigs(recMgr),
+  );
+}
+
+function BufferUsageProgressBar(recMgr: RecordingManager) {
+  if (!recMgr.state.recordingInProgress) return [];
+
+  const bufferUsage = recMgr.state.bufferUsage;
+  // Buffer usage is not available yet on Android.
+  if (bufferUsage === 0) return [];
+
+  return m(
+    'label',
+    'Buffer usage: ',
+    m('progress', {max: 100, value: bufferUsage * 100}),
+  );
+}
+
+function RecordingNotes(recMgr: RecordingManager) {
+  const sideloadUrl =
+    'https://perfetto.dev/docs/contributing/build-instructions#get-the-code';
+  const linuxUrl = 'https://perfetto.dev/docs/quickstart/linux-tracing';
+  const cmdlineUrl =
+    'https://perfetto.dev/docs/quickstart/android-tracing#perfetto-cmdline';
+  const extensionURL = `https://chrome.google.com/webstore/detail/perfetto-ui/lfmkphfpdbjijhpomgecfikhfohaoine`;
+
+  const notes: m.Children = [];
+
+  const msgFeatNotSupported = m(
+    'span',
+    `Some probes are only supported in Perfetto versions running
+      on Android Q+. `,
+  );
+
+  const msgPerfettoNotSupported = m(
+    'span',
+    `Perfetto is not supported natively before Android P. `,
+  );
+
+  const msgSideload = m(
+    'span',
+    `If you have a rooted device you can `,
+    m(
+      'a',
+      {href: sideloadUrl, target: '_blank'},
+      `sideload the latest version of
+         Perfetto.`,
+    ),
+  );
+
+  const msgRecordingNotSupported = m(
+    '.note',
+    `Recording Perfetto traces from the UI is not supported natively
+     before Android Q. If you are using a P device, please select 'Android P'
+     as the 'Target Platform' and `,
+    m(
+      'a',
+      {href: cmdlineUrl, target: '_blank'},
+      `collect the trace using ADB.`,
+    ),
+  );
+
+  const msgChrome = m(
+    '.note',
+    `To trace Chrome from the Perfetto UI, you need to install our `,
+    m('a', {href: extensionURL, target: '_blank'}, 'Chrome extension'),
+    ' and then reload this page. ',
+  );
+
+  const msgWinEtw = m(
+    '.note',
+    `To trace with Etw on Windows from the Perfetto UI, you to run chrome with`,
+    ` administrator permission and you need to install our `,
+    m('a', {href: extensionURL, target: '_blank'}, 'Chrome extension'),
+    ' and then reload this page.',
+  );
+
+  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.`,
+    ),
+  );
+
+  const msgZeroProbes = m(
+    '.note',
+    "It looks like you didn't add any probes. " +
+      'Please add at least one to get a non-empty trace.',
+  );
+
+  if (!hasActiveProbes(recMgr.state.recordConfig)) {
+    notes.push(msgZeroProbes);
+  }
+
+  if (isAdbTarget(recMgr.state.recordingTarget)) {
+    notes.push(msgRecordingNotSupported);
+  }
+  switch (recMgr.state.recordingTarget.os) {
+    case 'Q':
+      break;
+    case 'P':
+      notes.push(m('.note', msgFeatNotSupported, msgSideload));
+      break;
+    case 'O':
+      notes.push(m('.note', msgPerfettoNotSupported, msgSideload));
+      break;
+    case 'L':
+      notes.push(msgLinux);
+      break;
+    case 'C':
+      if (!recMgr.state.extensionInstalled) notes.push(msgChrome);
+      break;
+    case 'CrOS':
+      if (!recMgr.state.extensionInstalled) notes.push(msgChrome);
+      break;
+    case 'Win':
+      if (!recMgr.state.extensionInstalled) notes.push(msgWinEtw);
+      break;
+    default:
+  }
+  if (recMgr.state.recordConfig.mode === 'LONG_TRACE') {
+    notes.unshift(msgLongTraces);
+  }
+
+  return notes.length > 0 ? m('div', notes) : [];
+}
+
+function RecordingSnippet(recMgr: RecordingManager) {
+  const target = recMgr.state.recordingTarget;
+
+  // We don't need commands to start tracing on chrome
+  if (isChromeTarget(target)) {
+    return recMgr.state.extensionInstalled && !recMgr.state.recordingInProgress
+      ? m(
+          'div',
+          m(
+            'label',
+            `To trace Chrome from the Perfetto UI you just have to press
+         'Start Recording'.`,
+          ),
+        )
+      : [];
+  }
+  return m(CodeSnippet, {text: getRecordCommand(recMgr, target)});
+}
+
+function getRecordCommand(recMgr: RecordingManager, target: RecordingTarget) {
+  const data = recMgr.state.recordCmd;
+
+  const cfg = recMgr.state.recordConfig;
+  let time = cfg.durationMs / 1000;
+
+  if (time > MAX_TIME) {
+    time = MAX_TIME;
+  }
+
+  const pbBase64 = data ? data.pbBase64 : '';
+  const pbtx = data ? data.pbtxt : '';
+  let cmd = '';
+  if (isAndroidP(target)) {
+    cmd += `echo '${pbBase64}' | \n`;
+    cmd += 'base64 --decode | \n';
+    cmd += 'adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace"\n';
+  } else {
+    cmd += isAndroidTarget(target)
+      ? '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 recordingButtons(recMgr: RecordingManager) {
+  const state = recMgr.state;
+  const target = state.recordingTarget;
+  const recInProgress = state.recordingInProgress;
+
+  const start = m(
+    `button`,
+    {
+      class: recInProgress ? '' : 'selected',
+      onclick: () => onStartRecordingPressed(recMgr),
+    },
+    'Start Recording',
+  );
+
+  const buttons: m.Children = [];
+
+  if (isAndroidTarget(target)) {
+    if (
+      !recInProgress &&
+      isAdbTarget(target) &&
+      recMgr.state.recordConfig.mode !== 'LONG_TRACE'
+    ) {
+      buttons.push(start);
+    }
+  } else if (
+    (isWindowsTarget(target) || isChromeTarget(target)) &&
+    state.extensionInstalled
+  ) {
+    buttons.push(start);
+  }
+  return m('.button', buttons);
+}
+
+function StopCancelButtons(recMgr: RecordingManager) {
+  if (!recMgr.state.recordingInProgress) return [];
+
+  const stop = m(
+    `button.selected`,
+    {onclick: () => recMgr.stopRecording()},
+    'Stop',
+  );
+
+  const cancel = m(
+    `button`,
+    {onclick: () => recMgr.cancelRecording()},
+    'Cancel',
+  );
+
+  return [stop, cancel];
+}
+
+function onStartRecordingPressed(recMgr: RecordingManager) {
+  location.href = '#!/record/instructions';
+  scheduleFullRedraw();
+  autosaveConfigStore.save(recMgr.state.recordConfig);
+
+  const target = recMgr.state.recordingTarget;
+  if (
+    isAndroidTarget(target) ||
+    isChromeTarget(target) ||
+    isWindowsTarget(target)
+  ) {
+    recMgr.app.analytics.logEvent(
+      'Record Trace',
+      `Record trace (${target.os})`,
+    );
+    recMgr.startRecording();
+  }
+}
+
+function RecordingStatusLabel(recMgr: RecordingManager) {
+  const recordingStatus = recMgr.state.recordingStatus;
+  if (!recordingStatus) return [];
+  return m('label', recordingStatus);
+}
+
+export function ErrorLabel(recMgr: RecordingManager) {
+  const lastRecordingError = recMgr.state.lastRecordingError;
+  if (!lastRecordingError) return [];
+  return m('label.error-label', `Error:  ${lastRecordingError}`);
+}
+
+function recordingLog(recMgr: RecordingManager) {
+  const logs = recMgr.state.recordingLog;
+  if (logs === undefined) return [];
+  return m('.code-snippet.no-top-bar', m('code', logs));
+}
+
+// The connection must be done in the frontend. After it, the serial ID will
+// be inserted in the state, and the worker will be able to connect to the
+// correct device.
+async function addAndroidDevice(recMgr: RecordingManager) {
+  let device: USBDevice;
+  try {
+    device = await new AdbOverWebUsb().findDevice();
+  } catch (e) {
+    const err = `No device found: ${e.name}: ${e.message}`;
+    console.error(err, e);
+    alert(err);
+    return;
+  }
+
+  if (!device.serialNumber) {
+    console.error('serial number undefined');
+    return;
+  }
+
+  // After the user has selected a device with the chrome UI, it will be
+  // available when listing all the available device from WebUSB. Therefore,
+  // we update the list of available devices.
+  await recMgr.updateAvailableAdbDevices(device.serialNumber);
+}
+
+function recordMenu(recMgr: RecordingManager, routePage: string) {
+  const target = recMgr.state.recordingTarget;
+  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'),
+    ),
+  );
+  const recInProgress = recMgr.state.recordingInProgress;
+
+  const probes = [];
+  if (isLinuxTarget(target)) {
+    probes.push(cpuProbe, powerProbe, memoryProbe, chromeProbe, advancedProbe);
+  } else if (isChromeTarget(target) && !isCrOSTarget(target)) {
+    probes.push(chromeProbe);
+  } else if (isWindowsTarget(target)) {
+    probes.push(chromeProbe, etwProbe);
+  } else {
+    probes.push(
+      cpuProbe,
+      gpuProbe,
+      powerProbe,
+      memoryProbe,
+      androidProbe,
+      chromeProbe,
+      tracePerfProbe,
+      advancedProbe,
+    );
+  }
+
+  return m(
+    '.record-menu',
+    {
+      class: recInProgress ? '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),
+  );
+}
+
+export function maybeGetActiveCss(routePage: string, section: string): string {
+  return routePage === section ? '.active' : '';
+}
+
+export interface RecordPageAttrs extends PageAttrs {
+  app: App;
+  recMgr: RecordingManager;
+}
+
+export class RecordPage implements m.ClassComponent<RecordPageAttrs> {
+  private readonly recMgr: RecordingManager;
+  private lastSubpage: string | undefined = undefined;
+
+  constructor({attrs}: m.CVnode<RecordPageAttrs>) {
+    this.recMgr = attrs.recMgr;
+  }
+
+  oninit({attrs}: m.CVnode<RecordPageAttrs>) {
+    this.lastSubpage = attrs.subpage;
+    if (attrs.subpage !== undefined && attrs.subpage.startsWith('/share/')) {
+      const hash = attrs.subpage.substring(7);
+      loadRecordConfig(this.recMgr, hash);
+      attrs.app.navigate('#!/record/instructions');
+    }
+  }
+
+  view({attrs}: m.CVnode<RecordPageAttrs>) {
+    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.
+      this.recMgr.setRecordConfig(this.recMgr.state.recordConfig);
+    }
+
+    const pages: m.Children = [];
+    // we need to remove the `/` character from the route
+    let routePage = attrs.subpage ? attrs.subpage.substr(1) : '';
+    if (!RECORDING_SECTIONS.includes(routePage)) {
+      routePage = 'buffers';
+    }
+    pages.push(recordMenu(this.recMgr, routePage));
+
+    pages.push(
+      m(RecordingSettings, {
+        dataSources: [],
+        cssClass: maybeGetActiveCss(routePage, 'buffers'),
+        recState: this.recMgr.state,
+      }),
+    );
+    pages.push(
+      Instructions(this.recMgr, maybeGetActiveCss(routePage, 'instructions')),
+    );
+    pages.push(
+      Configurations(this.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: [],
+          cssClass: maybeGetActiveCss(routePage, section),
+          recState: this.recMgr.state,
+        }),
+      );
+    }
+
+    if (isChromeTarget(this.recMgr.state.recordingTarget)) {
+      this.recMgr.setFetchChromeCategories(true);
+    }
+
+    return m(
+      '.record-page',
+      this.recMgr.state.recordingInProgress ? m('.hider') : [],
+      m(
+        '.record-container',
+        RecordHeader(this.recMgr),
+        m(
+          '.record-container-content',
+          recordMenu(this.recMgr, routePage),
+          pages,
+        ),
+      ),
+    );
+  }
+}
+
+export async function uploadRecordingConfig(recordConfig: RecordConfig) {
+  const json = JSON.stringify(recordConfig);
+  const uploader: GcsUploader = new GcsUploader(json, {
+    mimeType: MIME_JSON,
+  });
+  await uploader.waitForCompletion();
+  const hash = uploader.uploadedFileName;
+  const url = `${self.location.origin}/#!/record/share/${hash}`;
+  showModal({
+    title: 'Shareable record settings',
+    content: m(CopyableLink, {url}),
+  });
+}
+
+export async function loadRecordConfig(recMgr: RecordingManager, hash: string) {
+  const url = `https://storage.googleapis.com/${BUCKET_NAME}/${hash}`;
+  const response = await fetch(url);
+  if (!response.ok) {
+    showModal({title: 'Load failed', content: `Could not fetch ${url}`});
+    return;
+  }
+  const text = await response.text();
+  const json = JSON.parse(text);
+  const res = RECORD_CONFIG_SCHEMA.safeParse(json);
+  if (!res.success) {
+    throw new Error(
+      'Failed to deserialize record settings ' + res.error.toString(),
+    );
+  }
+  recMgr.setRecordConfig(res.data);
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
new file mode 100644
index 0000000..3559332
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
@@ -0,0 +1,682 @@
+// 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
new file mode 100644
index 0000000..325237b
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
@@ -0,0 +1,456 @@
+// Copyright (C) 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
+//
+//      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 {copyToClipboard} from '../../base/clipboard';
+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;
+
+function defaultSort(a: string, b: string) {
+  return a.localeCompare(b);
+}
+
+// +---------------------------------------------------------------------------+
+// | Docs link with 'i' in circle icon.                                        |
+// +---------------------------------------------------------------------------+
+
+interface DocsChipAttrs {
+  href: string;
+}
+
+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',
+    );
+  }
+}
+
+// +---------------------------------------------------------------------------+
+// | Probe: the rectangular box on the right-hand-side with a toggle box.      |
+// +---------------------------------------------------------------------------+
+
+export interface ProbeAttrs {
+  recCfg: RecordConfig;
+  title: string;
+  img: string | null;
+  compact?: boolean;
+  descr: m.Children;
+  isEnabled: Getter<boolean>;
+  setEnabled: Setter<boolean>;
+}
+
+export class Probe implements m.ClassComponent<ProbeAttrs> {
+  view({attrs, children}: m.CVnode<ProbeAttrs>) {
+    const onToggle = (enabled: boolean) => {
+      attrs.setEnabled(attrs.recCfg, enabled);
+      scheduleFullRedraw();
+    };
+
+    const enabled = attrs.isEnabled(attrs.recCfg);
+
+    return m(
+      `.probe${attrs.compact ? '.compact' : ''}${enabled ? '.enabled' : ''}`,
+      attrs.img &&
+        m('img', {
+          src: assetSrc(`assets/${attrs.img}`),
+          onclick: () => onToggle(!enabled),
+        }),
+      m(
+        'label',
+        m(`input[type=checkbox]`, {
+          checked: enabled,
+          oninput: (e: InputEvent) => {
+            onToggle((e.target as HTMLInputElement).checked);
+          },
+        }),
+        m('span', attrs.title),
+      ),
+      attrs.compact
+        ? ''
+        : m(
+            `div${attrs.img ? '' : '.extended-desc'}`,
+            m('div', attrs.descr),
+            m('.probe-config', children),
+          ),
+    );
+  }
+}
+
+export function CompactProbe(args: {
+  recCfg: RecordConfig;
+  title: string;
+  isEnabled: Getter<boolean>;
+  setEnabled: Setter<boolean>;
+}) {
+  return m(Probe, {
+    recCfg: args.recCfg,
+    title: args.title,
+    img: null,
+    compact: true,
+    descr: '',
+    isEnabled: args.isEnabled,
+    setEnabled: args.setEnabled,
+  });
+}
+
+// +-------------------------------------------------------------+
+// | Toggle: an on/off switch.
+// +-------------------------------------------------------------+
+
+export interface ToggleAttrs {
+  recCfg: RecordConfig;
+  title: string;
+  descr: string;
+  cssClass?: string;
+  isEnabled: Getter<boolean>;
+  setEnabled: Setter<boolean>;
+}
+
+export class Toggle implements m.ClassComponent<ToggleAttrs> {
+  view({attrs}: m.CVnode<ToggleAttrs>) {
+    const onToggle = (enabled: boolean) => {
+      attrs.setEnabled(attrs.recCfg, enabled);
+      scheduleFullRedraw();
+    };
+
+    const enabled = attrs.isEnabled(attrs.recCfg);
+
+    return m(
+      `.toggle${enabled ? '.enabled' : ''}${attrs.cssClass ?? ''}`,
+      m(
+        'label',
+        m(`input[type=checkbox]`, {
+          checked: enabled,
+          oninput: (e: InputEvent) => {
+            onToggle((e.target as HTMLInputElement).checked);
+          },
+        }),
+        m('span', attrs.title),
+      ),
+      m('.descr', attrs.descr),
+    );
+  }
+}
+
+// +---------------------------------------------------------------------------+
+// | Slider: draggable horizontal slider with numeric spinner.                 |
+// +---------------------------------------------------------------------------+
+
+export interface SliderAttrs {
+  recCfg: RecordConfig;
+  title: string;
+  icon?: string;
+  cssClass?: string;
+  isTime?: boolean;
+  unit: string;
+  values: number[];
+  get: Getter<number>;
+  set: Setter<number>;
+  min?: number;
+  description?: string;
+  disabled?: boolean;
+  zeroIsDefault?: boolean;
+}
+
+export class Slider implements m.ClassComponent<SliderAttrs> {
+  onValueChange(attrs: SliderAttrs, newVal: number) {
+    attrs.set(attrs.recCfg, newVal);
+    scheduleFullRedraw();
+  }
+
+  onTimeValueChange(attrs: SliderAttrs, hms: string) {
+    try {
+      const date = new Date(`1970-01-01T${hms}.000Z`);
+      if (isNaN(date.getTime())) return;
+      this.onValueChange(attrs, date.getTime());
+    } catch {}
+  }
+
+  onSliderChange(attrs: SliderAttrs, newIdx: number) {
+    this.onValueChange(attrs, attrs.values[newIdx]);
+  }
+
+  view({attrs}: m.CVnode<SliderAttrs>) {
+    const id = attrs.title.replace(/[^a-z0-9]/gim, '_').toLowerCase();
+    const maxIdx = attrs.values.length - 1;
+    const val = attrs.get(attrs.recCfg);
+    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+    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().substr(11, 8),
+        oninput: (e: InputEvent) => {
+          this.onTimeValueChange(attrs, (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(attrs, +(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(attrs, +(e.target as HTMLInputElement).value);
+        },
+      }),
+      m(`input.spinner[min=${min}][for=${id}]`, spinnerCfg),
+      m('.unit', attrs.unit),
+    );
+  }
+}
+
+// +---------------------------------------------------------------------------+
+// | Dropdown: wrapper around <select>. Supports single an multiple selection. |
+// +---------------------------------------------------------------------------+
+
+export interface DropdownAttrs {
+  recCfg: RecordConfig;
+  title: string;
+  cssClass?: string;
+  options: Map<string, string>;
+  sort?: (a: string, b: string) => number;
+  get: Getter<string[]>;
+  set: Setter<string[]>;
+}
+
+export class Dropdown implements m.ClassComponent<DropdownAttrs> {
+  resetScroll(dom: HTMLSelectElement) {
+    // Chrome seems to override the scroll offset on creationa, b without this,
+    // even though we call it after having marked the options as selected.
+    setTimeout(() => {
+      // Don't reset the scroll position if the element is still focused.
+      if (dom !== document.activeElement) dom.scrollTop = 0;
+    }, 0);
+  }
+
+  onChange(attrs: DropdownAttrs, e: Event) {
+    const dom = e.target as HTMLSelectElement;
+    const selKeys: string[] = [];
+    for (let i = 0; i < dom.selectedOptions.length; i++) {
+      const item = assertExists(dom.selectedOptions.item(i));
+      selKeys.push(item.value);
+    }
+    attrs.set(attrs.recCfg, selKeys);
+    scheduleFullRedraw();
+  }
+
+  view({attrs}: m.CVnode<DropdownAttrs>) {
+    const options: m.Children = [];
+    const selItems = attrs.get(attrs.recCfg);
+    let numSelected = 0;
+    const entries = [...attrs.options.entries()];
+    const f = attrs.sort === undefined ? defaultSort : attrs.sort;
+    entries.sort((a, b) => f(a[1], b[1]));
+    for (const [key, label] of entries) {
+      const opts = {value: key, selected: false};
+      if (selItems.includes(key)) {
+        opts.selected = true;
+        numSelected++;
+      }
+      options.push(m('option', opts, label));
+    }
+    const label = `${attrs.title} ${numSelected ? `(${numSelected})` : ''}`;
+    return m(
+      `select.dropdown${attrs.cssClass ?? ''}[multiple=multiple]`,
+      {
+        onblur: (e: Event) => this.resetScroll(e.target as HTMLSelectElement),
+        onmouseleave: (e: Event) =>
+          this.resetScroll(e.target as HTMLSelectElement),
+        oninput: (e: Event) => this.onChange(attrs, e),
+        oncreate: (vnode) => this.resetScroll(vnode.dom as HTMLSelectElement),
+      },
+      m('optgroup', {label}, options),
+    );
+  }
+}
+
+// +---------------------------------------------------------------------------+
+// | Textarea: wrapper around <textarea>.                                      |
+// +---------------------------------------------------------------------------+
+
+export interface TextareaAttrs {
+  recCfg: RecordConfig;
+  placeholder: string;
+  docsLink?: string;
+  cssClass?: string;
+  get: Getter<string>;
+  set: Setter<string>;
+  title?: string;
+}
+
+export class Textarea implements m.ClassComponent<TextareaAttrs> {
+  onChange(attrs: TextareaAttrs, dom: HTMLTextAreaElement) {
+    attrs.set(attrs.recCfg, dom.value);
+    scheduleFullRedraw();
+  }
+
+  view({attrs}: m.CVnode<TextareaAttrs>) {
+    return m(
+      '.textarea-holder',
+      m(
+        'header',
+        attrs.title,
+        attrs.docsLink && [' ', m(DocsChip, {href: attrs.docsLink})],
+      ),
+      m(`textarea.extra-input${attrs.cssClass ?? ''}`, {
+        onchange: (e: Event) =>
+          this.onChange(attrs, e.target as HTMLTextAreaElement),
+        placeholder: attrs.placeholder,
+        value: attrs.get(attrs.recCfg),
+      }),
+    );
+  }
+}
+
+// +---------------------------------------------------------------------------+
+// | CodeSnippet: command-prompt-like box with code snippets to copy/paste.    |
+// +---------------------------------------------------------------------------+
+
+export interface CodeSnippetAttrs {
+  text: string;
+  hardWhitespace?: boolean;
+}
+
+export class CodeSnippet implements m.ClassComponent<CodeSnippetAttrs> {
+  view({attrs}: m.CVnode<CodeSnippetAttrs>) {
+    return m(
+      '.code-snippet',
+      m(
+        'button',
+        {
+          title: 'Copy to clipboard',
+          onclick: () => copyToClipboard(attrs.text),
+        },
+        m('i.material-icons', 'assignment'),
+      ),
+      m('code', attrs.text),
+    );
+  }
+}
+
+export interface CategoryGetter {
+  get: Getter<string[]>;
+  set: Setter<string[]>;
+}
+
+type CategoriesCheckboxListParams = CategoryGetter & {
+  recCfg: RecordConfig;
+  categories: Map<string, string>;
+  title: string;
+};
+
+export class CategoriesCheckboxList
+  implements m.ClassComponent<CategoriesCheckboxListParams>
+{
+  updateValue(
+    attrs: CategoriesCheckboxListParams,
+    value: string,
+    enabled: boolean,
+  ) {
+    const values = attrs.get(attrs.recCfg);
+    const index = values.indexOf(value);
+    if (enabled && index === -1) {
+      values.push(value);
+    }
+    if (!enabled && index !== -1) {
+      values.splice(index, 1);
+    }
+    scheduleFullRedraw();
+  }
+
+  view({attrs}: m.CVnode<CategoriesCheckboxListParams>) {
+    const enabled = new Set(attrs.get(attrs.recCfg));
+    return m(
+      '.categories-list',
+      m(
+        'h3',
+        attrs.title,
+        m(
+          'button.config-button',
+          {
+            onclick: () => {
+              attrs.set(attrs.recCfg, Array.from(attrs.categories.keys()));
+            },
+          },
+          'All',
+        ),
+        m(
+          'button.config-button',
+          {
+            onclick: () => {
+              attrs.set(attrs.recCfg, []);
+            },
+          },
+          'None',
+        ),
+      ),
+      m(
+        'ul.checkboxes',
+        Array.from(attrs.categories.entries()).map(([key, value]) => {
+          const id = `category-checkbox-${key}`;
+          return m(
+            'label',
+            {for: id},
+            m(
+              'li',
+              m('input[type=checkbox]', {
+                id,
+                checked: enabled.has(key),
+                onclick: (e: InputEvent) => {
+                  const target = e.target as HTMLInputElement;
+                  this.updateValue(attrs, key, target.checked);
+                },
+              }),
+              value,
+            ),
+          );
+        }),
+      ),
+    );
+  }
+}
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
new file mode 100644
index 0000000..33e0dc1
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts
@@ -0,0 +1,83 @@
+// 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
new file mode 100644
index 0000000..9c9d139
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts
@@ -0,0 +1,240 @@
+// 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
new file mode 100644
index 0000000..715d366
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts
@@ -0,0 +1,674 @@
+// 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
new file mode 100644
index 0000000..078726f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts
@@ -0,0 +1,125 @@
+// 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_auth.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
new file mode 100644
index 0000000..7ed275e
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
@@ -0,0 +1,199 @@
+// 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 {BigInteger, RSAKey} from 'jsbn-rsa';
+import {assertExists, assertTrue} from '../../../../base/logging';
+import {
+  base64Decode,
+  base64Encode,
+  hexEncode,
+} from '../../../../base/string_utils';
+import {RecordingError} from '../recording_error_handling';
+
+const WORD_SIZE = 4;
+const MODULUS_SIZE_BITS = 2048;
+const MODULUS_SIZE = MODULUS_SIZE_BITS / 8;
+const MODULUS_SIZE_WORDS = MODULUS_SIZE / WORD_SIZE;
+const PUBKEY_ENCODED_SIZE = 3 * WORD_SIZE + 2 * MODULUS_SIZE;
+const ADB_WEB_CRYPTO_ALGORITHM = {
+  name: 'RSASSA-PKCS1-v1_5',
+  hash: {name: 'SHA-1'},
+  publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537
+  modulusLength: MODULUS_SIZE_BITS,
+};
+
+const ADB_WEB_CRYPTO_EXPORTABLE = true;
+const ADB_WEB_CRYPTO_OPERATIONS: KeyUsage[] = ['sign'];
+
+const SIGNING_ASN1_PREFIX = [
+  0x00, 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05,
+  0x00, 0x04, 0x14,
+];
+
+const R32 = BigInteger.ONE.shiftLeft(32); // 1 << 32
+
+interface ValidJsonWebKey {
+  n: string;
+  e: string;
+  d: string;
+  p: string;
+  q: string;
+  dp: string;
+  dq: string;
+  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
+  // - create a public key and send it to the device
+  // - serialize the JsonWebKey and send it to the device (or retrieve it
+  // from the device and deserialize)
+  jwkPrivate: ValidJsonWebKey;
+
+  private constructor(jwkPrivate: ValidJsonWebKey) {
+    this.jwkPrivate = jwkPrivate;
+  }
+
+  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,
+      ADB_WEB_CRYPTO_EXPORTABLE,
+      ADB_WEB_CRYPTO_OPERATIONS,
+    );
+    const jwkPrivate = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
+    if (!isValidJsonWebKey(jwkPrivate)) {
+      throw new RecordingError('Could not generate a valid 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
+  // had the SHA-1 message digest applied.
+  //
+  // However, the adb token we receive from the device is made up of 20 randomly
+  // generated bytes that are treated like a SHA-1. Therefore, we need to update
+  // the message format.
+  sign(token: Uint8Array): Uint8Array {
+    const rsaKey = new RSAKey();
+    rsaKey.setPrivateEx(
+      hexEncode(base64Decode(this.jwkPrivate.n)),
+      hexEncode(base64Decode(this.jwkPrivate.e)),
+      hexEncode(base64Decode(this.jwkPrivate.d)),
+      hexEncode(base64Decode(this.jwkPrivate.p)),
+      hexEncode(base64Decode(this.jwkPrivate.q)),
+      hexEncode(base64Decode(this.jwkPrivate.dp)),
+      hexEncode(base64Decode(this.jwkPrivate.dq)),
+      hexEncode(base64Decode(this.jwkPrivate.qi)),
+    );
+    assertTrue(rsaKey.n.bitLength() === MODULUS_SIZE_BITS);
+
+    // Message Layout (size equals that of the key modulus):
+    // 00 01 FF FF FF FF ... FF [ASN.1 PREFIX] [TOKEN]
+    const message = new Uint8Array(MODULUS_SIZE);
+
+    // Initially fill the buffer with the padding
+    message.fill(0xff);
+
+    // add prefix
+    message[0] = 0x00;
+    message[1] = 0x01;
+
+    // add the ASN.1 prefix
+    message.set(
+      SIGNING_ASN1_PREFIX,
+      message.length - SIGNING_ASN1_PREFIX.length - token.length,
+    );
+
+    // then the actual token at the end
+    message.set(token, message.length - token.length);
+
+    const messageInteger = new BigInteger(Array.from(message));
+    const signature = rsaKey.doPrivate(messageInteger);
+    return new Uint8Array(bigIntToFixedByteArray(signature, MODULUS_SIZE));
+  }
+
+  // Construct public key to match the adb format:
+  // go/codesearch/rvc-arc/system/core/libcrypto_utils/android_pubkey.c;l=38-53
+  getPublicKey(): string {
+    const rsaKey = new RSAKey();
+    rsaKey.setPublic(
+      hexEncode(base64Decode(assertExists(this.jwkPrivate.n))),
+      hexEncode(base64Decode(assertExists(this.jwkPrivate.e))),
+    );
+
+    const n0inv = R32.subtract(rsaKey.n.modInverse(R32)).intValue();
+    const r = BigInteger.ONE.shiftLeft(1).pow(MODULUS_SIZE_BITS);
+    const rr = r.multiply(r).mod(rsaKey.n);
+
+    const buffer = new ArrayBuffer(PUBKEY_ENCODED_SIZE);
+    const dv = new DataView(buffer);
+    dv.setUint32(0, MODULUS_SIZE_WORDS, true);
+    dv.setUint32(WORD_SIZE, n0inv, true);
+
+    const dvU8 = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
+    dvU8.set(
+      bigIntToFixedByteArray(rsaKey.n, MODULUS_SIZE).reverse(),
+      2 * WORD_SIZE,
+    );
+    dvU8.set(
+      bigIntToFixedByteArray(rr, MODULUS_SIZE).reverse(),
+      2 * WORD_SIZE + MODULUS_SIZE,
+    );
+
+    dv.setUint32(2 * WORD_SIZE + 2 * MODULUS_SIZE, rsaKey.e, true);
+    return base64Encode(dvU8) + ' ui.perfetto.dev';
+  }
+
+  serializeKey(): string {
+    return JSON.stringify(this.jwkPrivate);
+  }
+}
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
new file mode 100644
index 0000000..0ce297b
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts
@@ -0,0 +1,101 @@
+// 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/common/recordingV2/auth/credentials_interfaces.d.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/credentials_interfaces.d.ts
similarity index 100%
rename from ui/src/common/recordingV2/auth/credentials_interfaces.d.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/credentials_interfaces.d.ts
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
new file mode 100644
index 0000000..9461190
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts
@@ -0,0 +1,236 @@
+// 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
new file mode 100644
index 0000000..a03b791
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts
@@ -0,0 +1,83 @@
+// 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_config_utils.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils.ts
new file mode 100644
index 0000000..e4eca50
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils.ts
@@ -0,0 +1,924 @@
+// 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 {isString} from '../../../base/object_utils';
+import {base64Encode} from '../../../base/string_utils';
+import {exists} from '../../../base/utils';
+import {RecordConfig} from '../record_config_types';
+import {
+  AndroidLogConfig,
+  AndroidLogId,
+  AndroidPowerConfig,
+  AtomId,
+  BufferConfig,
+  ChromeConfig,
+  DataSourceConfig,
+  EtwConfig,
+  FtraceConfig,
+  HeapprofdConfig,
+  JavaContinuousDumpConfig,
+  JavaHprofConfig,
+  MeminfoCounters,
+  NativeContinuousDumpConfig,
+  NetworkPacketTraceConfig,
+  PerfEventConfig,
+  PerfEvents,
+  ProcessStatsConfig,
+  StatsdTracingConfig,
+  StatsdPullAtomConfig,
+  SysStatsConfig,
+  TraceConfig,
+  TrackEventConfig,
+  VmstatCounters,
+} from '../../../protos';
+import {TargetInfo} from './recording_interfaces_v2';
+import PerfClock = PerfEvents.PerfClock;
+import Timebase = PerfEvents.Timebase;
+import CallstackSampling = PerfEventConfig.CallstackSampling;
+import Scope = PerfEventConfig.Scope;
+
+export interface ConfigProtoEncoded {
+  configProtoText?: string;
+  configProtoBase64?: string;
+  hasDataSources: boolean;
+}
+
+export class RecordingConfigUtils {
+  private lastConfig?: RecordConfig;
+  private lastTargetInfo?: TargetInfo;
+  private configProtoText?: string;
+  private configProtoBase64?: string;
+  private hasDataSources: boolean = false;
+
+  fetchLatestRecordCommand(
+    recordConfig: RecordConfig,
+    targetInfo: TargetInfo,
+  ): ConfigProtoEncoded {
+    if (
+      recordConfig === this.lastConfig &&
+      targetInfo === this.lastTargetInfo
+    ) {
+      return {
+        configProtoText: this.configProtoText,
+        configProtoBase64: this.configProtoBase64,
+        hasDataSources: this.hasDataSources,
+      };
+    }
+    this.lastConfig = recordConfig;
+    this.lastTargetInfo = targetInfo;
+
+    const traceConfig = genTraceConfig(recordConfig, targetInfo);
+    const configProto = TraceConfig.encode(traceConfig).finish();
+    this.configProtoText = toPbtxt(configProto);
+    this.configProtoBase64 = base64Encode(configProto);
+    this.hasDataSources = traceConfig.dataSources.length > 0;
+    return {
+      configProtoText: this.configProtoText,
+      configProtoBase64: this.configProtoBase64,
+      hasDataSources: this.hasDataSources,
+    };
+  }
+}
+
+function enableSchedBlockedReason(androidApiLevel?: number): boolean {
+  return androidApiLevel !== undefined && androidApiLevel >= 31;
+}
+
+function enableCompactSched(androidApiLevel?: number): boolean {
+  return androidApiLevel !== undefined && androidApiLevel >= 31;
+}
+
+export function genTraceConfig(
+  uiCfg: RecordConfig,
+  targetInfo: TargetInfo,
+): TraceConfig {
+  const isAndroid = targetInfo.targetType === 'ANDROID';
+  const isLinux = targetInfo.targetType === 'LINUX';
+  const androidApiLevel = isAndroid ? targetInfo.androidApiLevel : undefined;
+  const protoCfg = new TraceConfig();
+  protoCfg.durationMs = uiCfg.durationMs;
+
+  // Auxiliary buffer for slow-rate events.
+  // Set to 1/8th of the main buffer size, with reasonable limits.
+  let slowBufSizeKb = uiCfg.bufferSizeMb * (1024 / 8);
+  slowBufSizeKb = Math.min(slowBufSizeKb, 2 * 1024);
+  slowBufSizeKb = Math.max(slowBufSizeKb, 256);
+
+  // Main buffer for ftrace and other high-freq events.
+  const fastBufSizeKb = uiCfg.bufferSizeMb * 1024 - slowBufSizeKb;
+
+  protoCfg.buffers.push(new BufferConfig());
+  protoCfg.buffers.push(new BufferConfig());
+  protoCfg.buffers[0].sizeKb = fastBufSizeKb;
+  protoCfg.buffers[1].sizeKb = slowBufSizeKb;
+
+  if (uiCfg.mode === 'STOP_WHEN_FULL') {
+    protoCfg.buffers[0].fillPolicy = BufferConfig.FillPolicy.DISCARD;
+    protoCfg.buffers[1].fillPolicy = BufferConfig.FillPolicy.DISCARD;
+  } else {
+    protoCfg.buffers[0].fillPolicy = BufferConfig.FillPolicy.RING_BUFFER;
+    protoCfg.buffers[1].fillPolicy = BufferConfig.FillPolicy.RING_BUFFER;
+    protoCfg.flushPeriodMs = 30000;
+    if (uiCfg.mode === 'LONG_TRACE') {
+      protoCfg.writeIntoFile = true;
+      protoCfg.fileWritePeriodMs = uiCfg.fileWritePeriodMs;
+      protoCfg.maxFileSizeBytes = uiCfg.maxFileSizeMb * 1e6;
+    }
+
+    // Clear incremental state every 5 seconds when tracing into a ring
+    // buffer.
+    const incStateConfig = new TraceConfig.IncrementalStateConfig();
+    incStateConfig.clearPeriodMs = 5000;
+    protoCfg.incrementalStateConfig = incStateConfig;
+  }
+
+  const ftraceEvents = new Set<string>(uiCfg.ftrace ? uiCfg.ftraceEvents : []);
+  const atraceCats = new Set<string>(uiCfg.atrace ? uiCfg.atraceCats : []);
+  const atraceApps = new Set<string>();
+  const chromeCategories = new Set<string>();
+  uiCfg.chromeCategoriesSelected.forEach((it) => chromeCategories.add(it));
+  uiCfg.chromeHighOverheadCategoriesSelected.forEach((it) =>
+    chromeCategories.add(it),
+  );
+
+  let procThreadAssociationPolling = false;
+  let procThreadAssociationFtrace = false;
+  let trackInitialOomScore = false;
+
+  if (isAndroid) {
+    const ds = new TraceConfig.DataSource();
+    ds.config = new DataSourceConfig();
+    ds.config.targetBuffer = 1;
+    ds.config.name = 'android.packages_list';
+    protoCfg.dataSources.push(ds);
+  }
+
+  if (isAndroid || isLinux) {
+    const ds = new TraceConfig.DataSource();
+    ds.config = new DataSourceConfig();
+    ds.config.targetBuffer = 1;
+    ds.config.name = 'linux.system_info';
+    protoCfg.dataSources.push(ds);
+  }
+
+  let ftrace = false;
+  let symbolizeKsyms = false;
+  if (uiCfg.cpuSched) {
+    procThreadAssociationPolling = true;
+    procThreadAssociationFtrace = true;
+    ftrace = true;
+    if (enableSchedBlockedReason(androidApiLevel)) {
+      symbolizeKsyms = true;
+    }
+    ftraceEvents.add('sched/sched_switch');
+    ftraceEvents.add('power/suspend_resume');
+    ftraceEvents.add('sched/sched_wakeup');
+    ftraceEvents.add('sched/sched_wakeup_new');
+    ftraceEvents.add('sched/sched_waking');
+    ftraceEvents.add('power/suspend_resume');
+  }
+
+  let sysStatsCfg: SysStatsConfig | undefined = undefined;
+
+  if (uiCfg.cpuFreq) {
+    ftraceEvents.add('power/cpu_frequency');
+    ftraceEvents.add('power/cpu_idle');
+    ftraceEvents.add('power/suspend_resume');
+
+    sysStatsCfg = new SysStatsConfig();
+    sysStatsCfg.cpufreqPeriodMs = uiCfg.cpuFreqPollMs;
+  }
+
+  if (uiCfg.gpuFreq) {
+    ftraceEvents.add('power/gpu_frequency');
+  }
+
+  if (uiCfg.gpuMemTotal) {
+    ftraceEvents.add('gpu_mem/gpu_mem_total');
+
+    if (targetInfo.targetType !== 'CHROME') {
+      const ds = new TraceConfig.DataSource();
+      ds.config = new DataSourceConfig();
+      ds.config.name = 'android.gpu.memory';
+      protoCfg.dataSources.push(ds);
+    }
+  }
+
+  if (uiCfg.gpuWorkPeriod) {
+    ftraceEvents.add('power/gpu_work_period');
+  }
+
+  if (uiCfg.cpuSyscall) {
+    ftraceEvents.add('raw_syscalls/sys_enter');
+    ftraceEvents.add('raw_syscalls/sys_exit');
+  }
+
+  if (uiCfg.batteryDrain) {
+    const ds = new TraceConfig.DataSource();
+    ds.config = new DataSourceConfig();
+    if (
+      targetInfo.targetType === 'CHROME_OS' ||
+      targetInfo.targetType === 'LINUX'
+    ) {
+      ds.config.name = 'linux.sysfs_power';
+    } else {
+      ds.config.name = 'android.power';
+      ds.config.androidPowerConfig = new AndroidPowerConfig();
+      ds.config.androidPowerConfig.batteryPollMs = uiCfg.batteryDrainPollMs;
+      ds.config.androidPowerConfig.batteryCounters = [
+        AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CAPACITY_PERCENT,
+        AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CHARGE,
+        AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CURRENT,
+      ];
+      ds.config.androidPowerConfig.collectPowerRails = true;
+    }
+    if (targetInfo.targetType !== 'CHROME') {
+      protoCfg.dataSources.push(ds);
+    }
+  }
+
+  if (uiCfg.boardSensors) {
+    ftraceEvents.add('regulator/regulator_set_voltage');
+    ftraceEvents.add('regulator/regulator_set_voltage_complete');
+    ftraceEvents.add('power/clock_enable');
+    ftraceEvents.add('power/clock_disable');
+    ftraceEvents.add('power/clock_set_rate');
+    ftraceEvents.add('power/suspend_resume');
+  }
+
+  if (uiCfg.cpuCoarse) {
+    if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
+    sysStatsCfg.statPeriodMs = uiCfg.cpuCoarsePollMs;
+    sysStatsCfg.statCounters = [
+      SysStatsConfig.StatCounters.STAT_CPU_TIMES,
+      SysStatsConfig.StatCounters.STAT_FORK_COUNT,
+    ];
+  }
+
+  if (uiCfg.memHiFreq) {
+    procThreadAssociationPolling = true;
+    procThreadAssociationFtrace = true;
+    ftraceEvents.add('mm_event/mm_event_record');
+    ftraceEvents.add('kmem/rss_stat');
+    ftraceEvents.add('ion/ion_stat');
+    ftraceEvents.add('dmabuf_heap/dma_heap_stat');
+    ftraceEvents.add('kmem/ion_heap_grow');
+    ftraceEvents.add('kmem/ion_heap_shrink');
+  }
+
+  if (procThreadAssociationFtrace) {
+    ftraceEvents.add('sched/sched_process_exit');
+    ftraceEvents.add('sched/sched_process_free');
+    ftraceEvents.add('task/task_newtask');
+    ftraceEvents.add('task/task_rename');
+  }
+
+  if (uiCfg.linuxDeviceRpm) {
+    ftraceEvents.add('rpm/rpm_status');
+  }
+
+  if (uiCfg.meminfo) {
+    if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
+    sysStatsCfg.meminfoPeriodMs = uiCfg.meminfoPeriodMs;
+    sysStatsCfg.meminfoCounters = uiCfg.meminfoCounters.map((name) => {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      return MeminfoCounters[name as any as number] as any as number;
+    });
+  }
+
+  if (uiCfg.vmstat) {
+    if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
+    sysStatsCfg.vmstatPeriodMs = uiCfg.vmstatPeriodMs;
+    sysStatsCfg.vmstatCounters = uiCfg.vmstatCounters.map((name) => {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      return VmstatCounters[name as any as number] as any as number;
+    });
+  }
+
+  if (uiCfg.memLmk) {
+    // For in-kernel LMK (roughly older devices until Go and Pixel 3).
+    ftraceEvents.add('lowmemorykiller/lowmemory_kill');
+
+    // 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.
+    atraceApps.add('lmkd');
+
+    ftraceEvents.add('oom/oom_score_adj_update');
+    procThreadAssociationPolling = true;
+    trackInitialOomScore = true;
+  }
+
+  let heapprofd: HeapprofdConfig | undefined = undefined;
+  if (uiCfg.heapProfiling) {
+    // TODO(hjd): Check or inform user if buffer size are too small.
+    const cfg = new HeapprofdConfig();
+    cfg.samplingIntervalBytes = uiCfg.hpSamplingIntervalBytes;
+    if (
+      uiCfg.hpSharedMemoryBuffer >= 8192 &&
+      uiCfg.hpSharedMemoryBuffer % 4096 === 0
+    ) {
+      cfg.shmemSizeBytes = uiCfg.hpSharedMemoryBuffer;
+    }
+    for (const value of uiCfg.hpProcesses.split('\n')) {
+      if (value === '') {
+        // Ignore empty lines
+      } else if (isNaN(+value)) {
+        cfg.processCmdline.push(value);
+      } else {
+        cfg.pid.push(+value);
+      }
+    }
+    if (uiCfg.hpContinuousDumpsInterval > 0) {
+      const cdc = (cfg.continuousDumpConfig = new NativeContinuousDumpConfig());
+      cdc.dumpIntervalMs = uiCfg.hpContinuousDumpsInterval;
+      if (uiCfg.hpContinuousDumpsPhase > 0) {
+        cdc.dumpPhaseMs = uiCfg.hpContinuousDumpsPhase;
+      }
+    }
+    cfg.blockClient = uiCfg.hpBlockClient;
+    if (uiCfg.hpAllHeaps) {
+      cfg.allHeaps = true;
+    }
+    heapprofd = cfg;
+  }
+
+  let javaHprof: JavaHprofConfig | undefined = undefined;
+  if (uiCfg.javaHeapDump) {
+    const cfg = new JavaHprofConfig();
+    for (const value of uiCfg.jpProcesses.split('\n')) {
+      if (value === '') {
+        // Ignore empty lines
+      } else if (isNaN(+value)) {
+        cfg.processCmdline.push(value);
+      } else {
+        cfg.pid.push(+value);
+      }
+    }
+    if (uiCfg.jpContinuousDumpsInterval > 0) {
+      const cdc = (cfg.continuousDumpConfig = new JavaContinuousDumpConfig());
+      cdc.dumpIntervalMs = uiCfg.jpContinuousDumpsInterval;
+      if (uiCfg.hpContinuousDumpsPhase > 0) {
+        cdc.dumpPhaseMs = uiCfg.jpContinuousDumpsPhase;
+      }
+    }
+    javaHprof = cfg;
+  }
+
+  if (uiCfg.procStats || procThreadAssociationPolling || trackInitialOomScore) {
+    const ds = new TraceConfig.DataSource();
+    ds.config = new DataSourceConfig();
+    ds.config.targetBuffer = 1; // Aux
+    ds.config.name = 'linux.process_stats';
+    ds.config.processStatsConfig = new ProcessStatsConfig();
+    if (uiCfg.procStats) {
+      ds.config.processStatsConfig.procStatsPollMs = uiCfg.procStatsPeriodMs;
+    }
+    if (procThreadAssociationPolling || trackInitialOomScore) {
+      ds.config.processStatsConfig.scanAllProcessesOnStart = true;
+    }
+    if (targetInfo.targetType !== 'CHROME') {
+      protoCfg.dataSources.push(ds);
+    }
+  }
+
+  if (uiCfg.androidLogs) {
+    const ds = new TraceConfig.DataSource();
+    ds.config = new DataSourceConfig();
+    ds.config.name = 'android.log';
+    ds.config.androidLogConfig = new AndroidLogConfig();
+    ds.config.androidLogConfig.logIds = uiCfg.androidLogBuffers.map((name) => {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      return AndroidLogId[name as any as number] as any as number;
+    });
+
+    if (targetInfo.targetType !== 'CHROME') {
+      protoCfg.dataSources.push(ds);
+    }
+  }
+
+  if (uiCfg.androidFrameTimeline) {
+    const ds = new TraceConfig.DataSource();
+    ds.config = new DataSourceConfig();
+    ds.config.name = 'android.surfaceflinger.frametimeline';
+    if (targetInfo.targetType !== 'CHROME') {
+      protoCfg.dataSources.push(ds);
+    }
+  }
+
+  if (uiCfg.androidGameInterventionList) {
+    const ds = new TraceConfig.DataSource();
+    ds.config = new DataSourceConfig();
+    ds.config.name = 'android.game_interventions';
+    if (targetInfo.targetType !== 'CHROME') {
+      protoCfg.dataSources.push(ds);
+    }
+  }
+
+  if (uiCfg.androidNetworkTracing) {
+    if (targetInfo.targetType !== 'CHROME') {
+      const net = new TraceConfig.DataSource();
+      net.config = new DataSourceConfig();
+      net.config.name = 'android.network_packets';
+      net.config.networkPacketTraceConfig = new NetworkPacketTraceConfig();
+      net.config.networkPacketTraceConfig.pollMs =
+        uiCfg.androidNetworkTracingPollMs;
+      protoCfg.dataSources.push(net);
+
+      // Record package info so that Perfetto can display the package name for
+      // network packet events based on the event uid.
+      const pkg = new TraceConfig.DataSource();
+      pkg.config = new DataSourceConfig();
+      pkg.config.name = 'android.packages_list';
+      protoCfg.dataSources.push(pkg);
+    }
+  }
+
+  if (uiCfg.androidStatsd) {
+    const ds = new TraceConfig.DataSource();
+    ds.config = new DataSourceConfig();
+    ds.config.name = 'android.statsd';
+    ds.config.statsdTracingConfig = new StatsdTracingConfig();
+
+    if (uiCfg.androidStatsdRawPushedAtoms.length > 0) {
+      ds.config.statsdTracingConfig.rawPushAtomId = [];
+      for (const line of uiCfg.androidStatsdRawPushedAtoms.split('\n')) {
+        if (line.trim().length > 0) {
+          ds.config.statsdTracingConfig.rawPushAtomId.push(parseInt(line));
+        }
+      }
+    }
+
+    if (uiCfg.androidStatsdPushedAtoms.length > 0) {
+      ds.config.statsdTracingConfig.pushAtomId =
+        uiCfg.androidStatsdPushedAtoms.map((atom) => atom as unknown as AtomId);
+    }
+
+    const needPulledAtomConfig =
+      uiCfg.androidStatsdRawPulledAtoms.length > 0 ||
+      uiCfg.androidStatsdPulledAtoms.length > 0;
+
+    if (needPulledAtomConfig) {
+      const pullAtomConfig = new StatsdPullAtomConfig();
+      if (uiCfg.androidStatsdRawPulledAtoms.length > 0) {
+        for (const line of uiCfg.androidStatsdRawPulledAtoms.split('\n')) {
+          if (line.trim().length > 0) {
+            pullAtomConfig.rawPullAtomId.push(parseInt(line));
+          }
+        }
+      }
+      pullAtomConfig.pullAtomId = uiCfg.androidStatsdPulledAtoms.map(
+        (atom) => atom as unknown as AtomId,
+      );
+      pullAtomConfig.pullFrequencyMs =
+        uiCfg.androidStatsdPulledAtomPullFrequencyMs;
+      if (uiCfg.androidStatsdPulledAtomPackages.length > 0) {
+        for (const line of uiCfg.androidStatsdPulledAtomPackages.split('\n')) {
+          if (line.trim().length > 0) {
+            pullAtomConfig.packages.push(line);
+          }
+        }
+      }
+      ds.config.statsdTracingConfig.pullConfig = [pullAtomConfig];
+    }
+    if (targetInfo.targetType !== 'CHROME') {
+      protoCfg.dataSources.push(ds);
+    }
+  }
+
+  if (uiCfg.chromeLogs) {
+    chromeCategories.add('log');
+  }
+
+  if (uiCfg.taskScheduling) {
+    chromeCategories.add('toplevel');
+    chromeCategories.add('toplevel.flow');
+    chromeCategories.add('scheduler');
+    chromeCategories.add('sequence_manager');
+    chromeCategories.add('disabled-by-default-toplevel.flow');
+  }
+
+  if (uiCfg.ipcFlows) {
+    chromeCategories.add('toplevel');
+    chromeCategories.add('toplevel.flow');
+    chromeCategories.add('disabled-by-default-ipc.flow');
+    chromeCategories.add('mojom');
+  }
+
+  if (uiCfg.jsExecution) {
+    chromeCategories.add('toplevel');
+    chromeCategories.add('v8');
+  }
+
+  if (uiCfg.webContentRendering) {
+    chromeCategories.add('toplevel');
+    chromeCategories.add('blink');
+    chromeCategories.add('cc');
+    chromeCategories.add('gpu');
+  }
+
+  if (uiCfg.uiRendering) {
+    chromeCategories.add('toplevel');
+    chromeCategories.add('cc');
+    chromeCategories.add('gpu');
+    chromeCategories.add('viz');
+    chromeCategories.add('ui');
+    chromeCategories.add('views');
+  }
+
+  if (uiCfg.inputEvents) {
+    chromeCategories.add('toplevel');
+    chromeCategories.add('benchmark');
+    chromeCategories.add('evdev');
+    chromeCategories.add('input');
+    chromeCategories.add('disabled-by-default-toplevel.flow');
+  }
+
+  if (uiCfg.navigationAndLoading) {
+    chromeCategories.add('loading');
+    chromeCategories.add('net');
+    chromeCategories.add('netlog');
+    chromeCategories.add('navigation');
+    chromeCategories.add('browser');
+  }
+
+  if (uiCfg.audio) {
+    function addCategoryAndDisabledByDefault(category: string) {
+      chromeCategories.add(category);
+      chromeCategories.add('disabled-by-default-' + category);
+    }
+
+    addCategoryAndDisabledByDefault('audio');
+    addCategoryAndDisabledByDefault('webaudio');
+    addCategoryAndDisabledByDefault('webaudio.audionode');
+    addCategoryAndDisabledByDefault('webrtc');
+    addCategoryAndDisabledByDefault('audio-worklet');
+    addCategoryAndDisabledByDefault('mediastream');
+    addCategoryAndDisabledByDefault('v8.gc');
+    addCategoryAndDisabledByDefault('toplevel');
+    addCategoryAndDisabledByDefault('toplevel.flow');
+    addCategoryAndDisabledByDefault('wakeup.flow');
+    addCategoryAndDisabledByDefault('cpu_profiler');
+    addCategoryAndDisabledByDefault('scheduler');
+    addCategoryAndDisabledByDefault('p2p');
+    addCategoryAndDisabledByDefault('net');
+    chromeCategories.add('base');
+  }
+
+  if (uiCfg.video) {
+    chromeCategories.add('base');
+    chromeCategories.add('gpu');
+    chromeCategories.add('gpu.capture');
+    chromeCategories.add('media');
+    chromeCategories.add('toplevel');
+    chromeCategories.add('toplevel.flow');
+    chromeCategories.add('scheduler');
+    chromeCategories.add('wakeup.flow');
+    chromeCategories.add('webrtc');
+    chromeCategories.add('disabled-by-default-video_and_image_capture');
+    chromeCategories.add('disabled-by-default-webrtc');
+  }
+
+  // linux.perf stack sampling
+  if (uiCfg.tracePerf) {
+    const ds = new TraceConfig.DataSource();
+    ds.config = new DataSourceConfig();
+    ds.config.name = 'linux.perf';
+
+    const perfEventConfig = new PerfEventConfig();
+    perfEventConfig.timebase = new Timebase();
+    perfEventConfig.timebase.frequency = uiCfg.timebaseFrequency;
+    // TODO: The timestampClock needs to be changed to MONOTONIC once we start
+    // offering a choice of counter to record on through the recording UI, as
+    // not all clocks are compatible with hardware counters).
+    perfEventConfig.timebase.timestampClock = PerfClock.PERF_CLOCK_BOOTTIME;
+
+    const callstackSampling = new CallstackSampling();
+    if (uiCfg.targetCmdLine.length > 0) {
+      const scope = new Scope();
+      for (const cmdLine of uiCfg.targetCmdLine) {
+        if (cmdLine == '') {
+          continue;
+        }
+        scope.targetCmdline?.push(cmdLine.trim());
+      }
+      callstackSampling.scope = scope;
+    }
+
+    perfEventConfig.callstackSampling = callstackSampling;
+
+    ds.config.perfEventConfig = perfEventConfig;
+    protoCfg.dataSources.push(ds);
+  }
+
+  if (chromeCategories.size !== 0) {
+    let chromeRecordMode;
+    if (uiCfg.mode === 'STOP_WHEN_FULL') {
+      chromeRecordMode = 'record-until-full';
+    } else {
+      chromeRecordMode = 'record-continuously';
+    }
+    const configStruct = {
+      record_mode: chromeRecordMode,
+      included_categories: [...chromeCategories.values()],
+      // Only include explicitly selected categories
+      excluded_categories: ['*'],
+      memory_dump_config: {},
+    };
+    if (chromeCategories.has('disabled-by-default-memory-infra')) {
+      configStruct.memory_dump_config = {
+        allowed_dump_modes: ['background', 'light', 'detailed'],
+        triggers: [
+          {
+            min_time_between_dumps_ms: 10000,
+            mode: 'detailed',
+            type: 'periodic_interval',
+          },
+        ],
+      };
+    }
+    const chromeConfig = new ChromeConfig();
+    chromeConfig.clientPriority = ChromeConfig.ClientPriority.USER_INITIATED;
+    chromeConfig.privacyFilteringEnabled = uiCfg.chromePrivacyFiltering;
+    chromeConfig.traceConfig = JSON.stringify(configStruct);
+
+    const traceDs = new TraceConfig.DataSource();
+    traceDs.config = new DataSourceConfig();
+    traceDs.config.name = 'org.chromium.trace_event';
+    traceDs.config.chromeConfig = chromeConfig;
+    protoCfg.dataSources.push(traceDs);
+
+    // Configure "track_event" datasource for the Chrome SDK build.
+    const trackEventDs = new TraceConfig.DataSource();
+    trackEventDs.config = new DataSourceConfig();
+    trackEventDs.config.name = 'track_event';
+    trackEventDs.config.chromeConfig = chromeConfig;
+    trackEventDs.config.trackEventConfig = new TrackEventConfig();
+    trackEventDs.config.trackEventConfig.disabledCategories = ['*'];
+    trackEventDs.config.trackEventConfig.enabledCategories = [
+      ...chromeCategories.values(),
+      '__metadata',
+    ];
+    trackEventDs.config.trackEventConfig.enableThreadTimeSampling = true;
+    trackEventDs.config.trackEventConfig.timestampUnitMultiplier = 1000;
+    trackEventDs.config.trackEventConfig.filterDynamicEventNames =
+      uiCfg.chromePrivacyFiltering;
+    trackEventDs.config.trackEventConfig.filterDebugAnnotations =
+      uiCfg.chromePrivacyFiltering;
+    protoCfg.dataSources.push(trackEventDs);
+
+    const metadataDs = new TraceConfig.DataSource();
+    metadataDs.config = new DataSourceConfig();
+    metadataDs.config.name = 'org.chromium.trace_metadata';
+    metadataDs.config.chromeConfig = chromeConfig;
+    protoCfg.dataSources.push(metadataDs);
+
+    if (chromeCategories.has('disabled-by-default-memory-infra')) {
+      const memoryDs = new TraceConfig.DataSource();
+      memoryDs.config = new DataSourceConfig();
+      memoryDs.config.name = 'org.chromium.memory_instrumentation';
+      memoryDs.config.chromeConfig = chromeConfig;
+      protoCfg.dataSources.push(memoryDs);
+
+      const HeapProfDs = new TraceConfig.DataSource();
+      HeapProfDs.config = new DataSourceConfig();
+      HeapProfDs.config.name = 'org.chromium.native_heap_profiler';
+      HeapProfDs.config.chromeConfig = chromeConfig;
+      protoCfg.dataSources.push(HeapProfDs);
+    }
+
+    if (
+      chromeCategories.has('disabled-by-default-cpu_profiler') ||
+      chromeCategories.has('disabled-by-default-cpu_profiler.debug')
+    ) {
+      const dataSource = new TraceConfig.DataSource();
+      dataSource.config = new DataSourceConfig();
+      dataSource.config.name = 'org.chromium.sampler_profiler';
+      dataSource.config.chromeConfig = chromeConfig;
+      protoCfg.dataSources.push(dataSource);
+    }
+    if (chromeCategories.has('disabled-by-default-system_metrics')) {
+      const dataSource = new TraceConfig.DataSource();
+      dataSource.config = new DataSourceConfig();
+      dataSource.config.name = 'org.chromium.system_metrics';
+      dataSource.config.chromeConfig = chromeConfig;
+      protoCfg.dataSources.push(dataSource);
+    }
+  }
+
+  // Keep these last. The stages above can enrich them.
+  if (
+    targetInfo.targetType !== 'WINDOWS' &&
+    targetInfo.targetType !== 'CHROME'
+  ) {
+    if (sysStatsCfg !== undefined) {
+      const ds = new TraceConfig.DataSource();
+      ds.config = new DataSourceConfig();
+      ds.config.name = 'linux.sys_stats';
+      ds.config.sysStatsConfig = sysStatsCfg;
+      protoCfg.dataSources.push(ds);
+    }
+
+    if (heapprofd !== undefined) {
+      const ds = new TraceConfig.DataSource();
+      ds.config = new DataSourceConfig();
+      ds.config.targetBuffer = 0;
+      ds.config.name = 'android.heapprofd';
+      ds.config.heapprofdConfig = heapprofd;
+      protoCfg.dataSources.push(ds);
+    }
+
+    if (javaHprof !== undefined) {
+      const ds = new TraceConfig.DataSource();
+      ds.config = new DataSourceConfig();
+      ds.config.targetBuffer = 0;
+      ds.config.name = 'android.java_hprof';
+      ds.config.javaHprofConfig = javaHprof;
+      protoCfg.dataSources.push(ds);
+    }
+  }
+
+  if (
+    uiCfg.ftrace ||
+    ftrace ||
+    uiCfg.atrace ||
+    ftraceEvents.size > 0 ||
+    atraceCats.size > 0 ||
+    atraceApps.size > 0
+  ) {
+    const ds = new TraceConfig.DataSource();
+    ds.config = new DataSourceConfig();
+    ds.config.name = 'linux.ftrace';
+    ds.config.ftraceConfig = new FtraceConfig();
+    // Override the advanced ftrace parameters only if the user has ticked the
+    // "Advanced ftrace config" tab.
+    if (uiCfg.ftrace || ftrace) {
+      if (uiCfg.ftraceBufferSizeKb) {
+        ds.config.ftraceConfig.bufferSizeKb = uiCfg.ftraceBufferSizeKb;
+      }
+      if (uiCfg.ftraceDrainPeriodMs) {
+        ds.config.ftraceConfig.drainPeriodMs = uiCfg.ftraceDrainPeriodMs;
+      }
+      if (uiCfg.symbolizeKsyms || symbolizeKsyms) {
+        ds.config.ftraceConfig.symbolizeKsyms = true;
+        ftraceEvents.add('sched/sched_blocked_reason');
+      }
+      for (const line of uiCfg.ftraceExtraEvents.split('\n')) {
+        if (line.trim().length > 0) ftraceEvents.add(line.trim());
+      }
+    }
+
+    if (uiCfg.atrace) {
+      if (uiCfg.allAtraceApps) {
+        atraceApps.clear();
+        atraceApps.add('*');
+      } else {
+        for (const line of uiCfg.atraceApps.split('\n')) {
+          if (line.trim().length > 0) atraceApps.add(line.trim());
+        }
+      }
+    }
+
+    if (atraceCats.size > 0 || atraceApps.size > 0) {
+      ftraceEvents.add('ftrace/print');
+    }
+
+    let ftraceEventsArray: string[] = [];
+    if (exists(androidApiLevel) && androidApiLevel === 28) {
+      for (const ftraceEvent of ftraceEvents) {
+        // On P, we don't support groups so strip all group names from ftrace
+        // events.
+        const groupAndName = ftraceEvent.split('/');
+        if (groupAndName.length !== 2) {
+          ftraceEventsArray.push(ftraceEvent);
+          continue;
+        }
+        // Filter out any wildcard event groups which was not supported
+        // before Q.
+        if (groupAndName[1] === '*') {
+          continue;
+        }
+        ftraceEventsArray.push(groupAndName[1]);
+      }
+    } else {
+      ftraceEventsArray = Array.from(ftraceEvents);
+    }
+
+    ds.config.ftraceConfig.ftraceEvents = ftraceEventsArray;
+    ds.config.ftraceConfig.atraceCategories = Array.from(atraceCats);
+    ds.config.ftraceConfig.atraceApps = Array.from(atraceApps);
+
+    if (enableCompactSched(androidApiLevel)) {
+      const compact = new FtraceConfig.CompactSchedConfig();
+      compact.enabled = true;
+      ds.config.ftraceConfig.compactSched = compact;
+    }
+
+    if (targetInfo.targetType !== 'CHROME') {
+      protoCfg.dataSources.push(ds);
+    }
+  }
+
+  if (
+    targetInfo.targetType === 'WINDOWS' ||
+    uiCfg.etwCSwitch ||
+    uiCfg.etwThreadState
+  ) {
+    const ds = new TraceConfig.DataSource();
+    ds.config = new DataSourceConfig();
+    ds.config.name = 'org.chromium.etw_system';
+    ds.config.etwConfig = new EtwConfig();
+
+    const kernelFlags: EtwConfig.KernelFlag[] = [];
+
+    if (uiCfg.etwCSwitch) {
+      kernelFlags.push(EtwConfig.KernelFlag.CSWITCH);
+    }
+    if (uiCfg.etwThreadState) {
+      kernelFlags.push(EtwConfig.KernelFlag.DISPATCHER);
+    }
+    ds.config.etwConfig.kernelFlags = kernelFlags;
+    protoCfg.dataSources.push(ds);
+  }
+
+  return protoCfg;
+}
+
+function toPbtxt(configBuffer: Uint8Array): string {
+  const msg = TraceConfig.decode(configBuffer);
+  const json = msg.toJSON();
+  function snakeCase(s: string): string {
+    return s.replace(/[A-Z]/g, (c) => '_' + c.toLowerCase());
+  }
+  // With the ahead of time compiled protos we can't seem to tell which
+  // fields are enums.
+  function isEnum(value: string): boolean {
+    return (
+      value.startsWith('MEMINFO_') ||
+      value.startsWith('VMSTAT_') ||
+      value.startsWith('STAT_') ||
+      value.startsWith('LID_') ||
+      value.startsWith('BATTERY_COUNTER_') ||
+      value === 'DISCARD' ||
+      value === 'RING_BUFFER' ||
+      value.startsWith('PERF_CLOCK_')
+    );
+  }
+  // Since javascript doesn't have 64 bit numbers when converting protos to
+  // json the proto library encodes them as strings. This is lossy since
+  // we can't tell which strings that look like numbers are actually strings
+  // and which are actually numbers. Ideally we would reflect on the proto
+  // definition somehow but for now we just hard code keys which have this
+  // problem in the config.
+  function is64BitNumber(key: string): boolean {
+    return [
+      'maxFileSizeBytes',
+      'samplingIntervalBytes',
+      'shmemSizeBytes',
+      'pid',
+      'frequency',
+    ].includes(key);
+  }
+  function* message(msg: {}, indent: number): IterableIterator<string> {
+    for (const [key, value] of Object.entries(msg)) {
+      const isRepeated = Array.isArray(value);
+      const isNested = typeof value === 'object' && !isRepeated;
+      for (const entry of isRepeated ? (value as Array<{}>) : [value]) {
+        yield ' '.repeat(indent) + `${snakeCase(key)}${isNested ? '' : ':'} `;
+        if (isString(entry)) {
+          if (isEnum(entry) || is64BitNumber(key)) {
+            yield entry;
+          } else {
+            yield `"${entry.replace(new RegExp('"', 'g'), '\\"')}"`;
+          }
+        } else if (typeof entry === 'number') {
+          yield entry.toString();
+        } else if (typeof entry === 'boolean') {
+          yield entry.toString();
+        } else if (typeof entry === 'object' && entry !== null) {
+          yield '{\n';
+          yield* message(entry, indent + 4);
+          yield ' '.repeat(indent) + '}';
+        } else {
+          throw new Error(
+            `Record proto entry "${entry}" with unexpected type ${typeof entry}`,
+          );
+        }
+        yield '\n';
+      }
+    }
+  }
+  return [...message(json, 0)].join('');
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils_unittest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils_unittest.ts
new file mode 100644
index 0000000..dd96a69
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils_unittest.ts
@@ -0,0 +1,95 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {createEmptyRecordConfig} from '../record_config_types';
+import {genTraceConfig} from './recording_config_utils';
+import {AndroidTargetInfo} from './recording_interfaces_v2';
+
+test('genTraceConfig() can run without manipulating the input config', () => {
+  const config = createEmptyRecordConfig();
+  config.cpuSched = true; // Exercise ftrace
+
+  const targetInfo: AndroidTargetInfo = {
+    name: 'test',
+    targetType: 'ANDROID',
+    androidApiLevel: 31, // >= 32 to exercise symbolizeKsyms
+    dataSources: [],
+  };
+
+  Object.freeze(config);
+  const actual = genTraceConfig(config, targetInfo);
+
+  const expected = {
+    buffers: [
+      {
+        sizeKb: 63488,
+        fillPolicy: 'DISCARD',
+      },
+      {
+        sizeKb: 2048,
+        fillPolicy: 'DISCARD',
+      },
+    ],
+    dataSources: [
+      {
+        config: {
+          name: 'android.packages_list',
+          targetBuffer: 1,
+        },
+      },
+      {
+        config: {
+          name: 'linux.system_info',
+          targetBuffer: 1,
+        },
+      },
+      {
+        config: {
+          name: 'linux.process_stats',
+          targetBuffer: 1,
+          processStatsConfig: {
+            scanAllProcessesOnStart: true,
+          },
+        },
+      },
+      {
+        config: {
+          name: 'linux.ftrace',
+          ftraceConfig: {
+            ftraceEvents: [
+              'sched/sched_switch',
+              'power/suspend_resume',
+              'sched/sched_wakeup',
+              'sched/sched_wakeup_new',
+              'sched/sched_waking',
+              'sched/sched_process_exit',
+              'sched/sched_process_free',
+              'task/task_newtask',
+              'task/task_rename',
+              'sched/sched_blocked_reason',
+            ],
+            compactSched: {
+              enabled: true,
+            },
+            symbolizeKsyms: true,
+          },
+        },
+      },
+    ],
+    durationMs: 10000,
+  };
+
+  // Compare stringified versions to void issues with JS objects.
+  expect(JSON.stringify(actual)).toEqual(JSON.stringify(expected));
+});
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_error_handling.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_error_handling.ts
new file mode 100644
index 0000000..ba86e65
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_error_handling.ts
@@ -0,0 +1,263 @@
+// 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 {getErrorMessage} from '../../../base/errors';
+import {showModal} from '../../../widgets/modal';
+import {OnMessageCallback} from './recording_interfaces_v2';
+import {
+  ALLOW_USB_DEBUGGING,
+  BINARY_PUSH_FAILURE,
+  BINARY_PUSH_UNKNOWN_RESPONSE,
+  EXTENSION_NOT_INSTALLED,
+  EXTENSION_URL,
+  NO_DEVICE_SELECTED,
+  PARSING_UNABLE_TO_DECODE_METHOD,
+  PARSING_UNKNWON_REQUEST_ID,
+  PARSING_UNRECOGNIZED_MESSAGE,
+  PARSING_UNRECOGNIZED_PORT,
+  WEBSOCKET_UNABLE_TO_CONNECT,
+} from './recording_utils';
+
+// The pattern for handling recording error can have the following nesting in
+// case of errors:
+// A. wrapRecordingError -> wraps a promise
+// B. onFailure -> has user defined logic and calls showRecordingModal
+// C. showRecordingModal -> shows UX for a given error; this is not called
+//    directly by wrapRecordingError, because we want the caller (such as the
+//    UI) to dictate the UX
+
+// This method takes a promise and a callback to be execute in case the promise
+// fails. It then awaits the promise and executes the callback in case of
+// failure. In the recording code it is used to wrap:
+// 1. Acessing the WebUSB API.
+// 2. Methods returning promises which can be rejected. For instance:
+// a) When the user clicks 'Add a new device' but then doesn't select a valid
+//    device.
+// b) When the user starts a tracing session, but cancels it before they
+//    authorize the session on the device.
+export async function wrapRecordingError<T>(
+  promise: Promise<T>,
+  onFailure: OnMessageCallback,
+): Promise<T | undefined> {
+  try {
+    return await promise;
+  } catch (e) {
+    // Sometimes the message is wrapped in an Error object, sometimes not, so
+    // we make sure we transform it into a string.
+    const errorMessage = getErrorMessage(e);
+    onFailure(errorMessage);
+    return undefined;
+  }
+}
+
+// Shows a modal for every known type of error which can arise during recording.
+// In this way, errors occuring at different levels of the recording process
+// can be handled in a central location.
+export function showRecordingModal(message: string): void {
+  if (
+    [
+      'Unable to claim interface.',
+      'The specified endpoint is not part of a claimed and selected ' +
+        'alternate interface.',
+      // thrown when calling the 'reset' method on a WebUSB device.
+      'Unable to reset the device.',
+    ].some((partOfMessage) => message.includes(partOfMessage))
+  ) {
+    showWebUSBErrorV2();
+  } else if (
+    [
+      'A transfer error has occurred.',
+      'The device was disconnected.',
+      'The transfer was cancelled.',
+    ].some((partOfMessage) => message.includes(partOfMessage)) ||
+    isDeviceDisconnectedError(message)
+  ) {
+    showConnectionLostError();
+  } else if (message === ALLOW_USB_DEBUGGING) {
+    showAllowUSBDebugging();
+  } else if (
+    isMessageComposedOf(message, [
+      BINARY_PUSH_FAILURE,
+      BINARY_PUSH_UNKNOWN_RESPONSE,
+    ])
+  ) {
+    showFailedToPushBinary(message.substring(message.indexOf(':') + 1));
+  } else if (message === NO_DEVICE_SELECTED) {
+    showNoDeviceSelected();
+  } else if (WEBSOCKET_UNABLE_TO_CONNECT === message) {
+    showWebsocketConnectionIssue(message);
+  } else if (message === EXTENSION_NOT_INSTALLED) {
+    showExtensionNotInstalled();
+  } else if (
+    isMessageComposedOf(message, [
+      PARSING_UNKNWON_REQUEST_ID,
+      PARSING_UNABLE_TO_DECODE_METHOD,
+      PARSING_UNRECOGNIZED_PORT,
+      PARSING_UNRECOGNIZED_MESSAGE,
+    ])
+  ) {
+    showIssueParsingTheTracedResponse(message);
+  } else {
+    throw new Error(`${message}`);
+  }
+}
+
+function isDeviceDisconnectedError(message: string) {
+  return (
+    message.includes('Device with serial') &&
+    message.includes('was disconnected.')
+  );
+}
+
+function isMessageComposedOf(message: string, issues: string[]) {
+  for (const issue of issues) {
+    if (message.includes(issue)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+// Exception thrown by the Recording logic.
+export class RecordingError extends Error {}
+
+function showWebUSBErrorV2() {
+  showModal({
+    title: 'A WebUSB error occurred',
+    content: m(
+      'div',
+      m(
+        'span',
+        `Is adb already running on the host? Run this command and
+      try again.`,
+      ),
+      m('br'),
+      m('.modal-bash', '> adb kill-server'),
+      m('br'),
+      // The statement below covers the following edge case:
+      // 1. 'adb server' is running on the device.
+      // 2. The user selects the new Android target, so we try to fetch the
+      // OS version and do QSS.
+      // 3. The error modal is shown.
+      // 4. The user runs 'adb kill-server'.
+      // At this point we don't have a trigger to try fetching the OS version
+      // + QSS again. Therefore, the user will need to refresh the page.
+      m(
+        'span',
+        "If after running 'adb kill-server', you don't see " +
+          "a 'Start Recording' button on the page and you don't see " +
+          "'Allow USB debugging' on the device, " +
+          'you will need to reload this page.',
+      ),
+      m('br'),
+      m('br'),
+      m('span', 'For details see '),
+      m('a', {href: 'http://b/159048331', target: '_blank'}, 'b/159048331'),
+    ),
+  });
+}
+
+function showConnectionLostError(): void {
+  showModal({
+    title: 'Connection with the ADB device lost',
+    content: m(
+      'div',
+      m('span', `Please connect the device again to restart the recording.`),
+      m('br'),
+    ),
+  });
+}
+
+function showAllowUSBDebugging(): void {
+  showModal({
+    title: 'Could not connect to the device',
+    content: m(
+      'div',
+      m('span', 'Please allow USB debugging on the device.'),
+      m('br'),
+    ),
+  });
+}
+
+function showNoDeviceSelected(): void {
+  showModal({
+    title: 'No device was selected for recording',
+    content: m(
+      'div',
+      m(
+        'span',
+        `If you want to connect to an ADB device,
+           please select it from the list.`,
+      ),
+      m('br'),
+    ),
+  });
+}
+
+function showExtensionNotInstalled(): void {
+  showModal({
+    title: 'Perfetto Chrome extension not installed',
+    content: m(
+      'div',
+      m(
+        '.note',
+        `To trace Chrome from the Perfetto UI, you need to install our `,
+        m('a', {href: EXTENSION_URL, target: '_blank'}, 'Chrome extension'),
+        ' and then reload this page.',
+      ),
+      m('br'),
+    ),
+  });
+}
+
+function showIssueParsingTheTracedResponse(message: string): void {
+  showModal({
+    title:
+      'A problem was encountered while connecting to' +
+      ' the Perfetto tracing service',
+    content: m('div', m('span', message), m('br')),
+  });
+}
+
+function showFailedToPushBinary(message: string): void {
+  showModal({
+    title: 'Failed to push a binary to the device',
+    content: m(
+      'div',
+      m(
+        'span',
+        'This can happen if your Android device has an OS version lower ' +
+          'than Q. Perfetto tried to push the latest version of its ' +
+          'embedded binary but failed.',
+      ),
+      m('br'),
+      m('br'),
+      m('span', 'Error message:'),
+      m('br'),
+      m('span', message),
+    ),
+  });
+}
+
+function showWebsocketConnectionIssue(message: string): void {
+  showModal({
+    title: 'Unable to connect to the device via websocket',
+    content: m(
+      'div',
+      m('div', 'trace_processor_shell --httpd is unreachable or crashed.'),
+      m('pre', message),
+    ),
+  });
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_interfaces_v2.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_interfaces_v2.ts
new file mode 100644
index 0000000..c8a030e
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_interfaces_v2.ts
@@ -0,0 +1,228 @@
+// 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 {TraceConfig} from '../../../protos';
+
+// TargetFactory connects, disconnects and keeps track of targets.
+// There is one factory for AndroidWebusb, AndroidWebsocket, Chrome etc.
+// For instance, the AndroidWebusb factory returns a RecordingTargetV2 for each
+// device.
+export interface TargetFactory {
+  // Store the kind explicitly as a string as opposed to using class.kind in
+  // case we ever minify our code.
+  readonly kind: string;
+
+  // Setter for OnTargetChange, which is executed when a target is
+  // added/removed or when its information is updated.
+  setOnTargetChange(onTargetChange: OnTargetChangeCallback): void;
+
+  getName(): string;
+
+  listTargets(): RecordingTargetV2[];
+  // Returns recording problems that we encounter when not directly using the
+  // target. For instance we connect webusb devices when Perfetto is loaded. If
+  // there is an issue with connecting a webusb device, we do not want to crash
+  // all of Perfetto, as the user may not want to use the recording
+  // functionality at all.
+  listRecordingProblems(): string[];
+
+  connectNewTarget(): Promise<RecordingTargetV2>;
+}
+
+export interface DataSource {
+  name: string;
+
+  // Contains information that is opaque to the recording code. The caller can
+  // use the DataSource name to type cast the DataSource descriptor.
+  // For targets calling QueryServiceState, 'descriptor' will hold the
+  // datasource descriptor:
+  // https://source.corp.google.com/android/external/perfetto/protos/perfetto/
+  // common/data_source_descriptor.proto;l=28-60
+  // For Chrome, 'descriptor' will contain the answer received from
+  // 'GetCategories':
+  // https://source.corp.google.com/android/external/perfetto/ui/src/
+  // chrome_extension/chrome_tracing_controller.ts;l=220
+  descriptor: unknown;
+}
+
+// Common fields for all types of targetInfo: Chrome, Android, Linux etc.
+interface TargetInfoBase {
+  name: string;
+
+  // The dataSources exposed by a target. They are fetched from the target
+  // (ex: using QSS for Android or GetCategories for Chrome).
+  dataSources: DataSource[];
+}
+
+export interface AndroidTargetInfo extends TargetInfoBase {
+  targetType: 'ANDROID';
+
+  // This is the Android API level. For instance, it can be 32, 31, 30 etc.
+  // It is the "API level" column here:
+  // https://source.android.com/setup/start/build-numbers
+  androidApiLevel?: number;
+}
+
+export interface ChromeTargetInfo extends TargetInfoBase {
+  targetType: 'CHROME' | 'CHROME_OS' | 'WINDOWS';
+}
+
+export interface HostOsTargetInfo extends TargetInfoBase {
+  targetType: 'LINUX' | 'MACOS';
+}
+
+// Holds information about a target. It's used by the UI and the logic which
+// generates a config.
+export type TargetInfo =
+  | AndroidTargetInfo
+  | ChromeTargetInfo
+  | HostOsTargetInfo;
+
+// RecordingTargetV2 is subclassed by Android devices and the Chrome browser/OS.
+// It creates tracing sessions which are used by the UI. For Android, it manages
+// the connection with the device.
+export interface RecordingTargetV2 {
+  // Allows targets to surface target specific information such as
+  // well known key/value pairs: OS, targetType('ANDROID', 'CHROME', etc.)
+  getInfo(): TargetInfo;
+
+  // Disconnects the target.
+  disconnect(disconnectMessage?: string): Promise<void>;
+
+  // Returns true if we are able to connect to the target without interfering
+  // with other processes. For example, for adb devices connected over WebUSB,
+  // this will be false when we can not claim the interface (Which most likely
+  // means that 'adb server' is running locally.). After querrying this method,
+  // the caller can decide if they want to connect to the target and as a side
+  // effect take the connection away from other processes.
+  canConnectWithoutContention(): Promise<boolean>;
+
+  // Whether the recording target can be used in a tracing session. For example,
+  // virtual targets do not support a tracing session.
+  canCreateTracingSession(recordingMode?: string): boolean;
+
+  // Some target information can only be obtained after connecting to the
+  // target. This will establish a connection and retrieve data such as
+  // dataSources and apiLevel for Android.
+  fetchTargetInfo(
+    tracingSessionListener: TracingSessionListener,
+  ): Promise<void>;
+
+  createTracingSession(
+    tracingSessionListener: TracingSessionListener,
+  ): Promise<TracingSession>;
+}
+
+// TracingSession is used by the UI to record a trace. Depending on user
+// actions, the UI can start/stop/cancel a session. During the recording, it
+// provides updates about buffer usage. It is subclassed by
+// TracedTracingSession, which manages the communication with traced and has
+// logic for encoding/decoding Perfetto client requests/replies.
+export interface TracingSession {
+  // Starts the tracing session.
+  start(config: TraceConfig): void;
+
+  // Will stop the tracing session and NOT return any trace.
+  cancel(): void;
+
+  // Will stop the tracing session. The implementing class may also return
+  // the trace using a callback.
+  stop(): void;
+
+  // Returns the percentage of the trace buffer that is currently being
+  // occupied.
+  getTraceBufferUsage(): Promise<number>;
+}
+
+// Connection with an Adb device. Implementations will have logic specific to
+// the connection protocol used(Ex: WebSocket, WebUsb).
+export interface AdbConnection {
+  // Will push a binary to a given path.
+  push(binary: ArrayBuffer, path: string): Promise<void>;
+
+  // Will issue a shell command to the device.
+  shell(cmd: string): Promise<ByteStream>;
+
+  // Will establish a connection(a ByteStream) with the device.
+  connectSocket(path: string): Promise<ByteStream>;
+
+  // Returns true if we are able to connect without interfering
+  // with other processes. For example, for adb devices connected over WebUSB,
+  // this will be false when we can not claim the interface (Which most likely
+  // means that 'adb server' is running locally.).
+  canConnectWithoutContention(): Promise<boolean>;
+
+  // Ends the connection.
+  disconnect(disconnectMessage?: string): Promise<void>;
+}
+
+// A stream for a connection between a target and a tracing session.
+export interface ByteStream {
+  // The caller can add callbacks, to be executed when the stream receives new
+  // data or when it finished closing itself.
+  addOnStreamDataCallback(onStreamData: OnStreamDataCallback): void;
+  addOnStreamCloseCallback(onStreamClose: OnStreamCloseCallback): void;
+
+  isConnected(): boolean;
+  write(data: string | Uint8Array): void;
+
+  close(): void;
+  closeAndWaitForTeardown(): Promise<void>;
+}
+
+// Handles binary messages received over the ByteStream.
+export interface OnStreamDataCallback {
+  (data: Uint8Array): void;
+}
+
+// Called when the ByteStream is closed.
+export interface OnStreamCloseCallback {
+  (): void;
+}
+
+// OnTraceDataCallback will return the entire trace when it has been fully
+// assembled. This will be changed in the following CL aosp/2057640.
+export interface OnTraceDataCallback {
+  (trace: Uint8Array): void;
+}
+
+// Handles messages that are useful in the UI and that occur at any layer of the
+// recording (trace, connection). The messages includes both status messages and
+// error messages.
+export interface OnMessageCallback {
+  (message: string): void;
+}
+
+// Handles the loss of the connection at the connection layer (used by the
+// AdbConnection).
+export interface OnDisconnectCallback {
+  (errorMessage?: string): void;
+}
+
+// Called when there is a change of targets or within a target.
+// For instance, it's used when an Adb device becomes connected/disconnected.
+// It's also executed by a target when the information it stores gets updated.
+export interface OnTargetChangeCallback {
+  (): void;
+}
+
+// A collection of callbacks that is passed to RecordingTargetV2 and
+// subsequently to TracingSession. The callbacks are decided by the UI, so the
+// recording code is not coupled with the rendering logic.
+export interface TracingSessionListener {
+  onTraceData: OnTraceDataCallback;
+  onStatus: OnMessageCallback;
+  onDisconnect: OnDisconnectCallback;
+  onError: OnMessageCallback;
+}
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
new file mode 100644
index 0000000..76617d5
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
@@ -0,0 +1,577 @@
+// 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, 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,
+  TargetInfo,
+  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 {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
+// states:
+// a) because of a user actions - pressing a UI button ('Start', 'Stop',
+//    'Cancel', 'Force reset' of the target), selecting a different target in
+//    the UI, authorizing authentication on an Android device,
+//    pulling the cable which connects an Android device.
+// b) automatically - if there is no need to reset the device or if the user
+//    has previously authorised the device to be debugged via USB.
+//
+// Recording state machine: https://screenshot.googleplex.com/BaX5EGqQMajgV7G
+export enum RecordingState {
+  NO_TARGET = 0,
+  TARGET_SELECTED = 1,
+  // P1 stands for 'Part 1', where we first connect to the device in order to
+  // obtain target information.
+  ASK_TO_FORCE_P1 = 2,
+  AUTH_P1 = 3,
+  TARGET_INFO_DISPLAYED = 4,
+  // P2 stands for 'Part 2', where we connect to device for the 2nd+ times, to
+  // record a tracing session.
+  ASK_TO_FORCE_P2 = 5,
+  AUTH_P2 = 6,
+  RECORDING = 7,
+  WAITING_FOR_TRACE_DISPLAY = 8,
+}
+
+// Wraps a tracing session promise while the promise is being resolved (e.g.
+// while we are awaiting for ADB auth).
+class TracingSessionWrapper {
+  private tracingSession?: TracingSession = undefined;
+  private isCancelled = false;
+  // We only execute the logic in the callbacks if this TracingSessionWrapper
+  // is the one referenced by the controller. Otherwise this can hold a
+  // tracing session which the user has already cancelled, so it shouldn't
+  // influence the UI.
+  private tracingSessionListener: TracingSessionListener = {
+    onTraceData: (trace: Uint8Array) =>
+      this.controller.maybeOnTraceData(this, trace),
+    onStatus: (message) => this.controller.maybeOnStatus(this, message),
+    onDisconnect: (errorMessage?: string) =>
+      this.controller.maybeOnDisconnect(this, errorMessage),
+    onError: (errorMessage: string) =>
+      this.controller.maybeOnError(this, errorMessage),
+  };
+
+  private target: RecordingTargetV2;
+  private controller: RecordingPageController;
+
+  constructor(target: RecordingTargetV2, controller: RecordingPageController) {
+    this.target = target;
+    this.controller = controller;
+  }
+
+  async start(traceConfig: TraceConfig) {
+    let stateGeneratioNr = this.controller.getStateGeneration();
+    const createSession = async () => {
+      try {
+        this.controller.maybeSetState(
+          this,
+          RecordingState.AUTH_P2,
+          stateGeneratioNr,
+        );
+        stateGeneratioNr += 1;
+
+        const session = await this.target.createTracingSession(
+          this.tracingSessionListener,
+        );
+
+        // We check the `isCancelled` to see if the user has cancelled the
+        // tracing session before it becomes available in TracingSessionWrapper.
+        if (this.isCancelled) {
+          session.cancel();
+          return;
+        }
+
+        this.tracingSession = session;
+        this.controller.maybeSetState(
+          this,
+          RecordingState.RECORDING,
+          stateGeneratioNr,
+        );
+        // When the session is resolved, the traceConfig has been instantiated.
+        this.tracingSession.start(assertExists(traceConfig));
+      } catch (e) {
+        this.tracingSessionListener.onError(e.message);
+      }
+    };
+
+    if (await this.target.canConnectWithoutContention()) {
+      await createSession();
+    } else {
+      // If we need to reset the connection to be able to connect, we ask
+      // the user if they want to reset the connection.
+      this.controller.maybeSetState(
+        this,
+        RecordingState.ASK_TO_FORCE_P2,
+        stateGeneratioNr,
+      );
+      stateGeneratioNr += 1;
+      couldNotClaimInterface(createSession, () =>
+        this.controller.maybeClearRecordingState(this),
+      );
+    }
+  }
+
+  async fetchTargetInfo() {
+    let stateGeneratioNr = this.controller.getStateGeneration();
+    const createSession = async () => {
+      try {
+        this.controller.maybeSetState(
+          this,
+          RecordingState.AUTH_P1,
+          stateGeneratioNr,
+        );
+        stateGeneratioNr += 1;
+        await this.target.fetchTargetInfo(this.tracingSessionListener);
+        this.controller.maybeSetState(
+          this,
+          RecordingState.TARGET_INFO_DISPLAYED,
+          stateGeneratioNr,
+        );
+      } catch (e) {
+        this.tracingSessionListener.onError(e.message);
+      }
+    };
+
+    if (await this.target.canConnectWithoutContention()) {
+      await createSession();
+    } else {
+      // If we need to reset the connection to be able to connect, we ask
+      // the user if they want to reset the connection.
+      this.controller.maybeSetState(
+        this,
+        RecordingState.ASK_TO_FORCE_P1,
+        stateGeneratioNr,
+      );
+      stateGeneratioNr += 1;
+      couldNotClaimInterface(createSession, () =>
+        this.controller.maybeSetState(
+          this,
+          RecordingState.TARGET_SELECTED,
+          stateGeneratioNr,
+        ),
+      );
+    }
+  }
+
+  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
+// user actions such as starting/stopping/cancelling a tracing session.
+export class RecordingPageController {
+  private app: App;
+  private recMgr: RecordingManager;
+
+  // State of the recording page. This is set by user actions and/or automatic
+  // transitions. This is queried by the UI in order to
+  private state: RecordingState = RecordingState.NO_TARGET;
+  // Currently selected target.
+  private target?: RecordingTargetV2 = undefined;
+  // We wrap the tracing session in an object, because for some targets
+  // (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;
+
+  constructor(app: App, recMgr: RecordingManager) {
+    this.app = app;
+    this.recMgr = recMgr;
+  }
+
+  getBufferUsagePercentage(): number {
+    return this.bufferUsagePercentage;
+  }
+
+  getState(): RecordingState {
+    return this.state;
+  }
+
+  getStateGeneration(): number {
+    return this.stateGeneration;
+  }
+
+  maybeSetState(
+    tracingSessionWrapper: TracingSessionWrapper,
+    state: RecordingState,
+    stateGeneration: number,
+  ): void {
+    if (this.tracingSessionWrapper !== tracingSessionWrapper) {
+      return;
+    }
+    if (stateGeneration !== this.stateGeneration) {
+      throw new RecordingError('Recording page state transition out of order.');
+    }
+    this.setState(state);
+    this.recMgr.setRecordingStatus(undefined);
+    scheduleFullRedraw();
+  }
+
+  maybeClearRecordingState(tracingSessionWrapper: TracingSessionWrapper): void {
+    if (this.tracingSessionWrapper === tracingSessionWrapper) {
+      this.clearRecordingState();
+    }
+  }
+
+  maybeOnTraceData(
+    tracingSessionWrapper: TracingSessionWrapper,
+    trace: Uint8Array,
+  ) {
+    if (this.tracingSessionWrapper !== tracingSessionWrapper) {
+      return;
+    }
+    this.app.openTraceFromBuffer({
+      title: 'Recorded trace',
+      buffer: trace.buffer,
+      fileName: `trace_${currentDateHourAndMinute()}${TRACE_SUFFIX}`,
+    });
+    this.clearRecordingState();
+  }
+
+  maybeOnStatus(tracingSessionWrapper: TracingSessionWrapper, message: string) {
+    if (this.tracingSessionWrapper !== tracingSessionWrapper) {
+      return;
+    }
+    // For the 'Recording in progress for 7000ms we don't show a
+    // modal.'
+    if (message.startsWith(RECORDING_IN_PROGRESS)) {
+      this.recMgr.setRecordingStatus(message);
+    } else {
+      // For messages such as 'Please allow USB debugging on your
+      // device, which require a user action, we show a modal.
+      showRecordingModal(message);
+    }
+  }
+
+  maybeOnDisconnect(
+    tracingSessionWrapper: TracingSessionWrapper,
+    errorMessage?: string,
+  ) {
+    if (this.tracingSessionWrapper !== tracingSessionWrapper) {
+      return;
+    }
+    if (errorMessage) {
+      showRecordingModal(errorMessage);
+    }
+    this.clearRecordingState();
+    this.onTargetChange();
+  }
+
+  maybeOnError(
+    tracingSessionWrapper: TracingSessionWrapper,
+    errorMessage: string,
+  ) {
+    if (this.tracingSessionWrapper !== tracingSessionWrapper) {
+      return;
+    }
+    showRecordingModal(errorMessage);
+    this.clearRecordingState();
+  }
+
+  getTargetInfo(): TargetInfo | undefined {
+    if (!this.target) {
+      return undefined;
+    }
+    return this.target.getInfo();
+  }
+
+  canCreateTracingSession() {
+    if (!this.target) {
+      return false;
+    }
+    return this.target.canCreateTracingSession();
+  }
+
+  selectTarget(selectedTarget?: RecordingTargetV2) {
+    assertTrue(
+      RecordingState.NO_TARGET <= this.state &&
+        this.state < RecordingState.RECORDING,
+    );
+    // If the selected target exists and is the same as the previous one, we
+    // don't need to do anything.
+    if (selectedTarget && selectedTarget === this.target) {
+      return;
+    }
+
+    // We assign the new target and redraw the page.
+    this.target = selectedTarget;
+
+    if (!this.target) {
+      this.setState(RecordingState.NO_TARGET);
+      scheduleFullRedraw();
+      return;
+    }
+    this.setState(RecordingState.TARGET_SELECTED);
+    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();
+      return;
+    }
+    // If the change happens to a new target or the controller does not have a
+    // defined target, the selection process again is run again.
+    this.selectTarget();
+  }
+
+  private createTracingSessionWrapper(
+    target: RecordingTargetV2,
+  ): TracingSessionWrapper {
+    return new TracingSessionWrapper(target, this);
+  }
+
+  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();
+  }
+
+  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/common/recordingV2/recording_utils.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_utils.ts
similarity index 100%
rename from ui/src/common/recordingV2/recording_utils.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_utils.ts
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
new file mode 100644
index 0000000..03cda1f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts
@@ -0,0 +1,268 @@
+// 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/common/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
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/android_websocket_target_factory_unittest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory_unittest.ts
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
new file mode 100644
index 0000000..a969c31
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts
@@ -0,0 +1,155 @@
+// 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/common/recordingV2/target_factories/chrome_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/chrome_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory.ts
diff --git a/ui/src/common/recordingV2/target_factories/chrome_target_factory_unittest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory_unittest.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/chrome_target_factory_unittest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory_unittest.ts
diff --git a/ui/src/common/recordingV2/target_factories/host_os_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/host_os_target_factory.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/host_os_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/host_os_target_factory.ts
diff --git a/ui/src/common/recordingV2/target_factories/index.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/index.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/index.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/index.ts
diff --git a/ui/src/common/recordingV2/target_factories/virtual_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/virtual_target_factory.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/virtual_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/virtual_target_factory.ts
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factory_registry.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factory_registry.ts
new file mode 100644
index 0000000..b34070d
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factory_registry.ts
@@ -0,0 +1,46 @@
+// 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 {Registry} from '../../../base/registry';
+import {RecordingTargetV2, TargetFactory} from './recording_interfaces_v2';
+
+export class TargetFactoryRegistry extends Registry<TargetFactory> {
+  listTargets(): RecordingTargetV2[] {
+    const targets: RecordingTargetV2[] = [];
+    for (const factory of this.registry.values()) {
+      for (const target of factory.listTargets()) {
+        targets.push(target);
+      }
+    }
+    return targets;
+  }
+
+  listTargetFactories(): TargetFactory[] {
+    return Array.from(this.registry.values());
+  }
+
+  listRecordingProblems(): string[] {
+    const recordingProblems: string[] = [];
+    for (const factory of this.registry.values()) {
+      for (const recordingProblem of factory.listRecordingProblems()) {
+        recordingProblems.push(recordingProblem);
+      }
+    }
+    return recordingProblems;
+  }
+}
+
+export const targetFactoryRegistry = new TargetFactoryRegistry((f) => {
+  return f.kind;
+});
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
new file mode 100644
index 0000000..0bac1e4
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts
@@ -0,0 +1,170 @@
+// 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/common/recordingV2/targets/android_virtual_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_virtual_target.ts
similarity index 100%
rename from ui/src/common/recordingV2/targets/android_virtual_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_virtual_target.ts
diff --git a/ui/src/common/recordingV2/targets/android_websocket_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_websocket_target.ts
similarity index 100%
rename from ui/src/common/recordingV2/targets/android_websocket_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_websocket_target.ts
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
new file mode 100644
index 0000000..dc6e64d
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts
@@ -0,0 +1,44 @@
+// 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/common/recordingV2/targets/chrome_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/chrome_target.ts
similarity index 100%
rename from ui/src/common/recordingV2/targets/chrome_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/chrome_target.ts
diff --git a/ui/src/common/recordingV2/targets/host_os_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/host_os_target.ts
similarity index 100%
rename from ui/src/common/recordingV2/targets/host_os_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/host_os_target.ts
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
new file mode 100644
index 0000000..8687432
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts
@@ -0,0 +1,439 @@
+// 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
new file mode 100644
index 0000000..2da8f5b
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts
@@ -0,0 +1,74 @@
+// 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
new file mode 100644
index 0000000..be29691
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
@@ -0,0 +1,233 @@
+// 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 {createEmptyState} from './empty_state';
+import {
+  AdbRecordingTarget,
+  LoadedConfig,
+  RecordingState,
+  RecordingTarget,
+  getDefaultRecordingTargets,
+  isAdbTarget,
+} from './state';
+import {AdbOverWebUsb} from './adb';
+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';
+
+// TODO(primiano): this class and RecordController should be merged. I'm keeping
+// them separate for now to reduce scope of refactorings.
+export class RecordingManager {
+  readonly app: App;
+  private _state: RecordingState = createEmptyState();
+  private recCtl: RecordController;
+
+  constructor(app: App, useRecordingV2: boolean) {
+    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');
+      }
+    }
+  }
+
+  clearRecordConfig(): void {
+    this._state.recordConfig = createEmptyRecordConfig();
+    this._state.lastLoadedConfig = {type: 'NONE'};
+    this.recCtl.refreshOnStateChange();
+  }
+
+  setRecordConfig(config: RecordConfig, configType?: LoadedConfig): void {
+    this._state.recordConfig = config;
+    this._state.lastLoadedConfig = configType || {type: 'NONE'};
+    this.recCtl.refreshOnStateChange();
+  }
+
+  startRecording(): void {
+    this._state.recordingInProgress = true;
+    this._state.lastRecordingError = undefined;
+    this._state.recordingCancelled = false;
+    this.recCtl.refreshOnStateChange();
+  }
+
+  stopRecording(): void {
+    this._state.recordingInProgress = false;
+    this.recCtl.refreshOnStateChange();
+  }
+
+  cancelRecording(): void {
+    this._state.recordingInProgress = false;
+    this._state.recordingCancelled = true;
+    this.recCtl.refreshOnStateChange();
+  }
+
+  setRecordingTarget(target: RecordingTarget): void {
+    this._state.recordingTarget = target;
+    this.recCtl.refreshOnStateChange();
+  }
+
+  setFetchChromeCategories(fetch: boolean): void {
+    this._state.fetchChromeCategories = fetch;
+    this.recCtl.refreshOnStateChange();
+  }
+
+  setAvailableAdbDevices(devices: AdbRecordingTarget[]): void {
+    this._state.availableAdbDevices = devices;
+    this.recCtl.refreshOnStateChange();
+  }
+
+  setLastRecordingError(error?: string): void {
+    this._state.lastRecordingError = error;
+    this._state.recordingStatus = undefined;
+    this.recCtl.refreshOnStateChange();
+  }
+
+  setRecordingStatus(status?: string): void {
+    this._state.recordingStatus = status;
+    this._state.lastRecordingError = undefined;
+    this.recCtl.refreshOnStateChange();
+  }
+
+  get state() {
+    return this._state;
+  }
+
+  private setupExtentionPort(extensionLocalChannel: MessageChannel) {
+    // We proxy messages between the extension and the controller because the
+    // controller's worker can't access chrome.runtime.
+    const extensionPort =
+      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+      window.chrome && chrome.runtime
+        ? chrome.runtime.connect(EXTENSION_ID)
+        : undefined;
+
+    this._state.extensionInstalled = extensionPort !== undefined;
+
+    if (extensionPort) {
+      // Send messages to keep-alive the extension port.
+      const interval = setInterval(() => {
+        extensionPort.postMessage({
+          method: 'ExtensionVersion',
+        });
+      }, 25000);
+      extensionPort.onDisconnect.addListener((_) => {
+        this._state.extensionInstalled = false;
+        clearInterval(interval);
+        void chrome.runtime.lastError; // Needed to not receive an error log.
+      });
+      // This forwards the messages from the extension to the controller.
+      extensionPort.onMessage.addListener(
+        (message: object, _port: chrome.runtime.Port) => {
+          if (isGetCategoriesResponse(message)) {
+            this._state.chromeCategories = message.categories;
+            scheduleFullRedraw();
+            return;
+          }
+          extensionLocalChannel.port2.postMessage(message);
+        },
+      );
+    }
+
+    // This forwards the messages from the controller to the extension
+    extensionLocalChannel.port2.onmessage = ({data}) => {
+      if (extensionPort) extensionPort.postMessage(data);
+    };
+  }
+
+  async updateAvailableAdbDevices(preferredDeviceSerial?: string) {
+    const devices = await new AdbOverWebUsb().getPairedDevices();
+
+    let recordingTarget: AdbRecordingTarget | undefined = undefined;
+
+    const availableAdbDevices: AdbRecordingTarget[] = [];
+    devices.forEach((d) => {
+      if (d.productName && d.serialNumber) {
+        availableAdbDevices.push({
+          name: d.productName,
+          serial: d.serialNumber,
+          os: 'S',
+        });
+        if (preferredDeviceSerial && preferredDeviceSerial === d.serialNumber) {
+          recordingTarget = availableAdbDevices[availableAdbDevices.length - 1];
+        }
+      }
+    });
+
+    this.setAvailableAdbDevices(availableAdbDevices);
+    this.selectAndroidDeviceIfAvailable(availableAdbDevices, recordingTarget);
+    scheduleFullRedraw();
+    return availableAdbDevices;
+  }
+
+  private selectAndroidDeviceIfAvailable(
+    availableAdbDevices: AdbRecordingTarget[],
+    recordingTarget?: RecordingTarget,
+  ) {
+    if (!recordingTarget) {
+      recordingTarget = this.state.recordingTarget;
+    }
+    const deviceConnected = isAdbTarget(recordingTarget);
+    const connectedDeviceDisconnected =
+      deviceConnected &&
+      availableAdbDevices.find(
+        (e) => e.serial === (recordingTarget as AdbRecordingTarget).serial,
+      ) === undefined;
+
+    if (availableAdbDevices.length) {
+      // If there's an Android device available and the current selection isn't
+      // one, select the Android device by default. If the current device isn't
+      // available anymore, but another Android device is, select the other
+      // Android device instead.
+      if (!deviceConnected || connectedDeviceDisconnected) {
+        recordingTarget = availableAdbDevices[0];
+      }
+
+      this.setRecordingTarget(recordingTarget);
+      return;
+    }
+
+    // If the currently selected device was disconnected, reset the recording
+    // target to the default one.
+    if (connectedDeviceDisconnected) {
+      this.setRecordingTarget(getDefaultRecordingTargets()[0]);
+    }
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
new file mode 100644
index 0000000..0e34f5c
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
@@ -0,0 +1,115 @@
+// 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/recording_sections.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_sections.ts
new file mode 100644
index 0000000..c83b9e0
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_sections.ts
@@ -0,0 +1,24 @@
+// 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 {DataSource} from './recordingV2/recording_interfaces_v2';
+import {RecordingState} from './state';
+
+export interface RecordingSectionAttrs {
+  recState: RecordingState;
+  dataSources: DataSource[];
+  cssClass: string;
+}
+
+export const POLL_INTERVAL_MS = [250, 500, 1000, 2500, 5000, 30000, 60000];
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recording_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_settings.ts
new file mode 100644
index 0000000..e3058be
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_settings.ts
@@ -0,0 +1,100 @@
+// 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 {RecordMode} from './state';
+import {Slider} from './record_widgets';
+import {RecordingSectionAttrs} from './recording_sections';
+import {assetSrc} from '../../base/assets';
+
+export class RecordingSettings
+  implements m.ClassComponent<RecordingSectionAttrs>
+{
+  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
+    const S = (x: number) => x * 1000;
+    const M = (x: number) => x * 1000 * 60;
+    const H = (x: number) => x * 1000 * 60 * 60;
+
+    const recCfg = attrs.recState.recordConfig;
+
+    const recButton = (mode: RecordMode, title: string, img: string) => {
+      const checkboxArgs = {
+        checked: recCfg.mode === mode,
+        onchange: (e: InputEvent) => {
+          const checked = (e.target as HTMLInputElement).checked;
+          if (!checked) return;
+          recCfg.mode = mode;
+        },
+      };
+      return m(
+        `label${recCfg.mode === mode ? '.selected' : ''}`,
+        m(`input[type=radio][name=rec_mode]`, checkboxArgs),
+        m(`img[src=${assetSrc(`assets/${img}`)}]`),
+        m('span', title),
+      );
+    };
+
+    return m(
+      `.record-section${attrs.cssClass}`,
+      m('header', 'Recording mode'),
+      m(
+        '.record-mode',
+        recButton('STOP_WHEN_FULL', 'Stop when full', 'rec_one_shot.png'),
+        recButton('RING_BUFFER', 'Ring buffer', 'rec_ring_buf.png'),
+        recButton('LONG_TRACE', 'Long trace', 'rec_long_trace.png'),
+      ),
+
+      m(Slider, {
+        title: 'In-memory buffer size',
+        icon: '360',
+        values: [4, 8, 16, 32, 64, 128, 256, 512],
+        unit: 'MB',
+        set: (cfg, val) => (cfg.bufferSizeMb = val),
+        get: (cfg) => cfg.bufferSizeMb,
+        recCfg,
+      }),
+
+      m(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)],
+        isTime: true,
+        unit: 'h:m:s',
+        set: (cfg, val) => (cfg.durationMs = val),
+        get: (cfg) => cfg.durationMs,
+        recCfg,
+      }),
+      m(Slider, {
+        title: 'Max file size',
+        icon: 'save',
+        cssClass: recCfg.mode !== 'LONG_TRACE' ? '.hide' : '',
+        values: [5, 25, 50, 100, 500, 1000, 1000 * 5, 1000 * 10],
+        unit: 'MB',
+        set: (cfg, val) => (cfg.maxFileSizeMb = val),
+        get: (cfg) => cfg.maxFileSizeMb,
+        recCfg,
+      }),
+      m(Slider, {
+        title: 'Flush on disk every',
+        cssClass: recCfg.mode !== 'LONG_TRACE' ? '.hide' : '',
+        icon: 'av_timer',
+        values: [100, 250, 500, 1000, 2500, 5000],
+        unit: 'ms',
+        set: (cfg, val) => (cfg.fileWritePeriodMs = val),
+        get: (cfg) => cfg.fileWritePeriodMs || 0,
+        recCfg,
+      }),
+    );
+  }
+}
diff --git a/ui/src/frontend/recording/recording_ui_utils.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_ui_utils.ts
similarity index 100%
rename from ui/src/frontend/recording/recording_ui_utils.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_ui_utils.ts
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/reset_interface_modal.ts b/ui/src/plugins/dev.perfetto.RecordTrace/reset_interface_modal.ts
new file mode 100644
index 0000000..e612e32
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/reset_interface_modal.ts
@@ -0,0 +1,69 @@
+// 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 {showModal} from '../../widgets/modal';
+import {FORCE_RESET_MESSAGE} from './recording_ui_utils';
+
+export function couldNotClaimInterface(
+  onReset: () => Promise<void>,
+  onCancel: () => void,
+) {
+  let hasPressedAButton = false;
+  showModal({
+    title: 'Could not claim the USB interface',
+    content: m(
+      'div',
+      m(
+        'text',
+        'This can happen if you have the Android Debug Bridge ' +
+          '(adb) running on your workstation or any other tool which is ' +
+          'taking exclusive access of the USB interface.',
+      ),
+      m('br'),
+      m('br'),
+      m(
+        'text.small-font',
+        'Resetting will cause the ADB server to disconnect and ' +
+          'will try to reassign the interface to the current browser.',
+      ),
+    ),
+    buttons: [
+      {
+        text: FORCE_RESET_MESSAGE,
+        primary: true,
+        id: 'force_USB_interface',
+        action: () => {
+          hasPressedAButton = true;
+          onReset();
+        },
+      },
+      {
+        text: 'Cancel',
+        primary: false,
+        id: 'cancel_USB_interface',
+        action: () => {
+          hasPressedAButton = true;
+          onCancel();
+        },
+      },
+    ],
+  }).then(() => {
+    // If the user has clicked away from the modal, we interpret that as a
+    // 'Cancel'.
+    if (!hasPressedAButton) {
+      onCancel();
+    }
+  });
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts b/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
new file mode 100644
index 0000000..4d3d048
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
@@ -0,0 +1,186 @@
+// 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.RecordTrace/state.ts b/ui/src/plugins/dev.perfetto.RecordTrace/state.ts
new file mode 100644
index 0000000..b94074b
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/state.ts
@@ -0,0 +1,438 @@
+// 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 {RecordConfig} from './record_config_types';
+
+export const MAX_TIME = 180;
+
+export interface RecordingTarget {
+  name: string;
+  os: TargetOs;
+}
+
+export interface AdbRecordingTarget extends RecordingTarget {
+  serial: string;
+}
+
+export interface LoadedConfigNone {
+  type: 'NONE';
+}
+
+export interface LoadedConfigAutomatic {
+  type: 'AUTOMATIC';
+}
+
+export interface LoadedConfigNamed {
+  type: 'NAMED';
+  name: string;
+}
+
+export type LoadedConfig =
+  | LoadedConfigNone
+  | LoadedConfigAutomatic
+  | LoadedConfigNamed;
+
+export interface RecordCommand {
+  commandline: string;
+  pbtxt: string;
+  pbBase64: string;
+}
+
+export interface RecordingState {
+  /**
+   * State of the ConfigEditor.
+   */
+  recordConfig: RecordConfig;
+  lastLoadedConfig: LoadedConfig;
+
+  /**
+   * Trace recording
+   */
+  recordingInProgress: boolean;
+  recordingCancelled: boolean;
+  extensionInstalled: boolean;
+  recordingTarget: RecordingTarget;
+  availableAdbDevices: AdbRecordingTarget[];
+  lastRecordingError?: string;
+  recordingStatus?: string;
+
+  fetchChromeCategories: boolean;
+  chromeCategories: string[] | undefined;
+
+  bufferUsage: number;
+  recordingLog: string;
+  recordCmd?: RecordCommand;
+}
+
+export declare type RecordMode =
+  | 'STOP_WHEN_FULL'
+  | 'RING_BUFFER'
+  | 'LONG_TRACE';
+
+// 'Q','P','O' for Android, 'L' for Linux, 'C' for Chrome.
+export declare type TargetOs =
+  | 'S'
+  | 'R'
+  | 'Q'
+  | 'P'
+  | 'O'
+  | 'C'
+  | 'L'
+  | 'CrOS'
+  | 'Win';
+
+export function isAndroidP(target: RecordingTarget) {
+  return target.os === 'P';
+}
+
+export function isAndroidTarget(target: RecordingTarget) {
+  return ['Q', 'P', 'O', 'S'].includes(target.os);
+}
+
+export function isChromeTarget(target: RecordingTarget) {
+  return ['C', 'CrOS'].includes(target.os);
+}
+
+export function isCrOSTarget(target: RecordingTarget) {
+  return target.os === 'CrOS';
+}
+
+export function isLinuxTarget(target: RecordingTarget) {
+  return target.os === 'L';
+}
+
+export function isWindowsTarget(target: RecordingTarget) {
+  return target.os === 'Win';
+}
+
+export function isAdbTarget(
+  target: RecordingTarget,
+): target is AdbRecordingTarget {
+  return !!(target as AdbRecordingTarget).serial;
+}
+
+export function hasActiveProbes(config: RecordConfig) {
+  const fieldsWithEmptyResult = new Set<string>([
+    'hpBlockClient',
+    'allAtraceApps',
+    'chromePrivacyFiltering',
+  ]);
+  let key: keyof RecordConfig;
+  for (key in config) {
+    if (
+      typeof config[key] === 'boolean' &&
+      config[key] === true &&
+      !fieldsWithEmptyResult.has(key)
+    ) {
+      return true;
+    }
+  }
+  if (config.chromeCategoriesSelected.length > 0) {
+    return true;
+  }
+  return config.chromeHighOverheadCategoriesSelected.length > 0;
+}
+
+export function getDefaultRecordingTargets(): RecordingTarget[] {
+  return [
+    {os: 'Q', name: 'Android Q+ / 10+'},
+    {os: 'P', name: 'Android P / 9'},
+    {os: 'O', name: 'Android O- / 8-'},
+    {os: 'C', name: 'Chrome'},
+    {os: 'CrOS', name: 'Chrome OS (system trace)'},
+    {os: 'L', name: 'Linux desktop'},
+    {os: 'Win', name: 'Windows desktop'},
+  ];
+}
+
+export function getBuiltinChromeCategoryList(): string[] {
+  // List of static Chrome categories, last updated at 2024-05-15 from HEAD of
+  // Chromium's //base/trace_event/builtin_categories.h.
+  return [
+    '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.RecordTrace/trace_config_utils.ts b/ui/src/plugins/dev.perfetto.RecordTrace/trace_config_utils.ts
new file mode 100644
index 0000000..c7697dd
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/trace_config_utils.ts
@@ -0,0 +1,68 @@
+// Copyright (C) 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
+//
+//      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 {EnableTracingRequest, TraceConfig} from '../../protos';
+
+// In this file are contained a few functions to simplify the proto parsing.
+
+export function extractTraceConfig(
+  enableTracingRequest: Uint8Array,
+): Uint8Array | undefined {
+  try {
+    const enableTracingObject =
+      EnableTracingRequest.decode(enableTracingRequest);
+    if (!enableTracingObject.traceConfig) return undefined;
+    return TraceConfig.encode(enableTracingObject.traceConfig).finish();
+  } catch (e) {
+    // This catch is for possible proto encoding/decoding issues.
+    console.error('Error extracting the config: ', e.message);
+    return undefined;
+  }
+}
+
+export function extractDurationFromTraceConfig(traceConfigProto: Uint8Array) {
+  try {
+    return TraceConfig.decode(traceConfigProto).durationMs;
+  } catch (e) {
+    // This catch is for possible proto encoding/decoding issues.
+    return undefined;
+  }
+}
+
+export function browserSupportsPerfettoConfig(): boolean {
+  const minimumChromeVersion = '91.0.4448.0';
+  const runningVersion = String(
+    (/Chrome\/(([0-9]+\.?){4})/.exec(navigator.userAgent) || [, 0])[1],
+  );
+
+  if (!runningVersion) return false;
+
+  const minVerArray = minimumChromeVersion.split('.').map(Number);
+  const runVerArray = runningVersion.split('.').map(Number);
+
+  for (let index = 0; index < minVerArray.length; index++) {
+    if (runVerArray[index] === minVerArray[index]) continue;
+    return runVerArray[index] > minVerArray[index];
+  }
+  return true; // Exact version match.
+}
+
+export function hasSystemDataSourceConfig(config: TraceConfig): boolean {
+  for (const ds of config.dataSources) {
+    if (!(ds.config?.name ?? '').startsWith('org.chromium.')) {
+      return true;
+    }
+  }
+  return false;
+}
diff --git a/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
index c07a386..b20ecef 100644
--- a/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
+++ b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
@@ -12,13 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-  TrackRef,
-} from '../../public';
+import {TrackNode} from '../../public/workspace';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {TrackDescriptor} from '../../public/track';
 
 const PLUGIN_ID = 'dev.perfetto.RestorePinnedTrack';
 const SAVED_TRACKS_KEY = `${PLUGIN_ID}#savedPerfettoTracks`;
@@ -30,21 +27,20 @@
  * and group name. When no match is found for a saved track, it tries again
  * without numbers.
  */
-class RestorePinnedTrack implements Plugin {
-  onActivate(_ctx: PluginContext): void {}
+export default class implements PerfettoPlugin {
+  static readonly id = PLUGIN_ID;
+  private ctx!: Trace;
 
-  private ctx!: PluginContextTrace;
-
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+  async onTraceLoad(ctx: Trace): Promise<void> {
     this.ctx = ctx;
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: `${PLUGIN_ID}#save`,
       name: 'Save: Pinned tracks',
       callback: () => {
         this.saveTracks();
       },
     });
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: `${PLUGIN_ID}#restore`,
       name: 'Restore: Pinned tracks',
       callback: () => {
@@ -54,71 +50,198 @@
   }
 
   private saveTracks() {
-    const pinnedTracks = this.ctx.timeline.tracks.filter(
-      (trackRef) => trackRef.isPinned,
+    const workspace = this.ctx.workspace;
+    const pinnedTracks = workspace.pinnedTracks;
+    const tracksToSave: SavedPinnedTrack[] = pinnedTracks.map((track) =>
+      this.toSavedTrack(track),
     );
-    const tracksToSave: SavedPinnedTrack[] = pinnedTracks.map((trackRef) => ({
-      groupName: trackRef.groupName,
-      trackName: trackRef.title,
-    }));
-    window.localStorage.setItem(SAVED_TRACKS_KEY, JSON.stringify(tracksToSave));
+
+    this.savedState = {
+      tracks: tracksToSave,
+    };
   }
 
   private restoreTracks() {
-    const savedTracks = window.localStorage.getItem(SAVED_TRACKS_KEY);
-    if (!savedTracks) {
+    const savedState = this.savedState;
+    if (!savedState) {
       alert('No saved tracks. Use the Save command first');
       return;
     }
-    const tracksToRestore: SavedPinnedTrack[] = JSON.parse(savedTracks);
-    const tracks: TrackRef[] = this.ctx.timeline.tracks;
+    const tracksToRestore: SavedPinnedTrack[] = savedState.tracks;
+
+    const localTracks: Array<LocalTrack> = this.ctx.workspace.flatTracks.map(
+      (track) => ({
+        savedTrack: this.toSavedTrack(track),
+        track: track,
+      }),
+    );
+
     tracksToRestore.forEach((trackToRestore) => {
-      // Check for an exact match
-      const exactMatch = tracks.find((track) => {
-        return (
-          track.key &&
-          trackToRestore.trackName === track.title &&
-          trackToRestore.groupName === track.groupName
-        );
-      });
-
-      if (exactMatch) {
-        this.ctx.timeline.pinTrack(exactMatch.key!);
+      const foundTrack = this.findMatchingTrack(localTracks, trackToRestore);
+      if (foundTrack) {
+        foundTrack.pin();
       } else {
-        // We attempt a match after removing numbers to potentially pin a
-        // "similar" track from a different trace. Removing numbers allows
-        // flexibility; for instance, with multiple 'sysui' processes (e.g.
-        // track group name: "com.android.systemui 123") without this approach,
-        // any could be mistakenly pinned. The goal is to restore specific
-        // tracks within the same trace, ensuring that a previously pinned track
-        // is pinned again.
-        // If the specific process with that PID is unavailable, pinning any
-        // other process matching the package name is attempted.
-        const fuzzyMatch = tracks.find((track) => {
-          return (
-            track.key &&
-            this.removeNumbers(trackToRestore.trackName) ===
-              this.removeNumbers(track.title) &&
-            this.removeNumbers(trackToRestore.groupName) ===
-              this.removeNumbers(track.groupName)
-          );
-        });
-
-        if (fuzzyMatch) {
-          this.ctx.timeline.pinTrack(fuzzyMatch.key!);
-        } else {
-          console.warn(
-            '[RestorePinnedTracks] No track found that matches',
-            trackToRestore,
-          );
-        }
+        console.warn(
+          '[RestorePinnedTracks] No track found that matches',
+          trackToRestore,
+        );
       }
     });
   }
 
+  private findMatchingTrack(
+    localTracks: Array<LocalTrack>,
+    savedTrack: SavedPinnedTrack,
+  ): TrackNode | null {
+    let mostSimilarTrack: LocalTrack | null = null;
+    let mostSimilarTrackDifferenceScore: number = 0;
+
+    for (let i = 0; i < localTracks.length; i++) {
+      const localTrack = localTracks[i];
+      const differenceScore = this.calculateSimilarityScore(
+        localTrack.savedTrack,
+        savedTrack,
+      );
+
+      // Return immediately if we found the exact match
+      if (differenceScore === Number.MAX_SAFE_INTEGER) {
+        return localTrack.track;
+      }
+
+      // Ignore too different objects
+      if (differenceScore === 0) {
+        continue;
+      }
+
+      if (differenceScore > mostSimilarTrackDifferenceScore) {
+        mostSimilarTrackDifferenceScore = differenceScore;
+        mostSimilarTrack = localTrack;
+      }
+    }
+
+    return mostSimilarTrack?.track || null;
+  }
+
+  /**
+   * Returns the similarity score where 0 means the objects are completely
+   * different, and the higher the number, the smaller the difference is.
+   * Returns Number.MAX_SAFE_INTEGER if the objects are completely equal.
+   * We attempt a fuzzy match based on the similarity score.
+   * For example, one of the ways we do this is we remove the numbers
+   * from the title to potentially pin a "similar" track from a different trace.
+   * Removing numbers allows flexibility; for instance, with multiple 'sysui'
+   * processes (e.g. track group name: "com.android.systemui 123") without
+   * this approach, any could be mistakenly pinned. The goal is to restore
+   * specific tracks within the same trace, ensuring that a previously pinned
+   * track is pinned again.
+   * If the specific process with that PID is unavailable, pinning any
+   * other process matching the package name is attempted.
+   * @param track1 first saved track to compare
+   * @param track2 second saved track to compare
+   * @private
+   */
+  private calculateSimilarityScore(
+    track1: SavedPinnedTrack,
+    track2: SavedPinnedTrack,
+  ): number {
+    // Return immediately when objects are equal
+    if (
+      track1.trackName === track2.trackName &&
+      track1.groupName === track2.groupName &&
+      track1.pluginId === track2.pluginId &&
+      track1.kind === track2.kind &&
+      track1.isMainThread === track2.isMainThread
+    ) {
+      return Number.MAX_SAFE_INTEGER;
+    }
+
+    let similarityScore = 0;
+    if (track1.trackName === track2.trackName) {
+      similarityScore += 100;
+    } else if (
+      this.removeNumbers(track1.trackName) ===
+      this.removeNumbers(track2.trackName)
+    ) {
+      similarityScore += 50;
+    }
+
+    if (track1.groupName === track2.groupName) {
+      similarityScore += 90;
+    } else if (
+      this.removeNumbers(track1.groupName) ===
+      this.removeNumbers(track2.groupName)
+    ) {
+      similarityScore += 45;
+    }
+
+    // Do not consider other parameters if there is no match in name/group
+    if (similarityScore === 0) return similarityScore;
+
+    if (track1.pluginId === track2.pluginId) {
+      similarityScore += 30;
+    }
+
+    if (track1.kind === track2.kind) {
+      similarityScore += 20;
+    }
+
+    if (track1.isMainThread === track2.isMainThread) {
+      similarityScore += 10;
+    }
+
+    return similarityScore;
+  }
+
   private removeNumbers(inputString?: string): string | undefined {
     return inputString?.replace(/\d+/g, '');
   }
+
+  private toSavedTrack(track: TrackNode): SavedPinnedTrack {
+    let trackDescriptor: TrackDescriptor | undefined = undefined;
+    if (track.uri != null) {
+      trackDescriptor = this.ctx.tracks.getTrack(track.uri);
+    }
+
+    return {
+      groupName: groupName(track),
+      trackName: track.title,
+      pluginId: trackDescriptor?.pluginId,
+      kind: trackDescriptor?.tags?.kind,
+      isMainThread: trackDescriptor?.chips?.includes('main thread') || false,
+    };
+  }
+
+  private get savedState(): SavedState | null {
+    const savedStateString = window.localStorage.getItem(SAVED_TRACKS_KEY);
+    if (!savedStateString) {
+      return null;
+    }
+
+    const savedState: SavedState = JSON.parse(savedStateString);
+    if (!(savedState.tracks instanceof Array)) {
+      return null;
+    }
+
+    return savedState;
+  }
+
+  private set savedState(state: SavedState) {
+    window.localStorage.setItem(SAVED_TRACKS_KEY, JSON.stringify(state));
+  }
+}
+
+// Return the displayname of the containing group
+// If the track is a child of a workspace, return undefined...
+function groupName(track: TrackNode): string | undefined {
+  const parent = track.parent;
+  if (parent instanceof TrackNode) {
+    return parent.title;
+  }
+  return undefined;
+}
+
+interface SavedState {
+  tracks: Array<SavedPinnedTrack>;
 }
 
 interface SavedPinnedTrack {
@@ -127,9 +250,18 @@
 
   // Track name to restore.
   trackName: string;
+
+  // Plugin used to create this track
+  pluginId?: string;
+
+  // Kind of the track
+  kind?: string;
+
+  // If it's a thread track, it should be true in case it's a main thread track
+  isMainThread: boolean;
 }
 
-export const plugin: PluginDescriptor = {
-  pluginId: PLUGIN_ID,
-  plugin: RestorePinnedTrack,
-};
+interface LocalTrack {
+  savedTrack: SavedPinnedTrack;
+  track: TrackNode;
+}
diff --git a/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts b/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts
new file mode 100644
index 0000000..328d386
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts
@@ -0,0 +1,80 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {Icons} from '../../base/semantic_icons';
+import {sqliteString} from '../../base/string_utils';
+import {
+  BaseCounterTrack,
+  CounterOptions,
+} from '../../frontend/base_counter_track';
+import {TrackContext} from '../../public/track';
+import {Button} from '../../widgets/button';
+import {Trace} from '../../public/trace';
+
+export enum CPUType {
+  Big = 'big',
+  Mid = 'mid',
+  Little = 'little',
+}
+
+export class ActiveCPUCountTrack extends BaseCounterTrack {
+  private readonly cpuType?: CPUType;
+
+  constructor(ctx: TrackContext, trace: Trace, cpuType?: CPUType) {
+    super({
+      trace,
+      uri: ctx.trackUri,
+    });
+    this.cpuType = cpuType;
+  }
+
+  getTrackShellButtons(): m.Children {
+    return m(Button, {
+      onclick: () => {
+        this.trace.workspace.findTrackByUri(this.uri)?.remove();
+      },
+      icon: Icons.Close,
+      title: 'Close',
+      compact: true,
+    });
+  }
+
+  protected getDefaultCounterOptions(): CounterOptions {
+    const options = super.getDefaultCounterOptions();
+    options.yRangeRounding = 'strict';
+    options.yRange = 'viewport';
+    return options;
+  }
+
+  async onInit() {
+    await this.engine.query(`
+      INCLUDE PERFETTO MODULE sched.thread_level_parallelism;
+      INCLUDE PERFETTO MODULE android.cpu.cluster_type;
+    `);
+  }
+
+  getSqlSource() {
+    const sourceTable =
+      this.cpuType === undefined
+        ? 'sched_active_cpu_count'
+        : `_active_cpu_count_for_cluster_type(${sqliteString(this.cpuType)})`;
+    return `
+      select
+        ts,
+        active_cpu_count as value
+      from ${sourceTable}
+    `;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Sched/index.ts b/ui/src/plugins/dev.perfetto.Sched/index.ts
new file mode 100644
index 0000000..68d3d0b
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Sched/index.ts
@@ -0,0 +1,122 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {sqlTableRegistry} from '../../frontend/widgets/sql/table/sql_table_registry';
+import {TrackNode} from '../../public/workspace';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {ActiveCPUCountTrack, CPUType} from './active_cpu_count';
+import {
+  RunnableThreadCountTrack,
+  UninterruptibleSleepThreadCountTrack,
+} from './thread_count';
+import {getSchedTable} from './table';
+import {extensions} from '../../public/lib/extensions';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.Sched';
+  async onTraceLoad(ctx: Trace) {
+    const runnableThreadCountUri = `/runnable_thread_count`;
+    ctx.tracks.registerTrack({
+      uri: runnableThreadCountUri,
+      title: 'Runnable thread count',
+      track: new RunnableThreadCountTrack({
+        trace: ctx,
+        uri: runnableThreadCountUri,
+      }),
+    });
+    ctx.commands.registerCommand({
+      id: 'dev.perfetto.Sched.AddRunnableThreadCountTrackCommand',
+      name: 'Add track: runnable thread count',
+      callback: () =>
+        addPinnedTrack(ctx, runnableThreadCountUri, 'Runnable thread count'),
+    });
+
+    const uninterruptibleSleepThreadCountUri = `/uninterruptible_sleep_thread_count`;
+    ctx.tracks.registerTrack({
+      uri: uninterruptibleSleepThreadCountUri,
+      title: 'Uninterruptible Sleep thread count',
+      track: new UninterruptibleSleepThreadCountTrack({
+        trace: ctx,
+        uri: uninterruptibleSleepThreadCountUri,
+      }),
+    });
+    ctx.commands.registerCommand({
+      id: 'dev.perfetto.Sched.AddUninterruptibleSleepThreadCountTrackCommand',
+      name: 'Add track: uninterruptible sleep thread count',
+      callback: () =>
+        addPinnedTrack(
+          ctx,
+          uninterruptibleSleepThreadCountUri,
+          'Uninterruptible Sleep thread count',
+        ),
+    });
+
+    const uri = uriForActiveCPUCountTrack();
+    const title = 'Active CPU count';
+    ctx.tracks.registerTrack({
+      uri,
+      title: title,
+      track: new ActiveCPUCountTrack({trackUri: uri}, ctx),
+    });
+    ctx.commands.registerCommand({
+      id: 'dev.perfetto.Sched.AddActiveCPUCountTrackCommand',
+      name: 'Add track: active CPU count',
+      callback: () => addPinnedTrack(ctx, uri, title),
+    });
+
+    for (const cpuType of Object.values(CPUType)) {
+      const uri = uriForActiveCPUCountTrack(cpuType);
+      const title = `Active ${cpuType} CPU count`;
+      ctx.tracks.registerTrack({
+        uri,
+        title: title,
+        track: new ActiveCPUCountTrack({trackUri: uri}, ctx, cpuType),
+      });
+
+      ctx.commands.registerCommand({
+        id: `dev.perfetto.Sched.AddActiveCPUCountTrackCommand.${cpuType}`,
+        name: `Add track: active ${cpuType} CPU count`,
+        callback: () => addPinnedTrack(ctx, uri, title),
+      });
+    }
+
+    sqlTableRegistry['sched'] = getSchedTable();
+    ctx.commands.registerCommand({
+      id: 'perfetto.ShowTable.sched',
+      name: 'Open table: sched',
+      callback: () => {
+        extensions.addSqlTableTab(ctx, {
+          table: getSchedTable(),
+        });
+      },
+    });
+  }
+}
+
+function uriForActiveCPUCountTrack(cpuType?: CPUType): string {
+  const prefix = `/active_cpus`;
+  if (cpuType !== undefined) {
+    return `${prefix}_${cpuType}`;
+  } else {
+    return prefix;
+  }
+}
+
+function addPinnedTrack(ctx: Trace, uri: string, title: string) {
+  const track = new TrackNode({uri, title});
+  // Add track to the top of the stack
+  ctx.workspace.addChildFirst(track);
+  track.pin();
+}
diff --git a/ui/src/plugins/dev.perfetto.Sched/table.ts b/ui/src/plugins/dev.perfetto.Sched/table.ts
new file mode 100644
index 0000000..9f05315
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Sched/table.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 {SqlTableDescription} from '../../frontend/widgets/sql/table/table_description';
+import {
+  DurationColumn,
+  ProcessColumnSet,
+  SchedIdColumn,
+  StandardColumn,
+  ThreadColumnSet,
+  TimestampColumn,
+} from '../../frontend/widgets/sql/table/well_known_columns';
+
+export function getSchedTable(): SqlTableDescription {
+  return {
+    name: 'sched',
+    columns: [
+      new SchedIdColumn('id'),
+      new TimestampColumn('ts'),
+      new DurationColumn('dur'),
+      new StandardColumn('cpu', {aggregationType: 'nominal'}),
+      new StandardColumn('priority', {aggregationType: 'nominal'}),
+      new ThreadColumnSet('utid', {title: 'utid', notNull: true}),
+      new ProcessColumnSet(
+        {
+          column: 'upid',
+          source: {
+            table: 'thread',
+            joinOn: {
+              utid: 'utid',
+            },
+            innerJoin: true,
+          },
+        },
+        {title: 'upid', notNull: true},
+      ),
+      new StandardColumn('end_state'),
+      new StandardColumn('ucpu', {
+        aggregationType: 'nominal',
+        startsHidden: true,
+      }),
+    ],
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.Sched/thread_count.ts b/ui/src/plugins/dev.perfetto.Sched/thread_count.ts
new file mode 100644
index 0000000..88fe892
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Sched/thread_count.ts
@@ -0,0 +1,60 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+  BaseCounterTrack,
+  CounterOptions,
+} from '../../frontend/base_counter_track';
+import {NewTrackArgs} from '../../frontend/track';
+
+abstract class ThreadCountTrack extends BaseCounterTrack {
+  constructor(args: NewTrackArgs) {
+    super(args);
+  }
+
+  protected getDefaultCounterOptions(): CounterOptions {
+    const options = super.getDefaultCounterOptions();
+    options.yRangeRounding = 'strict';
+    options.yRange = 'viewport';
+    return options;
+  }
+
+  async onInit() {
+    await this.engine.query(
+      `INCLUDE PERFETTO MODULE sched.thread_level_parallelism`,
+    );
+  }
+}
+
+export class RunnableThreadCountTrack extends ThreadCountTrack {
+  getSqlSource() {
+    return `
+      select
+        ts,
+        runnable_thread_count as value
+      from sched_runnable_thread_count
+    `;
+  }
+}
+
+export class UninterruptibleSleepThreadCountTrack extends ThreadCountTrack {
+  getSqlSource() {
+    return `
+      select
+        ts,
+        uninterruptible_sleep_thread_count as value
+      from sched_uninterruptible_sleep_thread_count
+    `;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Screenshots/index.ts b/ui/src/plugins/dev.perfetto.Screenshots/index.ts
new file mode 100644
index 0000000..68c7d64
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Screenshots/index.ts
@@ -0,0 +1,50 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {TrackNode} from '../../public/workspace';
+import {NUM} from '../../trace_processor/query_result';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {ScreenshotsTrack} from './screenshots_track';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.Screenshots';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const res = await ctx.engine.query(`
+      INCLUDE PERFETTO MODULE android.screenshots;
+      select
+        count() as count
+      from android_screenshots
+    `);
+    const {count} = res.firstRow({count: NUM});
+
+    if (count > 0) {
+      const title = 'Screenshots';
+      const uri = '/screenshots';
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        track: new ScreenshotsTrack({
+          trace: ctx,
+          uri,
+        }),
+        tags: {
+          kind: ScreenshotsTrack.kind,
+        },
+      });
+      const trackNode = new TrackNode({uri, title, sortOrder: -60});
+      ctx.workspace.addChildInOrder(trackNode);
+    }
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Screenshots/screenshot_panel.ts b/ui/src/plugins/dev.perfetto.Screenshots/screenshot_panel.ts
new file mode 100644
index 0000000..a5424af
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Screenshots/screenshot_panel.ts
@@ -0,0 +1,52 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {assertTrue} from '../../base/logging';
+import {exists} from '../../base/utils';
+import {getSlice, SliceDetails} from '../../trace_processor/sql_utils/slice';
+import {asSliceSqlId} from '../../trace_processor/sql_utils/core_types';
+import {Engine} from '../../trace_processor/engine';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {TrackEventSelection} from '../../public/selection';
+
+export class ScreenshotDetailsPanel implements TrackEventDetailsPanel {
+  private sliceDetails?: SliceDetails;
+
+  constructor(private readonly engine: Engine) {}
+
+  async load(selection: TrackEventSelection) {
+    this.sliceDetails = await getSlice(
+      this.engine,
+      asSliceSqlId(selection.eventId),
+    );
+  }
+
+  render() {
+    if (
+      !exists(this.sliceDetails) ||
+      !exists(this.sliceDetails.args) ||
+      this.sliceDetails.args.length == 0
+    ) {
+      return m('h2', 'Loading Screenshot');
+    }
+    assertTrue(this.sliceDetails.args[0].key == 'screenshot.jpg_image');
+    return m(
+      '.screenshot-panel',
+      m('img', {
+        src: 'data:image/png;base64, ' + this.sliceDetails.args[0].displayValue,
+      }),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Screenshots/screenshots_track.ts b/ui/src/plugins/dev.perfetto.Screenshots/screenshots_track.ts
new file mode 100644
index 0000000..2364af3
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Screenshots/screenshots_track.ts
@@ -0,0 +1,35 @@
+// 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 {
+  CustomSqlTableDefConfig,
+  CustomSqlTableSliceTrack,
+} from '../../frontend/tracks/custom_sql_table_slice_track';
+import {TrackEventSelection} from '../../public/selection';
+import {ScreenshotDetailsPanel} from './screenshot_panel';
+
+export class ScreenshotsTrack extends CustomSqlTableSliceTrack {
+  static readonly kind = 'dev.perfetto.ScreenshotsTrack';
+
+  getSqlDataSource(): CustomSqlTableDefConfig {
+    return {
+      sqlTableName: 'android_screenshots',
+      columns: ['*'],
+    };
+  }
+
+  override detailsPanel(_sel: TrackEventSelection) {
+    return new ScreenshotDetailsPanel(this.trace.engine);
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Thread/index.ts b/ui/src/plugins/dev.perfetto.Thread/index.ts
new file mode 100644
index 0000000..b57bd9d
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Thread/index.ts
@@ -0,0 +1,80 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {sqlTableRegistry} from '../../frontend/widgets/sql/table/sql_table_registry';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {getThreadTable} from './table';
+import {extensions} from '../../public/lib/extensions';
+import {ThreadDesc, ThreadMap} from '../dev.perfetto.Thread/threads';
+import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
+import {assertExists} from '../../base/logging';
+
+async function listThreads(trace: Trace) {
+  const query = `select
+        utid,
+        tid,
+        pid,
+        ifnull(thread.name, '') as threadName,
+        ifnull(
+          case when length(process.name) > 0 then process.name else null end,
+          thread.name) as procName,
+        process.cmdline as cmdline
+        from (select * from thread order by upid) as thread
+        left join (select * from process order by upid) as process
+        using(upid)`;
+  const result = await trace.engine.query(query);
+  const threads = new Map<number, ThreadDesc>();
+  const it = result.iter({
+    utid: NUM,
+    tid: NUM,
+    pid: NUM_NULL,
+    threadName: STR,
+    procName: STR_NULL,
+    cmdline: STR_NULL,
+  });
+  for (; it.valid(); it.next()) {
+    const utid = it.utid;
+    const tid = it.tid;
+    const pid = it.pid === null ? undefined : it.pid;
+    const threadName = it.threadName;
+    const procName = it.procName === null ? undefined : it.procName;
+    const cmdline = it.cmdline === null ? undefined : it.cmdline;
+    threads.set(utid, {utid, tid, threadName, pid, procName, cmdline});
+  }
+  return threads;
+}
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.Thread';
+  private threads?: ThreadMap;
+
+  async onTraceLoad(ctx: Trace) {
+    sqlTableRegistry['thread'] = getThreadTable();
+    ctx.commands.registerCommand({
+      id: 'perfetto.ShowTable.thread',
+      name: 'Open table: thread',
+      callback: () => {
+        extensions.addSqlTableTab(ctx, {
+          table: getThreadTable(),
+        });
+      },
+    });
+    this.threads = await listThreads(ctx);
+  }
+
+  getThreadMap() {
+    return assertExists(this.threads);
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Thread/table.ts b/ui/src/plugins/dev.perfetto.Thread/table.ts
new file mode 100644
index 0000000..e96bc64
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Thread/table.ts
@@ -0,0 +1,38 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {SqlTableDescription} from '../../frontend/widgets/sql/table/table_description';
+import {
+  ProcessColumnSet,
+  StandardColumn,
+  ThreadIdColumn,
+  TimestampColumn,
+} from '../../frontend/widgets/sql/table/well_known_columns';
+
+export function getThreadTable(): SqlTableDescription {
+  return {
+    name: 'thread',
+    columns: [
+      new ThreadIdColumn('utid'),
+      new StandardColumn('tid', {aggregationType: 'nominal'}),
+      new StandardColumn('name'),
+      new TimestampColumn('start_ts'),
+      new TimestampColumn('end_ts'),
+      new ProcessColumnSet('upid', {title: 'upid', notNull: true}),
+      new StandardColumn('is_main_thread', {
+        aggregationType: 'nominal',
+      }),
+    ],
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.Thread/threads.ts b/ui/src/plugins/dev.perfetto.Thread/threads.ts
new file mode 100644
index 0000000..22c1eb9
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Thread/threads.ts
@@ -0,0 +1,24 @@
+// 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 ThreadDesc {
+  utid: number;
+  tid: number;
+  threadName: string;
+  pid?: number;
+  procName?: string;
+  cmdline?: string;
+}
+
+export type ThreadMap = ReadonlyMap<number, ThreadDesc>;
diff --git a/ui/src/plugins/dev.perfetto.ThreadState/index.ts b/ui/src/plugins/dev.perfetto.ThreadState/index.ts
new file mode 100644
index 0000000..a106654
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ThreadState/index.ts
@@ -0,0 +1,137 @@
+// 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 {THREAD_STATE_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_NULL} from '../../trace_processor/query_result';
+import {ThreadStateTrack} from './thread_state_track';
+import {removeFalsyValues} from '../../base/array_utils';
+import {getThreadStateTable} from './table';
+import {sqlTableRegistry} from '../../frontend/widgets/sql/table/sql_table_registry';
+import {TrackNode} from '../../public/workspace';
+import {getOrCreateGroupForThread} from '../../public/standard_groups';
+import {ThreadStateSelectionAggregator} from './thread_state_selection_aggregator';
+import {extensions} from '../../public/lib/extensions';
+
+function uriForThreadStateTrack(upid: number | null, utid: number): string {
+  return `${getThreadUriPrefix(upid, utid)}_state`;
+}
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.ThreadState';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const {engine} = ctx;
+
+    ctx.selection.registerAreaSelectionAggreagtor(
+      new ThreadStateSelectionAggregator(),
+    );
+
+    const result = await engine.query(`
+      include perfetto module viz.threads;
+      include perfetto module viz.summary.threads;
+
+      select
+        utid,
+        t.upid,
+        tid,
+        t.name as threadName,
+        is_main_thread as isMainThread,
+        is_kernel_thread as isKernelThread
+      from _threads_with_kernel_flag t
+      join _sched_summary using (utid)
+    `);
+
+    const it = result.iter({
+      utid: NUM,
+      upid: NUM_NULL,
+      tid: NUM_NULL,
+      threadName: STR_NULL,
+      isMainThread: NUM_NULL,
+      isKernelThread: NUM,
+    });
+    for (; it.valid(); it.next()) {
+      const {utid, upid, tid, threadName, isMainThread, isKernelThread} = it;
+      const title = getTrackName({
+        utid,
+        tid,
+        threadName,
+        kind: THREAD_STATE_TRACK_KIND,
+      });
+
+      const uri = uriForThreadStateTrack(upid, utid);
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        tags: {
+          kind: THREAD_STATE_TRACK_KIND,
+          utid,
+          upid: upid ?? undefined,
+          ...(isKernelThread === 1 && {kernelThread: true}),
+        },
+        chips: removeFalsyValues([
+          isKernelThread === 0 && isMainThread === 1 && 'main thread',
+        ]),
+        track: new ThreadStateTrack(
+          {
+            trace: ctx,
+            uri,
+          },
+          utid,
+        ),
+      });
+
+      const group = getOrCreateGroupForThread(ctx.workspace, utid);
+      const track = new TrackNode({uri, title, sortOrder: 10});
+      group.addChildInOrder(track);
+    }
+
+    sqlTableRegistry['thread_state'] = getThreadStateTable();
+    ctx.commands.registerCommand({
+      id: 'perfetto.ShowTable.thread_state',
+      name: 'Open table: thread_state',
+      callback: () => {
+        extensions.addSqlTableTab(ctx, {
+          table: getThreadStateTable(),
+        });
+      },
+    });
+
+    ctx.selection.registerSqlSelectionResolver({
+      sqlTableName: 'thread_state',
+      callback: async (id: number) => {
+        const result = await ctx.engine.query(`
+          select
+            thread_state.utid,
+            thread.upid
+          from
+            thread_state
+            join thread on thread_state.utid = thread.id
+          where thread_state.id = ${id}
+        `);
+
+        const {upid, utid} = result.firstRow({
+          upid: NUM_NULL,
+          utid: NUM,
+        });
+
+        return {
+          eventId: id,
+          trackUri: uriForThreadStateTrack(upid, utid),
+        };
+      },
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.ThreadState/table.ts b/ui/src/plugins/dev.perfetto.ThreadState/table.ts
new file mode 100644
index 0000000..c93fb40
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ThreadState/table.ts
@@ -0,0 +1,60 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {SqlTableDescription} from '../../frontend/widgets/sql/table/table_description';
+import {
+  DurationColumn,
+  ProcessColumnSet,
+  StandardColumn,
+  ThreadColumn,
+  ThreadColumnSet,
+  ThreadStateIdColumn,
+  TimestampColumn,
+} from '../../frontend/widgets/sql/table/well_known_columns';
+
+export function getThreadStateTable(): SqlTableDescription {
+  return {
+    name: 'thread_state',
+    columns: [
+      new ThreadStateIdColumn('threadStateSqlId', {notNull: true}),
+      new TimestampColumn('ts'),
+      new DurationColumn('dur'),
+      new StandardColumn('state'),
+      new StandardColumn('cpu', {aggregationType: 'nominal'}),
+      new ThreadColumnSet('utid', {title: 'utid', notNull: true}),
+      new ProcessColumnSet(
+        {
+          column: 'upid',
+          source: {
+            table: 'thread',
+            joinOn: {
+              utid: 'utid',
+            },
+            innerJoin: true,
+          },
+        },
+        {title: 'upid (process)', notNull: true},
+      ),
+      new StandardColumn('ioWait', {aggregationType: 'nominal'}),
+      new StandardColumn('blockedFunction'),
+      new ThreadColumn('wakerUtid', {title: 'Waker thread'}),
+      new ThreadStateIdColumn('wakerId'),
+      new StandardColumn('irqContext', {aggregationType: 'nominal'}),
+      new StandardColumn('ucpu', {
+        aggregationType: 'nominal',
+        startsHidden: true,
+      }),
+    ],
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.ThreadState/thread_state_details_panel.ts b/ui/src/plugins/dev.perfetto.ThreadState/thread_state_details_panel.ts
new file mode 100644
index 0000000..0a2287b7
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ThreadState/thread_state_details_panel.ts
@@ -0,0 +1,335 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use size 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 {Button} from '../../widgets/button';
+import {DetailsShell} from '../../widgets/details_shell';
+import {GridLayout} from '../../widgets/grid_layout';
+import {Section} from '../../widgets/section';
+import {SqlRef} from '../../widgets/sql_ref';
+import {Tree, TreeNode} from '../../widgets/tree';
+import {Intent} from '../../widgets/common';
+import {SchedSqlId} from '../../trace_processor/sql_utils/core_types';
+import {
+  getThreadState,
+  getThreadStateFromConstraints,
+  ThreadState,
+} from '../../trace_processor/sql_utils/thread_state';
+import {DurationWidget, renderDuration} from '../../frontend/widgets/duration';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {getProcessName} from '../../trace_processor/sql_utils/process';
+import {
+  getFullThreadName,
+  getThreadName,
+} from '../../trace_processor/sql_utils/thread';
+import {ThreadStateRef} from '../../frontend/widgets/thread_state';
+import {
+  CRITICAL_PATH_CMD,
+  CRITICAL_PATH_LITE_CMD,
+} from '../../public/exposed_commands';
+import {goToSchedSlice} from '../../frontend/widgets/sched';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
+
+interface RelatedThreadStates {
+  prev?: ThreadState;
+  next?: ThreadState;
+  waker?: ThreadState;
+  wakerInterruptCtx?: boolean;
+  wakee?: ThreadState[];
+}
+
+export class ThreadStateDetailsPanel implements TrackEventDetailsPanel {
+  private threadState?: ThreadState;
+  private relatedStates?: RelatedThreadStates;
+
+  constructor(
+    private readonly trace: Trace,
+    private readonly id: number,
+  ) {}
+
+  async load() {
+    const id = this.id;
+    this.threadState = await getThreadState(this.trace.engine, id);
+
+    if (!this.threadState) {
+      return;
+    }
+
+    const relatedStates: RelatedThreadStates = {};
+    relatedStates.prev = (
+      await getThreadStateFromConstraints(this.trace.engine, {
+        filters: [
+          `ts + dur = ${this.threadState.ts}`,
+          `utid = ${this.threadState.thread?.utid}`,
+        ],
+        limit: 1,
+      })
+    )[0];
+    relatedStates.next = (
+      await getThreadStateFromConstraints(this.trace.engine, {
+        filters: [
+          `ts = ${this.threadState.ts + this.threadState.dur}`,
+          `utid = ${this.threadState.thread?.utid}`,
+        ],
+        limit: 1,
+      })
+    )[0];
+    if (this.threadState.wakerId !== undefined) {
+      relatedStates.waker = await getThreadState(
+        this.trace.engine,
+        this.threadState.wakerId,
+      );
+    } else if (
+      this.threadState.state == 'Running' &&
+      relatedStates.prev.wakerId != undefined
+    ) {
+      relatedStates.waker = await getThreadState(
+        this.trace.engine,
+        relatedStates.prev.wakerId,
+      );
+    }
+    // note: this might be valid even if there is no |waker| slice, in the case
+    // of an interrupt wakeup while in the idle process (which is omitted from
+    // the thread_state table).
+    relatedStates.wakerInterruptCtx = this.threadState.wakerInterruptCtx;
+
+    relatedStates.wakee = await getThreadStateFromConstraints(
+      this.trace.engine,
+      {
+        filters: [
+          `waker_id = ${id}`,
+          `(irq_context is null or irq_context = 0)`,
+        ],
+      },
+    );
+    this.relatedStates = relatedStates;
+  }
+
+  render() {
+    // TODO(altimin/stevegolton): Differentiate between "Current Selection" and
+    // "Pinned" views in DetailsShell.
+    return m(
+      DetailsShell,
+      {title: 'Thread State', description: this.renderLoadingText()},
+      m(
+        GridLayout,
+        m(
+          Section,
+          {title: 'Details'},
+          this.threadState && this.renderTree(this.threadState),
+        ),
+        m(
+          Section,
+          {title: 'Related thread states'},
+          this.renderRelatedThreadStates(),
+        ),
+      ),
+    );
+  }
+
+  private renderLoadingText() {
+    if (!this.threadState) {
+      return 'Loading';
+    }
+    return this.id;
+  }
+
+  private renderTree(threadState: ThreadState) {
+    const thread = threadState.thread;
+    const process = threadState.thread?.process;
+    return m(
+      Tree,
+      m(TreeNode, {
+        left: 'Start time',
+        right: m(Timestamp, {ts: threadState.ts}),
+      }),
+      m(TreeNode, {
+        left: 'Duration',
+        right: m(DurationWidget, {dur: threadState.dur}),
+      }),
+      m(TreeNode, {
+        left: 'State',
+        right: this.renderState(
+          threadState.state,
+          threadState.cpu,
+          threadState.schedSqlId,
+        ),
+      }),
+      threadState.blockedFunction &&
+        m(TreeNode, {
+          left: 'Blocked function',
+          right: threadState.blockedFunction,
+        }),
+      process &&
+        m(TreeNode, {
+          left: 'Process',
+          right: getProcessName(process),
+        }),
+      thread && m(TreeNode, {left: 'Thread', right: getThreadName(thread)}),
+      threadState.priority !== undefined &&
+        m(TreeNode, {
+          left: 'Priority',
+          right: threadState.priority,
+        }),
+      m(TreeNode, {
+        left: 'SQL ID',
+        right: m(SqlRef, {table: 'thread_state', id: threadState.id}),
+      }),
+    );
+  }
+
+  private renderState(
+    state: string,
+    cpu: number | undefined,
+    id: SchedSqlId | undefined,
+  ): m.Children {
+    if (!state) {
+      return null;
+    }
+    if (id === undefined || cpu === undefined) {
+      return state;
+    }
+    return m(
+      Anchor,
+      {
+        title: 'Go to CPU slice',
+        icon: 'call_made',
+        onclick: () => goToSchedSlice(id),
+      },
+      `${state} on CPU ${cpu}`,
+    );
+  }
+
+  private renderRelatedThreadStates(): m.Children {
+    if (this.threadState === undefined || this.relatedStates === undefined) {
+      return 'Loading';
+    }
+    const startTs = this.threadState.ts;
+    const renderRef = (state: ThreadState, name?: string) =>
+      m(ThreadStateRef, {
+        id: state.id,
+        name,
+      });
+
+    const nameForNextOrPrev = (threadState: ThreadState) =>
+      `${threadState.state} for ${renderDuration(threadState.dur)}`;
+
+    const renderWaker = (related: RelatedThreadStates) => {
+      // Could be absent if:
+      // * this thread state wasn't woken up (e.g. it is a running slice).
+      // * the wakeup is from an interrupt during the idle process (which
+      //   isn't populated in thread_state).
+      // * at the start of the trace, before all per-cpu scheduling is known.
+      const hasWakerId = related.waker !== undefined;
+      // Interrupt context for the wakeups is absent from older traces.
+      const hasInterruptCtx = related.wakerInterruptCtx !== undefined;
+
+      if (!hasWakerId && !hasInterruptCtx) {
+        return null;
+      }
+      if (related.wakerInterruptCtx) {
+        return m(TreeNode, {
+          left: 'Woken by',
+          right: `Interrupt`,
+        });
+      }
+      return (
+        related.waker &&
+        m(TreeNode, {
+          left: hasInterruptCtx ? 'Woken by' : 'Woken by (maybe interrupt)',
+          right: renderRef(
+            related.waker,
+            getFullThreadName(related.waker.thread),
+          ),
+        })
+      );
+    };
+
+    const renderWakees = (related: RelatedThreadStates) => {
+      if (related.wakee === undefined || related.wakee.length == 0) {
+        return null;
+      }
+      const hasInterruptCtx = related.wakee[0].wakerInterruptCtx !== undefined;
+      return m(
+        TreeNode,
+        {
+          left: hasInterruptCtx
+            ? 'Woken threads'
+            : 'Woken threads (maybe interrupt)',
+        },
+        related.wakee.map((state) =>
+          m(TreeNode, {
+            left: m(Timestamp, {
+              ts: state.ts,
+              display: `+${renderDuration(state.ts - startTs)}`,
+            }),
+            right: renderRef(state, getFullThreadName(state.thread)),
+          }),
+        ),
+      );
+    };
+
+    return [
+      m(
+        Tree,
+        this.relatedStates.prev &&
+          m(TreeNode, {
+            left: 'Previous state',
+            right: renderRef(
+              this.relatedStates.prev,
+              nameForNextOrPrev(this.relatedStates.prev),
+            ),
+          }),
+        this.relatedStates.next &&
+          m(TreeNode, {
+            left: 'Next state',
+            right: renderRef(
+              this.relatedStates.next,
+              nameForNextOrPrev(this.relatedStates.next),
+            ),
+          }),
+        renderWaker(this.relatedStates),
+        renderWakees(this.relatedStates),
+      ),
+      this.trace.commands.hasCommand(CRITICAL_PATH_LITE_CMD) &&
+        m(Button, {
+          label: 'Critical path lite',
+          intent: Intent.Primary,
+          onclick: () => {
+            this.trace.commands.runCommand(
+              CRITICAL_PATH_LITE_CMD,
+              this.threadState?.thread?.utid,
+            );
+          },
+        }),
+      this.trace.commands.hasCommand(CRITICAL_PATH_CMD) &&
+        m(Button, {
+          label: 'Critical path',
+          intent: Intent.Primary,
+          onclick: () => {
+            this.trace.commands.runCommand(
+              CRITICAL_PATH_CMD,
+              this.threadState?.thread?.utid,
+            );
+          },
+        }),
+    ];
+  }
+
+  isLoading() {
+    return this.threadState === undefined || this.relatedStates === undefined;
+  }
+}
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
new file mode 100644
index 0000000..3794bf7
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ThreadState/thread_state_selection_aggregator.ts
@@ -0,0 +1,173 @@
+// 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.
+
+import {exists} from '../../base/utils';
+import {ColumnDef, Sorting, ThreadStateExtra} from '../../public/aggregation';
+import {AreaSelection} from '../../public/selection';
+import {THREAD_STATE_TRACK_KIND} from '../../public/track_kinds';
+import {Engine} from '../../trace_processor/engine';
+import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
+import {AreaSelectionAggregator} from '../../public/selection';
+import {translateState} from '../../trace_processor/sql_utils/thread_state';
+import {TrackDescriptor} from '../../public/track';
+
+export class ThreadStateSelectionAggregator implements AreaSelectionAggregator {
+  readonly id = 'thread_state_aggregation';
+  private utids?: number[];
+
+  setThreadStateUtids(tracks: ReadonlyArray<TrackDescriptor>) {
+    this.utids = [];
+    for (const trackInfo of tracks) {
+      if (trackInfo?.tags?.kind === THREAD_STATE_TRACK_KIND) {
+        exists(trackInfo.tags.utid) && this.utids.push(trackInfo.tags.utid);
+      }
+    }
+  }
+
+  async createAggregateView(engine: Engine, area: AreaSelection) {
+    this.setThreadStateUtids(area.tracks);
+    if (this.utids === undefined || this.utids.length === 0) return false;
+
+    await engine.query(`
+      create or replace perfetto table ${this.id} as
+      select
+        process.name as process_name,
+        process.pid,
+        thread.name as thread_name,
+        thread.tid,
+        tstate.state || ',' || ifnull(tstate.io_wait, 'NULL') as concat_state,
+        sum(tstate.dur) AS total_dur,
+        sum(tstate.dur) / count() as avg_dur,
+        count() as occurrences
+      from thread_state tstate
+      join thread using (utid)
+      left join process using (upid)
+      where
+        utid in (${this.utids})
+        and ts + dur > ${area.start}
+        and ts < ${area.end}
+      group by utid, concat_state
+    `);
+    return true;
+  }
+
+  async getExtra(
+    engine: Engine,
+    area: AreaSelection,
+  ): Promise<ThreadStateExtra | void> {
+    this.setThreadStateUtids(area.tracks);
+    if (this.utids === undefined || this.utids.length === 0) return;
+
+    const query = `
+      select
+        state,
+        io_wait as ioWait,
+        sum(dur) as totalDur
+      from thread
+      join thread_state using (utid)
+      where utid in (${this.utids})
+        and thread_state.ts + thread_state.dur > ${area.start}
+        and thread_state.ts < ${area.end}
+      group by state, io_wait
+    `;
+    const result = await engine.query(query);
+
+    const it = result.iter({
+      state: STR_NULL,
+      ioWait: NUM_NULL,
+      totalDur: NUM,
+    });
+
+    let totalMs = 0;
+    const values = new Float64Array(result.numRows());
+    const states = [];
+    for (let i = 0; it.valid(); ++i, it.next()) {
+      const state = it.state == null ? undefined : it.state;
+      const ioWait = it.ioWait === null ? undefined : it.ioWait > 0;
+      states.push(translateState(state, ioWait));
+      const ms = it.totalDur / 1000000;
+      values[i] = ms;
+      totalMs += ms;
+    }
+    return {
+      kind: 'THREAD_STATE',
+      states,
+      values,
+      totalMs,
+    };
+  }
+
+  getColumnDefinitions(): ColumnDef[] {
+    return [
+      {
+        title: 'Process',
+        kind: 'STRING',
+        columnConstructor: Uint16Array,
+        columnId: 'process_name',
+      },
+      {
+        title: 'PID',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'pid',
+      },
+      {
+        title: 'Thread',
+        kind: 'STRING',
+        columnConstructor: Uint16Array,
+        columnId: 'thread_name',
+      },
+      {
+        title: 'TID',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'tid',
+      },
+      {
+        title: 'State',
+        kind: 'STATE',
+        columnConstructor: Uint16Array,
+        columnId: 'concat_state',
+      },
+      {
+        title: 'Wall duration (ms)',
+        kind: 'TIMESTAMP_NS',
+        columnConstructor: Float64Array,
+        columnId: 'total_dur',
+        sum: true,
+      },
+      {
+        title: 'Avg Wall duration (ms)',
+        kind: 'TIMESTAMP_NS',
+        columnConstructor: Float64Array,
+        columnId: 'avg_dur',
+      },
+      {
+        title: 'Occurrences',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'occurrences',
+        sum: true,
+      },
+    ];
+  }
+
+  getTabName() {
+    return 'Thread States';
+  }
+
+  getDefaultSorting(): Sorting {
+    return {column: 'total_dur', direction: 'DESC'};
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.ThreadState/thread_state_track.ts b/ui/src/plugins/dev.perfetto.ThreadState/thread_state_track.ts
new file mode 100644
index 0000000..3935465
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ThreadState/thread_state_track.ts
@@ -0,0 +1,102 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {colorForState} from '../../public/lib/colorizer';
+import {
+  BASE_ROW,
+  BaseSliceTrack,
+  OnSliceClickArgs,
+} from '../../frontend/base_slice_track';
+import {
+  SLICE_LAYOUT_FLAT_DEFAULTS,
+  SliceLayout,
+} from '../../frontend/slice_layout';
+import {NewTrackArgs} from '../../frontend/track';
+import {NUM_NULL, STR} from '../../trace_processor/query_result';
+import {Slice} from '../../public/track';
+import {translateState} from '../../trace_processor/sql_utils/thread_state';
+import {TrackEventDetails, TrackEventSelection} from '../../public/selection';
+import {ThreadStateDetailsPanel} from './thread_state_details_panel';
+
+export const THREAD_STATE_ROW = {
+  ...BASE_ROW,
+  state: STR,
+  ioWait: NUM_NULL,
+};
+
+export type ThreadStateRow = typeof THREAD_STATE_ROW;
+
+export class ThreadStateTrack extends BaseSliceTrack<Slice, ThreadStateRow> {
+  protected sliceLayout: SliceLayout = {...SLICE_LAYOUT_FLAT_DEFAULTS};
+
+  constructor(
+    args: NewTrackArgs,
+    private utid: number,
+  ) {
+    super(args);
+  }
+
+  // This is used by the base class to call iter().
+  getRowSpec(): ThreadStateRow {
+    return THREAD_STATE_ROW;
+  }
+
+  getSqlSource(): string {
+    // Do not display states: 'S' (sleeping), 'I' (idle kernel thread).
+    return `
+      select
+        id,
+        ts,
+        dur,
+        cpu,
+        state,
+        io_wait as ioWait,
+        0 as depth
+      from thread_state
+      where
+        utid = ${this.utid} and
+        state not in ('S', 'I')
+    `;
+  }
+
+  rowToSlice(row: ThreadStateRow): Slice {
+    const baseSlice = this.rowToSliceBase(row);
+    const ioWait = row.ioWait === null ? undefined : !!row.ioWait;
+    const title = translateState(row.state, ioWait);
+    const color = colorForState(title);
+    return {...baseSlice, title, colorScheme: color};
+  }
+
+  onUpdatedSlices(slices: Slice[]) {
+    for (const slice of slices) {
+      slice.isHighlighted = slice === this.hoveredSlice;
+    }
+  }
+
+  onSliceClick(args: OnSliceClickArgs<Slice>) {
+    this.trace.selection.selectTrackEvent(this.uri, args.slice.id);
+  }
+
+  // Add utid to selection details
+  override async getSelectionDetails(
+    id: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const details = await super.getSelectionDetails(id);
+    return details && {...details, utid: this.utid};
+  }
+
+  detailsPanel({eventId}: TrackEventSelection) {
+    return new ThreadStateDetailsPanel(this.trace, eventId);
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
index 0c12ca2..aef7a76 100644
--- a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
+++ b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
@@ -13,13 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
 import {Time, TimeSpan} from '../../base/time';
 import {redrawModal, showModal} from '../../widgets/modal';
 import {assertExists} from '../../base/logging';
@@ -44,9 +39,10 @@
  * their durations don't match. The initial viewport bound for each trace is
  * selected when the enable command is called.
  */
-class TimelineSync implements Plugin {
+export default class implements PerfettoPlugin {
+  static readonly id = PLUGIN_ID;
   private _chan?: BroadcastChannel;
-  private _ctx?: PluginContextTrace;
+  private _ctx?: Trace;
   private _traceLoadTime = 0;
   // Attached to broadcast messages to allow other windows to remap viewports.
   private readonly _clientId: ClientId = Math.floor(Math.random() * 1_000_000);
@@ -68,18 +64,18 @@
     ViewportBoundsSnapshot
   >();
 
-  onActivate(ctx: PluginContext): void {
-    ctx.registerCommand({
+  async onTraceLoad(ctx: Trace) {
+    ctx.commands.registerCommand({
       id: `dev.perfetto.SplitScreen#enableTimelineSync`,
       name: 'Enable timeline sync with other Perfetto UI tabs',
       callback: () => this.showTimelineSyncDialog(),
     });
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: `dev.perfetto.SplitScreen#disableTimelineSync`,
       name: 'Disable timeline sync',
       callback: () => this.disableTimelineSync(this._sessionId),
     });
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: `dev.perfetto.SplitScreen#toggleTimelineSync`,
       name: 'Toggle timeline sync with other PerfettoUI tabs',
       callback: () => this.toggleTimelineSync(),
@@ -102,24 +98,17 @@
         ? parseInt(m[1].substring(1))
         : DEFAULT_SESSION_ID;
     }
-  }
 
-  onDeactivate(_: PluginContext) {
-    this.disableTimelineSync(this._sessionId);
-  }
-
-  async onTraceLoad(ctx: PluginContextTrace) {
     this._ctx = ctx;
     this._traceLoadTime = Date.now();
     this.advertise();
     if (this._sessionidFromUrl !== 0) {
       this.enableTimelineSync(this._sessionidFromUrl);
     }
-  }
-
-  async onTraceUnload(_: PluginContextTrace) {
-    this.disableTimelineSync(this._sessionId);
-    this._ctx = undefined;
+    ctx.trash.defer(() => {
+      this.disableTimelineSync(this._sessionId);
+      this._ctx = undefined;
+    });
   }
 
   private advertise() {
@@ -401,7 +390,7 @@
   }
 
   private getCurrentViewportBounds(): ViewportBounds {
-    return this._ctx!.timeline.viewport;
+    return this._ctx!.timeline.visibleWindow.toTimeSpan();
   }
 
   private purgeInactiveClients() {
@@ -465,8 +454,3 @@
   lastHeartbeat: number; // Datetime.now() of the last MSG_ADVERTISE.
   traceLoadTime: number; // Datetime.now() of the onTraceLoad().
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: PLUGIN_ID,
-  plugin: TimelineSync,
-};
diff --git a/ui/src/plugins/dev.perfetto.TraceInfoPage/index.ts b/ui/src/plugins/dev.perfetto.TraceInfoPage/index.ts
new file mode 100644
index 0000000..f019331
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.TraceInfoPage/index.ts
@@ -0,0 +1,32 @@
+// 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 {Trace} from '../../public/trace';
+import {TraceInfoPage} from './trace_info_page';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.TraceInfoPage';
+
+  async onTraceLoad(trace: Trace): Promise<void> {
+    trace.pages.registerPage({route: '/info', page: TraceInfoPage});
+    trace.sidebar.addMenuItem({
+      section: 'current_trace',
+      text: 'Info and stats',
+      href: '#!/info',
+      icon: 'info',
+      sortOrder: 10,
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.TraceInfoPage/trace_info_page.ts b/ui/src/plugins/dev.perfetto.TraceInfoPage/trace_info_page.ts
new file mode 100644
index 0000000..686b976
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.TraceInfoPage/trace_info_page.ts
@@ -0,0 +1,469 @@
+// 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.
+
+import m from 'mithril';
+import {Engine, EngineAttrs} from '../../trace_processor/engine';
+import {QueryResult, UNKNOWN} from '../../trace_processor/query_result';
+import {assertExists} from '../../base/logging';
+import {TraceAttrs} from '../../public/trace';
+import {PageWithTraceAttrs} from '../../public/page';
+
+/**
+ * Extracts and copies fields from a source object based on the keys present in
+ * a spec object, effectively creating a new object that includes only the
+ * fields that are present in the spec object.
+ *
+ * @template S - A type representing the spec object, a subset of T.
+ * @template T - A type representing the source object, a superset of S.
+ *
+ * @param {T} source - The source object containing the full set of properties.
+ * @param {S} spec - The specification object whose keys determine which fields
+ * should be extracted from the source object.
+ *
+ * @returns {S} A new object containing only the fields from the source object
+ * that are also present in the specification object.
+ *
+ * @example
+ * const fullObject = { foo: 123, bar: '123', baz: true };
+ * const spec = { foo: 0, bar: '' };
+ * const result = pickFields(fullObject, spec);
+ * console.log(result); // Output: { foo: 123, bar: '123' }
+ */
+function pickFields<S extends Record<string, unknown>, T extends S>(
+  source: T,
+  spec: S,
+): S {
+  const result: Record<string, unknown> = {};
+  for (const key of Object.keys(spec)) {
+    result[key] = source[key];
+  }
+  return result as S;
+}
+
+interface StatsSectionAttrs {
+  engine: Engine;
+  title: string;
+  subTitle: string;
+  sqlConstraints: string;
+  cssClass: string;
+  queryId: string;
+}
+
+const statsSpec = {
+  name: UNKNOWN,
+  value: UNKNOWN,
+  description: UNKNOWN,
+  idx: UNKNOWN,
+  severity: UNKNOWN,
+  source: UNKNOWN,
+};
+
+type StatsSectionRow = typeof statsSpec;
+
+// Generic class that generate a <section> + <table> from the stats table.
+// The caller defines the query constraint, title and styling.
+// Used for errors, data losses and debugging sections.
+class StatsSection implements m.ClassComponent<StatsSectionAttrs> {
+  private data?: StatsSectionRow[];
+
+  constructor({attrs}: m.CVnode<StatsSectionAttrs>) {
+    const engine = attrs.engine;
+    if (engine === undefined) {
+      return;
+    }
+    const query = `
+      select
+        name,
+        value,
+        cast(ifnull(idx, '') as text) as idx,
+        description,
+        severity,
+        source from stats
+      where ${attrs.sqlConstraints || '1=1'}
+      order by name, idx
+    `;
+
+    engine.query(query).then((resp) => {
+      const data: StatsSectionRow[] = [];
+      const it = resp.iter(statsSpec);
+      for (; it.valid(); it.next()) {
+        data.push(pickFields(it, statsSpec));
+      }
+      this.data = data;
+    });
+  }
+
+  view({attrs}: m.CVnode<StatsSectionAttrs>) {
+    const data = this.data;
+    if (data === undefined || data.length === 0) {
+      return m('');
+    }
+
+    const tableRows = data.map((row) => {
+      const help = [];
+      if (Boolean(row.description)) {
+        help.push(m('i.material-icons.contextual-help', 'help_outline'));
+      }
+      const idx = row.idx !== '' ? `[${row.idx}]` : '';
+      return m(
+        'tr',
+        m('td.name', {title: row.description}, `${row.name}${idx}`, help),
+        m('td', `${row.value}`),
+        m('td', `${row.severity} (${row.source})`),
+      );
+    });
+
+    return m(
+      `section${attrs.cssClass}`,
+      m('h2', attrs.title),
+      m('h3', attrs.subTitle),
+      m(
+        'table',
+        m('thead', m('tr', m('td', 'Name'), m('td', 'Value'), m('td', 'Type'))),
+        m('tbody', tableRows),
+      ),
+    );
+  }
+}
+
+class LoadingErrors implements m.ClassComponent<TraceAttrs> {
+  view({attrs}: m.CVnode<TraceAttrs>) {
+    const errors = attrs.trace.loadingErrors;
+    if (errors.length === 0) return;
+    return m(
+      `section.errors`,
+      m('h2', `Loading errors`),
+      m('h3', `The following errors were encountered while loading the trace:`),
+      m('pre.metric-error', errors.join('\n')),
+    );
+  }
+}
+
+const traceMetadataRowSpec = {name: UNKNOWN, value: UNKNOWN};
+
+type TraceMetadataRow = typeof traceMetadataRowSpec;
+
+class TraceMetadata implements m.ClassComponent<EngineAttrs> {
+  private data?: TraceMetadataRow[];
+
+  oncreate({attrs}: m.CVnodeDOM<EngineAttrs>) {
+    const engine = attrs.engine;
+    const query = `
+      with metadata_with_priorities as (
+        select
+          name,
+          ifnull(str_value, cast(int_value as text)) as value,
+          name in (
+            "trace_size_bytes",
+            "cr-os-arch",
+            "cr-os-name",
+            "cr-os-version",
+            "cr-physical-memory",
+            "cr-product-version",
+            "cr-hardware-class"
+          ) as priority
+        from metadata
+      )
+      select
+        name,
+        value
+      from metadata_with_priorities
+      order by
+        priority desc,
+        name
+    `;
+
+    engine.query(query).then((resp: QueryResult) => {
+      const tableRows: TraceMetadataRow[] = [];
+      const it = resp.iter(traceMetadataRowSpec);
+      for (; it.valid(); it.next()) {
+        tableRows.push(pickFields(it, traceMetadataRowSpec));
+      }
+      this.data = tableRows;
+    });
+  }
+
+  view() {
+    const data = this.data;
+    if (data === undefined || data.length === 0) {
+      return m('');
+    }
+
+    const tableRows = data.map((row) => {
+      return m('tr', m('td.name', `${row.name}`), m('td', `${row.value}`));
+    });
+
+    return m(
+      'section',
+      m('h2', 'System info and metadata'),
+      m(
+        'table',
+        m('thead', m('tr', m('td', 'Name'), m('td', 'Value'))),
+        m('tbody', tableRows),
+      ),
+    );
+  }
+}
+
+const androidGameInterventionRowSpec = {
+  package_name: UNKNOWN,
+  uid: UNKNOWN,
+  current_mode: UNKNOWN,
+  standard_mode_supported: UNKNOWN,
+  standard_mode_downscale: UNKNOWN,
+  standard_mode_use_angle: UNKNOWN,
+  standard_mode_fps: UNKNOWN,
+  perf_mode_supported: UNKNOWN,
+  perf_mode_downscale: UNKNOWN,
+  perf_mode_use_angle: UNKNOWN,
+  perf_mode_fps: UNKNOWN,
+  battery_mode_supported: UNKNOWN,
+  battery_mode_downscale: UNKNOWN,
+  battery_mode_use_angle: UNKNOWN,
+  battery_mode_fps: UNKNOWN,
+};
+
+type AndroidGameInterventionRow = typeof androidGameInterventionRowSpec;
+
+class AndroidGameInterventionList implements m.ClassComponent<EngineAttrs> {
+  private data?: AndroidGameInterventionRow[];
+
+  oncreate({attrs}: m.CVnodeDOM<EngineAttrs>) {
+    const engine = attrs.engine;
+    const query = `
+      select
+        package_name,
+        uid,
+        current_mode,
+        standard_mode_supported,
+        standard_mode_downscale,
+        standard_mode_use_angle,
+        standard_mode_fps,
+        perf_mode_supported,
+        perf_mode_downscale,
+        perf_mode_use_angle,
+        perf_mode_fps,
+        battery_mode_supported,
+        battery_mode_downscale,
+        battery_mode_use_angle,
+        battery_mode_fps
+      from android_game_intervention_list
+    `;
+
+    engine.query(query).then((resp) => {
+      const data: AndroidGameInterventionRow[] = [];
+      const it = resp.iter(androidGameInterventionRowSpec);
+      for (; it.valid(); it.next()) {
+        data.push(pickFields(it, androidGameInterventionRowSpec));
+      }
+      this.data = data;
+    });
+  }
+
+  view() {
+    const data = this.data;
+    if (data === undefined || data.length === 0) {
+      return m('');
+    }
+
+    const tableRows = [];
+    let standardInterventions = '';
+    let perfInterventions = '';
+    let batteryInterventions = '';
+
+    for (const row of data) {
+      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+      if (row.standard_mode_supported) {
+        standardInterventions = `angle=${row.standard_mode_use_angle},downscale=${row.standard_mode_downscale},fps=${row.standard_mode_fps}`;
+      } else {
+        standardInterventions = 'Not supported';
+      }
+
+      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+      if (row.perf_mode_supported) {
+        perfInterventions = `angle=${row.perf_mode_use_angle},downscale=${row.perf_mode_downscale},fps=${row.perf_mode_fps}`;
+      } else {
+        perfInterventions = 'Not supported';
+      }
+
+      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+      if (row.battery_mode_supported) {
+        batteryInterventions = `angle=${row.battery_mode_use_angle},downscale=${row.battery_mode_downscale},fps=${row.battery_mode_fps}`;
+      } else {
+        batteryInterventions = 'Not supported';
+      }
+      // Game mode numbers are defined in
+      // https://cs.android.com/android/platform/superproject/+/main:frameworks/base/core/java/android/app/GameManager.java;l=68
+      if (row.current_mode === 1) {
+        row.current_mode = 'Standard';
+      } else if (row.current_mode === 2) {
+        row.current_mode = 'Performance';
+      } else if (row.current_mode === 3) {
+        row.current_mode = 'Battery';
+      }
+      tableRows.push(
+        m(
+          'tr',
+          m('td.name', `${row.package_name}`),
+          m('td', `${row.current_mode}`),
+          m('td', standardInterventions),
+          m('td', perfInterventions),
+          m('td', batteryInterventions),
+        ),
+      );
+    }
+
+    return m(
+      'section',
+      m('h2', 'Game Intervention List'),
+      m(
+        'table',
+        m(
+          'thead',
+          m(
+            'tr',
+            m('td', 'Name'),
+            m('td', 'Current mode'),
+            m('td', 'Standard mode interventions'),
+            m('td', 'Performance mode interventions'),
+            m('td', 'Battery mode interventions'),
+          ),
+        ),
+        m('tbody', tableRows),
+      ),
+    );
+  }
+}
+
+const packageDataSpec = {
+  packageName: UNKNOWN,
+  versionCode: UNKNOWN,
+  debuggable: UNKNOWN,
+  profileableFromShell: UNKNOWN,
+};
+
+type PackageData = typeof packageDataSpec;
+
+class PackageListSection implements m.ClassComponent<EngineAttrs> {
+  private packageList?: PackageData[];
+
+  oncreate({attrs}: m.CVnodeDOM<EngineAttrs>) {
+    const engine = attrs.engine;
+    this.loadData(engine);
+  }
+
+  private async loadData(engine: Engine): Promise<void> {
+    const query = `
+      select
+        package_name as packageName,
+        version_code as versionCode,
+        debuggable,
+        profileable_from_shell as profileableFromShell
+      from package_list
+    `;
+
+    const packageList: PackageData[] = [];
+    const result = await engine.query(query);
+    const it = result.iter(packageDataSpec);
+    for (; it.valid(); it.next()) {
+      packageList.push(pickFields(it, packageDataSpec));
+    }
+
+    this.packageList = packageList;
+  }
+
+  view() {
+    const packageList = this.packageList;
+    if (packageList === undefined || packageList.length === 0) {
+      return undefined;
+    }
+
+    const tableRows = packageList.map((it) => {
+      return m(
+        'tr',
+        m('td.name', `${it.packageName}`),
+        m('td', `${it.versionCode}`),
+        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
+        m(
+          'td',
+          `${it.debuggable ? 'debuggable' : ''} ${
+            it.profileableFromShell ? 'profileable' : ''
+          }`,
+        ),
+        /* eslint-enable */
+      );
+    });
+
+    return m(
+      'section',
+      m('h2', 'Package list'),
+      m(
+        'table',
+        m(
+          'thead',
+          m('tr', m('td', 'Name'), m('td', 'Version code'), m('td', 'Flags')),
+        ),
+        m('tbody', tableRows),
+      ),
+    );
+  }
+}
+
+export class TraceInfoPage implements m.ClassComponent<PageWithTraceAttrs> {
+  private engine?: Engine;
+
+  oninit({attrs}: m.CVnode<PageWithTraceAttrs>) {
+    this.engine = attrs.trace.engine.getProxy('TraceInfoPage');
+  }
+
+  view({attrs}: m.CVnode<PageWithTraceAttrs>) {
+    const engine = assertExists(this.engine);
+    return m(
+      '.trace-info-page',
+      m(LoadingErrors, {trace: attrs.trace}),
+      m(StatsSection, {
+        engine,
+        queryId: 'info_errors',
+        title: 'Import errors',
+        cssClass: '.errors',
+        subTitle: `The following errors have been encountered while importing
+               the trace. These errors are usually non-fatal but indicate that
+               one or more tracks might be missing or showing erroneous data.`,
+        sqlConstraints: `severity = 'error' and value > 0`,
+      }),
+      m(StatsSection, {
+        engine,
+        queryId: 'info_data_losses',
+        title: 'Data losses',
+        cssClass: '.errors',
+        subTitle: `These counters are collected at trace recording time. The
+               trace data for one or more data sources was dropped and hence
+               some track contents will be incomplete.`,
+        sqlConstraints: `severity = 'data_loss' and value > 0`,
+      }),
+      m(TraceMetadata, {engine}),
+      m(PackageListSection, {engine}),
+      m(AndroidGameInterventionList, {engine}),
+      m(StatsSection, {
+        engine,
+        queryId: 'info_all',
+        title: 'Debugging stats',
+        cssClass: '',
+        subTitle: `Debugging statistics such as trace buffer usage and metrics
+                     coming from the TraceProcessor importer stages.`,
+        sqlConstraints: '',
+      }),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
index 6917eb0..a10afe1 100644
--- a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
+++ b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
@@ -12,11 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {NUM, Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {SimpleSliceTrack} from '../../frontend/simple_slice_track';
+import {NUM} from '../../trace_processor/query_result';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {createQuerySliceTrack} from '../../public/lib/tracks/query_slice_track';
+import {TrackNode} from '../../public/workspace';
 
-class TraceMetadata implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.TraceMetadata';
+  async onTraceLoad(ctx: Trace): Promise<void> {
     const res = await ctx.engine.query(`
       select count() as cnt from (select 1 from clock_snapshot limit 1)
     `);
@@ -24,27 +28,24 @@
     if (row.cnt === 0) {
       return;
     }
-    ctx.registerStaticTrack({
-      uri: `/clock_snapshots`,
-      title: 'Clock Snapshots',
-      trackFactory: (trackCtx) => {
-        return new SimpleSliceTrack(ctx.engine, trackCtx, {
-          data: {
-            sqlSource: `
-              select ts, 0 as dur, 'Snapshot' as name
-              from clock_snapshot
-            `,
-            columns: ['ts', 'dur', 'name'],
-          },
-          columns: {ts: 'ts', dur: 'dur', name: 'name'},
-          argColumns: [],
-        });
+    const uri = `/clock_snapshots`;
+    const title = 'Clock Snapshots';
+    const track = await createQuerySliceTrack({
+      trace: ctx,
+      uri,
+      data: {
+        sqlSource: `
+          select ts, 0 as dur, 'Snapshot' as name
+          from clock_snapshot
+          `,
       },
     });
+    ctx.tracks.registerTrack({
+      uri,
+      title,
+      track,
+    });
+    const trackNode = new TrackNode({uri, title});
+    ctx.workspace.addChildInOrder(trackNode);
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.perfetto.TraceMetadata',
-  plugin: TraceMetadata,
-};
diff --git a/ui/src/plugins/dev.perfetto.VizPage/index.ts b/ui/src/plugins/dev.perfetto.VizPage/index.ts
new file mode 100644
index 0000000..9476479
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.VizPage/index.ts
@@ -0,0 +1,32 @@
+// 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 {Trace} from '../../public/trace';
+import {VizPage} from './viz_page';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.VizPage';
+
+  async onTraceLoad(trace: Trace): Promise<void> {
+    trace.pages.registerPage({route: '/viz', page: VizPage});
+    trace.sidebar.addMenuItem({
+      section: 'current_trace',
+      text: 'Viz',
+      href: '#!/viz',
+      icon: 'area_chart',
+      sortOrder: 2,
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.VizPage/viz_page.ts b/ui/src/plugins/dev.perfetto.VizPage/viz_page.ts
new file mode 100644
index 0000000..a838a99
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.VizPage/viz_page.ts
@@ -0,0 +1,46 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {Editor} from '../../widgets/editor';
+import {VegaView} from '../../widgets/vega_view';
+import {PageWithTraceAttrs} from '../../public/page';
+import {Engine} from '../../trace_processor/engine';
+
+let SPEC = '';
+
+export class VizPage implements m.ClassComponent<PageWithTraceAttrs> {
+  private engine: Engine;
+
+  constructor({attrs}: m.CVnode<PageWithTraceAttrs>) {
+    this.engine = attrs.trace.engine.getProxy('VizPage');
+  }
+
+  view({attrs}: m.CVnode<PageWithTraceAttrs>) {
+    return m(
+      '.viz-page',
+      m(VegaView, {
+        spec: SPEC,
+        engine: this.engine,
+        data: {},
+      }),
+      m(Editor, {
+        onUpdate: (text: string) => {
+          SPEC = text;
+          attrs.trace.scheduleFullRedraw();
+        },
+      }),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/index.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/index.ts
new file mode 100644
index 0000000..44fade1
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/index.ts
@@ -0,0 +1,36 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {App} from '../../public/app';
+import {PerfettoPlugin} from '../../public/plugin';
+import {WidgetsPage} from './widgets_page';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.WidgetsPage';
+
+  static onActivate(app: App): void {
+    app.pages.registerPage({
+      route: '/widgets',
+      page: WidgetsPage,
+      traceless: true,
+    });
+    app.sidebar.addMenuItem({
+      section: 'navigation',
+      text: 'Widgets',
+      href: '#!/widgets',
+      icon: 'widgets',
+      sortOrder: 99,
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/table_showcase.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/table_showcase.ts
new file mode 100644
index 0000000..00936bc
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/table_showcase.ts
@@ -0,0 +1,69 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {
+  ColumnDescriptor,
+  numberColumn,
+  stringColumn,
+  Table,
+  TableData,
+} from '../../widgets/table';
+
+// This file serves as an example of a table component present in the widgets
+// showcase. Since table is somewhat complicated component that requires some
+// setup spread across several declarations, all the necessary code resides in a
+// separate file (this one) and provides a no-argument wrapper component that
+// can be used in the widgets showcase directly.
+
+interface ProgrammingLanguage {
+  id: number;
+  name: string;
+  year: number;
+}
+
+const languagesList: ProgrammingLanguage[] = [
+  {
+    id: 1,
+    name: 'TypeScript',
+    year: 2012,
+  },
+  {
+    id: 2,
+    name: 'JavaScript',
+    year: 1995,
+  },
+  {
+    id: 3,
+    name: 'Lean',
+    year: 2013,
+  },
+];
+
+const columns: ColumnDescriptor<ProgrammingLanguage>[] = [
+  numberColumn('ID', (x) => x.id),
+  stringColumn('Name', (x) => x.name),
+  numberColumn('Year', (x) => x.year),
+];
+
+export class TableShowcase implements m.ClassComponent {
+  data = new TableData(languagesList);
+
+  view(): m.Child {
+    return m(Table, {
+      data: this.data,
+      columns,
+    });
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
new file mode 100644
index 0000000..7bbcace
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
@@ -0,0 +1,1505 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {classNames} from '../../base/classnames';
+import {Hotkey, Platform} from '../../base/hotkeys';
+import {isString} from '../../base/object_utils';
+import {Icons} from '../../base/semantic_icons';
+import {Anchor} from '../../widgets/anchor';
+import {Button} from '../../widgets/button';
+import {Callout} from '../../widgets/callout';
+import {Checkbox} from '../../widgets/checkbox';
+import {Editor} from '../../widgets/editor';
+import {EmptyState} from '../../widgets/empty_state';
+import {Form, FormLabel} from '../../widgets/form';
+import {HotkeyGlyphs} from '../../widgets/hotkey_glyphs';
+import {Icon} from '../../widgets/icon';
+import {Menu, MenuDivider, MenuItem, PopupMenu2} from '../../widgets/menu';
+import {showModal} from '../../widgets/modal';
+import {
+  MultiSelect,
+  MultiSelectDiff,
+  PopupMultiSelect,
+} from '../../widgets/multiselect';
+import {Popup, PopupPosition} from '../../widgets/popup';
+import {Portal} from '../../widgets/portal';
+import {Select} from '../../widgets/select';
+import {Spinner} from '../../widgets/spinner';
+import {Switch} from '../../widgets/switch';
+import {TextInput} from '../../widgets/text_input';
+import {MultiParagraphText, TextParagraph} from '../../widgets/text_paragraph';
+import {LazyTreeNode, Tree, TreeNode} from '../../widgets/tree';
+import {VegaView} from '../../widgets/vega_view';
+import {PageAttrs} from '../../public/page';
+import {PopupMenuButton} from '../../widgets/popup_menu';
+import {TableShowcase} from './table_showcase';
+import {TreeTable, TreeTableAttrs} from '../../frontend/widgets/treetable';
+import {Intent} from '../../widgets/common';
+import {
+  VirtualTable,
+  VirtualTableAttrs,
+  VirtualTableRow,
+} from '../../widgets/virtual_table';
+import {TagInput} from '../../widgets/tag_input';
+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 {CopyableLink} from '../../widgets/copyable_link';
+
+const DATA_ENGLISH_LETTER_FREQUENCY = {
+  table: [
+    {category: 'a', amount: 8.167},
+    {category: 'b', amount: 1.492},
+    {category: 'c', amount: 2.782},
+    {category: 'd', amount: 4.253},
+    {category: 'e', amount: 12.7},
+    {category: 'f', amount: 2.228},
+    {category: 'g', amount: 2.015},
+    {category: 'h', amount: 6.094},
+    {category: 'i', amount: 6.966},
+    {category: 'j', amount: 0.253},
+    {category: 'k', amount: 1.772},
+    {category: 'l', amount: 4.025},
+    {category: 'm', amount: 2.406},
+    {category: 'n', amount: 6.749},
+    {category: 'o', amount: 7.507},
+    {category: 'p', amount: 1.929},
+    {category: 'q', amount: 0.095},
+    {category: 'r', amount: 5.987},
+    {category: 's', amount: 6.327},
+    {category: 't', amount: 9.056},
+    {category: 'u', amount: 2.758},
+    {category: 'v', amount: 0.978},
+    {category: 'w', amount: 2.36},
+    {category: 'x', amount: 0.25},
+    {category: 'y', amount: 1.974},
+    {category: 'z', amount: 0.074},
+  ],
+};
+
+const DATA_POLISH_LETTER_FREQUENCY = {
+  table: [
+    {category: 'a', amount: 8.965},
+    {category: 'b', amount: 1.482},
+    {category: 'c', amount: 3.988},
+    {category: 'd', amount: 3.293},
+    {category: 'e', amount: 7.921},
+    {category: 'f', amount: 0.312},
+    {category: 'g', amount: 1.377},
+    {category: 'h', amount: 1.072},
+    {category: 'i', amount: 8.286},
+    {category: 'j', amount: 2.343},
+    {category: 'k', amount: 3.411},
+    {category: 'l', amount: 2.136},
+    {category: 'm', amount: 2.911},
+    {category: 'n', amount: 5.6},
+    {category: 'o', amount: 7.59},
+    {category: 'p', amount: 3.101},
+    {category: 'q', amount: 0.003},
+    {category: 'r', amount: 4.571},
+    {category: 's', amount: 4.263},
+    {category: 't', amount: 3.966},
+    {category: 'u', amount: 2.347},
+    {category: 'v', amount: 0.034},
+    {category: 'w', amount: 4.549},
+    {category: 'x', amount: 0.019},
+    {category: 'y', amount: 3.857},
+    {category: 'z', amount: 5.62},
+  ],
+};
+
+const DATA_EMPTY = {};
+
+const SPEC_BAR_CHART = `
+{
+  "$schema": "https://vega.github.io/schema/vega/v5.json",
+  "description": "A basic bar chart example, with value labels shown upon mouse hover.",
+  "width": 400,
+  "height": 200,
+  "padding": 5,
+
+  "data": [
+    {
+      "name": "table"
+    }
+  ],
+
+  "signals": [
+    {
+      "name": "tooltip",
+      "value": {},
+      "on": [
+        {"events": "rect:mouseover", "update": "datum"},
+        {"events": "rect:mouseout",  "update": "{}"}
+      ]
+    }
+  ],
+
+  "scales": [
+    {
+      "name": "xscale",
+      "type": "band",
+      "domain": {"data": "table", "field": "category"},
+      "range": "width",
+      "padding": 0.05,
+      "round": true
+    },
+    {
+      "name": "yscale",
+      "domain": {"data": "table", "field": "amount"},
+      "nice": true,
+      "range": "height"
+    }
+  ],
+
+  "axes": [
+    { "orient": "bottom", "scale": "xscale" },
+    { "orient": "left", "scale": "yscale" }
+  ],
+
+  "marks": [
+    {
+      "type": "rect",
+      "from": {"data":"table"},
+      "encode": {
+        "enter": {
+          "x": {"scale": "xscale", "field": "category"},
+          "width": {"scale": "xscale", "band": 1},
+          "y": {"scale": "yscale", "field": "amount"},
+          "y2": {"scale": "yscale", "value": 0}
+        },
+        "update": {
+          "fill": {"value": "steelblue"}
+        },
+        "hover": {
+          "fill": {"value": "red"}
+        }
+      }
+    },
+    {
+      "type": "text",
+      "encode": {
+        "enter": {
+          "align": {"value": "center"},
+          "baseline": {"value": "bottom"},
+          "fill": {"value": "#333"}
+        },
+        "update": {
+          "x": {"scale": "xscale", "signal": "tooltip.category", "band": 0.5},
+          "y": {"scale": "yscale", "signal": "tooltip.amount", "offset": -2},
+          "text": {"signal": "tooltip.amount"},
+          "fillOpacity": [
+            {"test": "datum === tooltip", "value": 0},
+            {"value": 1}
+          ]
+        }
+      }
+    }
+  ]
+}
+`;
+
+const SPEC_BAR_CHART_LITE = `
+{
+  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
+  "description": "A simple bar chart with embedded data.",
+  "data": {
+    "name": "table"
+  },
+  "mark": "bar",
+  "encoding": {
+    "x": {"field": "category", "type": "nominal", "axis": {"labelAngle": 0}},
+    "y": {"field": "amount", "type": "quantitative"}
+  }
+}
+`;
+
+const SPEC_BROKEN = `{
+  "description": 123
+}
+`;
+
+enum SpecExample {
+  BarChart = 'Barchart',
+  BarChartLite = 'Barchart (Lite)',
+  Broken = 'Broken',
+}
+
+enum DataExample {
+  English = 'English',
+  Polish = 'Polish',
+  Empty = 'Empty',
+}
+
+function arg<T>(
+  anyArg: unknown,
+  valueIfTrue: T,
+  valueIfFalse: T | undefined = undefined,
+): T | undefined {
+  return Boolean(anyArg) ? valueIfTrue : valueIfFalse;
+}
+
+function getExampleSpec(example: SpecExample): string {
+  switch (example) {
+    case SpecExample.BarChart:
+      return SPEC_BAR_CHART;
+    case SpecExample.BarChartLite:
+      return SPEC_BAR_CHART_LITE;
+    case SpecExample.Broken:
+      return SPEC_BROKEN;
+    default:
+      const exhaustiveCheck: never = example;
+      throw new Error(`Unhandled case: ${exhaustiveCheck}`);
+  }
+}
+
+function getExampleData(example: DataExample) {
+  switch (example) {
+    case DataExample.English:
+      return DATA_ENGLISH_LETTER_FREQUENCY;
+    case DataExample.Polish:
+      return DATA_POLISH_LETTER_FREQUENCY;
+    case DataExample.Empty:
+      return DATA_EMPTY;
+    default:
+      const exhaustiveCheck: never = example;
+      throw new Error(`Unhandled case: ${exhaustiveCheck}`);
+  }
+}
+
+const options: {[key: string]: boolean} = {
+  foobar: false,
+  foo: false,
+  bar: false,
+  baz: false,
+  qux: false,
+  quux: false,
+  corge: false,
+  grault: false,
+  garply: false,
+  waldo: false,
+  fred: false,
+  plugh: false,
+  xyzzy: false,
+  thud: false,
+};
+
+function PortalButton() {
+  let portalOpen = false;
+
+  return {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    view: function ({attrs}: any) {
+      const {zIndex = true, absolute = true, top = true} = attrs;
+      return [
+        m(Button, {
+          label: 'Toggle Portal',
+          intent: Intent.Primary,
+          onclick: () => {
+            portalOpen = !portalOpen;
+            scheduleFullRedraw();
+          },
+        }),
+        portalOpen &&
+          m(
+            Portal,
+            {
+              style: {
+                position: arg(absolute, 'absolute'),
+                top: arg(top, '0'),
+                zIndex: arg(zIndex, '10', '0'),
+                background: 'white',
+              },
+            },
+            m(
+              '',
+              `A very simple portal - a div rendered outside of the normal
+              flow of the page`,
+            ),
+          ),
+      ];
+    },
+  };
+}
+
+function lorem() {
+  const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
+      tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
+      veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+      commodo consequat.Duis aute irure dolor in reprehenderit in voluptate
+      velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
+      cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
+      est laborum.`;
+  return m('', {style: {width: '200px'}}, text);
+}
+
+function ControlledPopup() {
+  let popupOpen = false;
+
+  return {
+    view: function () {
+      return m(
+        Popup,
+        {
+          trigger: m(Button, {label: `${popupOpen ? 'Close' : 'Open'} Popup`}),
+          isOpen: popupOpen,
+          onChange: (shouldOpen: boolean) => (popupOpen = shouldOpen),
+        },
+        m(Button, {
+          label: 'Close Popup',
+          onclick: () => {
+            popupOpen = !popupOpen;
+            scheduleFullRedraw();
+          },
+        }),
+      );
+    },
+  };
+}
+
+type Options = {
+  [key: string]: EnumOption | boolean | string | number;
+};
+
+class EnumOption {
+  constructor(
+    public initial: string,
+    public options: string[],
+  ) {}
+}
+
+interface WidgetTitleAttrs {
+  label: string;
+}
+
+function recursiveTreeNode(): m.Children {
+  return m(LazyTreeNode, {
+    left: 'Recursive',
+    right: '...',
+    fetchData: async () => {
+      // await new Promise((r) => setTimeout(r, 1000));
+      return () => recursiveTreeNode();
+    },
+  });
+}
+
+class WidgetTitle implements m.ClassComponent<WidgetTitleAttrs> {
+  view({attrs}: m.CVnode<WidgetTitleAttrs>) {
+    const {label} = attrs;
+    const id = label.replaceAll(' ', '').toLowerCase();
+    const href = `#!/widgets#${id}`;
+    return m(Anchor, {id, href}, m('h2', label));
+  }
+}
+
+interface WidgetShowcaseAttrs {
+  label: string;
+  description?: string;
+  initialOpts?: Options;
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  renderWidget: (options: any) => any;
+  wide?: boolean;
+}
+
+// A little helper class to render any vnode with a dynamic set of options
+class WidgetShowcase implements m.ClassComponent<WidgetShowcaseAttrs> {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  private optValues: any = {};
+  private opts?: Options;
+
+  renderOptions(listItems: m.Child[]): m.Child {
+    if (listItems.length === 0) {
+      return null;
+    }
+    return m('.widget-controls', m('h3', 'Options'), m('ul', listItems));
+  }
+
+  oninit({attrs: {initialOpts: opts}}: m.Vnode<WidgetShowcaseAttrs, this>) {
+    this.opts = opts;
+    if (opts) {
+      // Make the initial options values
+      for (const key in opts) {
+        if (Object.prototype.hasOwnProperty.call(opts, key)) {
+          const option = opts[key];
+          if (option instanceof EnumOption) {
+            this.optValues[key] = option.initial;
+          } else {
+            this.optValues[key] = option;
+          }
+        }
+      }
+    }
+  }
+
+  view({attrs}: m.CVnode<WidgetShowcaseAttrs>) {
+    const {renderWidget, wide, label, description} = attrs;
+    const listItems = [];
+
+    if (this.opts) {
+      for (const key in this.opts) {
+        if (Object.prototype.hasOwnProperty.call(this.opts, key)) {
+          listItems.push(m('li', this.renderControlForOption(key)));
+        }
+      }
+    }
+
+    return [
+      m(WidgetTitle, {label}),
+      description && m('p', description),
+      m(
+        '.widget-block',
+        m(
+          'div',
+          {
+            class: classNames(
+              'widget-container',
+              wide && 'widget-container-wide',
+            ),
+          },
+          renderWidget(this.optValues),
+        ),
+        this.renderOptions(listItems),
+      ),
+    ];
+  }
+
+  private renderControlForOption(key: string) {
+    if (!this.opts) return null;
+    const value = this.opts[key];
+    if (value instanceof EnumOption) {
+      return this.renderEnumOption(key, value);
+    } else if (typeof value === 'boolean') {
+      return this.renderBooleanOption(key);
+    } else if (isString(value)) {
+      return this.renderStringOption(key);
+    } else if (typeof value === 'number') {
+      return this.renderNumberOption(key);
+    } else {
+      return null;
+    }
+  }
+
+  private renderBooleanOption(key: string) {
+    return m(Checkbox, {
+      checked: this.optValues[key],
+      label: key,
+      onchange: () => {
+        this.optValues[key] = !Boolean(this.optValues[key]);
+        scheduleFullRedraw();
+      },
+    });
+  }
+
+  private renderStringOption(key: string) {
+    return m(
+      'label',
+      `${key}:`,
+      m(TextInput, {
+        placeholder: key,
+        value: this.optValues[key],
+        oninput: (e: Event) => {
+          this.optValues[key] = (e.target as HTMLInputElement).value;
+          scheduleFullRedraw();
+        },
+      }),
+    );
+  }
+
+  private renderNumberOption(key: string) {
+    return m(
+      'label',
+      `${key}:`,
+      m(TextInput, {
+        type: 'number',
+        placeholder: key,
+        value: this.optValues[key],
+        oninput: (e: Event) => {
+          this.optValues[key] = Number.parseInt(
+            (e.target as HTMLInputElement).value,
+          );
+          scheduleFullRedraw();
+        },
+      }),
+    );
+  }
+
+  private renderEnumOption(key: string, opt: EnumOption) {
+    const optionElements = opt.options.map((option: string) => {
+      return m('option', {value: option}, option);
+    });
+    return m(
+      'label',
+      `${key}:`,
+      m(
+        Select,
+        {
+          value: this.optValues[key],
+          onchange: (e: Event) => {
+            const el = e.target as HTMLSelectElement;
+            this.optValues[key] = el.value;
+            scheduleFullRedraw();
+          },
+        },
+        optionElements,
+      ),
+    );
+  }
+}
+
+interface File {
+  name: string;
+  size: string;
+  date: string;
+  children?: File[];
+}
+
+const files: File[] = [
+  {
+    name: 'foo',
+    size: '10MB',
+    date: '2023-04-02',
+  },
+  {
+    name: 'bar',
+    size: '123KB',
+    date: '2023-04-08',
+    children: [
+      {
+        name: 'baz',
+        size: '4KB',
+        date: '2023-05-07',
+      },
+      {
+        name: 'qux',
+        size: '18KB',
+        date: '2023-05-28',
+        children: [
+          {
+            name: 'quux',
+            size: '4KB',
+            date: '2023-05-07',
+          },
+          {
+            name: 'corge',
+            size: '18KB',
+            date: '2023-05-28',
+            children: [
+              {
+                name: 'grault',
+                size: '4KB',
+                date: '2023-05-07',
+              },
+              {
+                name: 'garply',
+                size: '18KB',
+                date: '2023-05-28',
+              },
+              {
+                name: 'waldo',
+                size: '87KB',
+                date: '2023-05-02',
+              },
+            ],
+          },
+        ],
+      },
+    ],
+  },
+  {
+    name: 'fred',
+    size: '8KB',
+    date: '2022-12-27',
+  },
+];
+
+let virtualTableData: {offset: number; rows: VirtualTableRow[]} = {
+  offset: 0,
+  rows: [],
+};
+
+function TagInputDemo() {
+  const tags: string[] = ['foo', 'bar', 'baz'];
+  let tagInputValue: string = '';
+
+  return {
+    view: () => {
+      return m(TagInput, {
+        tags,
+        value: tagInputValue,
+        onTagAdd: (tag) => {
+          tags.push(tag);
+          tagInputValue = '';
+          scheduleFullRedraw();
+        },
+        onChange: (value) => {
+          tagInputValue = value;
+        },
+        onTagRemove: (index) => {
+          tags.splice(index, 1);
+          scheduleFullRedraw();
+        },
+      });
+    },
+  };
+}
+
+function SegmentedButtonsDemo({attrs}: {attrs: {}}) {
+  let selectedIdx = 0;
+  return {
+    view: () => {
+      return m(SegmentedButtons, {
+        ...attrs,
+        options: [{label: 'Yes'}, {label: 'Maybe'}, {label: 'No'}],
+        selectedOption: selectedIdx,
+        onOptionSelected: (num) => {
+          selectedIdx = num;
+          scheduleFullRedraw();
+        },
+      });
+    },
+  };
+}
+
+export class WidgetsPage implements m.ClassComponent<PageAttrs> {
+  view() {
+    return m(
+      '.widgets-page',
+      m('h1', 'Widgets'),
+      m(WidgetShowcase, {
+        label: 'Button',
+        renderWidget: ({label, icon, rightIcon, ...rest}) =>
+          m(Button, {
+            icon: arg(icon, 'send'),
+            rightIcon: arg(rightIcon, 'arrow_forward'),
+            label: arg(label, 'Button', ''),
+            ...rest,
+          }),
+        initialOpts: {
+          label: true,
+          icon: true,
+          rightIcon: false,
+          disabled: false,
+          intent: new EnumOption(Intent.None, Object.values(Intent)),
+          active: false,
+          compact: false,
+          loading: false,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Segmented Buttons',
+        description: `
+          Segmented buttons are a group of buttons where one of them is
+          'selected'; they act similar to a set of radio buttons.
+        `,
+        renderWidget: (opts) => m(SegmentedButtonsDemo, opts),
+        initialOpts: {
+          disabled: false,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Checkbox',
+        renderWidget: (opts) => m(Checkbox, {label: 'Checkbox', ...opts}),
+        initialOpts: {
+          disabled: false,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Switch',
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        renderWidget: ({label, ...rest}: any) =>
+          m(Switch, {label: arg(label, 'Switch'), ...rest}),
+        initialOpts: {
+          label: true,
+          disabled: false,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Text Input',
+        renderWidget: ({placeholder, ...rest}) =>
+          m(TextInput, {
+            placeholder: arg(placeholder, 'Placeholder...', ''),
+            ...rest,
+          }),
+        initialOpts: {
+          placeholder: true,
+          disabled: false,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Select',
+        renderWidget: (opts) =>
+          m(Select, opts, [
+            m('option', {value: 'foo', label: 'Foo'}),
+            m('option', {value: 'bar', label: 'Bar'}),
+            m('option', {value: 'baz', label: 'Baz'}),
+          ]),
+        initialOpts: {
+          disabled: false,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Empty State',
+        renderWidget: ({header, content}) =>
+          m(
+            EmptyState,
+            {
+              title: arg(header, 'No search results found...'),
+            },
+            arg(content, m(Button, {label: 'Try again'})),
+          ),
+        initialOpts: {
+          header: true,
+          content: true,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Anchor',
+        renderWidget: ({icon}) =>
+          m(
+            Anchor,
+            {
+              icon: arg(icon, 'open_in_new'),
+              href: 'https://perfetto.dev/docs/',
+              target: '_blank',
+            },
+            'This is some really long text and it will probably overflow the container',
+          ),
+        initialOpts: {
+          icon: true,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'CopyableLink',
+        renderWidget: ({noicon}) =>
+          m(CopyableLink, {
+            noicon: arg(noicon, true),
+            url: 'https://perfetto.dev/docs/',
+          }),
+        initialOpts: {
+          noicon: false,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Table',
+        renderWidget: () => m(TableShowcase),
+        initialOpts: {},
+        wide: true,
+      }),
+      m(WidgetShowcase, {
+        label: 'Portal',
+        description: `A portal is a div rendered out of normal flow
+          of the hierarchy.`,
+        renderWidget: (opts) => m(PortalButton, opts),
+        initialOpts: {
+          absolute: true,
+          zIndex: true,
+          top: true,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Popup',
+        description: `A popup is a nicely styled portal element whose position is
+        dynamically updated to appear to float alongside a specific element on
+        the page, even as the element is moved and scrolled around.`,
+        renderWidget: (opts) =>
+          m(
+            Popup,
+            {
+              trigger: m(Button, {label: 'Toggle Popup'}),
+              ...opts,
+            },
+            lorem(),
+          ),
+        initialOpts: {
+          position: new EnumOption(
+            PopupPosition.Auto,
+            Object.values(PopupPosition),
+          ),
+          closeOnEscape: true,
+          closeOnOutsideClick: true,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Controlled Popup',
+        description: `The open/close state of a controlled popup is passed in via
+        the 'isOpen' attribute. This means we can get open or close the popup
+        from wherever we like. E.g. from a button inside the popup.
+        Keeping this state external also means we can modify other parts of the
+        page depending on whether the popup is open or not, such as the text
+        on this button.
+        Note, this is the same component as the popup above, but used in
+        controlled mode.`,
+        renderWidget: (opts) => m(ControlledPopup, opts),
+        initialOpts: {},
+      }),
+      m(WidgetShowcase, {
+        label: 'Icon',
+        renderWidget: (opts) => m(Icon, {icon: 'star', ...opts}),
+        initialOpts: {filled: false},
+      }),
+      m(WidgetShowcase, {
+        label: 'MultiSelect panel',
+        renderWidget: ({...rest}) =>
+          m(MultiSelect, {
+            options: Object.entries(options).map(([key, value]) => {
+              return {
+                id: key,
+                name: key,
+                checked: value,
+              };
+            }),
+            onChange: (diffs: MultiSelectDiff[]) => {
+              diffs.forEach(({id, checked}) => {
+                options[id] = checked;
+              });
+              scheduleFullRedraw();
+            },
+            ...rest,
+          }),
+        initialOpts: {
+          repeatCheckedItemsAtTop: false,
+          fixedSize: false,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Popup with MultiSelect',
+        renderWidget: ({icon, ...rest}) =>
+          m(PopupMultiSelect, {
+            options: Object.entries(options).map(([key, value]) => {
+              return {
+                id: key,
+                name: key,
+                checked: value,
+              };
+            }),
+            popupPosition: PopupPosition.Top,
+            label: 'Multi Select',
+            icon: arg(icon, Icons.LibraryAddCheck),
+            onChange: (diffs: MultiSelectDiff[]) => {
+              diffs.forEach(({id, checked}) => {
+                options[id] = checked;
+              });
+              scheduleFullRedraw();
+            },
+            ...rest,
+          }),
+        initialOpts: {
+          icon: true,
+          showNumSelected: true,
+          repeatCheckedItemsAtTop: false,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'PopupMenu',
+        renderWidget: () => {
+          return m(PopupMenuButton, {
+            icon: 'description',
+            items: [
+              {itemType: 'regular', text: 'New', callback: () => {}},
+              {itemType: 'regular', text: 'Open', callback: () => {}},
+              {itemType: 'regular', text: 'Save', callback: () => {}},
+              {itemType: 'regular', text: 'Delete', callback: () => {}},
+              {
+                itemType: 'group',
+                text: 'Share',
+                itemId: 'foo',
+                children: [
+                  {itemType: 'regular', text: 'Friends', callback: () => {}},
+                  {itemType: 'regular', text: 'Family', callback: () => {}},
+                  {itemType: 'regular', text: 'Everyone', callback: () => {}},
+                ],
+              },
+            ],
+          });
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Menu',
+        renderWidget: () =>
+          m(
+            Menu,
+            m(MenuItem, {label: 'New', icon: 'add'}),
+            m(MenuItem, {label: 'Open', icon: 'folder_open'}),
+            m(MenuItem, {label: 'Save', icon: 'save', disabled: true}),
+            m(MenuDivider),
+            m(MenuItem, {label: 'Delete', icon: 'delete'}),
+            m(MenuDivider),
+            m(
+              MenuItem,
+              {label: 'Share', icon: 'share'},
+              m(MenuItem, {label: 'Everyone', icon: 'public'}),
+              m(MenuItem, {label: 'Friends', icon: 'group'}),
+              m(
+                MenuItem,
+                {label: 'Specific people', icon: 'person_add'},
+                m(MenuItem, {label: 'Alice', icon: 'person'}),
+                m(MenuItem, {label: 'Bob', icon: 'person'}),
+              ),
+            ),
+            m(
+              MenuItem,
+              {label: 'More', icon: 'more_horiz'},
+              m(MenuItem, {label: 'Query', icon: 'database'}),
+              m(MenuItem, {label: 'Download', icon: 'download'}),
+              m(MenuItem, {label: 'Clone', icon: 'copy_all'}),
+            ),
+          ),
+      }),
+      m(WidgetShowcase, {
+        label: 'PopupMenu2',
+        renderWidget: (opts) =>
+          m(
+            PopupMenu2,
+            {
+              trigger: m(Button, {
+                label: 'Menu',
+                rightIcon: Icons.ContextMenu,
+              }),
+              ...opts,
+            },
+            m(MenuItem, {label: 'New', icon: 'add'}),
+            m(MenuItem, {label: 'Open', icon: 'folder_open'}),
+            m(MenuItem, {label: 'Save', icon: 'save', disabled: true}),
+            m(MenuDivider),
+            m(MenuItem, {label: 'Delete', icon: 'delete'}),
+            m(MenuDivider),
+            m(
+              MenuItem,
+              {label: 'Share', icon: 'share'},
+              m(MenuItem, {label: 'Everyone', icon: 'public'}),
+              m(MenuItem, {label: 'Friends', icon: 'group'}),
+              m(
+                MenuItem,
+                {label: 'Specific people', icon: 'person_add'},
+                m(MenuItem, {label: 'Alice', icon: 'person'}),
+                m(MenuItem, {label: 'Bob', icon: 'person'}),
+              ),
+            ),
+            m(
+              MenuItem,
+              {label: 'More', icon: 'more_horiz'},
+              m(MenuItem, {label: 'Query', icon: 'database'}),
+              m(MenuItem, {label: 'Download', icon: 'download'}),
+              m(MenuItem, {label: 'Clone', icon: 'copy_all'}),
+            ),
+          ),
+        initialOpts: {
+          popupPosition: new EnumOption(
+            PopupPosition.Bottom,
+            Object.values(PopupPosition),
+          ),
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Spinner',
+        description: `Simple spinner, rotates forever.
+            Width and height match the font size.`,
+        renderWidget: ({fontSize, easing}) =>
+          m('', {style: {fontSize}}, m(Spinner, {easing})),
+        initialOpts: {
+          fontSize: new EnumOption('16px', [
+            '12px',
+            '16px',
+            '24px',
+            '32px',
+            '64px',
+            '128px',
+          ]),
+          easing: false,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Tree',
+        description: `Hierarchical tree with left and right values aligned to
+        a grid.`,
+        renderWidget: (opts) =>
+          m(
+            Tree,
+            opts,
+            m(TreeNode, {left: 'Name', right: 'my_event', icon: 'badge'}),
+            m(TreeNode, {left: 'CPU', right: '2', icon: 'memory'}),
+            m(TreeNode, {
+              left: 'Start time',
+              right: '1s 435ms',
+              icon: 'schedule',
+            }),
+            m(TreeNode, {left: 'Duration', right: '86ms', icon: 'timer'}),
+            m(TreeNode, {
+              left: 'SQL',
+              right: m(
+                PopupMenu2,
+                {
+                  popupPosition: PopupPosition.RightStart,
+                  trigger: m(
+                    Anchor,
+                    {
+                      icon: Icons.ContextMenu,
+                    },
+                    'SELECT * FROM raw WHERE id = 123',
+                  ),
+                },
+                m(MenuItem, {
+                  label: 'Copy SQL Query',
+                  icon: 'content_copy',
+                }),
+                m(MenuItem, {
+                  label: 'Execute Query in new tab',
+                  icon: 'open_in_new',
+                }),
+              ),
+            }),
+            m(TreeNode, {
+              icon: 'account_tree',
+              left: 'Process',
+              right: m(Anchor, {icon: 'open_in_new'}, '/bin/foo[789]'),
+            }),
+            m(TreeNode, {
+              left: 'Thread',
+              right: m(Anchor, {icon: 'open_in_new'}, 'my_thread[456]'),
+            }),
+            m(
+              TreeNode,
+              {
+                left: 'Args',
+                summary: 'foo: string, baz: string, quux: string[4]',
+              },
+              m(TreeNode, {left: 'foo', right: 'bar'}),
+              m(TreeNode, {left: 'baz', right: 'qux'}),
+              m(
+                TreeNode,
+                {left: 'quux', summary: 'string[4]'},
+                m(TreeNode, {left: '[0]', right: 'corge'}),
+                m(TreeNode, {left: '[1]', right: 'grault'}),
+                m(TreeNode, {left: '[2]', right: 'garply'}),
+                m(TreeNode, {left: '[3]', right: 'waldo'}),
+              ),
+            ),
+            m(LazyTreeNode, {
+              left: 'Lazy',
+              icon: 'bedtime',
+              fetchData: async () => {
+                await new Promise((r) => setTimeout(r, 1000));
+                return () => m(TreeNode, {left: 'foo'});
+              },
+            }),
+            m(LazyTreeNode, {
+              left: 'Dynamic',
+              unloadOnCollapse: true,
+              icon: 'bedtime',
+              fetchData: async () => {
+                await new Promise((r) => setTimeout(r, 1000));
+                return () => m(TreeNode, {left: 'foo'});
+              },
+            }),
+            recursiveTreeNode(),
+          ),
+        wide: true,
+      }),
+      m(WidgetShowcase, {
+        label: 'Form',
+        renderWidget: () => renderForm('form'),
+      }),
+      m(WidgetShowcase, {
+        label: 'Nested Popups',
+        renderWidget: () =>
+          m(
+            Popup,
+            {
+              trigger: m(Button, {label: 'Open the popup'}),
+            },
+            m(
+              PopupMenu2,
+              {
+                trigger: m(Button, {label: 'Select an option'}),
+              },
+              m(MenuItem, {label: 'Option 1'}),
+              m(MenuItem, {label: 'Option 2'}),
+            ),
+            m(Button, {
+              label: 'Done',
+              dismissPopup: true,
+            }),
+          ),
+      }),
+      m(WidgetShowcase, {
+        label: 'Callout',
+        renderWidget: () =>
+          m(
+            Callout,
+            {
+              icon: 'info',
+            },
+            'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' +
+              'Nulla rhoncus tempor neque, sed malesuada eros dapibus vel. ' +
+              'Aliquam in ligula vitae tortor porttitor laoreet iaculis ' +
+              'finibus est.',
+          ),
+      }),
+      m(WidgetShowcase, {
+        label: 'Editor',
+        renderWidget: () => m(Editor),
+      }),
+      m(WidgetShowcase, {
+        label: 'VegaView',
+        renderWidget: (opt) =>
+          m(VegaView, {
+            spec: getExampleSpec(opt.exampleSpec),
+            data: getExampleData(opt.exampleData),
+          }),
+        initialOpts: {
+          exampleSpec: new EnumOption(
+            SpecExample.BarChart,
+            Object.values(SpecExample),
+          ),
+          exampleData: new EnumOption(
+            DataExample.English,
+            Object.values(DataExample),
+          ),
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Form within PopupMenu2',
+        description: `A form placed inside a popup menu works just fine,
+              and the cancel/submit buttons also dismiss the popup. A bit more
+              margin is added around it too, which improves the look and feel.`,
+        renderWidget: () =>
+          m(
+            PopupMenu2,
+            {
+              trigger: m(Button, {label: 'Popup!'}),
+            },
+            m(
+              MenuItem,
+              {
+                label: 'Open form...',
+              },
+              renderForm('popup-form'),
+            ),
+          ),
+      }),
+      m(WidgetShowcase, {
+        label: 'Hotkey',
+        renderWidget: (opts) => {
+          if (opts.platform === 'auto') {
+            return m(HotkeyGlyphs, {hotkey: opts.hotkey as Hotkey});
+          } else {
+            const platform = opts.platform as Platform;
+            return m(HotkeyGlyphs, {
+              hotkey: opts.hotkey as Hotkey,
+              spoof: platform,
+            });
+          }
+        },
+        initialOpts: {
+          hotkey: 'Mod+Shift+P',
+          platform: new EnumOption('auto', ['auto', 'Mac', 'PC']),
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Text Paragraph',
+        description: `A basic formatted text paragraph with wrapping. If
+              it is desirable to preserve the original text format/line breaks,
+              set the compressSpace attribute to false.`,
+        renderWidget: (opts) => {
+          return m(TextParagraph, {
+            text: `Lorem ipsum dolor sit amet, consectetur adipiscing
+                         elit. Nulla rhoncus tempor neque, sed malesuada eros
+                         dapibus vel. Aliquam in ligula vitae tortor porttitor
+                         laoreet iaculis finibus est.`,
+            compressSpace: opts.compressSpace,
+          });
+        },
+        initialOpts: {
+          compressSpace: true,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Multi Paragraph Text',
+        description: `A wrapper for multiple paragraph widgets.`,
+        renderWidget: () => {
+          return m(
+            MultiParagraphText,
+            m(TextParagraph, {
+              text: `Lorem ipsum dolor sit amet, consectetur adipiscing
+                         elit. Nulla rhoncus tempor neque, sed malesuada eros
+                         dapibus vel. Aliquam in ligula vitae tortor porttitor
+                         laoreet iaculis finibus est.`,
+              compressSpace: true,
+            }),
+            m(TextParagraph, {
+              text: `Sed ut perspiciatis unde omnis iste natus error sit
+                         voluptatem accusantium doloremque laudantium, totam rem
+                         aperiam, eaque ipsa quae ab illo inventore veritatis et
+                         quasi architecto beatae vitae dicta sunt explicabo.
+                         Nemo enim ipsam voluptatem quia voluptas sit aspernatur
+                         aut odit aut fugit, sed quia consequuntur magni dolores
+                         eos qui ratione voluptatem sequi nesciunt.`,
+              compressSpace: true,
+            }),
+          );
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Modal',
+        description: `A helper for modal dialog.`,
+        renderWidget: () => m(ModalShowcase),
+      }),
+      m(WidgetShowcase, {
+        label: 'TreeTable',
+        description: `Hierarchical tree with multiple columns`,
+        renderWidget: () => {
+          const attrs: TreeTableAttrs<File> = {
+            rows: files,
+            getChildren: (file) => file.children,
+            columns: [
+              {name: 'Name', getData: (file) => file.name},
+              {name: 'Size', getData: (file) => file.size},
+              {name: 'Date', getData: (file) => file.date},
+            ],
+          };
+          return m(TreeTable<File>, attrs);
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'VirtualTable',
+        description: `Virtualized table for efficient rendering of large datasets`,
+        renderWidget: () => {
+          const attrs: VirtualTableAttrs = {
+            columns: [
+              {header: 'x', width: '4em'},
+              {header: 'x^2', width: '8em'},
+            ],
+            rows: virtualTableData.rows,
+            firstRowOffset: virtualTableData.offset,
+            rowHeight: 20,
+            numRows: 500_000,
+            style: {height: '200px'},
+            onReload: (rowOffset, rowCount) => {
+              const rows = [];
+              for (let i = rowOffset; i < rowOffset + rowCount; i++) {
+                rows.push({id: i, cells: [i, i ** 2]});
+              }
+              virtualTableData = {
+                offset: rowOffset,
+                rows,
+              };
+              scheduleFullRedraw();
+            },
+          };
+          return m(VirtualTable, attrs);
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Tag Input',
+        description: `
+          TagInput displays Tag elements inside an input, followed by an
+          interactive text input. The container is styled to look like a
+          TextInput, but the actual editable element appears after the last tag.
+          Clicking anywhere on the container will focus the text input.`,
+        renderWidget: () => m(TagInputDemo),
+      }),
+      m(WidgetShowcase, {
+        label: 'Middle Ellipsis',
+        description: `
+          Sometimes the start and end of a bit of text are more important than
+          the middle. This element puts the ellipsis in the midde if the content
+          is too wide for its container.`,
+        renderWidget: (opts) =>
+          m(
+            'div',
+            {style: {width: Boolean(opts.squeeze) ? '150px' : '450px'}},
+            m(MiddleEllipsis, {
+              text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
+            }),
+          ),
+        initialOpts: {
+          squeeze: false,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Chip',
+        description: `A little chip or tag`,
+        renderWidget: (opts) => {
+          const {icon, ...rest} = opts;
+          return m(
+            ChipBar,
+            m(Chip, {
+              label: 'Foo',
+              icon: icon === true ? 'info' : undefined,
+              ...rest,
+            }),
+            m(Chip, {label: 'Bar', ...rest}),
+            m(Chip, {label: 'Baz', ...rest}),
+          );
+        },
+        initialOpts: {
+          intent: new EnumOption(Intent.None, Object.values(Intent)),
+          icon: true,
+          compact: false,
+          rounded: false,
+        },
+      }),
+      m(WidgetShowcase, {
+        label: 'Track',
+        description: `A track`,
+        renderWidget: (opts) => {
+          const {buttons, chips, multipleTracks, ...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,
+            });
+
+          return m(
+            '',
+            {
+              style: {width: '500px', boxShadow: '0px 0px 1px 1px lightgray'},
+            },
+            Boolean(multipleTracks)
+              ? [renderTrack(), renderTrack(), renderTrack()]
+              : renderTrack(),
+          );
+        },
+        initialOpts: {
+          title: 'This is the title of the track',
+          buttons: true,
+          chips: true,
+          heightPx: 32,
+          indentationLevel: 3,
+          collapsible: true,
+          collapsed: true,
+          isSummary: false,
+          highlight: false,
+          error: false,
+          multipleTracks: false,
+          reorderable: false,
+        },
+      }),
+    );
+  }
+}
+
+class ModalShowcase implements m.ClassComponent {
+  private static counter = 0;
+
+  private static log(txt: string) {
+    const mwlogs = document.getElementById('mwlogs');
+    if (!mwlogs || !(mwlogs instanceof HTMLTextAreaElement)) return;
+    const time = new Date().toLocaleTimeString();
+    mwlogs.value += `[${time}] ${txt}\n`;
+    mwlogs.scrollTop = mwlogs.scrollHeight;
+  }
+
+  private static showModalDialog(staticContent = false) {
+    const id = `N=${++ModalShowcase.counter}`;
+    ModalShowcase.log(`Open ${id}`);
+    const logOnClose = () => ModalShowcase.log(`Close ${id}`);
+
+    let content;
+    if (staticContent) {
+      content = m('.modal-pre', 'Content of the modal dialog.\nEnd of content');
+    } else {
+      const component = {
+        oninit: function (vnode: m.Vnode<{}, {progress: number}>) {
+          vnode.state.progress = ((vnode.state.progress as number) || 0) + 1;
+        },
+        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'),
+            m('progress', {value: vnode.state.progress, max: 100}),
+          );
+        },
+      } as m.Component<{}, {progress: number}>;
+      content = () => m(component);
+    }
+    const closePromise = showModal({
+      title: `Modal dialog ${id}`,
+      buttons: [
+        {text: 'OK', action: () => ModalShowcase.log(`OK ${id}`)},
+        {text: 'Cancel', action: () => ModalShowcase.log(`Cancel ${id}`)},
+        {
+          text: 'Show another now',
+          action: () => ModalShowcase.showModalDialog(),
+        },
+        {
+          text: 'Show another in 2s',
+          action: () => setTimeout(() => ModalShowcase.showModalDialog(), 2000),
+        },
+      ],
+      content,
+    });
+    closePromise.then(logOnClose);
+  }
+
+  view() {
+    return m(
+      'div',
+      {
+        style: {
+          'display': 'flex',
+          'flex-direction': 'column',
+          'width': '100%',
+        },
+      },
+      m('textarea', {
+        id: 'mwlogs',
+        readonly: 'readonly',
+        rows: '8',
+        placeholder: 'Logs will appear here',
+      }),
+      m('input[type=button]', {
+        value: 'Show modal (static)',
+        onclick: () => ModalShowcase.showModalDialog(true),
+      }),
+      m('input[type=button]', {
+        value: 'Show modal (dynamic)',
+        onclick: () => ModalShowcase.showModalDialog(false),
+      }),
+    );
+  }
+} // class ModalShowcase
+
+function renderForm(id: string) {
+  return m(
+    Form,
+    {
+      submitLabel: 'Submit',
+      submitIcon: 'send',
+      cancelLabel: 'Cancel',
+      resetLabel: 'Reset',
+      onSubmit: () => window.alert('Form submitted!'),
+    },
+    m(FormLabel, {for: `${id}-foo`}, 'Foo'),
+    m(TextInput, {id: `${id}-foo`}),
+    m(FormLabel, {for: `${id}-bar`}, 'Bar'),
+    m(Select, {id: `${id}-bar`}, [
+      m('option', {value: 'foo', label: 'Foo'}),
+      m('option', {value: 'bar', label: 'Bar'}),
+      m('option', {value: 'baz', label: 'Baz'}),
+    ]),
+  );
+}
diff --git a/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/critical_user_interaction_track.ts b/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/critical_user_interaction_track.ts
new file mode 100644
index 0000000..d377b23
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/critical_user_interaction_track.ts
@@ -0,0 +1,129 @@
+// 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 {NAMED_ROW} from '../../frontend/named_slice_track';
+import {LONG, NUM, STR} from '../../trace_processor/query_result';
+import {Slice} from '../../public/track';
+import {
+  CustomSqlImportConfig,
+  CustomSqlTableDefConfig,
+  CustomSqlTableSliceTrack,
+} from '../../frontend/tracks/custom_sql_table_slice_track';
+import {TrackEventDetails, TrackEventSelection} from '../../public/selection';
+import {Duration, Time} from '../../base/time';
+import {PageLoadDetailsPanel} from './page_load_details_panel';
+import {StartupDetailsPanel} from './startup_details_panel';
+import {WebContentInteractionPanel} from './web_content_interaction_details_panel';
+import {GenericSliceDetailsTab} from '../../frontend/generic_slice_details_tab';
+
+export const CRITICAL_USER_INTERACTIONS_KIND =
+  'org.chromium.CriticalUserInteraction.track';
+
+export const CRITICAL_USER_INTERACTIONS_ROW = {
+  ...NAMED_ROW,
+  scopedId: NUM,
+  type: STR,
+};
+export type CriticalUserInteractionRow = typeof CRITICAL_USER_INTERACTIONS_ROW;
+
+export interface CriticalUserInteractionSlice extends Slice {
+  scopedId: number;
+  type: string;
+}
+
+export class CriticalUserInteractionTrack extends CustomSqlTableSliceTrack {
+  static readonly kind = `/critical_user_interactions`;
+
+  getSqlDataSource(): CustomSqlTableDefConfig {
+    return {
+      columns: [
+        // The scoped_id is not a unique identifier within the table; generate
+        // a unique id from type and scoped_id on the fly to use for slice
+        // selection.
+        'hash(type, scoped_id) AS id',
+        'scoped_id AS scopedId',
+        'name',
+        'ts',
+        'dur',
+        'type',
+      ],
+      sqlTableName: 'chrome_interactions',
+    };
+  }
+
+  async getSelectionDetails(
+    id: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const query = `
+      SELECT
+        ts,
+        dur,
+        type
+      FROM (${this.getSqlSource()})
+      WHERE id = ${id}
+    `;
+
+    const result = await this.engine.query(query);
+    if (result.numRows() === 0) {
+      return undefined;
+    }
+
+    const row = result.iter({
+      ts: LONG,
+      dur: LONG,
+      type: STR,
+    });
+
+    return {
+      ts: Time.fromRaw(row.ts),
+      dur: Duration.fromRaw(row.dur),
+      interactionType: row.type,
+    };
+  }
+
+  getSqlImports(): CustomSqlImportConfig {
+    return {
+      modules: ['chrome.interactions'],
+    };
+  }
+
+  getRowSpec(): CriticalUserInteractionRow {
+    return CRITICAL_USER_INTERACTIONS_ROW;
+  }
+
+  rowToSlice(row: CriticalUserInteractionRow): CriticalUserInteractionSlice {
+    const baseSlice = super.rowToSlice(row);
+    const scopedId = row.scopedId;
+    const type = row.type;
+    return {...baseSlice, scopedId, type};
+  }
+
+  override detailsPanel(sel: TrackEventSelection) {
+    switch (sel.interactionType) {
+      case 'chrome_page_loads':
+        return new PageLoadDetailsPanel(this.trace, sel.eventId);
+      case 'chrome_startups':
+        return new StartupDetailsPanel(this.trace, sel.eventId);
+      case 'chrome_web_content_interactions':
+        return new WebContentInteractionPanel(this.trace, sel.eventId);
+      default:
+        return new GenericSliceDetailsTab(
+          this.trace,
+          'chrome_interactions',
+          sel.eventId,
+          'Chrome Interaction',
+        );
+    }
+  }
+}
diff --git a/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/index.ts b/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/index.ts
new file mode 100644
index 0000000..6b96209
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/index.ts
@@ -0,0 +1,48 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {CriticalUserInteractionTrack} from './critical_user_interaction_track';
+import {TrackNode} from '../../public/workspace';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'org.chromium.CriticalUserInteraction';
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    ctx.commands.registerCommand({
+      id: 'perfetto.CriticalUserInteraction.AddInteractionTrack',
+      name: 'Add track: Chrome interactions',
+      callback: () => {
+        const track = new TrackNode({
+          uri: CriticalUserInteractionTrack.kind,
+          title: 'Chrome Interactions',
+        });
+        ctx.workspace.addChildInOrder(track);
+        track.pin();
+      },
+    });
+
+    ctx.tracks.registerTrack({
+      uri: CriticalUserInteractionTrack.kind,
+      tags: {
+        kind: CriticalUserInteractionTrack.kind,
+      },
+      title: 'Chrome Interactions',
+      track: new CriticalUserInteractionTrack({
+        trace: ctx,
+        uri: CriticalUserInteractionTrack.kind,
+      }),
+    });
+  }
+}
diff --git a/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/page_load_details_panel.ts b/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/page_load_details_panel.ts
new file mode 100644
index 0000000..e2d0b54
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/page_load_details_panel.ts
@@ -0,0 +1,72 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {
+  Details,
+  DetailsSchema,
+} from '../../frontend/widgets/sql/details/details';
+import {DetailsShell} from '../../widgets/details_shell';
+import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
+import d = DetailsSchema;
+
+export class PageLoadDetailsPanel implements TrackEventDetailsPanel {
+  private data: Details;
+
+  constructor(
+    private readonly trace: Trace,
+    id: number,
+  ) {
+    this.data = new Details(this.trace, 'chrome_page_loads', id, {
+      'Navigation start': d.Timestamp('navigation_start_ts'),
+      'FCP event': d.Timestamp('fcp_ts'),
+      'FCP': d.Interval('navigation_start_ts', 'fcp'),
+      'LCP event': d.Timestamp('lcp_ts', {skipIfNull: true}),
+      'LCP': d.Interval('navigation_start_ts', 'lcp', {skipIfNull: true}),
+      'DOMContentLoaded': d.Timestamp('dom_content_loaded_event_ts', {
+        skipIfNull: true,
+      }),
+      'onload timestamp': d.Timestamp('load_event_ts', {skipIfNull: true}),
+      'performance.mark timings': d.Dict({
+        data: {
+          'Fully loaded': d.Timestamp('mark_fully_loaded_ts', {
+            skipIfNull: true,
+          }),
+          'Fully visible': d.Timestamp('mark_fully_visible_ts', {
+            skipIfNull: true,
+          }),
+          'Interactive': d.Timestamp('mark_interactive_ts', {
+            skipIfNull: true,
+          }),
+        },
+        skipIfEmpty: true,
+      }),
+      'Navigation ID': 'navigation_id',
+      'Browser process': d.SqlIdRef('process', 'browser_upid'),
+      'URL': d.URLValue('url'),
+    });
+  }
+
+  render() {
+    return m(
+      DetailsShell,
+      {
+        title: 'Chrome Page Load',
+      },
+      m(GridLayout, m(GridLayoutColumn, this.data.render())),
+    );
+  }
+}
diff --git a/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/startup_details_panel.ts b/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/startup_details_panel.ts
new file mode 100644
index 0000000..0c26623
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/startup_details_panel.ts
@@ -0,0 +1,127 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {duration, Time, time} from '../../base/time';
+import {DurationWidget} from '../../frontend/widgets/duration';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result';
+import {DetailsShell} from '../../widgets/details_shell';
+import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
+import {Section} from '../../widgets/section';
+import {SqlRef} from '../../widgets/sql_ref';
+import {dictToTreeNodes, Tree} from '../../widgets/tree';
+import {asUpid, Upid} from '../../trace_processor/sql_utils/core_types';
+import {Trace} from '../../public/trace';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+
+interface Data {
+  startupId: number;
+  eventName: string;
+  startupBeginTs: time;
+  durToFirstVisibleContent: duration;
+  launchCause?: string;
+  upid: Upid;
+}
+
+export class StartupDetailsPanel implements TrackEventDetailsPanel {
+  private data?: Data;
+
+  constructor(
+    private readonly trace: Trace,
+    private readonly id: number,
+  ) {}
+
+  async load() {
+    const queryResult = await this.trace.engine.query(`
+      SELECT
+        activity_id AS startupId,
+        name,
+        startup_begin_ts AS startupBeginTs,
+        CASE
+          WHEN first_visible_content_ts IS NULL THEN 0
+          ELSE first_visible_content_ts - startup_begin_ts
+        END AS durTofirstVisibleContent,
+        launch_cause AS launchCause,
+        browser_upid AS upid
+      FROM chrome_startups
+      WHERE id = ${this.id};
+    `);
+
+    const iter = queryResult.firstRow({
+      startupId: NUM,
+      name: STR,
+      startupBeginTs: LONG,
+      durTofirstVisibleContent: LONG,
+      launchCause: STR_NULL,
+      upid: NUM,
+    });
+
+    this.data = {
+      startupId: iter.startupId,
+      eventName: iter.name,
+      startupBeginTs: Time.fromRaw(iter.startupBeginTs),
+      durToFirstVisibleContent: iter.durTofirstVisibleContent,
+      upid: asUpid(iter.upid),
+    };
+
+    if (iter.launchCause) {
+      this.data.launchCause = iter.launchCause;
+    }
+  }
+
+  private getDetailsDictionary() {
+    const details: {[key: string]: m.Child} = {};
+    if (this.data === undefined) return details;
+    details['Activity ID'] = this.data.startupId;
+    details['Browser Upid'] = this.data.upid;
+    details['Startup Event'] = this.data.eventName;
+    details['Startup Timestamp'] = m(Timestamp, {ts: this.data.startupBeginTs});
+    details['Duration to First Visible Content'] = m(DurationWidget, {
+      dur: this.data.durToFirstVisibleContent,
+    });
+    if (this.data.launchCause) {
+      details['Launch Cause'] = this.data.launchCause;
+    }
+    details['SQL ID'] = m(SqlRef, {
+      table: 'chrome_startups',
+      id: this.id,
+    });
+    return details;
+  }
+
+  render() {
+    if (!this.data) {
+      return m('h2', 'Loading');
+    }
+
+    return m(
+      DetailsShell,
+      {
+        title: 'Chrome Startup',
+      },
+      m(
+        GridLayout,
+        m(
+          GridLayoutColumn,
+          m(
+            Section,
+            {title: 'Details'},
+            m(Tree, dictToTreeNodes(this.getDetailsDictionary())),
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/web_content_interaction_details_panel.ts b/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/web_content_interaction_details_panel.ts
new file mode 100644
index 0000000..25e49aa
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/web_content_interaction_details_panel.ts
@@ -0,0 +1,128 @@
+// 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.
+
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {duration, Time, time} from '../../base/time';
+import {asUpid, Upid} from '../../trace_processor/sql_utils/core_types';
+import {DurationWidget} from '../../frontend/widgets/duration';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {LONG, NUM, STR} from '../../trace_processor/query_result';
+import {DetailsShell} from '../../widgets/details_shell';
+import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
+import {Section} from '../../widgets/section';
+import {SqlRef} from '../../widgets/sql_ref';
+import {dictToTreeNodes, Tree} from '../../widgets/tree';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
+
+interface Data {
+  ts: time;
+  dur: duration;
+  interactionType: string;
+  totalDurationMs: duration;
+  upid: Upid;
+}
+
+export class WebContentInteractionPanel implements TrackEventDetailsPanel {
+  private data?: Data;
+
+  constructor(
+    private readonly trace: Trace,
+    private readonly id: number,
+  ) {}
+
+  async load() {
+    const queryResult = await this.trace.engine.query(`
+      SELECT
+        ts,
+        dur,
+        interaction_type AS interactionType,
+        total_duration_ms AS totalDurationMs,
+        renderer_upid AS upid
+      FROM chrome_web_content_interactions
+      WHERE id = ${this.id};
+    `);
+
+    const iter = queryResult.firstRow({
+      ts: LONG,
+      dur: LONG,
+      interactionType: STR,
+      totalDurationMs: LONG,
+      upid: NUM,
+    });
+
+    this.data = {
+      ts: Time.fromRaw(iter.ts),
+      dur: iter.ts,
+      interactionType: iter.interactionType,
+      totalDurationMs: iter.totalDurationMs,
+      upid: asUpid(iter.upid),
+    };
+  }
+
+  private getDetailsDictionary() {
+    const details: {[key: string]: m.Child} = {};
+    if (this.data === undefined) return details;
+    details['Interaction'] = this.data.interactionType;
+    details['Timestamp'] = m(Timestamp, {ts: this.data.ts});
+    details['Duration'] = m(DurationWidget, {dur: this.data.dur});
+    details['Renderer Upid'] = this.data.upid;
+    details['Total duration of all events'] = m(DurationWidget, {
+      dur: this.data.totalDurationMs,
+    });
+    details['SQL ID'] = m(SqlRef, {
+      table: 'chrome_web_content_interactions',
+      id: this.id,
+    });
+    return details;
+  }
+
+  render() {
+    if (!this.data) {
+      return m('h2', 'Loading');
+    }
+
+    return m(
+      DetailsShell,
+      {
+        title: 'Chrome Web Content Interaction',
+      },
+      m(
+        GridLayout,
+        m(
+          GridLayoutColumn,
+          m(
+            Section,
+            {title: 'Details'},
+            m(Tree, dictToTreeNodes(this.getDetailsDictionary())),
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/ui/src/plugins/org.chromium.ChromeTasks/details.ts b/ui/src/plugins/org.chromium.ChromeTasks/details.ts
new file mode 100644
index 0000000..ac96447
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeTasks/details.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 m from 'mithril';
+import {
+  Details,
+  DetailsSchema,
+} from '../../frontend/widgets/sql/details/details';
+import {DetailsShell} from '../../widgets/details_shell';
+import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
+import d = DetailsSchema;
+
+export class ChromeTasksDetailsPanel implements TrackEventDetailsPanel {
+  private readonly data: Details;
+
+  constructor(trace: Trace, eventId: number) {
+    this.data = new Details(trace, 'chrome_tasks', eventId, {
+      'Task name': 'name',
+      'Start time': d.Timestamp('ts'),
+      'Duration': d.Interval('ts', 'dur'),
+      'Process': d.SqlIdRef('process', 'upid'),
+      'Thread': d.SqlIdRef('thread', 'utid'),
+      'Slice': d.SqlIdRef('slice', 'id'),
+    });
+  }
+
+  render() {
+    return m(
+      DetailsShell,
+      {
+        title: 'Chrome Tasks',
+      },
+      m(GridLayout, m(GridLayoutColumn, this.data.render())),
+    );
+  }
+}
diff --git a/ui/src/plugins/org.chromium.ChromeTasks/index.ts b/ui/src/plugins/org.chromium.ChromeTasks/index.ts
new file mode 100644
index 0000000..6c0f426
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeTasks/index.ts
@@ -0,0 +1,107 @@
+// 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 {asUtid} from '../../trace_processor/sql_utils/core_types';
+import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {chromeTasksTable} from './table';
+import {ChromeTasksThreadTrack} from './track';
+import {TrackNode} from '../../public/workspace';
+import {extensions} from '../../public/lib/extensions';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'org.chromium.ChromeTasks';
+  async onTraceLoad(ctx: Trace) {
+    await this.createTracks(ctx);
+
+    ctx.commands.registerCommand({
+      id: 'org.chromium.ChromeTasks.ShowChromeTasksTable',
+      name: 'Show chrome_tasks table',
+      callback: () =>
+        extensions.addSqlTableTab(ctx, {
+          table: chromeTasksTable,
+        }),
+    });
+  }
+
+  async createTracks(ctx: Trace) {
+    const it = (
+      await ctx.engine.query(`
+      INCLUDE PERFETTO MODULE chrome.tasks;
+
+      with relevant_threads as (
+        select distinct utid from chrome_tasks
+      )
+      select
+        (CASE process.name
+          WHEN 'Browser' THEN 1
+          WHEN 'Gpu' THEN 2
+          WHEN 'Renderer' THEN 4
+          ELSE 3
+        END) as processRank,
+        process.name as processName,
+        process.pid,
+        process.upid,
+        (CASE thread.name
+          WHEN 'CrBrowserMain' THEN 1
+          WHEN 'CrRendererMain' THEN 1
+          WHEN 'CrGpuMain' THEN 1
+          WHEN 'Chrome_IOThread' THEN 2
+          WHEN 'Chrome_ChildIOThread' THEN 2
+          WHEN 'VizCompositorThread' THEN 3
+          WHEN 'NetworkService' THEN 3
+          WHEN 'Compositor' THEN 3
+          WHEN 'CompositorGpuThread' THEN 4
+          WHEN 'CompositorTileWorker&' THEN 5
+          WHEN 'ThreadPoolService' THEN 6
+          WHEN 'ThreadPoolSingleThreadForegroundBlocking&' THEN 6
+          WHEN 'ThreadPoolForegroundWorker' THEN 6
+          ELSE 7
+         END) as threadRank,
+         thread.name as threadName,
+         thread.tid,
+         thread.utid
+      from relevant_threads
+      join thread using (utid)
+      join process using (upid)
+      order by processRank, upid, threadRank, utid
+    `)
+    ).iter({
+      processRank: NUM,
+      processName: STR_NULL,
+      pid: NUM_NULL,
+      upid: NUM,
+      threadRank: NUM,
+      threadName: STR_NULL,
+      tid: NUM_NULL,
+      utid: NUM,
+    });
+
+    const group = new TrackNode({title: 'Chrome Tasks', isSummary: true});
+    for (; it.valid(); it.next()) {
+      const utid = it.utid;
+      const uri = `org.chromium.ChromeTasks#thread.${utid}`;
+      const title = `${it.threadName} ${it.tid}`;
+      ctx.tracks.registerTrack({
+        uri,
+        track: new ChromeTasksThreadTrack(ctx, uri, asUtid(utid)),
+        title,
+      });
+      const track = new TrackNode({uri, title});
+      group.addChildInOrder(track);
+      ctx.workspace.addChildInOrder(group);
+    }
+  }
+}
diff --git a/ui/src/plugins/org.chromium.ChromeTasks/table.ts b/ui/src/plugins/org.chromium.ChromeTasks/table.ts
new file mode 100644
index 0000000..f0231e5
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeTasks/table.ts
@@ -0,0 +1,41 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {SqlTableDescription} from '../../frontend/widgets/sql/table/table_description';
+import {
+  ArgSetColumnSet,
+  DurationColumn,
+  SliceIdColumn,
+  StandardColumn,
+  TimestampColumn,
+} from '../../frontend/widgets/sql/table/well_known_columns';
+
+export const chromeTasksTable: SqlTableDescription = {
+  imports: ['chrome.tasks'],
+  name: 'chrome_tasks',
+  columns: [
+    new SliceIdColumn('id', {title: 'ID'}),
+    new TimestampColumn('ts', {title: 'Timestamp'}),
+    new DurationColumn('dur', {title: 'Duration'}),
+    new DurationColumn('thread_dur', {title: 'Thread duration'}),
+    new StandardColumn('name', {title: 'Name'}),
+    new StandardColumn('track_id', {title: 'Track ID', startsHidden: true}),
+    new StandardColumn('thread_name', {title: 'Thread name'}),
+    new StandardColumn('utid', {startsHidden: true}),
+    new StandardColumn('tid'),
+    new StandardColumn('process_name', {title: 'Process name'}),
+    new StandardColumn('upid', {startsHidden: true}),
+    new ArgSetColumnSet('arg_set_id'),
+  ],
+};
diff --git a/ui/src/plugins/org.chromium.ChromeTasks/track.ts b/ui/src/plugins/org.chromium.ChromeTasks/track.ts
new file mode 100644
index 0000000..ff2f361
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeTasks/track.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.
+
+import {Utid} from '../../trace_processor/sql_utils/core_types';
+import {
+  CustomSqlTableDefConfig,
+  CustomSqlTableSliceTrack,
+} from '../../frontend/tracks/custom_sql_table_slice_track';
+import {Trace} from '../../public/trace';
+import {TrackEventSelection} from '../../public/selection';
+import {ChromeTasksDetailsPanel} from './details';
+
+export class ChromeTasksThreadTrack extends CustomSqlTableSliceTrack {
+  constructor(
+    trace: Trace,
+    uri: string,
+    private utid: Utid,
+  ) {
+    super({trace, uri});
+  }
+
+  getSqlDataSource(): CustomSqlTableDefConfig {
+    return {
+      columns: ['name', 'id', 'ts', 'dur'],
+      sqlTableName: 'chrome_tasks',
+      whereClause: `utid = ${this.utid}`,
+    };
+  }
+
+  override detailsPanel(sel: TrackEventSelection) {
+    return new ChromeTasksDetailsPanel(this.trace, sel.eventId);
+  }
+}
diff --git a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
deleted file mode 100644
index ab2cc47..0000000
--- a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
+++ /dev/null
@@ -1,73 +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 {
-  NUM,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  STR_NULL,
-} from '../../public';
-import {AsyncSliceTrack} from '../../core_plugins/async_slices/async_slice_track';
-import {ASYNC_SLICE_TRACK_KIND} from '../../public';
-
-// This plugin renders visualizations of runtime power state transitions for
-// Linux kernel devices (devices managed by Linux drivers).
-class LinuxKernelDevices implements Plugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const result = await ctx.engine.query(`
-      select
-        t.id as trackId,
-        t.name
-      from linux_device_track t
-      join _slice_track_summary using (id)
-      order by t.name;
-    `);
-
-    const it = result.iter({
-      name: STR_NULL,
-      trackId: NUM,
-    });
-
-    for (; it.valid(); it.next()) {
-      const trackId = it.trackId;
-      const displayName = it.name ?? `${trackId}`;
-
-      ctx.registerStaticTrack({
-        uri: `/kernel_devices/${displayName}`,
-        title: displayName,
-        trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrack(
-            {
-              engine: ctx.engine,
-              trackKey,
-            },
-            0,
-            [trackId],
-          );
-        },
-        tags: {
-          kind: ASYNC_SLICE_TRACK_KIND,
-          trackIds: [trackId],
-        },
-        groupName: `Linux Kernel Devices`,
-      });
-    }
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'org.kernel.LinuxKernelDevices',
-  plugin: LinuxKernelDevices,
-};
diff --git a/ui/src/plugins/org.kernel.LinuxKernelDevices/OWNERS b/ui/src/plugins/org.kernel.LinuxKernelSubsystems/OWNERS
similarity index 100%
rename from ui/src/plugins/org.kernel.LinuxKernelDevices/OWNERS
rename to ui/src/plugins/org.kernel.LinuxKernelSubsystems/OWNERS
diff --git a/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts b/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts
new file mode 100644
index 0000000..0bca424
--- /dev/null
+++ b/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts
@@ -0,0 +1,88 @@
+// 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 {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 {SLICE_TRACK_KIND} from '../../public/track_kinds';
+import {TrackNode} from '../../public/workspace';
+import AsyncSlicesPlugin from '../dev.perfetto.AsyncSlices';
+
+// 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];
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const kernel = new TrackNode({
+      title: 'Linux Kernel',
+      isSummary: true,
+    });
+    const rpm = await this.addRpmTracks(ctx);
+    if (rpm.hasChildren) {
+      ctx.workspace.addChildInOrder(kernel);
+      kernel.addChildInOrder(rpm);
+    }
+  }
+
+  // Add tracks to visualize the runtime power state transitions for Linux
+  // kernel devices (devices managed by Linux drivers).
+  async addRpmTracks(ctx: Trace) {
+    const result = await ctx.engine.query(`
+      select
+        t.id as trackId,
+        extract_arg(t.dimension_arg_set_id, 'linux_device_name') as deviceName
+      from track t
+      join _slice_track_summary using (id)
+      where classification = 'linux_rpm'
+      order by deviceName;
+    `);
+
+    const it = result.iter({
+      deviceName: STR_NULL,
+      trackId: NUM,
+    });
+    const rpm = new TrackNode({
+      title: 'Runtime Power Management',
+      isSummary: true,
+    });
+    for (; it.valid(); it.next()) {
+      const trackId = it.trackId;
+      const title = it.deviceName ?? `${trackId}`;
+
+      const uri = `/linux/rpm/${title}`;
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        track: new AsyncSliceTrack(
+          {
+            trace: ctx,
+            uri,
+          },
+          0,
+          [trackId],
+        ),
+        tags: {
+          kind: SLICE_TRACK_KIND,
+          trackIds: [trackId],
+          groupName: `Linux Kernel Devices`,
+        },
+      });
+      const track = new TrackNode({uri, title});
+      rpm.addChildInOrder(track);
+    }
+    return rpm;
+  }
+}
diff --git a/ui/src/plugins/org.kernel.LinuxKernelDevices/OWNERS b/ui/src/plugins/org.kernel.SuspendResumeLatency/OWNERS
similarity index 100%
copy from ui/src/plugins/org.kernel.LinuxKernelDevices/OWNERS
copy to ui/src/plugins/org.kernel.SuspendResumeLatency/OWNERS
diff --git a/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts b/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts
new file mode 100644
index 0000000..96b0ddb
--- /dev/null
+++ b/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts
@@ -0,0 +1,111 @@
+// 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 {NUM, STR_NULL} from '../../trace_processor/query_result';
+import {AsyncSliceTrack} from '../dev.perfetto.AsyncSlices/async_slice_track';
+import {NewTrackArgs} from '../../frontend/track';
+import {PerfettoPlugin} from '../../public/plugin';
+import {Trace} from '../../public/trace';
+import {TrackNode} from '../../public/workspace';
+import {SLICE_TRACK_KIND} from '../../public/track_kinds';
+import {SuspendResumeDetailsPanel} from './suspend_resume_details';
+import {Slice} from '../../public/track';
+import {OnSliceClickArgs} from '../../frontend/base_slice_track';
+import {ThreadMap} from '../dev.perfetto.Thread/threads';
+import ThreadPlugin from '../dev.perfetto.Thread';
+import AsyncSlicesPlugin from '../dev.perfetto.AsyncSlices';
+
+// SuspendResumeSliceTrack exists so as to override the `onSliceClick` function
+// in AsyncSliceTrack.
+// TODO(stevegolton): Remove this?
+class SuspendResumeSliceTrack extends AsyncSliceTrack {
+  constructor(
+    args: NewTrackArgs,
+    maxDepth: number,
+    trackIds: number[],
+    private readonly threads: ThreadMap,
+  ) {
+    super(args, maxDepth, trackIds);
+  }
+
+  onSliceClick(args: OnSliceClickArgs<Slice>) {
+    this.trace.selection.selectTrackEvent(this.uri, args.slice.id);
+  }
+
+  override detailsPanel() {
+    return new SuspendResumeDetailsPanel(this.trace, this.threads);
+  }
+}
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'org.kernel.SuspendResumeLatency';
+  static readonly dependencies = [ThreadPlugin, AsyncSlicesPlugin];
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const threads = ctx.plugins.getPlugin(ThreadPlugin).getThreadMap();
+    const {engine} = ctx;
+    const rawGlobalAsyncTracks = await engine.query(`
+      with global_tracks_grouped as (
+        select
+          name,
+          group_concat(distinct t.id) as trackIds,
+          count() as trackCount
+        from track t
+        where t.name = "Suspend/Resume Latency"
+      )
+      select
+        t.trackIds as trackIds,
+        case
+          when
+            t.trackCount > 0
+          then
+            __max_layout_depth(t.trackCount, t.trackIds)
+          else 0
+        end as maxDepth
+      from global_tracks_grouped t
+    `);
+    const it = rawGlobalAsyncTracks.iter({
+      trackIds: STR_NULL,
+      maxDepth: NUM,
+    });
+    // If no Suspend/Resume tracks exist, then nothing to do.
+    if (it.trackIds == null) {
+      return;
+    }
+    const rawTrackIds = it.trackIds;
+    const trackIds = rawTrackIds.split(',').map((v) => Number(v));
+    const maxDepth = it.maxDepth;
+
+    const uri = `/suspend_resume_latency`;
+    const displayName = `Suspend/Resume Latency`;
+    ctx.tracks.registerTrack({
+      uri,
+      title: displayName,
+      tags: {
+        trackIds,
+        kind: SLICE_TRACK_KIND,
+      },
+      track: new SuspendResumeSliceTrack(
+        {uri, trace: ctx},
+        maxDepth,
+        trackIds,
+        threads,
+      ),
+    });
+
+    // Display the track in the UI.
+    const track = new TrackNode({uri, title: displayName});
+    ctx.workspace.addChildInOrder(track);
+  }
+}
diff --git a/ui/src/plugins/org.kernel.SuspendResumeLatency/suspend_resume_details.ts b/ui/src/plugins/org.kernel.SuspendResumeLatency/suspend_resume_details.ts
new file mode 100644
index 0000000..74bc497
--- /dev/null
+++ b/ui/src/plugins/org.kernel.SuspendResumeLatency/suspend_resume_details.ts
@@ -0,0 +1,230 @@
+// 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 {Duration, duration, Time, time} from '../../base/time';
+import {LONG, NUM, STR_NULL} from '../../trace_processor/query_result';
+import m from 'mithril';
+import {DetailsShell} from '../../widgets/details_shell';
+import {GridLayout} from '../../widgets/grid_layout';
+import {Section} from '../../widgets/section';
+import {Tree, TreeNode} from '../../widgets/tree';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {DurationWidget} from '../../frontend/widgets/duration';
+import {Anchor} from '../../widgets/anchor';
+import {Engine} from '../../trace_processor/engine';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {TrackEventSelection} from '../../public/selection';
+import {Trace} from '../../public/trace';
+import {ThreadMap} from '../dev.perfetto.Thread/threads';
+
+interface SuspendResumeEventDetails {
+  ts: time;
+  dur: duration;
+  utid: number;
+  cpu: number;
+  event_type: string;
+  device_name: string;
+  driver_name: string;
+  callback_phase: string;
+  thread_state_id: number;
+}
+
+export class SuspendResumeDetailsPanel implements TrackEventDetailsPanel {
+  private suspendResumeEventDetails?: SuspendResumeEventDetails;
+
+  constructor(
+    private readonly trace: Trace,
+    private readonly threads: ThreadMap,
+  ) {}
+
+  async load({eventId}: TrackEventSelection) {
+    this.suspendResumeEventDetails = await loadSuspendResumeEventDetails(
+      this.trace.engine,
+      eventId,
+    );
+  }
+
+  render() {
+    const eventDetails = this.suspendResumeEventDetails;
+    if (eventDetails) {
+      const threadInfo = this.threads.get(eventDetails.utid);
+      if (!threadInfo) {
+        return null;
+      }
+      return m(
+        DetailsShell,
+        {title: 'Suspend / Resume Event'},
+        m(
+          GridLayout,
+          m(
+            Section,
+            {title: 'Properties'},
+            m(
+              Tree,
+              m(TreeNode, {
+                left: 'Device Name',
+                right: eventDetails.device_name,
+              }),
+              m(TreeNode, {
+                left: 'Start time',
+                right: m(Timestamp, {ts: eventDetails.ts}),
+              }),
+              m(TreeNode, {
+                left: 'Duration',
+                right: m(DurationWidget, {dur: eventDetails.dur}),
+              }),
+              m(TreeNode, {
+                left: 'Driver Name',
+                right: eventDetails.driver_name,
+              }),
+              m(TreeNode, {
+                left: 'Callback Phase',
+                right: eventDetails.callback_phase,
+              }),
+              m(TreeNode, {
+                left: 'Thread',
+                right: m(
+                  Anchor,
+                  {
+                    icon: 'call_made',
+                    onclick: () => {
+                      this.goToThread(eventDetails.thread_state_id);
+                    },
+                  },
+                  `${threadInfo.threadName} [${threadInfo.tid}]`,
+                ),
+              }),
+              m(TreeNode, {left: 'CPU', right: eventDetails.cpu}),
+              m(TreeNode, {left: 'Event Type', right: eventDetails.event_type}),
+            ),
+          ),
+        ),
+      );
+    } else {
+      return m(DetailsShell, {
+        title: 'Suspend / Resume Event',
+        description: 'Loading...',
+      });
+    }
+  }
+
+  isLoading(): boolean {
+    return this.suspendResumeEventDetails === undefined;
+  }
+
+  goToThread(threadStateId: number) {
+    this.trace.selection.selectSqlEvent('thread_state', threadStateId, {
+      scrollToSelection: true,
+    });
+  }
+}
+
+async function loadSuspendResumeEventDetails(
+  engine: Engine,
+  id: number,
+): Promise<SuspendResumeEventDetails> {
+  const suspendResumeDetailsQuery = `
+        SELECT ts,
+               dur,
+               EXTRACT_ARG(arg_set_id, 'utid') as utid,
+               EXTRACT_ARG(arg_set_id, 'ucpu') as ucpu,
+               EXTRACT_ARG(arg_set_id, 'event_type') as event_type,
+               EXTRACT_ARG(arg_set_id, 'device_name') as device_name,
+               EXTRACT_ARG(arg_set_id, 'driver_name') as driver_name,
+               EXTRACT_ARG(arg_set_id, 'callback_phase') as callback_phase
+        FROM slice
+        WHERE slice_id = ${id};
+    `;
+
+  const suspendResumeDetailsResult = await engine.query(
+    suspendResumeDetailsQuery,
+  );
+  const suspendResumeEventRow = suspendResumeDetailsResult.iter({
+    ts: LONG,
+    dur: LONG,
+    utid: NUM,
+    ucpu: NUM,
+    event_type: STR_NULL,
+    device_name: STR_NULL,
+    driver_name: STR_NULL,
+    callback_phase: STR_NULL,
+  });
+  if (!suspendResumeEventRow.valid()) {
+    return {
+      ts: Time.fromRaw(0n),
+      dur: Duration.fromRaw(0n),
+      utid: 0,
+      cpu: 0,
+      event_type: 'Error',
+      device_name: 'Error',
+      driver_name: 'Error',
+      callback_phase: 'Error',
+      thread_state_id: 0,
+    };
+  }
+
+  const threadStateQuery = `
+        SELECT t.id as threadStateId
+        FROM thread_state t
+        WHERE t.utid = ${suspendResumeEventRow.utid}
+              AND t.ts <= ${suspendResumeEventRow.ts}
+              AND t.ts + t.dur > ${suspendResumeEventRow.ts};
+  `;
+  const threadStateResult = await engine.query(threadStateQuery);
+  let threadStateId = 0;
+  if (threadStateResult.numRows() > 0) {
+    const threadStateRow = threadStateResult.firstRow({
+      threadStateId: NUM,
+    });
+    threadStateId = threadStateRow.threadStateId;
+  }
+
+  const cpuQuery = `
+        SELECT cpu
+        FROM cpu
+        WHERE cpu.id = ${suspendResumeEventRow.ucpu}
+  `;
+  const cpuResult = await engine.query(cpuQuery);
+  let cpu = 0;
+  if (cpuResult.numRows() > 0) {
+    const cpuRow = cpuResult.firstRow({
+      cpu: NUM,
+    });
+    cpu = cpuRow.cpu;
+  }
+
+  return {
+    ts: Time.fromRaw(suspendResumeEventRow.ts),
+    dur: Duration.fromRaw(suspendResumeEventRow.dur),
+    utid: suspendResumeEventRow.utid,
+    cpu: cpu,
+    event_type:
+      suspendResumeEventRow.event_type !== null
+        ? suspendResumeEventRow.event_type
+        : 'N/A',
+    device_name:
+      suspendResumeEventRow.device_name !== null
+        ? suspendResumeEventRow.device_name
+        : 'N/A',
+    driver_name:
+      suspendResumeEventRow.driver_name !== null
+        ? suspendResumeEventRow.driver_name
+        : 'N/A',
+    callback_phase:
+      suspendResumeEventRow.callback_phase !== null
+        ? suspendResumeEventRow.callback_phase
+        : 'N/A',
+    thread_state_id: threadStateId,
+  };
+}
diff --git a/ui/src/core_plugins/wattson/OWNERS b/ui/src/plugins/org.kernel.Wattson/OWNERS
similarity index 100%
rename from ui/src/core_plugins/wattson/OWNERS
rename to ui/src/plugins/org.kernel.Wattson/OWNERS
diff --git a/ui/src/plugins/org.kernel.Wattson/estimate_aggregator.ts b/ui/src/plugins/org.kernel.Wattson/estimate_aggregator.ts
new file mode 100644
index 0000000..e6038b8
--- /dev/null
+++ b/ui/src/plugins/org.kernel.Wattson/estimate_aggregator.ts
@@ -0,0 +1,121 @@
+// 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 {ColumnDef, Sorting} from '../../public/aggregation';
+import {Area, AreaSelection} from '../../public/selection';
+import {Engine} from '../../trace_processor/engine';
+import {CPUSS_ESTIMATE_TRACK_KIND} from '../../public/track_kinds';
+import {AreaSelectionAggregator} from '../../public/selection';
+import {exists} from '../../base/utils';
+
+export class WattsonEstimateSelectionAggregator
+  implements AreaSelectionAggregator
+{
+  readonly id = 'wattson_estimate_aggregation';
+
+  async createAggregateView(engine: Engine, area: AreaSelection) {
+    await engine.query(`drop view if exists ${this.id};`);
+
+    const estimateTracks: string[] = [];
+    for (const trackInfo of area.tracks) {
+      if (
+        trackInfo?.tags?.kind === CPUSS_ESTIMATE_TRACK_KIND &&
+        exists(trackInfo.tags?.wattson)
+      ) {
+        estimateTracks.push(`${trackInfo.tags.wattson}`);
+      }
+    }
+    if (estimateTracks.length === 0) return false;
+
+    const query = this.getEstimateTracksQuery(area, estimateTracks);
+    engine.query(query);
+
+    return true;
+  }
+
+  getEstimateTracksQuery(
+    area: Area,
+    estimateTracks: ReadonlyArray<string>,
+  ): string {
+    const duration = area.end - area.start;
+    let query = `
+      INCLUDE PERFETTO MODULE wattson.curves.estimates;
+
+      CREATE OR REPLACE PERFETTO TABLE _ui_selection_window AS
+      SELECT
+        ${area.start} as ts,
+        ${duration} as dur;
+
+      DROP TABLE IF EXISTS _windowed_cpuss_estimate;
+      CREATE VIRTUAL TABLE _windowed_cpuss_estimate
+      USING
+        SPAN_JOIN(_ui_selection_window, _system_state_mw);
+
+      CREATE VIEW ${this.id} AS
+    `;
+
+    // Convert average power track to total energy in UI window, then divide by
+    // duration of window to get average estimated power of the window
+    estimateTracks.forEach((estimateTrack, i) => {
+      if (i != 0) {
+        query += `UNION ALL `;
+      }
+      query += `
+        SELECT
+        '${estimateTrack}' as name,
+        ROUND(SUM(${estimateTrack}_mw * dur) / ${duration}, 2) as power,
+        ROUND(SUM(${estimateTrack}_mw * dur) / 1000000000, 2) as energy
+        FROM _windowed_cpuss_estimate
+      `;
+    });
+    query += `;`;
+
+    return query;
+  }
+
+  getColumnDefinitions(): ColumnDef[] {
+    return [
+      {
+        title: 'Name',
+        kind: 'STRING',
+        columnConstructor: Uint16Array,
+        columnId: 'name',
+      },
+      {
+        title: 'Power (estimated mW)',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'power',
+        sum: true,
+      },
+      {
+        title: 'Energy (estimated mWs)',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'energy',
+        sum: true,
+      },
+    ];
+  }
+
+  async getExtra() {}
+
+  getTabName() {
+    return 'Wattson estimates';
+  }
+
+  getDefaultSorting(): Sorting {
+    return {column: 'name', direction: 'ASC'};
+  }
+}
diff --git a/ui/src/plugins/org.kernel.Wattson/index.ts b/ui/src/plugins/org.kernel.Wattson/index.ts
new file mode 100644
index 0000000..539366d
--- /dev/null
+++ b/ui/src/plugins/org.kernel.Wattson/index.ts
@@ -0,0 +1,151 @@
+// 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 {
+  BaseCounterTrack,
+  CounterOptions,
+} from '../../frontend/base_counter_track';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {CPUSS_ESTIMATE_TRACK_KIND} from '../../public/track_kinds';
+import {TrackNode} from '../../public/workspace';
+import {WattsonEstimateSelectionAggregator} from './estimate_aggregator';
+import {WattsonPackageSelectionAggregator} from './package_aggregator';
+import {WattsonProcessSelectionAggregator} from './process_aggregator';
+import {WattsonThreadSelectionAggregator} from './thread_aggregator';
+import {Engine} from '../../trace_processor/engine';
+import {NUM} from '../../trace_processor/query_result';
+
+export default class implements PerfettoPlugin {
+  static readonly id = `org.kernel.Wattson`;
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    // Short circuit if Wattson is not supported for this Perfetto trace
+    if (!(await hasWattsonSupport(ctx.engine))) return;
+
+    ctx.engine.query(`INCLUDE PERFETTO MODULE wattson.curves.estimates;`);
+
+    const group = new TrackNode({title: 'Wattson', isSummary: true});
+    ctx.workspace.addChildInOrder(group);
+
+    // CPUs estimate as part of CPU subsystem
+    const cpus = ctx.traceInfo.cpus;
+    for (const cpu of cpus) {
+      const queryKey = `cpu${cpu}_curve`;
+      const uri = `/wattson/cpu_subsystem_estimate_cpu${cpu}`;
+      const title = `Cpu${cpu} Estimate`;
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        track: new CpuSubsystemEstimateTrack(ctx, uri, queryKey),
+        tags: {
+          kind: CPUSS_ESTIMATE_TRACK_KIND,
+          wattson: `CPU${cpu}`,
+          groupName: `Wattson`,
+        },
+      });
+      group.addChildInOrder(new TrackNode({uri, title}));
+    }
+
+    const uri = `/wattson/cpu_subsystem_estimate_dsu_scu`;
+    const title = `DSU/SCU Estimate`;
+    ctx.tracks.registerTrack({
+      uri,
+      title,
+      track: new CpuSubsystemEstimateTrack(ctx, uri, `dsu_scu`),
+      tags: {
+        kind: CPUSS_ESTIMATE_TRACK_KIND,
+        wattson: 'Dsu_Scu',
+        groupName: `Wattson`,
+      },
+    });
+    group.addChildInOrder(new TrackNode({uri, title}));
+
+    // Register selection aggregators.
+    // NOTE: the registration order matters because the laste two aggregators
+    // depend on views created by the first two.
+    ctx.selection.registerAreaSelectionAggreagtor(
+      new WattsonEstimateSelectionAggregator(),
+    );
+    ctx.selection.registerAreaSelectionAggreagtor(
+      new WattsonThreadSelectionAggregator(),
+    );
+    ctx.selection.registerAreaSelectionAggreagtor(
+      new WattsonPackageSelectionAggregator(),
+    );
+    ctx.selection.registerAreaSelectionAggreagtor(
+      new WattsonProcessSelectionAggregator(),
+    );
+  }
+}
+
+class CpuSubsystemEstimateTrack extends BaseCounterTrack {
+  readonly queryKey: string;
+
+  constructor(trace: Trace, uri: string, queryKey: string) {
+    super({
+      trace,
+      uri,
+    });
+    this.queryKey = queryKey;
+  }
+
+  protected getDefaultCounterOptions(): CounterOptions {
+    const options = super.getDefaultCounterOptions();
+    options.yRangeSharingKey = `CpuSubsystem`;
+    options.unit = `mW`;
+    return options;
+  }
+
+  getSqlSource() {
+    if (this.queryKey.startsWith(`cpu`)) {
+      return `select ts, ${this.queryKey} as value from _system_state_curves`;
+    } else {
+      return `
+        select
+          ts,
+          -- L3 values are scaled by 1000 because it's divided by ns and L3 LUTs
+          -- are scaled by 10^6. This brings to same units as static_curve (mW)
+          ((IFNULL(l3_hit_value, 0) + IFNULL(l3_miss_value, 0)) * 1000 / dur)
+            + static_curve  as value
+        from _system_state_curves
+      `;
+    }
+  }
+}
+
+async function hasWattsonSupport(engine: Engine): Promise<boolean> {
+  // These tables are hard requirements and are the bare minimum needed for
+  // Wattson to run, so check that these tables are populated
+  const queryChecks: string[] = [
+    `
+    INCLUDE PERFETTO MODULE wattson.device_infos;
+    SELECT COUNT(*) as numRows FROM _wattson_device
+    `,
+    `
+    INCLUDE PERFETTO MODULE linux.cpu.frequency;
+    SELECT COUNT(*) as numRows FROM cpu_frequency_counters
+    `,
+    `
+    INCLUDE PERFETTO MODULE linux.cpu.idle;
+    SELECT COUNT(*) as numRows FROM cpu_idle_counters
+    `,
+  ];
+  for (const queryCheck of queryChecks) {
+    const checkValue = await engine.query(queryCheck);
+    if (checkValue.firstRow({numRows: NUM}).numRows === 0) return false;
+  }
+
+  return true;
+}
diff --git a/ui/src/plugins/org.kernel.Wattson/package_aggregator.ts b/ui/src/plugins/org.kernel.Wattson/package_aggregator.ts
new file mode 100644
index 0000000..7b4d1b0
--- /dev/null
+++ b/ui/src/plugins/org.kernel.Wattson/package_aggregator.ts
@@ -0,0 +1,112 @@
+// 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 {ColumnDef, Sorting} from '../../public/aggregation';
+import {AreaSelection} from '../../public/selection';
+import {Engine} from '../../trace_processor/engine';
+import {NUM} from '../../trace_processor/query_result';
+import {CPU_SLICE_TRACK_KIND} from '../../public/track_kinds';
+import {AreaSelectionAggregator} from '../../public/selection';
+
+export class WattsonPackageSelectionAggregator
+  implements AreaSelectionAggregator
+{
+  readonly id = 'wattson_package_aggregation';
+
+  async createAggregateView(engine: Engine, area: AreaSelection) {
+    await engine.query(`drop view if exists ${this.id};`);
+
+    const packageInfo = await engine.query(`
+      INCLUDE PERFETTO MODULE android.process_metadata;
+      SELECT COUNT(*) as isValid FROM android_process_metadata
+      WHERE package_name IS NOT NULL
+    `);
+    if (packageInfo.firstRow({isValid: NUM}).isValid === 0) return false;
+
+    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;
+
+    const duration = area.end - area.start;
+
+    // Prerequisite tables are already generated by Wattson thread aggregation,
+    // which is run prior to execution of this module
+    engine.query(`
+      -- Grouped by UID and made CPU agnostic
+      CREATE VIEW ${this.id} AS
+      SELECT
+        ROUND(SUM(total_pws) / ${duration}, 2) as active_mw,
+        ROUND(SUM(total_pws) / 1000000000, 2) as active_mws,
+        ROUND(SUM(dur) / 1000000.0, 2) as dur_ms,
+        uid,
+        package_name
+      FROM _unioned_per_cpu_total
+      GROUP BY uid;
+    `);
+
+    return true;
+  }
+
+  getColumnDefinitions(): ColumnDef[] {
+    return [
+      {
+        title: 'Package Name',
+        kind: 'STRING',
+        columnConstructor: Uint16Array,
+        columnId: 'package_name',
+      },
+      {
+        title: 'Android app UID',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'uid',
+      },
+      {
+        title: 'Total Duration (ms)',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'dur_ms',
+      },
+      {
+        title: 'Active power (estimated mW)',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'active_mw',
+        sum: true,
+      },
+      {
+        title: 'Active energy (estimated mWs)',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'active_mws',
+        sum: true,
+      },
+    ];
+  }
+
+  async getExtra() {}
+
+  getTabName() {
+    return 'Wattson by package';
+  }
+
+  getDefaultSorting(): Sorting {
+    return {column: 'active_mws', direction: 'DESC'};
+  }
+}
diff --git a/ui/src/plugins/org.kernel.Wattson/process_aggregator.ts b/ui/src/plugins/org.kernel.Wattson/process_aggregator.ts
new file mode 100644
index 0000000..f2325cb
--- /dev/null
+++ b/ui/src/plugins/org.kernel.Wattson/process_aggregator.ts
@@ -0,0 +1,116 @@
+// 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 {ColumnDef, Sorting} from '../../public/aggregation';
+import {AreaSelection, AreaSelectionAggregator} from '../../public/selection';
+import {CPU_SLICE_TRACK_KIND} from '../../public/track_kinds';
+import {Engine} from '../../trace_processor/engine';
+
+export class WattsonProcessSelectionAggregator
+  implements AreaSelectionAggregator
+{
+  readonly id = 'wattson_process_aggregation';
+
+  async createAggregateView(engine: Engine, area: AreaSelection) {
+    await engine.query(`drop view if exists ${this.id};`);
+
+    const selectedCpus: number[] = [];
+    for (const trackInfo of area.tracks) {
+      trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND &&
+        exists(trackInfo.tags.cpu) &&
+        selectedCpus.push(trackInfo.tags.cpu);
+    }
+    if (selectedCpus.length === 0) return false;
+
+    const cpusCsv = `(` + selectedCpus.join() + `)`;
+    const duration = area.end - area.start;
+
+    // Prerequisite tables are already generated by Wattson thread aggregation,
+    // which is run prior to execution of this module
+    engine.query(`
+      -- Only get idle attribution in user defined window and filter by selected
+      -- CPUs and GROUP BY process
+      CREATE OR REPLACE PERFETTO TABLE _per_process_idle_attribution AS
+      SELECT
+        ROUND(SUM(idle_cost_mws), 2) as idle_cost_mws,
+        upid
+      FROM _filter_idle_attribution(${area.start}, ${duration})
+      WHERE cpu in ${cpusCsv}
+      GROUP BY upid;
+
+      -- Grouped by UPID and made CPU agnostic
+      CREATE VIEW ${this.id} AS
+      SELECT
+        ROUND(SUM(total_pws) / ${duration}, 2) as active_mw,
+        ROUND(SUM(total_pws) / 1000000000, 2) as active_mws,
+        COALESCE(idle_cost_mws, 0) as idle_cost_mws,
+        pid,
+        process_name
+      FROM _unioned_per_cpu_total
+      LEFT JOIN _per_process_idle_attribution USING (upid)
+      GROUP BY upid;
+    `);
+
+    return true;
+  }
+
+  getColumnDefinitions(): ColumnDef[] {
+    return [
+      {
+        title: 'Process Name',
+        kind: 'STRING',
+        columnConstructor: Uint16Array,
+        columnId: 'process_name',
+      },
+      {
+        title: 'PID',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'pid',
+      },
+      {
+        title: 'Active power (estimated mW)',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'active_mw',
+        sum: true,
+      },
+      {
+        title: 'Active energy (estimated mWs)',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'active_mws',
+        sum: true,
+      },
+      {
+        title: 'Idle transitions overhead (estimated mWs)',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'idle_cost_mws',
+        sum: true,
+      },
+    ];
+  }
+
+  async getExtra() {}
+
+  getTabName() {
+    return 'Wattson by process';
+  }
+
+  getDefaultSorting(): Sorting {
+    return {column: 'active_mws', direction: 'DESC'};
+  }
+}
diff --git a/ui/src/plugins/org.kernel.Wattson/thread_aggregator.ts b/ui/src/plugins/org.kernel.Wattson/thread_aggregator.ts
new file mode 100644
index 0000000..2ca428d
--- /dev/null
+++ b/ui/src/plugins/org.kernel.Wattson/thread_aggregator.ts
@@ -0,0 +1,202 @@
+// 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 {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';
+
+export class WattsonThreadSelectionAggregator
+  implements AreaSelectionAggregator
+{
+  readonly id = 'wattson_thread_aggregation';
+
+  async createAggregateView(engine: Engine, area: AreaSelection) {
+    await engine.query(`drop view if exists ${this.id};`);
+
+    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;
+
+    const duration = area.end - area.start;
+    const cpusCsv = `(` + selectedCpus.join() + `)`;
+    engine.query(`
+      INCLUDE PERFETTO MODULE viz.summary.threads_w_processes;
+      INCLUDE PERFETTO MODULE wattson.curves.idle_attribution;
+      INCLUDE PERFETTO MODULE wattson.curves.estimates;
+
+      CREATE OR REPLACE PERFETTO TABLE _ui_selection_window AS
+      SELECT
+        ${area.start} as ts,
+        ${duration} as dur;
+
+      -- Processes filtered by CPU within the UI defined time window
+      DROP TABLE IF EXISTS _windowed_summary;
+      CREATE VIRTUAL TABLE _windowed_summary
+      USING
+        SPAN_JOIN(_ui_selection_window, _sched_w_thread_process_package_summary);
+
+      -- 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
+      SELECT
+        ROUND(SUM(idle_cost_mws), 2) as idle_cost_mws,
+        utid
+      FROM _filter_idle_attribution(${area.start}, ${duration})
+      WHERE cpu in ${cpusCsv}
+      GROUP BY utid;
+    `);
+    this.runEstimateThreadsQuery(engine, selectedCpus, duration);
+
+    return true;
+  }
+
+  // This function returns a query that gets the average and estimate from
+  // Wattson for the selection in the UI window based on thread. The grouping by
+  // thread needs to 'remove' 2 dimensions; the threads need to be grouped over
+  // time and the threads need to be grouped over CPUs.
+  // 1. Window and associate thread with proper Wattson estimate slice
+  // 2. Group all threads over time on a per CPU basis
+  // 3. Group all threads over all CPUs
+  runEstimateThreadsQuery(
+    engine: Engine,
+    selectedCpu: number[],
+    duration: bigint,
+  ) {
+    // Estimate and total per UTID per CPU
+    selectedCpu.forEach((cpu) => {
+      engine.query(`
+        -- Packages filtered by CPU
+        CREATE OR REPLACE PERFETTO VIEW _windowed_summary_per_cpu${cpu} AS
+        SELECT *
+        FROM _windowed_summary WHERE cpu = ${cpu};
+
+        -- CPU specific track with slices for curves
+        CREATE OR REPLACE PERFETTO VIEW _per_cpu${cpu}_curve AS
+        SELECT ts, dur, cpu${cpu}_curve
+        FROM _system_state_curves;
+
+        -- Filter out track when threads are available
+        DROP TABLE IF EXISTS _windowed_thread_curve${cpu};
+        CREATE VIRTUAL TABLE _windowed_thread_curve${cpu}
+        USING
+          SPAN_JOIN(_per_cpu${cpu}_curve, _windowed_summary_per_cpu${cpu});
+
+        -- Total estimate per UTID per CPU
+        CREATE OR REPLACE PERFETTO VIEW _total_per_cpu${cpu} AS
+        SELECT
+          SUM(cpu${cpu}_curve * dur) as total_pws,
+          SUM(dur) as dur,
+          tid,
+          pid,
+          uid,
+          utid,
+          upid,
+          thread_name,
+          process_name,
+          package_name
+        FROM _windowed_thread_curve${cpu}
+        GROUP BY utid;
+      `);
+    });
+
+    // Estimate and total per UTID, removing CPU dimension
+    let query = `CREATE OR REPLACE PERFETTO TABLE _unioned_per_cpu_total AS `;
+    selectedCpu.forEach((cpu, i) => {
+      query += i != 0 ? `UNION ALL\n` : ``;
+      query += `SELECT * from _total_per_cpu${cpu}\n`;
+    });
+    query += `
+      ;
+
+      -- Grouped again by UTID, but this time to make it CPU agnostic
+      CREATE VIEW ${this.id} AS
+      SELECT
+        ROUND(SUM(total_pws) / ${duration}, 2) as active_mw,
+        ROUND(SUM(total_pws) / 1000000000, 2) as active_mws,
+        COALESCE(idle_cost_mws, 0) as idle_cost_mws,
+        thread_name,
+        utid,
+        tid,
+        pid
+      FROM _unioned_per_cpu_total
+      LEFT JOIN _per_thread_idle_attribution USING (utid)
+      GROUP BY utid;
+    `;
+
+    engine.query(query);
+
+    return;
+  }
+
+  getColumnDefinitions(): ColumnDef[] {
+    return [
+      {
+        title: 'Thread Name',
+        kind: 'STRING',
+        columnConstructor: Uint16Array,
+        columnId: 'thread_name',
+      },
+      {
+        title: 'TID',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'tid',
+      },
+      {
+        title: 'PID',
+        kind: 'NUMBER',
+        columnConstructor: Uint16Array,
+        columnId: 'pid',
+      },
+      {
+        title: 'Active power (estimated mW)',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'active_mw',
+        sum: true,
+      },
+      {
+        title: 'Active energy (estimated mWs)',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'active_mws',
+        sum: true,
+      },
+      {
+        title: 'Idle transitions overhead (estimated mWs)',
+        kind: 'NUMBER',
+        columnConstructor: Float64Array,
+        columnId: 'idle_cost_mws',
+        sum: true,
+      },
+    ];
+  }
+
+  async getExtra() {}
+
+  getTabName() {
+    return 'Wattson by thread';
+  }
+
+  getDefaultSorting(): Sorting {
+    return {column: 'active_mws', direction: 'DESC'};
+  }
+}
diff --git a/ui/src/protos/index.ts b/ui/src/protos/index.ts
index 44270c2..7048b49 100644
--- a/ui/src/protos/index.ts
+++ b/ui/src/protos/index.ts
@@ -19,6 +19,7 @@
 import AndroidLogConfig = protos.perfetto.protos.AndroidLogConfig;
 import AndroidLogId = protos.perfetto.protos.AndroidLogId;
 import AndroidPowerConfig = protos.perfetto.protos.AndroidPowerConfig;
+import AtomId = protos.perfetto.protos.AtomId;
 import BatteryCounters = protos.perfetto.protos.AndroidPowerConfig.BatteryCounters;
 import BufferConfig = protos.perfetto.protos.TraceConfig.BufferConfig;
 import ChromeConfig = protos.perfetto.protos.ChromeConfig;
@@ -71,8 +72,12 @@
 import QueryServiceStateResponse = protos.perfetto.protos.QueryServiceStateResponse;
 import ReadBuffersRequest = protos.perfetto.protos.ReadBuffersRequest;
 import ReadBuffersResponse = protos.perfetto.protos.ReadBuffersResponse;
+import RegisterSqlPackageArgs = protos.perfetto.protos.RegisterSqlPackageArgs;
+import RegisterSqlPackageResult = protos.perfetto.protos.RegisterSqlPackageResult;
 import ResetTraceProcessorArgs = protos.perfetto.protos.ResetTraceProcessorArgs;
 import StatCounters = protos.perfetto.protos.SysStatsConfig.StatCounters;
+import StatsdPullAtomConfig = protos.perfetto.protos.StatsdPullAtomConfig;;
+import StatsdTracingConfig = protos.perfetto.protos.StatsdTracingConfig;
 import StatusResult = protos.perfetto.protos.StatusResult;
 import SysStatsConfig = protos.perfetto.protos.SysStatsConfig;
 import TraceConfig = protos.perfetto.protos.TraceConfig;
@@ -86,6 +91,7 @@
   AndroidLogConfig,
   AndroidLogId,
   AndroidPowerConfig,
+  AtomId,
   BatteryCounters,
   BufferConfig,
   ChromeConfig,
@@ -138,8 +144,12 @@
   QueryServiceStateResponse,
   ReadBuffersRequest,
   ReadBuffersResponse,
+  RegisterSqlPackageArgs,
+  RegisterSqlPackageResult,
   ResetTraceProcessorArgs,
   StatCounters,
+  StatsdPullAtomConfig,
+  StatsdTracingConfig,
   StatusResult,
   SysStatsConfig,
   TraceConfig,
diff --git a/ui/src/public/aggregation.ts b/ui/src/public/aggregation.ts
new file mode 100644
index 0000000..b06e6e5
--- /dev/null
+++ b/ui/src/public/aggregation.ts
@@ -0,0 +1,81 @@
+// Copyright (C) 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
+//
+//      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 Column = (
+  | StringColumn
+  | TimestampColumn
+  | NumberColumn
+  | StateColumn
+) & {
+  readonly title: string;
+  readonly columnId: string;
+};
+
+export interface StringColumn {
+  readonly kind: 'STRING';
+  readonly data: Uint16Array;
+}
+
+export interface TimestampColumn {
+  readonly kind: 'TIMESTAMP_NS';
+  readonly data: Float64Array;
+}
+
+export interface NumberColumn {
+  readonly kind: 'NUMBER';
+  readonly data: Uint16Array;
+}
+
+export interface StateColumn {
+  readonly kind: 'STATE';
+  readonly data: Uint16Array;
+}
+
+type TypedArrayConstructor =
+  | Uint16ArrayConstructor
+  | Float64ArrayConstructor
+  | Uint32ArrayConstructor;
+export interface ColumnDef {
+  readonly title: string;
+  readonly kind: string;
+  readonly sum?: boolean;
+  readonly columnConstructor: TypedArrayConstructor;
+  readonly columnId: string;
+}
+
+export interface AggregateData {
+  readonly tabName: string;
+  readonly columns: Column[];
+  readonly columnSums: string[];
+  // For string interning.
+  readonly strings: string[];
+  // Some aggregations will have extra info to display;
+  readonly extra?: ThreadStateExtra;
+}
+
+export function isEmptyData(data: AggregateData) {
+  return data.columns.length === 0 || data.columns[0].data.length === 0;
+}
+
+export interface ThreadStateExtra {
+  readonly kind: 'THREAD_STATE';
+  readonly states: string[];
+  readonly values: Float64Array;
+  readonly totalMs: number;
+}
+
+export interface Sorting {
+  readonly column: string;
+  readonly direction: 'DESC' | 'ASC';
+}
diff --git a/ui/src/public/analytics.ts b/ui/src/public/analytics.ts
new file mode 100644
index 0000000..653b1ea
--- /dev/null
+++ b/ui/src/public/analytics.ts
@@ -0,0 +1,23 @@
+// 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 {ErrorDetails} from '../base/logging';
+
+export type TraceCategories = 'Trace Actions' | 'Record Trace' | 'User Actions';
+
+export interface Analytics {
+  logEvent(category: TraceCategories | null, event: string): void;
+  logError(err: ErrorDetails): void;
+  isEnabled(): boolean;
+}
diff --git a/ui/src/public/app.ts b/ui/src/public/app.ts
new file mode 100644
index 0000000..50def57
--- /dev/null
+++ b/ui/src/public/app.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.
+
+import {RouteArgs} from './route_schema';
+import {CommandManager} from './command';
+import {OmniboxManager} from './omnibox';
+import {SidebarManager} from './sidebar';
+import {Analytics} from './analytics';
+import {PluginManager} from './plugin';
+import {Trace} from './trace';
+import {PageManager} from './page';
+import {FeatureFlagManager} from './feature_flag';
+
+/**
+ * The API endpoint to interact programmaticaly with the UI before a trace has
+ * been loaded. This is passed to plugins' OnActivate().
+ */
+export interface App {
+  /**
+   * The unique id for this plugin (as specified in the PluginDescriptor),
+   * or '__core__' for the interface exposed to the core.
+   */
+  readonly pluginId: string;
+  readonly commands: CommandManager;
+  readonly sidebar: SidebarManager;
+  readonly omnibox: OmniboxManager;
+  readonly analytics: Analytics;
+  readonly plugins: PluginManager;
+  readonly pages: PageManager;
+  readonly featureFlags: FeatureFlagManager;
+
+  /**
+   * The parsed querystring passed when starting the app, before any navigation
+   * happens.
+   */
+  readonly initialRouteArgs: RouteArgs;
+
+  /**
+   * Returns the current trace object, if any. The instance being returned is
+   * bound to the same plugin of App.pluginId.
+   */
+  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(): void;
+
+  /**
+   * Navigate to a new page.
+   */
+  navigate(newHash: string): void;
+
+  openTraceFromFile(file: File): void;
+  openTraceFromUrl(url: string): void;
+  openTraceFromBuffer(args: {
+    buffer: ArrayBuffer;
+    title: string;
+    fileName: string;
+  }): void;
+}
diff --git a/ui/src/public/color.ts b/ui/src/public/color.ts
new file mode 100644
index 0000000..f9e1430
--- /dev/null
+++ b/ui/src/public/color.ts
@@ -0,0 +1,289 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {hsluvToRgb} from 'hsluv';
+import {clamp} from '../base/math_utils';
+
+// This file contains a library for working with colors in various color spaces
+// and formats.
+
+const LIGHTNESS_MIN = 0;
+const LIGHTNESS_MAX = 100;
+
+const SATURATION_MIN = 0;
+const SATURATION_MAX = 100;
+
+// Most color formats can be defined using 3 numbers in a standardized order, so
+// this tuple serves as a compact way to store various color formats.
+// E.g. HSL, RGB
+type ColorTuple = [number, number, number];
+
+// Definition of an HSL color with named fields.
+interface HSL {
+  readonly h: number; // 0-360
+  readonly s: number; // 0-100
+  readonly l: number; // 0-100
+}
+
+// Defines an interface to an immutable color object, which can be defined in
+// any arbitrary format or color space and provides function to modify the color
+// and conversions to CSS compatible style strings.
+// Because this color object is effectively immutable, a new color object is
+// returned when modifying the color, rather than editing the current object
+// in-place.
+// Also, because these objects are immutable, it's expected that readonly
+// properties such as |cssString| are efficient, as they can be computed at
+// creation time, so they may be used in the hot path (render loop).
+export interface Color {
+  readonly cssString: string;
+
+  // The perceived brightness of the color using a weighted average of the
+  // r, g and b channels based on human perception.
+  readonly perceivedBrightness: number;
+
+  // Bring up the lightness by |percent| percent.
+  lighten(percent: number, max?: number): Color;
+
+  // Bring down the lightness by |percent| percent.
+  darken(percent: number, min?: number): Color;
+
+  // Bring up the saturation by |percent| percent.
+  saturate(percent: number, max?: number): Color;
+
+  // Bring down the saturation by |percent| percent.
+  desaturate(percent: number, min?: number): Color;
+
+  // Set one or more HSL values.
+  setHSL(hsl: Partial<HSL>): Color;
+
+  setAlpha(alpha: number | undefined): Color;
+}
+
+// Common base class for HSL colors. Avoids code duplication.
+abstract class HSLColorBase<T extends Color> {
+  readonly hsl: ColorTuple;
+  readonly alpha?: number;
+
+  // Values are in the range:
+  // Hue:        0-360
+  // Saturation: 0-100
+  // Lightness:  0-100
+  // Alpha:      0-1
+  constructor(init: ColorTuple | HSL | string, alpha?: number) {
+    if (Array.isArray(init)) {
+      this.hsl = init;
+    } else if (typeof init === 'string') {
+      const rgb = hexToRgb(init);
+      this.hsl = rgbToHsl(rgb);
+    } else {
+      this.hsl = [init.h, init.s, init.l];
+    }
+    this.alpha = alpha;
+  }
+
+  // Subclasses should implement this to teach the base class how to create a
+  // new object of the subclass type.
+  abstract create(hsl: ColorTuple | HSL, alpha?: number): T;
+
+  lighten(amount: number, max = LIGHTNESS_MAX): T {
+    const [h, s, l] = this.hsl;
+    const newLightness = clamp(l + amount, LIGHTNESS_MIN, max);
+    return this.create([h, s, newLightness], this.alpha);
+  }
+
+  darken(amount: number, min = LIGHTNESS_MIN): T {
+    const [h, s, l] = this.hsl;
+    const newLightness = clamp(l - amount, min, LIGHTNESS_MAX);
+    return this.create([h, s, newLightness], this.alpha);
+  }
+
+  saturate(amount: number, max = SATURATION_MAX): T {
+    const [h, s, l] = this.hsl;
+    const newSaturation = clamp(s + amount, SATURATION_MIN, max);
+    return this.create([h, newSaturation, l], this.alpha);
+  }
+
+  desaturate(amount: number, min = SATURATION_MIN): T {
+    const [h, s, l] = this.hsl;
+    const newSaturation = clamp(s - amount, min, SATURATION_MAX);
+    return this.create([h, newSaturation, l], this.alpha);
+  }
+
+  setHSL(hsl: Partial<HSL>): T {
+    const [h, s, l] = this.hsl;
+    return this.create({h, s, l, ...hsl}, this.alpha);
+  }
+
+  setAlpha(alpha: number | undefined): T {
+    return this.create(this.hsl, alpha);
+  }
+}
+
+// Describes a color defined in standard HSL color space.
+export class HSLColor extends HSLColorBase<HSLColor> implements Color {
+  readonly cssString: string;
+  readonly perceivedBrightness: number;
+
+  // Values are in the range:
+  // Hue:        0-360
+  // Saturation: 0-100
+  // Lightness:  0-100
+  // Alpha:      0-1
+  constructor(hsl: ColorTuple | HSL | string, alpha?: number) {
+    super(hsl, alpha);
+
+    const [r, g, b] = hslToRgb(...this.hsl);
+
+    this.perceivedBrightness = perceivedBrightness(r, g, b);
+
+    if (this.alpha === undefined) {
+      this.cssString = `rgb(${r} ${g} ${b})`;
+    } else {
+      this.cssString = `rgb(${r} ${g} ${b} / ${this.alpha})`;
+    }
+  }
+
+  create(values: ColorTuple | HSL, alpha?: number | undefined): HSLColor {
+    return new HSLColor(values, alpha);
+  }
+}
+
+// Describes a color defined in HSLuv color space.
+// See: https://www.hsluv.org/
+export class HSLuvColor extends HSLColorBase<HSLuvColor> implements Color {
+  readonly cssString: string;
+  readonly perceivedBrightness: number;
+
+  constructor(hsl: ColorTuple | HSL, alpha?: number) {
+    super(hsl, alpha);
+
+    const rgb = hsluvToRgb(this.hsl);
+    const r = Math.floor(rgb[0] * 255);
+    const g = Math.floor(rgb[1] * 255);
+    const b = Math.floor(rgb[2] * 255);
+
+    this.perceivedBrightness = perceivedBrightness(r, g, b);
+
+    if (this.alpha === undefined) {
+      this.cssString = `rgb(${r} ${g} ${b})`;
+    } else {
+      this.cssString = `rgb(${r} ${g} ${b} / ${this.alpha})`;
+    }
+  }
+
+  create(raw: ColorTuple | HSL, alpha?: number | undefined): HSLuvColor {
+    return new HSLuvColor(raw, alpha);
+  }
+}
+
+// Hue: 0-360
+// Saturation: 0-100
+// Lightness: 0-100
+// RGB: 0-255
+export function hslToRgb(h: number, s: number, l: number): ColorTuple {
+  h = h;
+  s = s / SATURATION_MAX;
+  l = l / LIGHTNESS_MAX;
+
+  const c = (1 - Math.abs(2 * l - 1)) * s;
+  const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
+  const m = l - c / 2;
+
+  let [r, g, b] = [0, 0, 0];
+
+  if (0 <= h && h < 60) {
+    [r, g, b] = [c, x, 0];
+  } else if (60 <= h && h < 120) {
+    [r, g, b] = [x, c, 0];
+  } else if (120 <= h && h < 180) {
+    [r, g, b] = [0, c, x];
+  } else if (180 <= h && h < 240) {
+    [r, g, b] = [0, x, c];
+  } else if (240 <= h && h < 300) {
+    [r, g, b] = [x, 0, c];
+  } else if (300 <= h && h < 360) {
+    [r, g, b] = [c, 0, x];
+  }
+
+  // Convert to 0-255 range
+  r = Math.round((r + m) * 255);
+  g = Math.round((g + m) * 255);
+  b = Math.round((b + m) * 255);
+
+  return [r, g, b];
+}
+
+export function hexToRgb(hex: string): ColorTuple {
+  // Convert hex to RGB first
+  let r: number = 0;
+  let g: number = 0;
+  let b: number = 0;
+
+  if (hex.length === 4) {
+    r = parseInt(hex[1] + hex[1], 16);
+    g = parseInt(hex[2] + hex[2], 16);
+    b = parseInt(hex[3] + hex[3], 16);
+  } else if (hex.length === 7) {
+    r = parseInt(hex.substring(1, 3), 16);
+    g = parseInt(hex.substring(3, 5), 16);
+    b = parseInt(hex.substring(5, 7), 16);
+  }
+
+  return [r, g, b];
+}
+
+export function rgbToHsl(rgb: ColorTuple): ColorTuple {
+  let [r, g, b] = rgb;
+  r /= 255;
+  g /= 255;
+  b /= 255;
+  const max = Math.max(r, g, b);
+  const min = Math.min(r, g, b);
+  let h: number = (max + min) / 2;
+  let s: number = (max + min) / 2;
+  const l: number = (max + min) / 2;
+
+  if (max === min) {
+    h = s = 0; // achromatic
+  } else {
+    const d = max - min;
+    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+    switch (max) {
+      case r:
+        h = (g - b) / d + (g < b ? 6 : 0);
+        break;
+      case g:
+        h = (b - r) / d + 2;
+        break;
+      case b:
+        h = (r - g) / d + 4;
+        break;
+    }
+    h /= 6;
+  }
+
+  return [h * 360, s * 100, l * 100];
+}
+
+// Return the perceived brightness of a color using a weighted average of the
+// r, g and b channels based on human perception.
+function perceivedBrightness(r: number, g: number, b: number): number {
+  // YIQ calculation from https://24ways.org/2010/calculating-color-contrast
+  return (r * 299 + g * 587 + b * 114) / 1000;
+}
+
+// Comparison function used for sorting colors.
+export function colorCompare(a: Color, b: Color): number {
+  return a.cssString.localeCompare(b.cssString);
+}
diff --git a/ui/src/public/color_scheme.ts b/ui/src/public/color_scheme.ts
new file mode 100644
index 0000000..28100c2
--- /dev/null
+++ b/ui/src/public/color_scheme.ts
@@ -0,0 +1,35 @@
+// 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 {Color} from './color';
+
+// |ColorScheme| defines a collection of colors which can be used for various UI
+// elements. In the future we would expand this interface to include light and
+// dark variants.
+
+export interface ColorScheme {
+  // The base color to be used for the bulk of the element.
+  readonly base: Color;
+
+  // A variant on the base color, commonly used for highlighting.
+  readonly variant: Color;
+
+  // Grayed out color to represent a disabled state.
+  readonly disabled: Color;
+
+  // Appropriate colors for text to be displayed on top of the above colors.
+  readonly textBase: Color;
+  readonly textVariant: Color;
+  readonly textDisabled: Color;
+}
diff --git a/ui/src/public/command.ts b/ui/src/public/command.ts
new file mode 100644
index 0000000..abf884f
--- /dev/null
+++ b/ui/src/public/command.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.
+
+import {Hotkey} from '../base/hotkeys';
+
+export interface CommandManager {
+  registerCommand(command: Command): void;
+
+  hasCommand(commandId: string): boolean;
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  runCommand(id: string, ...args: any[]): any;
+}
+
+export interface Command {
+  // A unique id for this command.
+  id: string;
+  // A human-friendly name for this command.
+  name: string;
+  // Callback is called when the command is invoked.
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  callback: (...args: any[]) => any;
+  // Default hotkey for this command.
+  // Note: this is just the default and may be changed by the user.
+  // Examples:
+  // - 'P'
+  // - 'Shift+P'
+  // - '!Mod+Shift+P'
+  // See hotkeys.ts for guidance on hotkey syntax.
+  defaultHotkey?: Hotkey;
+}
diff --git a/ui/src/public/debug_tracks.ts b/ui/src/public/debug_tracks.ts
new file mode 100644
index 0000000..15577e3
--- /dev/null
+++ b/ui/src/public/debug_tracks.ts
@@ -0,0 +1,17 @@
+// 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.
+
+// TODO(primiano): in near future the code to create debug tracks from an App
+// context will be moved here. For now i'm just re-exporting the function as-is.
+export {addDebugSliceTrack} from './lib/tracks/debug_tracks';
diff --git a/ui/src/public/details_panel.ts b/ui/src/public/details_panel.ts
new file mode 100644
index 0000000..3046275
--- /dev/null
+++ b/ui/src/public/details_panel.ts
@@ -0,0 +1,65 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {Selection, TrackEventSelection} from './selection';
+import {z} from 'zod';
+
+export interface DetailsPanel {
+  render(selection: Selection): m.Children;
+  isLoading?(): boolean;
+}
+
+export interface TrackEventDetailsPanelSerializeArgs<T> {
+  // The Zod schema which will be used the parse the state in a serialized
+  // permalink JSON object.
+  readonly schema: z.ZodType<T>;
+
+  // The serializable state of the details panel. The usage of this field is
+  // as follows
+  //  1) default initialize this field in the constructor.
+  //  2) if the trace is being restored from a permalink, the UI will use
+  //     `schema` to parse the serialized state and will write the result into
+  //     `state`. If parsing failed or the trace is not being restored,
+  //     `state` will not be touched.
+  //  3) if a permalink is requested, the UI will read the value of `state`
+  //     and stash it in the permalink serialzed state.
+  //
+  // This flow has the following consequences:
+  //  1) Details panels *must* respect changes to this object between their
+  //     constructor and the first call to `load()`. This is the point where
+  //     the core will "inject" the permalink deserialized object
+  //     if available.
+  //  2) The `state` object *must* be serializable: that is, it should be a
+  //     pure Javascript object.
+  state: T;
+}
+
+export interface TrackEventDetailsPanel {
+  // Optional: Do any loading required to render the details panel in here and
+  // the core will:
+  // - Ensure that no more than one concurrent loads are enqueued at any given
+  //   time in order to keep the UI snappy.
+  // - Hold off switching to this tab for up to around 50ms while this loading
+  //   is going, to avoid flickering when loading is fast.
+  load?(id: TrackEventSelection): Promise<void>;
+
+  // Called every render cycle to render the details panel. Note: This function
+  // is called regardless of whether |load| has completed yet.
+  render(): m.Children;
+
+  // Optional interface to implement by details panels which want to support
+  // saving/restoring state from a permalink.
+  readonly serialization?: TrackEventDetailsPanelSerializeArgs<unknown>;
+}
diff --git a/ui/src/public/exposed_commands.ts b/ui/src/public/exposed_commands.ts
new file mode 100644
index 0000000..d75275e
--- /dev/null
+++ b/ui/src/public/exposed_commands.ts
@@ -0,0 +1,26 @@
+// 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 file contains constants for some command IDs that are used directly
+// from frontend code (e.g. the details panel that has buttons for critical
+// path). They exist to deal with all cases where some feature cannot be done
+// just with the existing API (e.g. the command palette), and a more direct
+// coupling between frontend and commands is necessary.
+// Adding entries to this file usually is the symptom of a missing API in the
+// plugin surface (e.g. the ability to customize context menus).
+// These constants exist just to make the dependency evident at code
+// search time, rather than copy-pasting the string in two places.
+
+export const CRITICAL_PATH_CMD = 'perfetto.CriticalPath';
+export const CRITICAL_PATH_LITE_CMD = 'perfetto.CriticalPathLite';
diff --git a/ui/src/public/extra_sql_packages.ts b/ui/src/public/extra_sql_packages.ts
new file mode 100644
index 0000000..17feab7
--- /dev/null
+++ b/ui/src/public/extra_sql_packages.ts
@@ -0,0 +1,25 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Interfaces for extra SQL packages injected via google-internal deployments.
+
+export interface SqlModule {
+  readonly name: string;
+  readonly sql: string;
+}
+
+export interface SqlPackage {
+  readonly name: string;
+  readonly modules: SqlModule[];
+}
diff --git a/ui/src/public/feature_flag.ts b/ui/src/public/feature_flag.ts
new file mode 100644
index 0000000..c82d38a
--- /dev/null
+++ b/ui/src/public/feature_flag.ts
@@ -0,0 +1,64 @@
+// 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 FeatureFlagManager {
+  register(settings: FlagSettings): Flag;
+}
+
+export interface FlagSettings {
+  id: string;
+  defaultValue: boolean;
+  description: string;
+  name?: string;
+  devOnly?: boolean;
+}
+
+export interface Flag {
+  // A unique identifier for this flag ("magicSorting")
+  readonly id: string;
+
+  // The name of the flag the user sees ("New track sorting algorithm")
+  readonly name: string;
+
+  // A longer description which is displayed to the user.
+  // "Sort tracks using an embedded tfLite model based on your expression
+  // while waiting for the trace to load."
+  readonly description: string;
+
+  // Whether the flag defaults to true or false.
+  // If !flag.isOverridden() then flag.get() === flag.defaultValue
+  readonly defaultValue: boolean;
+
+  // Get the current value of the flag.
+  get(): boolean;
+
+  // Override the flag and persist the new value.
+  set(value: boolean): void;
+
+  // If the flag has been overridden.
+  // Note: A flag can be overridden to its default value.
+  isOverridden(): boolean;
+
+  // Reset the flag to its default setting.
+  reset(): void;
+
+  // Get the current state of the flag.
+  overriddenState(): OverrideState;
+}
+
+export enum OverrideState {
+  DEFAULT = 'DEFAULT',
+  TRUE = 'OVERRIDE_TRUE',
+  FALSE = 'OVERRIDE_FALSE',
+}
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
deleted file mode 100644
index da7240a..0000000
--- a/ui/src/public/index.ts
+++ /dev/null
@@ -1,395 +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 {Hotkey} from '../base/hotkeys';
-import {TimeSpan, duration, time} from '../base/time';
-import {Migrate, Store} from '../base/store';
-import {ColorScheme} from '../core/colorizer';
-import {PrimaryTrackSortKey} from '../common/state';
-import {Engine} from '../trace_processor/engine';
-import {PromptOption} from '../frontend/omnibox_manager';
-import {LegacyDetailsPanel, TrackDescriptor} from './tracks';
-import {TraceContext} from '../frontend/trace_context';
-
-export {Engine} from '../trace_processor/engine';
-export {
-  LONG,
-  LONG_NULL,
-  NUM,
-  NUM_NULL,
-  STR,
-  STR_NULL,
-} from '../trace_processor/query_result';
-export {BottomTabToSCSAdapter} from './utils';
-export {createStore, Migrate, Store} from '../base/store';
-export {PromptOption} from '../frontend/omnibox_manager';
-export {PrimaryTrackSortKey} from '../common/state';
-
-export {addDebugSliceTrack} from '../frontend/debug_tracks/debug_tracks';
-export * from '../core/track_kinds';
-export {
-  TrackDescriptor,
-  Track,
-  TrackContext,
-  TrackTags,
-  SliceRect,
-  DetailsPanel,
-  LegacyDetailsPanel,
-  TrackSelectionDetailsPanel,
-} from './tracks';
-
-export interface Slice {
-  // These properties are updated only once per query result when the Slice
-  // object is created and don't change afterwards.
-  readonly id: number;
-  readonly startNs: time;
-  readonly endNs: time;
-  readonly durNs: duration;
-  readonly ts: time;
-  readonly dur: duration;
-  readonly depth: number;
-  readonly flags: number;
-
-  // Each slice can represent some extra numerical information by rendering a
-  // portion of the slice with a lighter tint.
-  // |fillRatio\ describes the ratio of the normal area to the tinted area
-  // width of the slice, normalized between 0.0 -> 1.0.
-  // 0.0 means the whole slice is tinted.
-  // 1.0 means none of the slice is tinted.
-  // E.g. If |fillRatio| = 0.65 the slice will be rendered like this:
-  // [############|*******]
-  // ^------------^-------^
-  //     Normal     Light
-  readonly fillRatio: number;
-
-  // These can be changed by the Impl.
-  title: string;
-  subTitle: string;
-  colorScheme: ColorScheme;
-  isHighlighted: boolean;
-}
-
-export interface Command {
-  // A unique id for this command.
-  id: string;
-  // A human-friendly name for this command.
-  name: string;
-  // Callback is called when the command is invoked.
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  callback: (...args: any[]) => any;
-  // Default hotkey for this command.
-  // Note: this is just the default and may be changed by the user.
-  // Examples:
-  // - 'P'
-  // - 'Shift+P'
-  // - '!Mod+Shift+P'
-  // See hotkeys.ts for guidance on hotkey syntax.
-  defaultHotkey?: Hotkey;
-}
-
-export interface MetricVisualisation {
-  // The name of the metric e.g. 'android_camera'
-  metric: string;
-
-  // A vega or vega-lite visualisation spec.
-  // The data from the metric under path will be exposed as a
-  // datasource named "metric" in Vega(-Lite)
-  spec: string;
-
-  // A path index into the metric.
-  // For example if the metric returns the folowing protobuf:
-  // {
-  //   foo {
-  //     bar {
-  //       baz: { name: "a" }
-  //       baz: { name: "b" }
-  //       baz: { name: "c" }
-  //     }
-  //   }
-  // }
-  // That becomes the following json:
-  // { "foo": { "bar": { "baz": [
-  //  {"name": "a"},
-  //  {"name": "b"},
-  //  {"name": "c"},
-  // ]}}}
-  // And given path = ["foo", "bar", "baz"]
-  // We extract:
-  // [ {"name": "a"}, {"name": "b"}, {"name": "c"} ]
-  // And pass that to the vega(-lite) visualisation.
-  path: string[];
-}
-
-export interface SidebarMenuItem {
-  readonly commandId: string;
-  readonly group:
-    | 'navigation'
-    | 'current_trace'
-    | 'convert_trace'
-    | 'example_traces'
-    | 'support';
-  when?(): boolean;
-  readonly icon: string;
-  readonly priority?: number;
-}
-
-// This interface defines a context for a plugin, which is an object passed to
-// most hooks within the plugin. It should be used to interact with Perfetto.
-export interface PluginContext {
-  // The unique ID for this plugin.
-  readonly pluginId: string;
-
-  // Register command against this plugin context.
-  registerCommand(command: Command): void;
-
-  // Run a command, optionally passing some args.
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  runCommand(id: string, ...args: any[]): any;
-
-  // Adds a new menu item to the sidebar.
-  // All entries must map to a command. This will allow the shortcut and
-  // optional shortcut to be displayed on the UI.
-  addSidebarMenuItem(menuItem: SidebarMenuItem): void;
-}
-
-export interface SliceTrackColNames {
-  ts: string;
-  name: string;
-  dur: string;
-}
-
-export interface DebugSliceTrackArgs {
-  // Title of the track. If omitted a placeholder name will be chosen instead.
-  trackName?: string;
-
-  // Mapping definitions of the 'ts', 'dur', and 'name' columns.
-  // By default, columns called ts, dur and name will be used.
-  // If dur is assigned the value '0', all slices shall be instant events.
-  columnMapping?: Partial<SliceTrackColNames>;
-
-  // Any extra columns to be used as args.
-  args?: string[];
-
-  // Optional renaming of columns.
-  columns?: string[];
-}
-
-export interface CounterTrackColNames {
-  ts: string;
-  value: string;
-}
-
-export interface DebugCounterTrackArgs {
-  // Title of the track. If omitted a placeholder name will be chosen instead.
-  trackName?: string;
-
-  // Mapping definitions of the ts and value columns.
-  columnMapping?: Partial<CounterTrackColNames>;
-}
-
-export interface Tab {
-  render(): m.Children;
-  getTitle(): string;
-}
-
-export interface TabDescriptor {
-  uri: string; // TODO(stevegolton): Maybe optional for ephemeral tabs.
-  content: Tab;
-  isEphemeral?: boolean; // Defaults false
-  onHide?(): void;
-  onShow?(): void;
-}
-
-// Similar to PluginContext but with additional methods to operate on the
-// currently loaded trace. Passed to trace-relevant hooks on a plugin instead of
-// PluginContext.
-export interface PluginContextTrace extends PluginContext {
-  readonly engine: Engine;
-
-  // Control over the main timeline.
-  timeline: {
-    // Add a new track to the scrolling track section, returning the newly
-    // created track key.
-    addTrack(uri: string, displayName: string, params?: unknown): string;
-
-    // Remove a single track from the timeline.
-    removeTrack(key: string): void;
-
-    // Pin a single track.
-    pinTrack(key: string): void;
-
-    // Unpin a single track.
-    unpinTrack(key: string): void;
-
-    // Pin all tracks that match a predicate.
-    pinTracksByPredicate(predicate: TrackPredicate): void;
-
-    // Unpin all tracks that match a predicate.
-    unpinTracksByPredicate(predicate: TrackPredicate): void;
-
-    // Remove all tracks that match a predicate.
-    removeTracksByPredicate(predicate: TrackPredicate): void;
-
-    // Expand all groups that match a predicate.
-    expandGroupsByPredicate(predicate: GroupPredicate): void;
-
-    // Collapse all groups that match a predicate.
-    collapseGroupsByPredicate(predicate: GroupPredicate): void;
-
-    // Retrieve a list of tracks on the timeline.
-    tracks: TrackRef[];
-
-    // Bring a timestamp into view.
-    panToTimestamp(ts: time): void;
-
-    // Move the viewport
-    setViewportTime(start: time, end: time): void;
-
-    // A span representing the current viewport location
-    readonly viewport: TimeSpan;
-  };
-
-  // Control over the bottom details pane.
-  tabs: {
-    // Creates a new tab running the provided query.
-    openQuery(query: string, title: string): void;
-
-    // Add a tab to the tab bar (if not already) and focus it.
-    showTab(uri: string): void;
-
-    // Remove a tab from the tab bar.
-    hideTab(uri: string): void;
-  };
-
-  // Register a new track against a unique key known as a URI.
-  // Once a track is registered it can be referenced multiple times on the
-  // timeline with different params to allow customising each instance.
-  registerTrack(trackDesc: TrackDescriptor): void;
-
-  // Add a new entry to the pool of default tracks. Default tracks are a list
-  // of track references that describe the list of tracks that should be added
-  // to the main timeline on startup.
-  // Default tracks are only used when a trace is first loaded, not when
-  // loading from a permalink, where the existing list of tracks from the
-  // shared state is used instead.
-  addDefaultTrack(track: TrackRef): void;
-
-  // Simultaneously register a track and add it as a default track in one go.
-  // This is simply a helper which calls registerTrack() and addDefaultTrack()
-  // with the same URI.
-  registerStaticTrack(track: TrackDescriptor & TrackRef): void;
-
-  // Register a new tab for this plugin. Will be unregistered when the plugin
-  // is deactivated or when the trace is unloaded.
-  registerTab(tab: TabDescriptor): void;
-
-  // Suggest that a tab should be shown immediately.
-  addDefaultTab(uri: string): void;
-
-  // Register a hook into the current selection tab rendering logic that allows
-  // customization of the current selection tab content.
-  registerDetailsPanel(sel: LegacyDetailsPanel): void;
-
-  // Create a store mounted over the top of this plugin's persistent state.
-  mountStore<T>(migrate: Migrate<T>): Store<T>;
-
-  readonly trace: TraceContext;
-
-  // When the trace is opened via postMessage deep-linking, returns the sub-set
-  // of postMessageData.pluginArgs[pluginId] for the current plugin. If not
-  // present returns undefined.
-  readonly openerPluginArgs?: {[key: string]: unknown};
-
-  prompt(text: string, options?: PromptOption[]): Promise<string>;
-}
-
-export interface Plugin {
-  // Lifecycle methods.
-  onActivate?(ctx: PluginContext): void;
-  onTraceLoad?(ctx: PluginContextTrace): Promise<void>;
-  onTraceReady?(ctx: PluginContextTrace): Promise<void>;
-  onTraceUnload?(ctx: PluginContextTrace): Promise<void>;
-  onDeactivate?(ctx: PluginContext): void;
-
-  // Extension points.
-  metricVisualisations?(ctx: PluginContext): MetricVisualisation[];
-}
-
-// This interface defines what a plugin factory should look like.
-// This can be defined in the plugin class definition by defining a constructor
-// and the relevant static methods:
-// E.g.
-// class MyPlugin implements TracePlugin<MyState> {
-//   migrate(initialState: unknown): MyState {...}
-//   constructor(store: Store<MyState>, engine: EngineProxy) {...}
-//   ... methods from the TracePlugin interface go here ...
-// }
-// ... which can then be passed around by class i.e. MyPlugin
-export interface PluginClass {
-  // Instantiate the plugin.
-  new (): Plugin;
-}
-
-// Describes a reference to a registered track.
-export interface TrackRef {
-  // URI of the registered track.
-  readonly uri: string;
-
-  // A human readable name for this track - displayed in the track shell.
-  readonly title: string;
-
-  // Optional: Used to define default sort order for new traces.
-  // Note: This will be deprecated soon in favour of tags & sort rules.
-  readonly sortKey?: PrimaryTrackSortKey;
-
-  // Optional: Add tracks to a group with this name.
-  readonly groupName?: string;
-
-  // Optional: Track key
-  readonly key?: string;
-
-  // Optional: Whether the track is pinned
-  readonly isPinned?: boolean;
-}
-
-// A predicate for selecting a subset of tracks.
-export type TrackPredicate = (info: TrackDescriptor) => boolean;
-
-// Describes a reference to a group of tracks.
-export interface GroupRef {
-  // A human readable name for this track group.
-  displayName: string;
-
-  // True if the track is open else false.
-  collapsed: boolean;
-}
-
-// A predicate for selecting a subset of groups.
-export type GroupPredicate = (info: GroupRef) => boolean;
-
-// Plugins can be class refs or concrete plugin implementations.
-export type PluginFactory = PluginClass | Plugin;
-
-export interface PluginDescriptor {
-  // A unique string for your plugin. To ensure the name is unique you
-  // may wish to use a URL with reversed components in the manner of
-  // Java package names.
-  pluginId: string;
-
-  // The plugin factory used to instantiate the plugin object, or if this is
-  // an actual plugin implementation, it's just used as-is.
-  plugin: PluginFactory;
-}
diff --git a/ui/src/public/lib/colorizer.ts b/ui/src/public/lib/colorizer.ts
new file mode 100644
index 0000000..52ef512
--- /dev/null
+++ b/ui/src/public/lib/colorizer.ts
@@ -0,0 +1,230 @@
+// Copyright (C) 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
+//
+//      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 {hsl} from 'color-convert';
+import {hash} from '../../base/hash';
+import {featureFlags} from '../../core/feature_flags';
+import {Color, HSLColor, HSLuvColor} from '../color';
+import {ColorScheme} from '../color_scheme';
+import {RandState, pseudoRand} from '../../base/rand';
+
+// 128 would provide equal weighting between dark and light text.
+// However, we want to prefer light text for stylistic reasons.
+// A higher value means color must be brighter before switching to dark text.
+const PERCEIVED_BRIGHTNESS_LIMIT = 180;
+
+// This file defines some opinionated colors and provides functions to access
+// random but predictable colors based on a seed, as well as standardized ways
+// to access colors for core objects such as slices and thread states.
+
+// We have, over the years, accumulated a number of different color palettes
+// which are used for different parts of the UI.
+// It would be nice to combine these into a single palette in the future, but
+// changing colors is difficult especially for slice colors, as folks get used
+// to certain slices being certain colors and are resistant to change.
+// However we do it, we should make it possible for folks to switch back the a
+// previous palette, or define their own.
+
+const USE_CONSISTENT_COLORS = featureFlags.register({
+  id: 'useConsistentColors',
+  name: 'Use common color palette for timeline elements',
+  description: 'Use the same color palette for all timeline elements.',
+  defaultValue: false,
+});
+
+const randColourState: RandState = {seed: 0};
+
+const MD_PALETTE_RAW: Color[] = [
+  new HSLColor({h: 4, s: 90, l: 58}),
+  new HSLColor({h: 340, s: 82, l: 52}),
+  new HSLColor({h: 291, s: 64, l: 42}),
+  new HSLColor({h: 262, s: 52, l: 47}),
+  new HSLColor({h: 231, s: 48, l: 48}),
+  new HSLColor({h: 207, s: 90, l: 54}),
+  new HSLColor({h: 199, s: 98, l: 48}),
+  new HSLColor({h: 187, s: 100, l: 42}),
+  new HSLColor({h: 174, s: 100, l: 29}),
+  new HSLColor({h: 122, s: 39, l: 49}),
+  new HSLColor({h: 88, s: 50, l: 53}),
+  new HSLColor({h: 66, s: 70, l: 54}),
+  new HSLColor({h: 45, s: 100, l: 51}),
+  new HSLColor({h: 36, s: 100, l: 50}),
+  new HSLColor({h: 14, s: 100, l: 57}),
+  new HSLColor({h: 16, s: 25, l: 38}),
+  new HSLColor({h: 200, s: 18, l: 46}),
+  new HSLColor({h: 54, s: 100, l: 62}),
+];
+
+const WHITE_COLOR = new HSLColor([0, 0, 100]);
+const BLACK_COLOR = new HSLColor([0, 0, 0]);
+const GRAY_COLOR = new HSLColor([0, 0, 90]);
+
+const MD_PALETTE: ColorScheme[] = MD_PALETTE_RAW.map((color): ColorScheme => {
+  const base = color.lighten(10, 60).desaturate(20);
+  const variant = base.lighten(30, 80).desaturate(20);
+
+  return {
+    base,
+    variant,
+    disabled: GRAY_COLOR,
+    textBase: WHITE_COLOR, // White text suits MD colors quite well
+    textVariant: WHITE_COLOR,
+    textDisabled: WHITE_COLOR, // Low contrast is on purpose
+  };
+});
+
+// Create a color scheme based on a single color, which defines the variant
+// color as a slightly darker and more saturated version of the base color.
+export function makeColorScheme(base: Color, variant?: Color): ColorScheme {
+  variant = variant ?? base.darken(15).saturate(15);
+
+  return {
+    base,
+    variant,
+    disabled: GRAY_COLOR,
+    textBase:
+      base.perceivedBrightness >= PERCEIVED_BRIGHTNESS_LIMIT
+        ? BLACK_COLOR
+        : WHITE_COLOR,
+    textVariant:
+      variant.perceivedBrightness >= PERCEIVED_BRIGHTNESS_LIMIT
+        ? BLACK_COLOR
+        : WHITE_COLOR,
+    textDisabled: WHITE_COLOR, // Low contrast is on purpose
+  };
+}
+
+const GRAY = makeColorScheme(new HSLColor([0, 0, 62]));
+const DESAT_RED = makeColorScheme(new HSLColor([3, 30, 49]));
+const DARK_GREEN = makeColorScheme(new HSLColor([120, 44, 34]));
+const LIME_GREEN = makeColorScheme(new HSLColor([75, 55, 47]));
+const TRANSPARENT_WHITE = makeColorScheme(new HSLColor([0, 1, 97], 0.55));
+const ORANGE = makeColorScheme(new HSLColor([36, 100, 50]));
+const INDIGO = makeColorScheme(new HSLColor([231, 48, 48]));
+
+// A piece of wisdom from a long forgotten blog post: "Don't make
+// colors you want to change something normal like grey."
+export const UNEXPECTED_PINK = makeColorScheme(new HSLColor([330, 100, 70]));
+
+// Selects a predictable color scheme from a palette of material design colors,
+// based on a string seed.
+function materialColorScheme(seed: string): ColorScheme {
+  const colorIdx = hash(seed, MD_PALETTE.length);
+  return MD_PALETTE[colorIdx];
+}
+
+const proceduralColorCache = new Map<string, ColorScheme>();
+
+// Procedurally generates a predictable color scheme based on a string seed.
+function proceduralColorScheme(seed: string): ColorScheme {
+  const colorScheme = proceduralColorCache.get(seed);
+  if (colorScheme) {
+    return colorScheme;
+  } else {
+    const hue = hash(seed, 360);
+    // Saturation 100 would give the most differentiation between colors, but
+    // it's garish.
+    const saturation = 80;
+
+    // Prefer using HSLuv, not the browser's built-in vanilla HSL handling. This
+    // is because this function chooses hue/lightness uniform at random, but HSL
+    // is not perceptually uniform.
+    // See https://www.boronine.com/2012/03/26/Color-Spaces-for-Human-Beings/.
+    const base = new HSLuvColor({
+      h: hue,
+      s: saturation,
+      l: hash(seed + 'x', 40) + 40,
+    });
+    const variant = new HSLuvColor({h: hue, s: saturation, l: 30});
+    const colorScheme = makeColorScheme(base, variant);
+
+    proceduralColorCache.set(seed, colorScheme);
+
+    return colorScheme;
+  }
+}
+
+export function colorForState(state: string): ColorScheme {
+  if (state === 'Running') {
+    return DARK_GREEN;
+  } else if (state.startsWith('Runnable')) {
+    return LIME_GREEN;
+  } else if (state.includes('Uninterruptible Sleep')) {
+    if (state.includes('non-IO')) {
+      return DESAT_RED;
+    }
+    return ORANGE;
+  } else if (state.includes('Dead')) {
+    return GRAY;
+  } else if (state.includes('Sleeping') || state.includes('Idle')) {
+    return TRANSPARENT_WHITE;
+  }
+  return INDIGO;
+}
+
+export function colorForTid(tid: number): ColorScheme {
+  return materialColorScheme(tid.toString());
+}
+
+export function colorForThread(thread?: {
+  pid?: number;
+  tid: number;
+}): ColorScheme {
+  if (thread === undefined) {
+    return GRAY;
+  }
+  const tid = thread.pid ?? thread.tid;
+  return colorForTid(tid);
+}
+
+export function colorForCpu(cpu: number): Color {
+  if (USE_CONSISTENT_COLORS.get()) {
+    return materialColorScheme(cpu.toString()).base;
+  } else {
+    const hue = (128 + 32 * cpu) % 256;
+    return new HSLColor({h: hue, s: 50, l: 50});
+  }
+}
+
+export function randomColor(): string {
+  const rand = pseudoRand(randColourState);
+  if (USE_CONSISTENT_COLORS.get()) {
+    return materialColorScheme(rand.toString()).base.cssString;
+  } else {
+    // 40 different random hues 9 degrees apart.
+    const hue = Math.floor(rand * 40) * 9;
+    return '#' + hsl.hex([hue, 90, 30]);
+  }
+}
+
+export function getColorForSlice(sliceName: string): ColorScheme {
+  const name = sliceName.replace(/( )?\d+/g, '');
+  if (USE_CONSISTENT_COLORS.get()) {
+    return materialColorScheme(name);
+  } else {
+    return proceduralColorScheme(name);
+  }
+}
+
+export function colorForFtrace(name: string): ColorScheme {
+  return materialColorScheme(name);
+}
+
+export function getColorForSample(callsiteId: number): ColorScheme {
+  if (USE_CONSISTENT_COLORS.get()) {
+    return materialColorScheme(String(callsiteId));
+  } else {
+    return proceduralColorScheme(String(callsiteId));
+  }
+}
diff --git a/ui/src/core/colorizer_unittest.ts b/ui/src/public/lib/colorizer_unittest.ts
similarity index 100%
rename from ui/src/core/colorizer_unittest.ts
rename to ui/src/public/lib/colorizer_unittest.ts
diff --git a/ui/src/public/lib/extensions.ts b/ui/src/public/lib/extensions.ts
new file mode 100644
index 0000000..00b9555
--- /dev/null
+++ b/ui/src/public/lib/extensions.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 {type addDebugSliceTrack} from '../debug_tracks';
+import {type addDebugCounterTrack} from './tracks/debug_tracks';
+import {type addSqlTableTab} from '../../frontend/sql_table_tab';
+import {type addVisualizedArgTracks} from '../../frontend/visualized_args_tracks';
+import {type addQueryResultsTab} from './query_table/query_result_tab';
+
+// TODO(primiano & stevegolton): This injection is to break the circular
+// dependency cycle that there is between various tabs and tracks.
+//
+// For example: DebugSliceTrack has a DebugSliceDetailsTab which shows details
+// about slices, which have a context menu, which allows to create a debug track
+// from it. We will break this cycle "more properly" by either:
+// 1. having a registry for context menu items for slices
+// 2. allowing plugins to expose API for the use of other plugins, and putting
+//    these extension points there instead
+
+export interface ExtensionApi {
+  addDebugSliceTrack: typeof addDebugSliceTrack;
+  addDebugCounterTrack: typeof addDebugCounterTrack;
+  addSqlTableTab: typeof addSqlTableTab;
+  addVisualizedArgTracks: typeof addVisualizedArgTracks;
+  addQueryResultsTab: typeof addQueryResultsTab;
+}
+
+export let extensions: ExtensionApi;
+
+export function configureExtensions(e: ExtensionApi) {
+  extensions = e;
+}
diff --git a/ui/src/public/lib/query_flamegraph.ts b/ui/src/public/lib/query_flamegraph.ts
new file mode 100644
index 0000000..ae9474b
--- /dev/null
+++ b/ui/src/public/lib/query_flamegraph.ts
@@ -0,0 +1,477 @@
+// 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 {AsyncLimiter} from '../../base/async_limiter';
+import {AsyncDisposableStack} from '../../base/disposable_stack';
+import {assertExists} from '../../base/logging';
+import {Monitor} from '../../base/monitor';
+import {uuidv4Sql} from '../../base/uuid';
+import {Engine} from '../../trace_processor/engine';
+import {
+  createPerfettoIndex,
+  createPerfettoTable,
+} from '../../trace_processor/sql_utils';
+import {
+  NUM,
+  NUM_NULL,
+  STR,
+  STR_NULL,
+  UNKNOWN,
+} from '../../trace_processor/query_result';
+import {
+  Flamegraph,
+  FlamegraphQueryData,
+  FlamegraphState,
+  FlamegraphView,
+} from '../../widgets/flamegraph';
+import {Trace} from '../trace';
+
+export interface QueryFlamegraphColumn {
+  // The name of the column in SQL.
+  readonly name: string;
+
+  // The human readable name describing the contents of the column.
+  readonly displayName: string;
+}
+
+export interface AggQueryFlamegraphColumn extends QueryFlamegraphColumn {
+  // The aggregation to be run when nodes are merged together in the flamegraph.
+  //
+  // TODO(lalitm): consider adding extra functions here (e.g. a top 5 or similar).
+  readonly mergeAggregation: 'ONE_OR_NULL' | 'SUM';
+}
+
+export interface QueryFlamegraphMetric {
+  // The human readable name of the metric: will be shown to the user to change
+  // between metrics.
+  readonly name: string;
+
+  // The human readable SI-style unit of `selfValue`. Values will be shown to
+  // the user suffixed with this.
+  readonly unit: string;
+
+  // SQL statement which need to be run in preparation for being able to execute
+  // `statement`.
+  readonly dependencySql?: string;
+
+  // A single SQL statement which returns the columns `id`, `parentId`, `name`
+  // `selfValue`, all columns specified by `unaggregatableProperties` and
+  // `aggregatableProperties`.
+  readonly statement: string;
+
+  // Additional contextual columns containing data which should not be merged
+  // between sibling nodes, even if they have the same name.
+  //
+  // Examples include the mapping that a name comes from, the heap graph root
+  // type etc.
+  //
+  // Note: the name is always unaggregatable and should not be specified here.
+  readonly unaggregatableProperties?: ReadonlyArray<QueryFlamegraphColumn>;
+
+  // Additional contextual columns containing data which will be displayed to
+  // the user if there is no merging. If there is merging, currently the value
+  // will not be shown.
+  //
+  // Examples include the source file and line number.
+  readonly aggregatableProperties?: ReadonlyArray<AggQueryFlamegraphColumn>;
+}
+
+export interface QueryFlamegraphState {
+  state: FlamegraphState;
+}
+
+// Given a table and columns on those table (corresponding to metrics),
+// returns an array of `QueryFlamegraphMetric` structs which can be passed
+// in QueryFlamegraph's attrs.
+//
+// `tableOrSubquery` should have the columns `id`, `parentId`, `name` and all
+// columns specified by `tableMetrics[].name`, `unaggregatableProperties` and
+// `aggregatableProperties`.
+export function metricsFromTableOrSubquery(
+  tableOrSubquery: string,
+  tableMetrics: ReadonlyArray<{name: string; unit: string; columnName: string}>,
+  dependencySql?: string,
+  unaggregatableProperties?: ReadonlyArray<QueryFlamegraphColumn>,
+  aggregatableProperties?: ReadonlyArray<AggQueryFlamegraphColumn>,
+): QueryFlamegraphMetric[] {
+  const metrics = [];
+  for (const {name, unit, columnName} of tableMetrics) {
+    metrics.push({
+      name,
+      unit,
+      dependencySql,
+      statement: `
+        select *, ${columnName} as value
+        from ${tableOrSubquery}
+      `,
+      unaggregatableProperties,
+      aggregatableProperties,
+    });
+  }
+  return metrics;
+}
+
+// A Perfetto UI component which wraps the `Flamegraph` widget and fetches the
+// data for the widget by querying an `Engine`.
+export class QueryFlamegraph {
+  private data?: FlamegraphQueryData;
+  private readonly selMonitor = new Monitor([() => this.state.state]);
+  private readonly queryLimiter = new AsyncLimiter();
+
+  constructor(
+    private readonly trace: Trace,
+    private readonly metrics: ReadonlyArray<QueryFlamegraphMetric>,
+    private state: QueryFlamegraphState,
+  ) {}
+
+  render() {
+    if (this.selMonitor.ifStateChanged()) {
+      const metric = assertExists(
+        this.metrics.find(
+          (x) => this.state.state.selectedMetricName === x.name,
+        ),
+      );
+      const engine = this.trace.engine;
+      const state = this.state;
+      this.data = undefined;
+      this.queryLimiter.schedule(async () => {
+        this.data = undefined;
+        this.data = await computeFlamegraphTree(engine, metric, state.state);
+      });
+    }
+    return m(Flamegraph, {
+      metrics: this.metrics,
+      data: this.data,
+      state: this.state.state,
+      onStateChange: (state) => {
+        this.state.state = state;
+        this.trace.scheduleFullRedraw();
+      },
+    });
+  }
+}
+
+async function computeFlamegraphTree(
+  engine: Engine,
+  {
+    dependencySql,
+    statement,
+    unaggregatableProperties,
+    aggregatableProperties,
+  }: QueryFlamegraphMetric,
+  {filters, view}: FlamegraphState,
+): Promise<FlamegraphQueryData> {
+  const showStack = filters
+    .filter((x) => x.kind === 'SHOW_STACK')
+    .map((x) => x.filter);
+  const hideStack = filters
+    .filter((x) => x.kind === 'HIDE_STACK')
+    .map((x) => x.filter);
+  const showFromFrame = filters
+    .filter((x) => x.kind === 'SHOW_FROM_FRAME')
+    .map((x) => x.filter);
+  const hideFrame = filters
+    .filter((x) => x.kind === 'HIDE_FRAME')
+    .map((x) => x.filter);
+
+  // Pivot also essentially acts as a "show stack" filter so treat it like one.
+  const showStackAndPivot = [...showStack];
+  if (view.kind === 'PIVOT') {
+    showStackAndPivot.push(view.pivot);
+  }
+
+  const showStackFilter =
+    showStackAndPivot.length === 0
+      ? '0'
+      : showStackAndPivot
+          .map(
+            (x, i) => `((name like '${makeSqlFilter(x)}' escape '\\') << ${i})`,
+          )
+          .join(' | ');
+  const showStackBits = (1 << showStackAndPivot.length) - 1;
+
+  const hideStackFilter =
+    hideStack.length === 0
+      ? 'false'
+      : hideStack
+          .map((x) => `name like '${makeSqlFilter(x)}' escape '\\'`)
+          .join(' OR ');
+
+  const showFromFrameFilter =
+    showFromFrame.length === 0
+      ? '0'
+      : showFromFrame
+          .map(
+            (x, i) => `((name like '${makeSqlFilter(x)}' escape '\\') << ${i})`,
+          )
+          .join(' | ');
+  const showFromFrameBits = (1 << showFromFrame.length) - 1;
+
+  const hideFrameFilter =
+    hideFrame.length === 0
+      ? 'false'
+      : hideFrame
+          .map((x) => `name like '${makeSqlFilter(x)}' escape '\\'`)
+          .join(' OR ');
+
+  const pivotFilter = getPivotFilter(view);
+
+  const unagg = unaggregatableProperties ?? [];
+  const unaggCols = unagg.map((x) => x.name);
+
+  const agg = aggregatableProperties ?? [];
+  const aggCols = agg.map((x) => x.name);
+
+  const groupingColumns = `(${(unaggCols.length === 0 ? ['groupingColumn'] : unaggCols).join()})`;
+  const groupedColumns = `(${(aggCols.length === 0 ? ['groupedColumn'] : aggCols).join()})`;
+
+  if (dependencySql !== undefined) {
+    await engine.query(dependencySql);
+  }
+  await engine.query(`include perfetto module viz.flamegraph;`);
+
+  const uuid = uuidv4Sql();
+  await using disposable = new AsyncDisposableStack();
+
+  disposable.use(
+    await createPerfettoTable(
+      engine,
+      `_flamegraph_materialized_statement_${uuid}`,
+      statement,
+    ),
+  );
+  disposable.use(
+    await createPerfettoIndex(
+      engine,
+      `_flamegraph_materialized_statement_${uuid}_index`,
+      `_flamegraph_materialized_statement_${uuid}(parentId)`,
+    ),
+  );
+
+  // TODO(lalitm): this doesn't need to be called unless we have
+  // a non-empty set of filters.
+  disposable.use(
+    await createPerfettoTable(
+      engine,
+      `_flamegraph_source_${uuid}`,
+      `
+        select *
+        from _viz_flamegraph_prepare_filter!(
+          (
+            select
+              s.id,
+              s.parentId,
+              s.name,
+              s.value,
+              ${(unaggCols.length === 0
+                ? [`'' as groupingColumn`]
+                : unaggCols.map((x) => `s.${x}`)
+              ).join()},
+              ${(aggCols.length === 0
+                ? [`'' as groupedColumn`]
+                : aggCols.map((x) => `s.${x}`)
+              ).join()}
+            from _flamegraph_materialized_statement_${uuid} s
+          ),
+          (${showStackFilter}),
+          (${hideStackFilter}),
+          (${showFromFrameFilter}),
+          (${hideFrameFilter}),
+          (${pivotFilter}),
+          ${1 << showStackAndPivot.length},
+          ${groupingColumns}
+        )
+      `,
+    ),
+  );
+  // TODO(lalitm): this doesn't need to be called unless we have
+  // a non-empty set of filters.
+  disposable.use(
+    await createPerfettoTable(
+      engine,
+      `_flamegraph_filtered_${uuid}`,
+      `
+        select *
+        from _viz_flamegraph_filter_frames!(
+          _flamegraph_source_${uuid},
+          ${showFromFrameBits}
+        )
+      `,
+    ),
+  );
+  disposable.use(
+    await createPerfettoTable(
+      engine,
+      `_flamegraph_accumulated_${uuid}`,
+      `
+        select *
+        from _viz_flamegraph_accumulate!(
+          _flamegraph_filtered_${uuid},
+          ${showStackBits}
+        )
+      `,
+    ),
+  );
+  disposable.use(
+    await createPerfettoTable(
+      engine,
+      `_flamegraph_hash_${uuid}`,
+      `
+        select *
+        from _viz_flamegraph_downwards_hash!(
+          _flamegraph_source_${uuid},
+          _flamegraph_filtered_${uuid},
+          _flamegraph_accumulated_${uuid},
+          ${groupingColumns},
+          ${groupedColumns},
+          ${view.kind === 'BOTTOM_UP' ? 'FALSE' : 'TRUE'}
+        )
+        union all
+        select *
+        from _viz_flamegraph_upwards_hash!(
+          _flamegraph_source_${uuid},
+          _flamegraph_filtered_${uuid},
+          _flamegraph_accumulated_${uuid},
+          ${groupingColumns},
+          ${groupedColumns}
+        )
+        order by hash
+      `,
+    ),
+  );
+  disposable.use(
+    await createPerfettoTable(
+      engine,
+      `_flamegraph_merged_${uuid}`,
+      `
+        select *
+        from _viz_flamegraph_merge_hashes!(
+          _flamegraph_hash_${uuid},
+          ${groupingColumns},
+          ${computeGroupedAggExprs(agg)}
+        )
+      `,
+    ),
+  );
+  disposable.use(
+    await createPerfettoTable(
+      engine,
+      `_flamegraph_layout_${uuid}`,
+      `
+        select *
+        from _viz_flamegraph_local_layout!(
+          _flamegraph_merged_${uuid}
+        );
+      `,
+    ),
+  );
+  const res = await engine.query(`
+    select *
+    from _viz_flamegraph_global_layout!(
+      _flamegraph_merged_${uuid},
+      _flamegraph_layout_${uuid},
+      ${groupingColumns},
+      ${groupedColumns}
+    )
+  `);
+
+  const it = res.iter({
+    id: NUM,
+    parentId: NUM,
+    depth: NUM,
+    name: STR,
+    selfValue: NUM,
+    cumulativeValue: NUM,
+    parentCumulativeValue: NUM_NULL,
+    xStart: NUM,
+    xEnd: NUM,
+    ...Object.fromEntries(unaggCols.map((m) => [m, STR_NULL])),
+    ...Object.fromEntries(aggCols.map((m) => [m, UNKNOWN])),
+  });
+  let postiveRootsValue = 0;
+  let negativeRootsValue = 0;
+  let minDepth = 0;
+  let maxDepth = 0;
+  const nodes = [];
+  for (; it.valid(); it.next()) {
+    const properties = new Map<string, string>();
+    for (const a of [...agg, ...unagg]) {
+      const r = it.get(a.name);
+      if (r !== null) {
+        properties.set(a.displayName, r as string);
+      }
+    }
+    nodes.push({
+      id: it.id,
+      parentId: it.parentId,
+      depth: it.depth,
+      name: it.name,
+      selfValue: it.selfValue,
+      cumulativeValue: it.cumulativeValue,
+      parentCumulativeValue: it.parentCumulativeValue ?? undefined,
+      xStart: it.xStart,
+      xEnd: it.xEnd,
+      properties,
+    });
+    if (it.depth === 1) {
+      postiveRootsValue += it.cumulativeValue;
+    } else if (it.depth === -1) {
+      negativeRootsValue += it.cumulativeValue;
+    }
+    minDepth = Math.min(minDepth, it.depth);
+    maxDepth = Math.max(maxDepth, it.depth);
+  }
+  const sumQuery = await engine.query(
+    `select sum(value) v from _flamegraph_source_${uuid}`,
+  );
+  const unfilteredCumulativeValue = sumQuery.firstRow({v: NUM_NULL}).v ?? 0;
+  return {
+    nodes,
+    allRootsCumulativeValue:
+      view.kind === 'BOTTOM_UP' ? negativeRootsValue : postiveRootsValue,
+    unfilteredCumulativeValue,
+    minDepth,
+    maxDepth,
+  };
+}
+
+function makeSqlFilter(x: string) {
+  if (x.startsWith('^') && x.endsWith('$')) {
+    return x.slice(1, -1);
+  }
+  return `%${x}%`;
+}
+
+function getPivotFilter(view: FlamegraphView) {
+  if (view.kind === 'PIVOT') {
+    return `name like '${makeSqlFilter(view.pivot)}'`;
+  }
+  if (view.kind === 'BOTTOM_UP') {
+    return 'value > 0';
+  }
+  return '0';
+}
+
+function computeGroupedAggExprs(agg: ReadonlyArray<AggQueryFlamegraphColumn>) {
+  const aggFor = (x: AggQueryFlamegraphColumn) => {
+    switch (x.mergeAggregation) {
+      case 'ONE_OR_NULL':
+        return `IIF(COUNT() = 1, ${x.name}, NULL) AS ${x.name}`;
+      case 'SUM':
+        return `SUM(${x.name}) AS ${x.name}`;
+    }
+  };
+  return `(${agg.length === 0 ? 'groupedColumn' : agg.map((x) => aggFor(x)).join(',')})`;
+}
diff --git a/ui/src/public/lib/query_table/queries.ts b/ui/src/public/lib/query_table/queries.ts
new file mode 100644
index 0000000..147989a
--- /dev/null
+++ b/ui/src/public/lib/query_table/queries.ts
@@ -0,0 +1,98 @@
+// 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 {Engine} from '../../../trace_processor/engine';
+import {Row} from '../../../trace_processor/query_result';
+
+const MAX_DISPLAY_ROWS = 10000;
+
+export interface QueryResponse {
+  query: string;
+  error?: string;
+  totalRowCount: number;
+  durationMs: number;
+  columns: string[];
+  rows: Row[];
+  statementCount: number;
+  statementWithOutputCount: number;
+  lastStatementSql: string;
+}
+
+export interface QueryRunParams {
+  // If true, replaces nulls with "NULL" string. Default is true.
+  convertNullsToString?: boolean;
+}
+
+export async function runQuery(
+  sqlQuery: string,
+  engine: Engine,
+  params?: QueryRunParams,
+): Promise<QueryResponse> {
+  const startMs = performance.now();
+
+  // TODO(primiano): once the controller thread is gone we should pass down
+  // the result objects directly to the frontend, iterate over the result
+  // and deal with pagination there. For now we keep the old behavior and
+  // truncate to 10k rows.
+
+  const maybeResult = await engine.tryQuery(sqlQuery);
+
+  if (maybeResult.success) {
+    const queryRes = maybeResult.result;
+    const convertNullsToString = params?.convertNullsToString ?? true;
+
+    const durationMs = performance.now() - startMs;
+    const rows: Row[] = [];
+    const columns = queryRes.columns();
+    let numRows = 0;
+    for (const iter = queryRes.iter({}); iter.valid(); iter.next()) {
+      const row: Row = {};
+      for (const colName of columns) {
+        const value = iter.get(colName);
+        row[colName] = value === null && convertNullsToString ? 'NULL' : value;
+      }
+      rows.push(row);
+      if (++numRows >= MAX_DISPLAY_ROWS) break;
+    }
+
+    const result: QueryResponse = {
+      query: sqlQuery,
+      durationMs,
+      error: queryRes.error(),
+      totalRowCount: queryRes.numRows(),
+      columns,
+      rows,
+      statementCount: queryRes.statementCount(),
+      statementWithOutputCount: queryRes.statementWithOutputCount(),
+      lastStatementSql: queryRes.lastStatementSql(),
+    };
+    return result;
+  } else {
+    // In the case of a query error we don't want the exception to bubble up
+    // as a crash. The |queryRes| object will be populated anyways.
+    // queryRes.error() is used to tell if the query errored or not. If it
+    // errored, the frontend will show a graceful message instead.
+    return {
+      query: sqlQuery,
+      durationMs: performance.now() - startMs,
+      error: maybeResult.error.message,
+      totalRowCount: 0,
+      columns: [],
+      rows: [],
+      statementCount: 0,
+      statementWithOutputCount: 0,
+      lastStatementSql: '',
+    };
+  }
+}
diff --git a/ui/src/public/lib/query_table/query_result_tab.ts b/ui/src/public/lib/query_table/query_result_tab.ts
new file mode 100644
index 0000000..bf30cd0
--- /dev/null
+++ b/ui/src/public/lib/query_table/query_result_tab.ts
@@ -0,0 +1,152 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {v4 as uuidv4} from 'uuid';
+import {assertExists} from '../../../base/logging';
+import {QueryResponse, runQuery} from './queries';
+import {QueryError} from '../../../trace_processor/query_result';
+import {AddDebugTrackMenu} from '../tracks/add_debug_track_menu';
+import {Button} from '../../../widgets/button';
+import {PopupMenu2} from '../../../widgets/menu';
+import {PopupPosition} from '../../../widgets/popup';
+import {QueryTable} from './query_table';
+import {Trace} from '../../../public/trace';
+import {Tab} from '../../tab';
+
+interface QueryResultTabConfig {
+  readonly query: string;
+  readonly title: string;
+  // Optional data to display in this tab instead of fetching it again
+  // (e.g. when duplicating an existing tab which already has the data).
+  readonly prefetchedResponse?: QueryResponse;
+}
+
+// External interface for adding a new query results tab
+// Automatically decided whether to add v1 or v2 tab
+export function addQueryResultsTab(
+  trace: Trace,
+  config: QueryResultTabConfig,
+  tag?: string,
+): void {
+  const queryResultsTab = new QueryResultTab(trace, config);
+
+  const uri = 'queryResults#' + (tag ?? uuidv4());
+
+  trace.tabs.registerTab({
+    uri,
+    content: queryResultsTab,
+    isEphemeral: true,
+  });
+  trace.tabs.showTab(uri);
+}
+
+export class QueryResultTab implements Tab {
+  private queryResponse?: QueryResponse;
+  private sqlViewName?: string;
+
+  constructor(
+    private readonly trace: Trace,
+    private readonly args: QueryResultTabConfig,
+  ) {
+    this.initTrack();
+  }
+
+  private async initTrack() {
+    if (this.args.prefetchedResponse !== undefined) {
+      this.queryResponse = this.args.prefetchedResponse;
+    } else {
+      const result = await runQuery(this.args.query, this.trace.engine);
+      this.queryResponse = result;
+      if (result.error !== undefined) {
+        return;
+      }
+    }
+
+    // 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 {
+    const suffix = this.queryResponse
+      ? ` (${this.queryResponse.rows.length})`
+      : '';
+    return `${this.args.title}${suffix}`;
+  }
+
+  render(): m.Children {
+    return m(QueryTable, {
+      trace: this.trace,
+      query: this.args.query,
+      resp: this.queryResponse,
+      fillParent: true,
+      contextButtons: [
+        this.sqlViewName === undefined
+          ? null
+          : m(
+              PopupMenu2,
+              {
+                trigger: m(Button, {label: 'Show debug track'}),
+                popupPosition: PopupPosition.Top,
+              },
+              m(AddDebugTrackMenu, {
+                trace: this.trace,
+                dataSource: {
+                  sqlSource: `select * from ${this.sqlViewName}`,
+                  columns: assertExists(this.queryResponse).columns,
+                },
+              }),
+            ),
+      ],
+    });
+  }
+
+  isLoading() {
+    return this.queryResponse === undefined;
+  }
+
+  async createViewForDebugTrack(uuid: string): Promise<string> {
+    const viewId = uuidToViewName(uuid);
+    // Assuming that the query results come from a SELECT query, try creating a
+    // view to allow us to reuse it for further queries.
+    const hasValidQueryResponse =
+      this.queryResponse && this.queryResponse.error === undefined;
+    const sqlQuery = hasValidQueryResponse
+      ? this.queryResponse!.lastStatementSql
+      : this.args.query;
+    try {
+      const createViewResult = await this.trace.engine.query(
+        `create view ${viewId} as ${sqlQuery}`,
+      );
+      if (createViewResult.error()) {
+        // If it failed, do nothing.
+        return '';
+      }
+    } catch (e) {
+      if (e instanceof QueryError) {
+        // If it failed, do nothing.
+        return '';
+      }
+      throw e;
+    }
+    return viewId;
+  }
+}
+
+export function uuidToViewName(uuid: string): string {
+  return `view_${uuid.split('-').join('_')}`;
+}
diff --git a/ui/src/public/lib/query_table/query_table.ts b/ui/src/public/lib/query_table/query_table.ts
new file mode 100644
index 0000000..2ab39e1
--- /dev/null
+++ b/ui/src/public/lib/query_table/query_table.ts
@@ -0,0 +1,313 @@
+// 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.
+
+import m from 'mithril';
+import {BigintMath} from '../../../base/bigint_math';
+import {copyToClipboard} from '../../../base/clipboard';
+import {isString} from '../../../base/object_utils';
+import {Time} from '../../../base/time';
+import {QueryResponse} from './queries';
+import {Row} from '../../../trace_processor/query_result';
+import {Anchor} from '../../../widgets/anchor';
+import {Button} from '../../../widgets/button';
+import {Callout} from '../../../widgets/callout';
+import {DetailsShell} from '../../../widgets/details_shell';
+import {downloadData} from '../../../frontend/download_utils';
+import {Router} from '../../../core/router';
+import {scrollTo} from '../../scroll_helper';
+import {AppImpl} from '../../../core/app_impl';
+import {Trace} from '../../trace';
+
+interface QueryTableRowAttrs {
+  trace: Trace;
+  row: Row;
+  columns: string[];
+}
+
+type Numeric = bigint | number;
+
+function isIntegral(x: Row[string]): x is Numeric {
+  return (
+    typeof x === 'bigint' || (typeof x === 'number' && Number.isInteger(x))
+  );
+}
+
+function hasTs(row: Row): row is Row & {ts: Numeric} {
+  return 'ts' in row && isIntegral(row.ts);
+}
+
+function hasDur(row: Row): row is Row & {dur: Numeric} {
+  return 'dur' in row && isIntegral(row.dur);
+}
+
+function hasTrackId(row: Row): row is Row & {track_id: Numeric} {
+  return 'track_id' in row && isIntegral(row.track_id);
+}
+
+function hasType(row: Row): row is Row & {type: string} {
+  return 'type' in row && isString(row.type);
+}
+
+function hasId(row: Row): row is Row & {id: Numeric} {
+  return 'id' in row && isIntegral(row.id);
+}
+
+function hasSliceId(row: Row): row is Row & {slice_id: Numeric} {
+  return 'slice_id' in row && isIntegral(row.slice_id);
+}
+
+// These are properties that a row should have in order to be "slice-like",
+// insofar as it represents a time range and a track id which can be revealed
+// or zoomed-into on the timeline.
+type Sliceish = {
+  ts: Numeric;
+  dur: Numeric;
+  track_id: Numeric;
+};
+
+export function isSliceish(row: Row): row is Row & Sliceish {
+  return hasTs(row) && hasDur(row) && hasTrackId(row);
+}
+
+// Attempts to extract a slice ID from a row, or undefined if none can be found
+export function getSliceId(row: Row): number | undefined {
+  if (hasType(row) && row.type.includes('slice')) {
+    if (hasId(row)) {
+      return Number(row.id);
+    }
+  } else {
+    if (hasSliceId(row)) {
+      return Number(row.slice_id);
+    }
+  }
+  return undefined;
+}
+
+class QueryTableRow implements m.ClassComponent<QueryTableRowAttrs> {
+  private readonly trace: Trace;
+
+  constructor({attrs}: m.Vnode<QueryTableRowAttrs>) {
+    this.trace = attrs.trace;
+  }
+
+  view(vnode: m.Vnode<QueryTableRowAttrs>) {
+    const {row, columns} = vnode.attrs;
+    const cells = columns.map((col) => this.renderCell(col, row[col]));
+
+    // TODO(dproy): Make click handler work from analyze page.
+    if (
+      Router.parseUrl(window.location.href).page === '/viewer' &&
+      isSliceish(row)
+    ) {
+      return m(
+        'tr',
+        {
+          onclick: () => this.selectAndRevealSlice(row, false),
+          // TODO(altimin): Consider improving the logic here (e.g. delay?) to
+          // account for cases when dblclick fires late.
+          ondblclick: () => this.selectAndRevealSlice(row, true),
+          clickable: true,
+          title: 'Go to slice',
+        },
+        cells,
+      );
+    } else {
+      return m('tr', cells);
+    }
+  }
+
+  private renderCell(name: string, value: Row[string]) {
+    if (value instanceof Uint8Array) {
+      return m('td', this.renderBlob(name, value));
+    } else {
+      return m('td', `${value}`);
+    }
+  }
+
+  private renderBlob(name: string, value: Uint8Array) {
+    return m(
+      Anchor,
+      {
+        onclick: () => downloadData(`${name}.blob`, value),
+      },
+      `Blob (${value.length} bytes)`,
+    );
+  }
+
+  private selectAndRevealSlice(
+    row: Row & Sliceish,
+    switchToCurrentSelectionTab: boolean,
+  ) {
+    const trackId = Number(row.track_id);
+    const sliceStart = Time.fromRaw(BigInt(row.ts));
+    // row.dur can be negative. Clamp to 1ns.
+    const sliceDur = BigintMath.max(BigInt(row.dur), 1n);
+    const trackUri = this.trace.tracks.findTrack((td) =>
+      td.tags?.trackIds?.includes(trackId),
+    )?.uri;
+    if (trackUri !== undefined) {
+      scrollTo({
+        track: {uri: trackUri, expandGroup: true},
+        time: {start: sliceStart, end: Time.add(sliceStart, sliceDur)},
+      });
+      const sliceId = getSliceId(row);
+      if (sliceId !== undefined) {
+        this.selectSlice(sliceId, switchToCurrentSelectionTab);
+      }
+    }
+  }
+
+  private selectSlice(sliceId: number, switchToCurrentSelectionTab: boolean) {
+    this.trace.selection.selectSqlEvent('slice', sliceId, {
+      switchToCurrentSelectionTab,
+      scrollToSelection: true,
+    });
+  }
+}
+
+interface QueryTableContentAttrs {
+  trace: Trace;
+  resp: QueryResponse;
+}
+
+class QueryTableContent implements m.ClassComponent<QueryTableContentAttrs> {
+  private previousResponse?: QueryResponse;
+
+  onbeforeupdate(vnode: m.CVnode<QueryTableContentAttrs>) {
+    return vnode.attrs.resp !== this.previousResponse;
+  }
+
+  view(vnode: m.CVnode<QueryTableContentAttrs>) {
+    const resp = vnode.attrs.resp;
+    this.previousResponse = resp;
+    const cols = [];
+    for (const col of resp.columns) {
+      cols.push(m('td', col));
+    }
+    const tableHeader = m('tr', cols);
+
+    const rows = resp.rows.map((row) =>
+      m(QueryTableRow, {trace: vnode.attrs.trace, row, columns: resp.columns}),
+    );
+
+    if (resp.error) {
+      return m('.query-error', `SQL error: ${resp.error}`);
+    } else {
+      return m(
+        'table.pf-query-table',
+        m('thead', tableHeader),
+        m('tbody', rows),
+      );
+    }
+  }
+}
+
+interface QueryTableAttrs {
+  trace: Trace;
+  query: string;
+  resp?: QueryResponse;
+  contextButtons?: m.Child[];
+  fillParent: boolean;
+}
+
+export class QueryTable implements m.ClassComponent<QueryTableAttrs> {
+  private readonly trace: Trace;
+
+  constructor({attrs}: m.CVnode<QueryTableAttrs>) {
+    this.trace = attrs.trace;
+  }
+
+  view({attrs}: m.CVnode<QueryTableAttrs>) {
+    const {resp, query, contextButtons = [], fillParent} = attrs;
+
+    return m(
+      DetailsShell,
+      {
+        title: this.renderTitle(resp),
+        description: query,
+        buttons: this.renderButtons(query, contextButtons, resp),
+        fillParent,
+      },
+      resp && this.renderTableContent(resp),
+    );
+  }
+
+  renderTitle(resp?: QueryResponse) {
+    if (!resp) {
+      return 'Query - running';
+    }
+    const result = resp.error ? 'error' : `${resp.rows.length} rows`;
+    if (AppImpl.instance.testingMode) {
+      // Omit the duration in tests, they cause screenshot diff failures.
+      return `Query result (${result})`;
+    }
+    return `Query result (${result}) - ${resp.durationMs.toLocaleString()}ms`;
+  }
+
+  renderButtons(
+    query: string,
+    contextButtons: m.Child[],
+    resp?: QueryResponse,
+  ) {
+    return [
+      contextButtons,
+      m(Button, {
+        label: 'Copy query',
+        onclick: () => {
+          copyToClipboard(query);
+        },
+      }),
+      resp &&
+        resp.error === undefined &&
+        m(Button, {
+          label: 'Copy result (.tsv)',
+          onclick: () => {
+            queryResponseToClipboard(resp);
+          },
+        }),
+    ];
+  }
+
+  renderTableContent(resp: QueryResponse) {
+    return m(
+      '.pf-query-panel',
+      resp.statementWithOutputCount > 1 &&
+        m(
+          '.pf-query-warning',
+          m(
+            Callout,
+            {icon: 'warning'},
+            `${resp.statementWithOutputCount} out of ${resp.statementCount} `,
+            'statements returned a result. ',
+            'Only the results for the last statement are displayed.',
+          ),
+        ),
+      m(QueryTableContent, {trace: this.trace, resp}),
+    );
+  }
+}
+
+async function queryResponseToClipboard(resp: QueryResponse): Promise<void> {
+  const lines: string[][] = [];
+  lines.push(resp.columns);
+  for (const row of resp.rows) {
+    const line = [];
+    for (const col of resp.columns) {
+      const value = row[col];
+      line.push(value === null ? 'NULL' : `${value}`);
+    }
+    lines.push(line);
+  }
+  copyToClipboard(lines.map((line) => line.join('\t')).join('\n'));
+}
diff --git a/ui/src/frontend/query_table_unittest.ts b/ui/src/public/lib/query_table/query_table_unittest.ts
similarity index 100%
rename from ui/src/frontend/query_table_unittest.ts
rename to ui/src/public/lib/query_table/query_table_unittest.ts
diff --git a/ui/src/public/lib/stdlib_docs.ts b/ui/src/public/lib/stdlib_docs.ts
new file mode 100644
index 0000000..fcdcf18
--- /dev/null
+++ b/ui/src/public/lib/stdlib_docs.ts
@@ -0,0 +1,22 @@
+// 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 {assetSrc} from '../../base/assets';
+
+// Fetch the stdlib docs
+export async function getStdlibDocs(): Promise<string> {
+  const resp = await fetch(assetSrc('stdlib_docs.json'));
+  const json = await resp.json();
+  return JSON.parse(json);
+}
diff --git a/ui/src/public/lib/tracks/add_debug_track_menu.ts b/ui/src/public/lib/tracks/add_debug_track_menu.ts
new file mode 100644
index 0000000..19eef19
--- /dev/null
+++ b/ui/src/public/lib/tracks/add_debug_track_menu.ts
@@ -0,0 +1,264 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {findRef} from '../../../base/dom_utils';
+import {Form, FormLabel} from '../../../widgets/form';
+import {Select} from '../../../widgets/select';
+import {TextInput} from '../../../widgets/text_input';
+import {
+  addDebugCounterTrack,
+  addDebugSliceTrack,
+  addPivotedTracks,
+} from './debug_tracks';
+import {Trace} from '../../trace';
+import {SliceColumnMapping, SqlDataSource} from './query_slice_track';
+import {CounterColumnMapping} from './query_counter_track';
+
+interface AddDebugTrackMenuAttrs {
+  dataSource: Required<SqlDataSource>;
+  trace: Trace;
+}
+
+const TRACK_NAME_FIELD_REF = 'TRACK_NAME_FIELD';
+
+export class AddDebugTrackMenu
+  implements m.ClassComponent<AddDebugTrackMenuAttrs>
+{
+  readonly columns: string[];
+
+  name: string = '';
+  trackType: 'slice' | 'counter' = 'slice';
+  // Names of columns which will be used as data sources for rendering.
+  // We store the config for all possible columns used for rendering (i.e.
+  // 'value' for slice and 'name' for counter) and then just don't the values
+  // which don't match the currently selected track type (so changing track type
+  // from A to B and back to A is a no-op).
+  renderParams: {
+    ts: string;
+    dur: string;
+    name: string;
+    value: string;
+    pivot: string;
+  };
+
+  constructor(vnode: m.Vnode<AddDebugTrackMenuAttrs>) {
+    this.columns = [...vnode.attrs.dataSource.columns];
+
+    const chooseDefaultOption = (name: string) => {
+      for (const column of this.columns) {
+        if (column === name) return column;
+      }
+      for (const column of this.columns) {
+        if (column.endsWith(`_${name}`)) return column;
+      }
+      // Debug tracks support data without dur, in which case it's treated as
+      // 0.
+      if (name === 'dur') {
+        return '0';
+      }
+      return this.columns[0];
+    };
+
+    this.renderParams = {
+      ts: chooseDefaultOption('ts'),
+      dur: chooseDefaultOption('dur'),
+      name: chooseDefaultOption('name'),
+      value: chooseDefaultOption('value'),
+      pivot: '',
+    };
+  }
+
+  oncreate({dom}: m.VnodeDOM<AddDebugTrackMenuAttrs>) {
+    this.focusTrackNameField(dom);
+  }
+
+  private focusTrackNameField(dom: Element) {
+    const element = findRef(dom, TRACK_NAME_FIELD_REF);
+    if (element) {
+      if (element instanceof HTMLInputElement) {
+        element.focus();
+      }
+    }
+  }
+
+  private renderTrackTypeSelect(trace: Trace) {
+    const options = [];
+    for (const type of ['slice', 'counter']) {
+      options.push(
+        m(
+          'option',
+          {
+            value: type,
+            selected: this.trackType === type ? true : undefined,
+          },
+          type,
+        ),
+      );
+    }
+    return m(
+      Select,
+      {
+        id: 'track_type',
+        oninput: (e: Event) => {
+          if (!e.target) return;
+          this.trackType = (e.target as HTMLSelectElement).value as
+            | 'slice'
+            | 'counter';
+          trace.scheduleFullRedraw();
+        },
+      },
+      options,
+    );
+  }
+
+  view(vnode: m.Vnode<AddDebugTrackMenuAttrs>) {
+    const renderSelect = (name: 'ts' | 'dur' | 'name' | 'value' | 'pivot') => {
+      const options = [];
+
+      if (name === 'pivot') {
+        options.push(
+          m(
+            'option',
+            {selected: this.renderParams[name] === '' ? true : undefined},
+            m('i', ''),
+          ),
+        );
+      }
+      for (const column of this.columns) {
+        options.push(
+          m(
+            'option',
+            {selected: this.renderParams[name] === column ? true : undefined},
+            column,
+          ),
+        );
+      }
+      if (name === 'dur') {
+        options.push(
+          m(
+            'option',
+            {selected: this.renderParams[name] === '0' ? true : undefined},
+            m('i', '0'),
+          ),
+        );
+      }
+      return [
+        m(FormLabel, {for: name}, name),
+        m(
+          Select,
+          {
+            id: name,
+            oninput: (e: Event) => {
+              if (!e.target) return;
+              this.renderParams[name] = (e.target as HTMLSelectElement).value;
+            },
+          },
+          options,
+        ),
+      ];
+    };
+
+    return m(
+      Form,
+      {
+        onSubmit: () => {
+          switch (this.trackType) {
+            case 'slice':
+              const sliceColumns: SliceColumnMapping = {
+                ts: this.renderParams.ts,
+                dur: this.renderParams.dur,
+                name: this.renderParams.name,
+              };
+              if (this.renderParams.pivot) {
+                addPivotedTracks(
+                  vnode.attrs.trace,
+                  vnode.attrs.dataSource,
+                  this.name,
+                  this.renderParams.pivot,
+                  async (ctx, data, trackName) =>
+                    addDebugSliceTrack({
+                      trace: ctx,
+                      data,
+                      title: trackName,
+                      columns: sliceColumns,
+                      argColumns: this.columns,
+                    }),
+                );
+              } else {
+                addDebugSliceTrack({
+                  trace: vnode.attrs.trace,
+                  data: vnode.attrs.dataSource,
+                  title: this.name,
+                  columns: sliceColumns,
+                  argColumns: this.columns,
+                });
+              }
+              break;
+            case 'counter':
+              const counterColumns: CounterColumnMapping = {
+                ts: this.renderParams.ts,
+                value: this.renderParams.value,
+              };
+
+              if (this.renderParams.pivot) {
+                addPivotedTracks(
+                  vnode.attrs.trace,
+                  vnode.attrs.dataSource,
+                  this.name,
+                  this.renderParams.pivot,
+                  async (ctx, data, trackName) =>
+                    addDebugCounterTrack({
+                      trace: ctx,
+                      data,
+                      title: trackName,
+                      columns: counterColumns,
+                    }),
+                );
+              } else {
+                addDebugCounterTrack({
+                  trace: vnode.attrs.trace,
+                  data: vnode.attrs.dataSource,
+                  title: this.name,
+                  columns: counterColumns,
+                });
+              }
+              break;
+          }
+        },
+        submitLabel: 'Show',
+      },
+      m(FormLabel, {for: 'track_name'}, 'Track name'),
+      m(TextInput, {
+        id: 'track_name',
+        ref: TRACK_NAME_FIELD_REF,
+        onkeydown: (e: KeyboardEvent) => {
+          // Allow Esc to close popup.
+          if (e.key === 'Escape') return;
+        },
+        oninput: (e: KeyboardEvent) => {
+          if (!e.target) return;
+          this.name = (e.target as HTMLInputElement).value;
+        },
+      }),
+      m(FormLabel, {for: 'track_type'}, 'Track type'),
+      this.renderTrackTypeSelect(vnode.attrs.trace),
+      renderSelect('ts'),
+      this.trackType === 'slice' && renderSelect('dur'),
+      this.trackType === 'slice' && renderSelect('name'),
+      this.trackType === 'counter' && renderSelect('value'),
+      renderSelect('pivot'),
+    );
+  }
+}
diff --git a/ui/src/public/lib/tracks/debug_tracks.ts b/ui/src/public/lib/tracks/debug_tracks.ts
new file mode 100644
index 0000000..e1caad0
--- /dev/null
+++ b/ui/src/public/lib/tracks/debug_tracks.ts
@@ -0,0 +1,133 @@
+// 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 {
+  matchesSqlValue,
+  sqlValueToReadableString,
+} from '../../../trace_processor/sql_utils';
+import {TrackNode} from '../../workspace';
+import {Trace} from '../../trace';
+import {
+  createQuerySliceTrack,
+  SliceColumnMapping,
+  SqlDataSource,
+} from './query_slice_track';
+import {
+  CounterColumnMapping,
+  createQueryCounterTrack,
+} from './query_counter_track';
+
+let trackCounter = 0; // For reproducible ids.
+
+export async function addPivotedTracks(
+  trace: Trace,
+  data: SqlDataSource,
+  trackName: string,
+  pivotColumn: string,
+  createTrack: (
+    trace: Trace,
+    data: SqlDataSource,
+    trackName: string,
+  ) => Promise<void>,
+) {
+  const iter = (
+    await trace.engine.query(`
+    with all_vals as (${data.sqlSource})
+    select DISTINCT ${pivotColumn} from all_vals
+    order by ${pivotColumn}
+  `)
+  ).iter({});
+
+  for (; iter.valid(); iter.next()) {
+    await createTrack(
+      trace,
+      {
+        sqlSource: `select * from
+        (${data.sqlSource})
+        where ${pivotColumn} ${matchesSqlValue(iter.get(pivotColumn))}`,
+      },
+      `${trackName.trim() || 'Pivot Track'}: ${sqlValueToReadableString(iter.get(pivotColumn))}`,
+    );
+  }
+}
+
+export interface DebugSliceTrackArgs {
+  readonly trace: Trace;
+  readonly data: SqlDataSource;
+  readonly title?: string;
+  readonly columns?: Partial<SliceColumnMapping>;
+  readonly argColumns?: string[];
+}
+
+/**
+ * Adds a new debug slice track to the workspace.
+ *
+ * See {@link createQuerySliceTrack} for details about the configuration args.
+ *
+ * A debug slice track is a track based on a query which is:
+ * - Based on a query.
+ * - Uses automatic slice layout.
+ * - Automatically added to the top of the current workspace.
+ * - Pinned.
+ * - Has a close button.
+ */
+export async function addDebugSliceTrack(args: DebugSliceTrackArgs) {
+  const trace = args.trace;
+  const cnt = trackCounter++;
+  const uri = `debugSliceTrack/${cnt}`;
+  const title = args.title?.trim() || `Debug Slice Track ${cnt}`;
+
+  // Create & register the track renderer
+  const track = await createQuerySliceTrack({...args, uri});
+  trace.tracks.registerTrack({uri, title, track});
+
+  // Create the track node and pin it
+  const trackNode = new TrackNode({uri, title, removable: true});
+  trace.workspace.addChildFirst(trackNode);
+  trackNode.pin();
+}
+
+export interface DebugCounterTrackArgs {
+  readonly trace: Trace;
+  readonly data: SqlDataSource;
+  readonly title?: string;
+  readonly columns?: Partial<CounterColumnMapping>;
+}
+
+/**
+ * Adds a new debug counter track to the workspace.
+ *
+ * See {@link createQueryCounterTrack} for details about the configuration args.
+ *
+ * A debug counter track is a track based on a query which is:
+ * - Based on a query.
+ * - Automatically added to the top of the current workspace.
+ * - Pinned.
+ * - Has a close button.
+ */
+export async function addDebugCounterTrack(args: DebugCounterTrackArgs) {
+  const trace = args.trace;
+  const cnt = trackCounter++;
+  const uri = `debugCounterTrack/${cnt}`;
+  const title = args.title?.trim() || `Debug Counter Track ${cnt}`;
+
+  // Create & register the track renderer
+  const track = await createQueryCounterTrack({...args, uri});
+  trace.tracks.registerTrack({uri, title, track});
+
+  // Create the track node and pin it
+  const trackNode = new TrackNode({uri, title, removable: true});
+  trace.workspace.addChildFirst(trackNode);
+  trackNode.pin();
+}
diff --git a/ui/src/public/lib/tracks/query_counter_track.ts b/ui/src/public/lib/tracks/query_counter_track.ts
new file mode 100644
index 0000000..dc25d69
--- /dev/null
+++ b/ui/src/public/lib/tracks/query_counter_track.ts
@@ -0,0 +1,122 @@
+// 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 {createPerfettoTable} from '../../../trace_processor/sql_utils';
+import {Trace} from '../../trace';
+import {sqlNameSafe} from '../../../base/string_utils';
+import {
+  BaseCounterTrack,
+  CounterOptions,
+} from '../../../frontend/base_counter_track';
+import {Engine} from '../../../trace_processor/engine';
+
+export interface QueryCounterTrackArgs {
+  // The trace object used to run queries.
+  readonly trace: Trace;
+
+  // A unique, reproducible ID for this track.
+  readonly uri: string;
+
+  // The query and optional column remapping.
+  readonly data: SqlDataSource;
+
+  // Optional: Which columns should be used for ts, and value. If omitted,
+  // the defaults 'ts', and 'value' will be used.
+  readonly columns?: Partial<CounterColumnMapping>;
+
+  // Optional: Display options for the counter track.
+  readonly options?: Partial<CounterOptions>;
+}
+
+export interface SqlDataSource {
+  // SQL source selecting the necessary data.
+  readonly sqlSource: string;
+
+  // Optional: Rename columns from the query result.
+  // If omitted, original column names from the query are used instead.
+  // The caller is responsible for ensuring that the number of items in this
+  // list matches the number of columns returned by sqlSource.
+  readonly columns?: string[];
+}
+
+export interface CounterColumnMapping {
+  readonly ts: string;
+  readonly value: string;
+}
+
+/**
+ * Creates a counter track based on a query.
+ *
+ * The query must provide the following columns:
+ * - ts: INTEGER - The timestamp of each sample.
+ * - value: REAL | INTEGER - The value of each sample.
+ *
+ * The column names don't have to be 'ts' and 'value', and can be remapped if
+ * convenient using the config.columns parameter.
+ */
+export async function createQueryCounterTrack(args: QueryCounterTrackArgs) {
+  const tableName = `__query_counter_track_${sqlNameSafe(args.uri)}`;
+  await createPerfettoTableForTrack(
+    args.trace.engine,
+    tableName,
+    args.data,
+    args.columns,
+  );
+  return new SqlTableCounterTrack(
+    args.trace,
+    args.uri,
+    tableName,
+    args.options,
+  );
+}
+
+async function createPerfettoTableForTrack(
+  engine: Engine,
+  tableName: string,
+  data: SqlDataSource,
+  columnMapping: Partial<CounterColumnMapping> = {},
+) {
+  const {ts = 'ts', value = 'value'} = columnMapping;
+  const query = `
+    with data as (
+      ${data.sqlSource}
+    )
+    select
+      ${ts} as ts,
+      ${value} as value
+    from data
+    order by ts
+  `;
+
+  return await createPerfettoTable(engine, tableName, query);
+}
+
+class SqlTableCounterTrack extends BaseCounterTrack {
+  constructor(
+    trace: Trace,
+    uri: string,
+    private readonly sqlTableName: string,
+    options?: Partial<CounterOptions>,
+  ) {
+    super({
+      trace,
+      uri,
+      options,
+    });
+  }
+
+  getSqlSource(): string {
+    return `select * from ${this.sqlTableName}`;
+  }
+}
diff --git a/ui/src/public/lib/tracks/query_slice_track.ts b/ui/src/public/lib/tracks/query_slice_track.ts
new file mode 100644
index 0000000..f7455a9
--- /dev/null
+++ b/ui/src/public/lib/tracks/query_slice_track.ts
@@ -0,0 +1,159 @@
+// 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 {
+  CustomSqlTableDefConfig,
+  CustomSqlTableSliceTrack,
+} from '../../../frontend/tracks/custom_sql_table_slice_track';
+import {
+  ARG_PREFIX,
+  SqlTableSliceTrackDetailsPanel,
+} from './sql_table_slice_track_details_tab';
+import {createPerfettoTable} from '../../../trace_processor/sql_utils';
+import {Trace} from '../../trace';
+import {TrackEventSelection} from '../../selection';
+import {sqlNameSafe} from '../../../base/string_utils';
+import {Engine} from '../../../trace_processor/engine';
+
+export interface QuerySliceTrackArgs {
+  // The trace object used to run queries.
+  readonly trace: Trace;
+
+  // A unique, reproducible ID for this track.
+  readonly uri: string;
+
+  // The query and optional column remapping.
+  readonly data: SqlDataSource;
+
+  // Optional: Which columns should be used for ts, dur, and name. If omitted,
+  // the defaults 'ts', 'dur', and 'name' will be used.
+  readonly columns?: Partial<SliceColumnMapping>;
+
+  // Optional: A list of column names which are displayed in the details panel
+  // when a slice is selected.
+  readonly argColumns?: string[];
+}
+
+export interface SqlDataSource {
+  // SQL source selecting the necessary data.
+  readonly sqlSource: string;
+
+  // Optional: Rename columns from the query result.
+  // If omitted, original column names from the query are used instead.
+  // The caller is responsible for ensuring that the number of items in this
+  // list matches the number of columns returned by sqlSource.
+  readonly columns?: string[];
+}
+
+export interface SliceColumnMapping {
+  readonly ts: string;
+  readonly dur: string;
+  readonly name: string;
+}
+
+/**
+ * Creates a slice track based on a query with automatic slice layout.
+ *
+ * The query must provide the following columns:
+ * - ts: INTEGER - The timestamp of the start of each slice.
+ * - dur: INTEGER - The length of each slice.
+ * - name: TEXT - A name to show on each slice, which is also used to derive the
+ *   color.
+ *
+ * The column names don't have to be 'ts', 'dur', and 'name' and can be remapped
+ * if convenient using the config.columns parameter.
+ *
+ * An optional set of columns can be provided which will be displayed in the
+ * details panel when a slice is selected.
+ *
+ * The layout (vertical depth) of each slice will be determined automatically to
+ * avoid overlapping slices.
+ */
+export async function createQuerySliceTrack(args: QuerySliceTrackArgs) {
+  const tableName = `__query_slice_track_${sqlNameSafe(args.uri)}`;
+  await createPerfettoTableForTrack(
+    args.trace.engine,
+    tableName,
+    args.data,
+    args.columns,
+    args.argColumns,
+  );
+  return new SqlTableSliceTrack(args.trace, args.uri, tableName);
+}
+
+async function createPerfettoTableForTrack(
+  engine: Engine,
+  tableName: string,
+  data: SqlDataSource,
+  columns: Partial<SliceColumnMapping> = {},
+  argColumns: string[] = [],
+) {
+  const {ts = 'ts', dur = 'dur', name = 'name'} = columns;
+
+  // If the view has clashing names (e.g. "name" coming from joining two
+  // different tables, we will see names like "name_1", "name_2", but they
+  // won't be addressable from the SQL. So we explicitly name them through a
+  // list of columns passed to CTE.
+  const dataColumns =
+    data.columns !== undefined ? `(${data.columns.join(', ')})` : '';
+
+  const query = `
+    with data${dataColumns} as (
+      ${data.sqlSource}
+    ),
+    prepared_data as (
+      select
+        ${ts} as ts,
+        ifnull(cast(${dur} as int), -1) as dur,
+        printf('%s', ${name}) as name
+        ${argColumns.length > 0 ? ',' : ''}
+        ${argColumns.map((c) => `${c} as ${ARG_PREFIX}${c}`).join(',\n')}
+      from data
+    )
+    select
+      row_number() over (order by ts) as id,
+      *
+    from prepared_data
+    order by ts
+  `;
+
+  return await createPerfettoTable(engine, tableName, query);
+}
+
+class SqlTableSliceTrack extends CustomSqlTableSliceTrack {
+  constructor(
+    trace: Trace,
+    uri: string,
+    private readonly sqlTableName: string,
+  ) {
+    super({
+      trace,
+      uri,
+    });
+  }
+
+  override async getSqlDataSource(): Promise<CustomSqlTableDefConfig> {
+    return {
+      sqlTableName: this.sqlTableName,
+    };
+  }
+
+  override detailsPanel({eventId}: TrackEventSelection) {
+    return new SqlTableSliceTrackDetailsPanel(
+      this.trace,
+      this.sqlTableName,
+      eventId,
+    );
+  }
+}
diff --git a/ui/src/public/lib/tracks/sql_table_slice_track_details_tab.ts b/ui/src/public/lib/tracks/sql_table_slice_track_details_tab.ts
new file mode 100644
index 0000000..621d9d3
--- /dev/null
+++ b/ui/src/public/lib/tracks/sql_table_slice_track_details_tab.ts
@@ -0,0 +1,276 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {duration, Time, time} from '../../../base/time';
+import {hasArgs, renderArguments} from '../../../frontend/slice_args';
+import {getSlice, SliceDetails} from '../../../trace_processor/sql_utils/slice';
+import {
+  asSliceSqlId,
+  Utid,
+} from '../../../trace_processor/sql_utils/core_types';
+import {
+  getThreadState,
+  ThreadState,
+} from '../../../trace_processor/sql_utils/thread_state';
+import {DurationWidget} from '../../../frontend/widgets/duration';
+import {Timestamp} from '../../../frontend/widgets/timestamp';
+import {
+  ColumnType,
+  durationFromSql,
+  LONG,
+  STR,
+  timeFromSql,
+} from '../../../trace_processor/query_result';
+import {sqlValueToReadableString} from '../../../trace_processor/sql_utils';
+import {DetailsShell} from '../../../widgets/details_shell';
+import {GridLayout} from '../../../widgets/grid_layout';
+import {Section} from '../../../widgets/section';
+import {
+  dictToTree,
+  dictToTreeNodes,
+  Tree,
+  TreeNode,
+} from '../../../widgets/tree';
+import {threadStateRef} from '../../../frontend/widgets/thread_state';
+import {getThreadName} from '../../../trace_processor/sql_utils/thread';
+import {getProcessName} from '../../../trace_processor/sql_utils/process';
+import {sliceRef} from '../../../frontend/widgets/slice';
+import {TrackEventDetailsPanel} from '../../details_panel';
+import {Trace} from '../../trace';
+
+export const ARG_PREFIX = 'arg_';
+
+function sqlValueToNumber(value?: ColumnType): number | undefined {
+  if (typeof value === 'bigint') return Number(value);
+  if (typeof value !== 'number') return undefined;
+  return value;
+}
+
+function sqlValueToUtid(value?: ColumnType): Utid | undefined {
+  if (typeof value === 'bigint') return Number(value) as Utid;
+  if (typeof value !== 'number') return undefined;
+  return value as Utid;
+}
+
+function renderTreeContents(dict: {[key: string]: m.Child}): m.Child[] {
+  const children: m.Child[] = [];
+  for (const key of Object.keys(dict)) {
+    if (dict[key] === null || dict[key] === undefined) continue;
+    children.push(
+      m(TreeNode, {
+        left: key,
+        right: dict[key],
+      }),
+    );
+  }
+  return children;
+}
+
+export class SqlTableSliceTrackDetailsPanel implements TrackEventDetailsPanel {
+  private data?: {
+    name: string;
+    ts: time;
+    dur: duration;
+    args: {[key: string]: ColumnType};
+  };
+  // We will try to interpret the arguments as references into well-known
+  // tables. These values will be set if the relevant columns exist and
+  // are consistent (e.g. 'ts' and 'dur' for this slice correspond to values
+  // in these well-known tables).
+  private threadState?: ThreadState;
+  private slice?: SliceDetails;
+
+  constructor(
+    private readonly trace: Trace,
+    private readonly tableName: string,
+    private readonly eventId: number,
+  ) {}
+
+  private async maybeLoadThreadState(
+    id: number | undefined,
+    ts: time,
+    dur: duration,
+    table: string | undefined,
+    utid?: Utid,
+  ): Promise<ThreadState | undefined> {
+    if (id === undefined) return undefined;
+    if (utid === undefined) return undefined;
+
+    const threadState = await getThreadState(this.trace.engine, id);
+    if (threadState === undefined) return undefined;
+    if (
+      table === 'thread_state' ||
+      (threadState.ts === ts &&
+        threadState.dur === dur &&
+        threadState.thread?.utid === utid)
+    ) {
+      return threadState;
+    } else {
+      return undefined;
+    }
+  }
+
+  private renderThreadStateInfo(): m.Child {
+    if (this.threadState === undefined) return null;
+    return m(
+      TreeNode,
+      {
+        left: threadStateRef(this.threadState),
+        right: '',
+      },
+      renderTreeContents({
+        Thread: getThreadName(this.threadState.thread),
+        Process: getProcessName(this.threadState.thread?.process),
+        State: this.threadState.state,
+      }),
+    );
+  }
+
+  private async maybeLoadSlice(
+    id: number | undefined,
+    ts: time,
+    dur: duration,
+    table: string | undefined,
+    trackId?: number,
+  ): Promise<SliceDetails | undefined> {
+    if (id === undefined) return undefined;
+    if (table !== 'slice' && trackId === undefined) return undefined;
+
+    const slice = await getSlice(this.trace.engine, asSliceSqlId(id));
+    if (slice === undefined) return undefined;
+    if (
+      table === 'slice' ||
+      (slice.ts === ts && slice.dur === dur && slice.trackId === trackId)
+    ) {
+      return slice;
+    } else {
+      return undefined;
+    }
+  }
+
+  private renderSliceInfo(): m.Child {
+    if (this.slice === undefined) return null;
+    return m(
+      TreeNode,
+      {
+        left: sliceRef(this.slice, 'Slice'),
+        right: '',
+      },
+      m(TreeNode, {
+        left: 'Name',
+        right: this.slice.name,
+      }),
+      m(TreeNode, {
+        left: 'Thread',
+        right: getThreadName(this.slice.thread),
+      }),
+      m(TreeNode, {
+        left: 'Process',
+        right: getProcessName(this.slice.process),
+      }),
+      hasArgs(this.slice.args) &&
+        m(
+          TreeNode,
+          {
+            left: 'Args',
+          },
+          renderArguments(this.trace, this.slice.args),
+        ),
+    );
+  }
+
+  async load() {
+    const queryResult = await this.trace.engine.query(
+      `select * from ${this.tableName} where id = ${this.eventId}`,
+    );
+    const row = queryResult.firstRow({
+      ts: LONG,
+      dur: LONG,
+      name: STR,
+    });
+    this.data = {
+      name: row.name,
+      ts: Time.fromRaw(row.ts),
+      dur: row.dur,
+      args: {},
+    };
+
+    for (const key of Object.keys(row)) {
+      if (key.startsWith(ARG_PREFIX)) {
+        this.data.args[key.substr(ARG_PREFIX.length)] = (
+          row as {[key: string]: ColumnType}
+        )[key];
+      }
+    }
+
+    this.threadState = await this.maybeLoadThreadState(
+      sqlValueToNumber(this.data.args['id']),
+      this.data.ts,
+      this.data.dur,
+      sqlValueToReadableString(this.data.args['table_name']),
+      sqlValueToUtid(this.data.args['utid']),
+    );
+
+    this.slice = await this.maybeLoadSlice(
+      sqlValueToNumber(this.data.args['id']) ??
+        sqlValueToNumber(this.data.args['slice_id']),
+      this.data.ts,
+      this.data.dur,
+      sqlValueToReadableString(this.data.args['table_name']),
+      sqlValueToNumber(this.data.args['track_id']),
+    );
+
+    this.trace.scheduleFullRedraw();
+  }
+
+  render() {
+    if (this.data === undefined) {
+      return m('h2', 'Loading');
+    }
+    const details = dictToTreeNodes({
+      'Name': this.data['name'] as string,
+      'Start time': m(Timestamp, {ts: timeFromSql(this.data['ts'])}),
+      'Duration': m(DurationWidget, {dur: durationFromSql(this.data['dur'])}),
+      'Slice id': `${this.tableName}[${this.eventId}]`,
+    });
+    details.push(this.renderThreadStateInfo());
+    details.push(this.renderSliceInfo());
+
+    const args: {[key: string]: m.Child} = {};
+    for (const key of Object.keys(this.data.args)) {
+      args[key] = sqlValueToReadableString(this.data.args[key]);
+    }
+
+    return m(
+      DetailsShell,
+      {
+        title: 'Slice',
+      },
+      m(
+        GridLayout,
+        m(Section, {title: 'Details'}, m(Tree, details)),
+        m(Section, {title: 'Arguments'}, dictToTree(args)),
+      ),
+    );
+  }
+
+  getTitle(): string {
+    return `Current Selection`;
+  }
+
+  isLoading() {
+    return this.data === undefined;
+  }
+}
diff --git a/ui/src/public/note.ts b/ui/src/public/note.ts
new file mode 100644
index 0000000..20b462a
--- /dev/null
+++ b/ui/src/public/note.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 size 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 {time} from '../base/time';
+
+export interface NoteManager {
+  getNote(id: string): Note | SpanNote | undefined;
+
+  // Adds a note (a flag on the timeline marker). Returns the id.
+  addNote(args: AddNoteArgs): string;
+
+  // Adds a span note (a flagged range). Returns the id.
+  addSpanNote(args: AddSpanNoteArgs): string;
+}
+
+export interface AddNoteArgs {
+  readonly timestamp: time;
+  readonly color?: string; // Default: randomColor().
+  readonly text?: string; // Default: ''.
+  // The id is optional. If present, allows overriding a previosly created note.
+  // If not present it will be auto-assigned with a montonic counter.
+  readonly id?: string;
+}
+
+export interface Note extends AddNoteArgs {
+  readonly noteType: 'DEFAULT';
+  readonly id: string;
+  readonly color: string;
+  readonly text: string;
+}
+
+export interface AddSpanNoteArgs {
+  readonly start: time;
+  readonly end: time;
+  readonly color?: string; // Default: randomColor().
+  readonly text?: string; // Default: ''.
+  // The id is optional. If present, allows overriding a previosly created note.
+  // If not present it will be auto-assigned with a montonic counter.
+  readonly id?: string;
+}
+
+export interface SpanNote extends AddSpanNoteArgs {
+  readonly noteType: 'SPAN';
+  readonly id: string;
+  readonly color: string;
+  readonly text: string;
+}
diff --git a/ui/src/public/omnibox.ts b/ui/src/public/omnibox.ts
new file mode 100644
index 0000000..26e2110
--- /dev/null
+++ b/ui/src/public/omnibox.ts
@@ -0,0 +1,34 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export interface OmniboxManager {
+  /**
+   * Turns the omnibox into an interfactive prompt for the user. Think of
+   * window.prompt() but non-modal and more integrated with the UI.
+   * @param text The question showed to the user (e.g.
+   * "Select a process to jump to")
+   * @param options If defined, it shows a list of options in a select-box
+   * fashion, where the user can move with Up/Down arrows. If omitted the
+   * input is freeform, like in the case of window.prompt().
+   * @returns the free-form user input, if `options` === undefined; returns
+   * the chosen PromptOption.key if `options` was provided; returns undefined
+   * if the user dimisses the prompt by pressing Esc or clicking eslewhere.
+   */
+  prompt(text: string, options?: PromptOption[]): Promise<string | undefined>;
+}
+
+export interface PromptOption {
+  key: string;
+  displayName: string;
+}
diff --git a/ui/src/public/page.ts b/ui/src/public/page.ts
new file mode 100644
index 0000000..08efbc0
--- /dev/null
+++ b/ui/src/public/page.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.
+
+import m from 'mithril';
+import {Trace} from './trace';
+
+/**
+ * Allows to register custom page endpoints that response to given routes, e.g.
+ * /viewer, /record etc.
+ */
+export interface PageManager {
+  /**
+   * Example usage:
+   *   registerPage({route: '/foo', page: FooPage})
+   *   class FooPage implements m.ClassComponent<PageWithTrace> {
+   *     view({attrs}: m.CVnode<PageWithTrace>) {
+   *        return m('div', ...
+   *            onclick: () => attrs.trace.timeline.zoom(...);
+   *        )
+   *     }
+   *   }
+   */
+  registerPage(pageHandler: PageHandler): Disposable;
+}
+
+/**
+ * Mithril attrs for pages that don't require a Trace object. These pages are
+ * always accessible, even before a trace is loaded.
+ */
+export interface PageAttrs {
+  subpage?: string;
+  trace?: Trace;
+}
+
+/**
+ * Mithril attrs for pages that require a Trace object. These pages are
+ * reachable only after a trace is loaded. Trying to access the route without a
+ * trace loaded results in the HomePage (route: '/') to be displayed instead.
+ */
+export interface PageWithTraceAttrs extends PageAttrs {
+  trace: Trace;
+}
+
+export type PageHandler<PWT = m.ComponentTypes<PageWithTraceAttrs>> = {
+  route: string; // e.g. '/' (default route), '/viewer'
+  pluginId?: string; // Not needed, the internal impl will fill it.
+} & (
+  | {
+      // If true, the route will be available even when there is no trace
+      // loaded. The component needs to deal with a possibly undefined attr.
+      traceless: true;
+      page: m.ComponentTypes<PageAttrs>;
+    }
+  | {
+      // If is omitted, the route will be available only when a trace is loaded.
+      // The component is guarranteed to get a defined Trace in its attrs.
+      traceless?: false;
+      page: PWT;
+    }
+);
diff --git a/ui/src/public/plugin.ts b/ui/src/public/plugin.ts
new file mode 100644
index 0000000..2360acf
--- /dev/null
+++ b/ui/src/public/plugin.ts
@@ -0,0 +1,79 @@
+// 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 {Trace} from './trace';
+import {App} from './app';
+
+/**
+ * This interface defines the shape of the plugins's class constructor (i.e. the
+ * the constructor and all static members of the plugin's class.
+ *
+ * This class constructor is registered with the core.
+ *
+ * On trace load, the core will create a new class instance by calling new on
+ * this constructor and then call its onTraceLoad() function.
+ */
+export interface PerfettoPluginStatic<T extends PerfettoPlugin> {
+  readonly id: string;
+  readonly dependencies?: ReadonlyArray<PerfettoPluginStatic<PerfettoPlugin>>;
+  onActivate?(app: App): void;
+  metricVisualisations?(): MetricVisualisation[];
+  new (trace: Trace): T;
+}
+
+/**
+ * This interface defines the shape of a plugin's trace-scoped instance, which
+ * is created from the class constructor above at trace load time.
+ */
+export interface PerfettoPlugin {
+  onTraceLoad?(ctx: Trace): Promise<void>;
+}
+
+export interface MetricVisualisation {
+  // The name of the metric e.g. 'android_camera'
+  metric: string;
+
+  // A vega or vega-lite visualisation spec.
+  // The data from the metric under path will be exposed as a
+  // datasource named "metric" in Vega(-Lite)
+  spec: string;
+
+  // A path index into the metric.
+  // For example if the metric returns the folowing protobuf:
+  // {
+  //   foo {
+  //     bar {
+  //       baz: { name: "a" }
+  //       baz: { name: "b" }
+  //       baz: { name: "c" }
+  //     }
+  //   }
+  // }
+  // That becomes the following json:
+  // { "foo": { "bar": { "baz": [
+  //  {"name": "a"},
+  //  {"name": "b"},
+  //  {"name": "c"},
+  // ]}}}
+  // And given path = ["foo", "bar", "baz"]
+  // We extract:
+  // [ {"name": "a"}, {"name": "b"}, {"name": "c"} ]
+  // And pass that to the vega(-lite) visualisation.
+  path: string[];
+}
+
+export interface PluginManager {
+  getPlugin<T extends PerfettoPlugin>(plugin: PerfettoPluginStatic<T>): T;
+  metricVisualisations(): MetricVisualisation[];
+}
diff --git a/ui/src/public/route_schema.ts b/ui/src/public/route_schema.ts
new file mode 100644
index 0000000..46ae91b
--- /dev/null
+++ b/ui/src/public/route_schema.ts
@@ -0,0 +1,74 @@
+// 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';
+
+// We use .catch(undefined) on every field below to make sure that passing an
+// invalid value doesn't invalidate the other keys which might be valid.
+// Zod default behaviour is atomic: either everything validates correctly or
+// the whole parsing fails.
+export const ROUTE_SCHEMA = z
+  .object({
+    // The local_cache_key is special and is persisted across navigations.
+    local_cache_key: z.string().optional().catch(undefined),
+
+    // These are transient and are really set only on startup.
+
+    // Are we loading a trace via ABT.
+    openFromAndroidBugTool: z.boolean().optional().catch(undefined),
+
+    // For permalink hash.
+    s: z.string().optional().catch(undefined),
+
+    // DEPRECATED: for #!/record?p=cpu subpages (b/191255021).
+    p: z.string().optional().catch(undefined),
+
+    // For fetching traces from Cloud Storage or local servers
+    // as with record_android_trace.
+    url: z.string().optional().catch(undefined),
+
+    // For connecting to a trace_processor_shell --httpd instance running on a
+    // non-standard port. This requires the CSP_WS_PERMISSIVE_PORT flag to relax
+    // the Content Security Policy.
+    rpc_port: z.string().regex(/\d+/).optional().catch(undefined),
+
+    // Override the referrer. Useful for scripts such as
+    // record_android_trace to record where the trace is coming from.
+    referrer: z.string().optional().catch(undefined),
+
+    // For the 'mode' of the UI. For example when the mode is 'embedded'
+    // some features are disabled.
+    mode: z.enum(['embedded']).optional().catch(undefined),
+
+    // Should we hide the sidebar?
+    hideSidebar: z.boolean().optional().catch(undefined),
+
+    // A comma-separated list of plugins to enable for the current session.
+    enablePlugins: z.string().optional().catch(undefined),
+
+    // Deep link support
+    table: z.string().optional().catch(undefined),
+    ts: z.string().optional().catch(undefined),
+    dur: z.string().optional().catch(undefined),
+    tid: z.string().optional().catch(undefined),
+    pid: z.string().optional().catch(undefined),
+    query: z.string().optional().catch(undefined),
+    visStart: z.string().optional().catch(undefined),
+    visEnd: z.string().optional().catch(undefined),
+  })
+  // default({}) ensures at compile-time that every entry is either optional or
+  // has a default value.
+  .default({});
+
+export type RouteArgs = z.infer<typeof ROUTE_SCHEMA>;
diff --git a/ui/src/public/scroll_helper.ts b/ui/src/public/scroll_helper.ts
new file mode 100644
index 0000000..742f826
--- /dev/null
+++ b/ui/src/public/scroll_helper.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 {time} from '../base/time';
+
+/**
+ * A helper to scroll to a combination of tracks and time ranges.
+ * This exist to decouple the selection logic to the scrolling logic. Nothing in
+ * this file changes the selection status. Use SelectionManager for that.
+ */
+export interface ScrollToArgs {
+  // Given a start and end timestamp (in ns), move the viewport to center this
+  //  range and zoom if necessary:
+  //  - If [viewPercentage] is specified, the viewport will be zoomed so that
+  //    the given time range takes up this percentage of the viewport.
+  //  The following scenarios assume [viewPercentage] is undefined.
+  //  - If the new range is more than 50% of the viewport, zoom out to a level
+  //  where
+  //    the range is 1/5 of the viewport.
+  //  - If the new range is already centered, update the zoom level for the
+  //  viewport
+  //    to cover 1/5 of the viewport.
+  //  - Otherwise, preserve the zoom range.
+  //
+  time?: {
+    start: time;
+    end?: time;
+    viewPercentage?: number;
+  };
+  // Find the track with a given uri in the current workspace and scroll it into
+  // view. Iftrack is nested inside a track group, scroll to that track group
+  // instead. If `expandGroup` == true, open the track group and scroll to the
+  // track.
+  // TODO(primiano): 90% of the times we seem to want expandGroup: true, so we
+  // should probably flip the default value, and pass false in the few places
+  // where we do NOT want this behavior.
+  track?: {
+    uri: string;
+    expandGroup?: boolean;
+  };
+}
+
+// TODO(primiano): remove this injection once we plumb Trace into all the
+// components. Right now too many places need this. This is a temporary solution
+// to avoid too many invasive refactorings at once.
+
+type ScrollToFunction = (a: ScrollToArgs) => void;
+let _scrollToFunction: ScrollToFunction | undefined = undefined;
+
+// If a Trace object is avilable, prefer Trace.scrollTo(). It points to the
+// same function.
+export function scrollTo(args: ScrollToArgs) {
+  _scrollToFunction?.(args);
+}
+
+export function setScrollToFunction(f: ScrollToFunction | undefined) {
+  _scrollToFunction = f;
+}
diff --git a/ui/src/public/search.ts b/ui/src/public/search.ts
new file mode 100644
index 0000000..3392e8f
--- /dev/null
+++ b/ui/src/public/search.ts
@@ -0,0 +1,26 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use size 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 {time} from '../base/time';
+
+export type SearchSource = 'cpu' | 'log' | 'slice' | 'track';
+
+export interface SearchResult {
+  eventId: number;
+  ts: time;
+  trackUri: string;
+  source: SearchSource;
+}
+
+export type ResultStepEventHandler = (r: SearchResult) => void;
diff --git a/ui/src/public/selection.ts b/ui/src/public/selection.ts
new file mode 100644
index 0000000..d515a4c
--- /dev/null
+++ b/ui/src/public/selection.ts
@@ -0,0 +1,186 @@
+// 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 {time, duration, TimeSpan} from '../base/time';
+import {Engine} from '../trace_processor/engine';
+import {ColumnDef, Sorting, ThreadStateExtra} from './aggregation';
+import {TrackDescriptor} from './track';
+
+export interface SelectionManager {
+  readonly selection: Selection;
+
+  findTimeRangeOfSelection(): TimeSpan | undefined;
+  clear(): void;
+
+  /**
+   * Select a track event.
+   *
+   * @param trackUri - The URI of the track to select.
+   * @param eventId - The value of the events ID column.
+   * @param opts - Additional options.
+   */
+  selectTrackEvent(
+    trackUri: string,
+    eventId: number,
+    opts?: SelectionOpts,
+  ): void;
+
+  /**
+   * Select a track.
+   *
+   * @param trackUri - The URI for the track to select.
+   * @param opts - Additional options.
+   */
+  selectTrack(trackUri: string, opts?: SelectionOpts): void;
+
+  /**
+   * Select a track event via a sql table name + id.
+   *
+   * @param sqlTableName - The name of the SQL table to resolve.
+   * @param id - The ID of the event in that table.
+   * @param opts - Additional options.
+   */
+  selectSqlEvent(sqlTableName: string, id: number, opts?: SelectionOpts): void;
+
+  /**
+   * Create an area selection for the purposes of aggregation.
+   *
+   * @param args - The area to select.
+   * @param opts - Additional options.
+   */
+  selectArea(args: Area, opts?: SelectionOpts): void;
+
+  scrollToCurrentSelection(): void;
+  registerAreaSelectionAggreagtor(aggr: AreaSelectionAggregator): void;
+
+  /**
+   * Register a new SQL selection resolver.
+   *
+   * A resolver consists of a SQL table name and a callback. When someone
+   * expresses an interest in selecting a slice on a matching table, the
+   * callback is called which can return a selection object or undefined.
+   */
+  registerSqlSelectionResolver(resolver: SqlSelectionResolver): void;
+}
+
+export interface AreaSelectionAggregator {
+  readonly id: string;
+  createAggregateView(engine: Engine, area: AreaSelection): Promise<boolean>;
+  getExtra(
+    engine: Engine,
+    area: AreaSelection,
+  ): Promise<ThreadStateExtra | void>;
+  getTabName(): string;
+  getDefaultSorting(): Sorting;
+  getColumnDefinitions(): ColumnDef[];
+}
+
+export type Selection =
+  | TrackEventSelection
+  | TrackSelection
+  | AreaSelection
+  | NoteSelection
+  | EmptySelection;
+
+/** Defines how changes to selection affect the rest of the UI state */
+export interface SelectionOpts {
+  clearSearch?: boolean; // Default: true.
+  switchToCurrentSelectionTab?: boolean; // Default: true.
+  scrollToSelection?: boolean; // Default: false.
+}
+
+export interface TrackEventSelection extends TrackEventDetails {
+  readonly kind: 'track_event';
+  readonly trackUri: string;
+  readonly eventId: number;
+}
+
+export interface TrackSelection {
+  readonly kind: 'track';
+  readonly trackUri: string;
+}
+
+export interface TrackEventDetails {
+  // ts and dur are required by the core, and must be provided.
+  readonly ts: time;
+  // Note: dur can be -1 for instant events.
+  readonly dur: duration;
+
+  // Optional additional information.
+  // TODO(stevegolton): Find an elegant way of moving this information out of
+  // the core.
+  readonly wakeupTs?: time;
+  readonly wakerCpu?: number;
+  readonly upid?: number;
+  readonly utid?: number;
+  readonly tableName?: string;
+  readonly profileType?: ProfileType;
+  readonly interactionType?: string;
+}
+
+export interface Area {
+  readonly start: time;
+  readonly end: time;
+  // TODO(primiano): this should be ReadonlyArray<> after the pivot table state
+  // doesn't use State/Immer anymore.
+  readonly trackUris: string[];
+}
+
+export interface AreaSelection extends Area {
+  readonly kind: 'area';
+
+  // This array contains the resolved TrackDescriptor from Area.trackUris.
+  // The resolution is done by SelectionManager whenever a kind='area' selection
+  // is performed.
+  readonly tracks: ReadonlyArray<TrackDescriptor>;
+}
+
+export interface NoteSelection {
+  readonly kind: 'note';
+  readonly id: string;
+}
+
+export interface EmptySelection {
+  readonly kind: 'empty';
+}
+
+export enum ProfileType {
+  HEAP_PROFILE = 'heap_profile',
+  MIXED_HEAP_PROFILE = 'heap_profile:com.android.art,libc.malloc',
+  NATIVE_HEAP_PROFILE = 'heap_profile:libc.malloc',
+  JAVA_HEAP_SAMPLES = 'heap_profile:com.android.art',
+  JAVA_HEAP_GRAPH = 'graph',
+  PERF_SAMPLE = 'perf',
+}
+
+export function profileType(s: string): ProfileType {
+  if (s === 'heap_profile:libc.malloc,com.android.art') {
+    s = 'heap_profile:com.android.art,libc.malloc';
+  }
+  if (Object.values(ProfileType).includes(s as ProfileType)) {
+    return s as ProfileType;
+  }
+  if (s.startsWith('heap_profile')) {
+    return ProfileType.HEAP_PROFILE;
+  }
+  throw new Error('Unknown type ${s}');
+}
+
+export interface SqlSelectionResolver {
+  readonly sqlTableName: string;
+  readonly callback: (
+    id: number,
+    sqlTable: string,
+  ) => Promise<{trackUri: string; eventId: number} | undefined>;
+}
diff --git a/ui/src/public/sidebar.ts b/ui/src/public/sidebar.ts
new file mode 100644
index 0000000..caba71d
--- /dev/null
+++ b/ui/src/public/sidebar.ts
@@ -0,0 +1,115 @@
+// 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.
+
+// For now sections are fixed and cannot be extended by plugins.
+export const SIDEBAR_SECTIONS = {
+  navigation: {
+    title: 'Navigation',
+    summary: 'Open or record a new trace',
+  },
+  current_trace: {
+    title: 'Current Trace',
+    summary: 'Actions on the current trace',
+  },
+  convert_trace: {
+    title: 'Convert trace',
+    summary: 'Convert to other formats',
+  },
+  example_traces: {
+    title: 'Example Traces',
+    summary: 'Open an example trace',
+  },
+  support: {
+    title: 'Support',
+    summary: 'Documentation & Bugs',
+  },
+} as const;
+
+export type SidebarSections = keyof typeof SIDEBAR_SECTIONS;
+
+export interface SidebarManager {
+  readonly enabled: boolean;
+
+  /**
+   * Adds a new menu item to the sidebar.
+   * All entries must map to a command. This will allow the shortcut and
+   * optional shortcut to be displayed on the UI.
+   */
+  addMenuItem(menuItem: SidebarMenuItem): void;
+
+  /**
+   * Gets the current visibility of the sidebar.
+   */
+  get visible(): boolean;
+
+  /**
+   * Toggles the visibility of the sidebar. Can only be called when
+   * `sidebarEnabled` returns `ENABLED`.
+   */
+  toggleVisibility(): void;
+}
+
+export type SidebarMenuItem = {
+  readonly section: SidebarSections;
+  readonly sortOrder?: number;
+
+  // The properties below can be mutated by passing a callback rather than a
+  // direct value. The callback is invoked on every render frame, keep it cheap.
+  // readonly text: string | (() => string);
+  readonly icon?: string | (() => string);
+  readonly tooltip?: string | (() => string);
+  readonly cssClass?: string | (() => string); // Without trailing '.'.
+
+  // If false or omitted the item works normally.
+  // If true the item is striken through and the action/href will be a no-op.
+  // If a string, the item acts as disabled and clicking on it shows a popup
+  // that shows the returned text (the string has "disabled reason" semantic);
+  readonly disabled?: string | boolean | (() => string | boolean);
+
+  // One of the three following arguments must be specified.
+} & (
+  | {
+      /** The text of the menu entry. Required. */
+      readonly text: string | (() => string);
+
+      /**
+       * The URL to navigate to. It can be either:
+       * - A local route (e.g. ''#!/query').
+       * - An absolute URL (e.g. 'https://example.com'). In this case the link will
+       *   be open in a target=_blank new tag.
+       */
+      readonly href: string;
+    }
+  | {
+      /** The text of the menu entry. Required. */
+      readonly text: string | (() => string);
+
+      /**
+       * The function that will be invoked when clicking. If the function returns
+       * a promise, a spinner will be drawn next to the sidebar entry until the
+       * promise resolves.
+       */
+      readonly action: () => unknown | Promise<unknown>;
+
+      /** Optional. If omitted href = '#'. */
+      readonly href?: string;
+    }
+  | {
+      /** Optional. If omitted uses the command name. */
+      readonly text?: string | (() => string);
+
+      /** The ID of the command that will be invoked when clicking */
+      readonly commandId: string;
+    }
+);
diff --git a/ui/src/public/standard_groups.ts b/ui/src/public/standard_groups.ts
new file mode 100644
index 0000000..61b844a
--- /dev/null
+++ b/ui/src/public/standard_groups.ts
@@ -0,0 +1,81 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {TrackNode, TrackNodeArgs, Workspace} from './workspace';
+
+/**
+ * Gets or creates a group for a given process given the normal grouping
+ * conventions.
+ *
+ * @param workspace - The workspace to search for the group on.
+ * @param upid - The upid of teh process to find.
+ */
+export function getOrCreateGroupForProcess(
+  workspace: Workspace,
+  upid: number,
+): TrackNode {
+  return getOrCreateGroup(workspace, `process${upid}`, {
+    title: `Process ${upid}`,
+    isSummary: true,
+  });
+}
+
+/**
+ * Gets or creates a group for a given thread given the normal grouping
+ * conventions.
+ *
+ * @param workspace - The workspace to search for the group on.
+ * @param utid - The utid of the thread to find.
+ */
+export function getOrCreateGroupForThread(
+  workspace: Workspace,
+  utid: number,
+): TrackNode {
+  return getOrCreateGroup(workspace, `thread${utid}`, {
+    title: `Thread ${utid}`,
+    isSummary: true,
+  });
+}
+
+/**
+ * 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/tab.ts b/ui/src/public/tab.ts
new file mode 100644
index 0000000..12258c2
--- /dev/null
+++ b/ui/src/public/tab.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 m from 'mithril';
+import {DetailsPanel} from './details_panel';
+
+export interface TabManager {
+  registerTab(tab: TabDescriptor): void;
+  registerDetailsPanel(detailsPanel: DetailsPanel): Disposable;
+  showTab(uri: string): void;
+  hideTab(uri: string): void;
+  addDefaultTab(uri: string): void;
+}
+
+export interface Tab {
+  render(): m.Children;
+  getTitle(): string;
+}
+
+export interface TabDescriptor {
+  uri: string; // TODO(stevegolton): Maybe optional for ephemeral tabs.
+  content: Tab;
+  isEphemeral?: boolean; // Defaults false
+  onHide?(): void;
+  onShow?(): void;
+}
diff --git a/ui/src/public/timeline.ts b/ui/src/public/timeline.ts
new file mode 100644
index 0000000..ca881e5
--- /dev/null
+++ b/ui/src/public/timeline.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.
+
+import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
+import {time} from '../base/time';
+
+export interface Timeline {
+  // Bring a timestamp into view.
+  panToTimestamp(ts: time): void;
+
+  // Move the viewport.
+  setViewportTime(start: time, end: time): void;
+
+  // A span representing the current viewport location.
+  readonly visibleWindow: HighPrecisionTimeSpan;
+
+  // Render a vertical line on the timeline at this timestamp.
+  hoverCursorTimestamp: time | undefined;
+
+  hoveredNoteTimestamp: time | undefined;
+  highlightedSliceId: number | undefined;
+
+  hoveredUtid: number | undefined;
+  hoveredPid: number | undefined;
+
+  // Get the current timestamp offset.
+  timestampOffset(): time;
+
+  // Get a time in the current domain as specified by timestampOffset.
+  toDomainTime(ts: time): time;
+}
diff --git a/ui/src/public/trace.ts b/ui/src/public/trace.ts
new file mode 100644
index 0000000..95db546
--- /dev/null
+++ b/ui/src/public/trace.ts
@@ -0,0 +1,100 @@
+// 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 {Migrate, Store} from '../base/store';
+import {TraceInfo} from './trace_info';
+import {Engine} from '../trace_processor/engine';
+import {App} from './app';
+import {TabManager} from './tab';
+import {TrackManager} from './track';
+import {Timeline} from './timeline';
+import {Workspace, WorkspaceManager} from './workspace';
+import {SelectionManager} from './selection';
+import {ScrollToArgs} from './scroll_helper';
+import {NoteManager} from './note';
+import {DisposableStack} from '../base/disposable_stack';
+
+// Lists all the possible event listeners using the key as the event name and
+// the type as the type of the callback.
+export interface EventListeners {
+  traceready: () => Promise<void> | void;
+}
+
+/**
+ * The main API endpoint to interact programmaticaly with the UI and alter its
+ * state once a trace is loaded. There are N+1 instances of this interface,
+ * one for each plugin and one for the core (which, however, gets to see the
+ * full AppImpl behind this to acces all the internal methods).
+ * This interface is passed to plugins' onTraceLoad() hook and is injected
+ * pretty much everywhere in core.
+ */
+export interface Trace extends App {
+  readonly engine: Engine;
+  readonly notes: NoteManager;
+  readonly timeline: Timeline;
+  readonly tabs: TabManager;
+  readonly tracks: TrackManager;
+  readonly selection: SelectionManager;
+  readonly workspace: Workspace;
+  readonly workspaces: WorkspaceManager;
+  readonly traceInfo: TraceInfo;
+
+  // Scrolls to the given track and/or time. Does NOT change the current
+  // selection.
+  scrollTo(args: ScrollToArgs): void;
+
+  // Create a store mounted over the top of this plugin's persistent state.
+  mountStore<T>(migrate: Migrate<T>): Store<T>;
+
+  // Returns the blob of the current trace file.
+  // If the trace is opened from a file or postmessage, the blob is returned
+  // immediately. If the trace is opened from URL, this causes a re-download of
+  // the trace. It will throw if traceInfo.downloadable === false.
+  getTraceFile(): Promise<Blob>;
+
+  // List of errors that were encountered while loading the trace by the TS
+  // code. These are on top of traceInfo.importErrors, which is a summary of
+  // what TraceProcessor reports on the stats table at import time.
+  get loadingErrors(): ReadonlyArray<string>;
+
+  // When the trace is opened via postMessage deep-linking, returns the sub-set
+  // of postMessageData.pluginArgs[pluginId] for the current plugin. If not
+  // present returns undefined.
+  readonly openerPluginArgs?: {[key: string]: unknown};
+
+  // Trace scoped disposables. Will be destroyed when the trace is unloaded.
+  readonly trash: DisposableStack;
+
+  // Register event listeners for trace-level events, e.g. trace ready
+  addEventListener<T extends keyof EventListeners>(
+    event: T,
+    callback: EventListeners[T],
+  ): void;
+}
+
+/**
+ * A convenience interface to inject the App in Mithril components.
+ * Example usage:
+ *
+ * class MyComponent implements m.ClassComponent<TraceAttrs> {
+ *   oncreate({attrs}: m.CVnodeDOM<AppAttrs>): void {
+ *     attrs.trace.engine.runQuery(...);
+ *   }
+ * }
+ */
+export interface TraceAttrs {
+  trace: Trace;
+}
+
+export const TRACE_SUFFIX = '.perfetto-trace';
diff --git a/ui/src/public/trace_info.ts b/ui/src/public/trace_info.ts
new file mode 100644
index 0000000..71d977b
--- /dev/null
+++ b/ui/src/public/trace_info.ts
@@ -0,0 +1,67 @@
+// 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 {time} from '../base/time';
+
+export interface TraceInfo {
+  readonly traceTitle: string; // File name and size of the current trace.
+  readonly traceUrl: string; // URL of the Trace.
+
+  readonly start: time;
+  readonly end: time;
+
+  // This is the ts value at the time of the Unix epoch.
+  // Normally some large negative value, because the unix epoch is normally in
+  // the past compared to ts=0.
+  readonly realtimeOffset: time;
+
+  // This is the timestamp that we should use for our offset when in UTC mode.
+  // Usually the most recent UTC midnight compared to the trace start time.
+  readonly utcOffset: time;
+
+  // Trace TZ is like UTC but keeps into account also the timezone_off_mins
+  // recorded into the trace, to show timestamps in the device local time.
+  readonly traceTzOffset: time;
+
+  // The list of CPUs in the trace
+  readonly cpus: number[];
+
+  // The number of import/analysis errors present in the `stats` table.
+  readonly importErrors: number;
+
+  // The trace type inferred by TraceProcessor (e.g. 'proto', 'json, ...).
+  // See TraceTypeToString() in src/trace_processor/util/trace_type.cc for
+  // all the available types.
+  readonly traceType?: string;
+
+  // True if the trace contains any ftrace data (sched or other ftrace events).
+  readonly hasFtrace: boolean;
+
+  // The UUID of the trace. This is generated by TraceProcessor by either
+  // looking at the TraceUuid packet emitted by traced or, as a fallback, by
+  // hashing the first KB of the trace. This can be an empty string in rare
+  // cases (e.g., opening an empty trace).
+  readonly uuid: string;
+
+  // Wheteher the current trace has been successfully stored into cache storage.
+  readonly cached: boolean;
+
+  // Returns true if the current trace can be downloaded via getTraceFile().
+  // The trace isn't downloadable in the following cases:
+  // - It comes from a source (e.g. HTTP+RPC) that doesn't support re-download
+  //   due to technical limitations.
+  // - Download is disabled because the trace was pushed via postMessage and
+  //   the caller has asked to disable downloads.
+  readonly downloadable: boolean;
+}
diff --git a/ui/src/public/track.ts b/ui/src/public/track.ts
new file mode 100644
index 0000000..94ac9e7
--- /dev/null
+++ b/ui/src/public/track.ts
@@ -0,0 +1,262 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {duration, time} from '../base/time';
+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 {TrackEventDetailsPanel} from './details_panel';
+import {TrackEventDetails, TrackEventSelection} from './selection';
+
+export interface TrackManager {
+  /**
+   * Register a new track against a unique key known as a URI. The track is not
+   * shown by default and callers need to either manually add it to a
+   * Workspace or use registerTrackAndShowOnTraceLoad() below.
+   */
+  registerTrack(trackDesc: TrackDescriptor): void;
+
+  findTrack(
+    predicate: (desc: TrackDescriptor) => boolean | undefined,
+  ): TrackDescriptor | undefined;
+
+  getAllTracks(): TrackDescriptor[];
+
+  getTrack(uri: string): TrackDescriptor | undefined;
+}
+
+export interface TrackContext {
+  // This track's URI, used for making selections et al.
+  readonly trackUri: string;
+}
+
+/**
+ * Contextual information about the track passed to track lifecycle hooks &
+ * render hooks with additional information about the timeline/canvas.
+ */
+export interface TrackRenderContext extends TrackContext {
+  /**
+   * The time span of the visible window.
+   */
+  readonly visibleWindow: HighPrecisionTimeSpan;
+
+  /**
+   * The dimensions of the track on the canvas in pixels.
+   */
+  readonly size: Size2D;
+
+  /**
+   * Suggested data resolution.
+   *
+   * This number is the number of time units that corresponds to 1 pixel on the
+   * screen, rounded down to the nearest power of 2. The minimum value is 1.
+   *
+   * It's up to the track whether it would like to use this resolution or
+   * calculate their own based on the timespan and the track dimensions.
+   */
+  readonly resolution: duration;
+
+  /**
+   * Canvas context used for rendering.
+   */
+  readonly ctx: CanvasRenderingContext2D;
+
+  /**
+   * A time scale used for translating between pixels and time.
+   */
+  readonly timescale: TimeScale;
+}
+
+// A definition of a track, including a renderer implementation and metadata.
+export interface TrackDescriptor {
+  // A unique identifier for this track.
+  readonly uri: string;
+
+  // A factory function returning a new track instance.
+  readonly track: Track;
+
+  // Human readable title. Always displayed.
+  readonly title: string;
+
+  // Human readable subtitle. Sometimes displayed if there is room.
+  readonly subtitle?: string;
+
+  // Optional: A list of tags used for sorting, grouping and "chips".
+  readonly tags?: TrackTags;
+
+  readonly chips?: ReadonlyArray<string>;
+
+  readonly pluginId?: string;
+}
+
+/**
+ * Contextual information passed to mouse events.
+ */
+export interface TrackMouseEvent {
+  /**
+   * X coordinate of the mouse event w.r.t. the top-left of the track.
+   */
+  readonly x: number;
+
+  /**
+   * Y coordinate of the mouse event w.r.t the top-left of the track.
+   */
+  readonly y: number;
+
+  /**
+   * A time scale used for translating between pixels and time.
+   */
+  readonly timescale: TimeScale;
+}
+
+export interface Track {
+  /**
+   * Optional lifecycle hook called on the first render cycle. Should be used to
+   * create any required resources.
+   *
+   * These lifecycle hooks are asynchronous, but they are run synchronously,
+   * meaning that perfetto will wait for each one to complete before calling the
+   * next one, so the user doesn't have to serialize these calls manually.
+   *
+   * Exactly when this hook is called is left purposely undefined. The only
+   * guarantee is that it will be called exactly once before the first call to
+   * onUpdate().
+   *
+   * Note: On the first render cycle, both onCreate and onUpdate are called one
+   * after another.
+   */
+  onCreate?(ctx: TrackContext): Promise<void>;
+
+  /**
+   * Optional lifecycle hook called on every render cycle.
+   *
+   * The track should inspect things like the visible window, track size, and
+   * resolution to work out whether any data needs to be reloaded based on these
+   * properties and perform a reload.
+   */
+  onUpdate?(ctx: TrackRenderContext): Promise<void>;
+
+  /**
+   * Optional lifecycle hook called when the track is no longer visible. Should
+   * be used to clear up any resources.
+   */
+  onDestroy?(): Promise<void>;
+
+  /**
+   * Required method used to render the track's content to the canvas, called
+   * synchronously on every render cycle.
+   */
+  render(ctx: TrackRenderContext): void;
+  onFullRedraw?(): void;
+
+  /**
+   * Return the vertical bounds (top & bottom) of a slice were it to be rendered
+   * at a specific depth, given the slice height and padding/spacing that this
+   * track uses.
+   */
+  getSliceVerticalBounds?(depth: number): VerticalBounds | undefined;
+  getHeight(): number;
+  getTrackShellButtons?(): m.Children;
+  onMouseMove?(event: TrackMouseEvent): void;
+  onMouseClick?(event: TrackMouseEvent): boolean;
+  onMouseOut?(): void;
+
+  /**
+   * Optional: Get details of a track event given by eventId on this track.
+   */
+  getSelectionDetails?(eventId: number): Promise<TrackEventDetails | undefined>;
+
+  // Optional: A factory that returns a details panel object for a given track
+  // event selection. This is called each time the selection is changed (and the
+  // selection is relevant to this track).
+  detailsPanel?(sel: TrackEventSelection): TrackEventDetailsPanel;
+}
+
+// An set of key/value pairs describing a given track. These are used for
+// selecting tracks to pin/unpin, diplsaying "chips" in the track shell, and
+// (in future) the sorting and grouping of tracks.
+// We define a handful of well known fields, and the rest are arbitrary key-
+// value pairs.
+export type TrackTags = Partial<WellKnownTrackTags> & {
+  // There may be arbitrary other key/value pairs.
+  [key: string]:
+    | undefined
+    | string
+    | number
+    | boolean
+    | ReadonlyArray<string>
+    | ReadonlyArray<number>;
+};
+
+interface WellKnownTrackTags {
+  // The track "kind", used by various subsystems e.g. aggregation controllers.
+  // This is where "XXX_TRACK_KIND" values should be placed.
+  // TODO(stevegolton): This will be deprecated once we handle group selections
+  // in a more generic way - i.e. EventSet.
+  kind: string;
+
+  // Optional: list of track IDs represented by this trace.
+  // This list is used for participation in track indexing by track ID.
+  // This index is used by various subsystems to find links between tracks based
+  // on the track IDs used by trace processor.
+  trackIds: ReadonlyArray<number>;
+
+  // Optional: The CPU number associated with this track.
+  cpu: number;
+
+  // Optional: The UTID associated with this track.
+  utid: number;
+
+  // Optional: The UPID associated with this track.
+  upid: number;
+
+  // Used for sorting and grouping
+  scope: string;
+
+  // Group name, used as a hint to ask track decider to put this in a group
+  groupName: string;
+}
+
+export interface Slice {
+  // These properties are updated only once per query result when the Slice
+  // object is created and don't change afterwards.
+  readonly id: number;
+  readonly startNs: time;
+  readonly endNs: time;
+  readonly durNs: duration;
+  readonly ts: time;
+  readonly dur: duration;
+  readonly depth: number;
+  readonly flags: number;
+
+  // Each slice can represent some extra numerical information by rendering a
+  // portion of the slice with a lighter tint.
+  // |fillRatio\ describes the ratio of the normal area to the tinted area
+  // width of the slice, normalized between 0.0 -> 1.0.
+  // 0.0 means the whole slice is tinted.
+  // 1.0 means none of the slice is tinted.
+  // E.g. If |fillRatio| = 0.65 the slice will be rendered like this:
+  // [############|*******]
+  // ^------------^-------^
+  //     Normal     Light
+  readonly fillRatio: number;
+
+  // These can be changed by the Impl.
+  title: string;
+  subTitle: string;
+  colorScheme: ColorScheme;
+  isHighlighted: boolean;
+}
diff --git a/ui/src/public/track_kinds.ts b/ui/src/public/track_kinds.ts
new file mode 100644
index 0000000..d01e43e
--- /dev/null
+++ b/ui/src/public/track_kinds.ts
@@ -0,0 +1,29 @@
+// 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 file contains a list of well known (to the core) track kinds.
+// This file exists purely to keep legacy systems in place without introducing a
+// ton of circular imports.
+export const CPU_SLICE_TRACK_KIND = 'CpuSliceTrack';
+export const CPU_FREQ_TRACK_KIND = 'CpuFreqTrack';
+export const THREAD_STATE_TRACK_KIND = 'ThreadStateTrack';
+export const SLICE_TRACK_KIND = 'SliceTrack';
+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 COUNTER_TRACK_KIND = 'CounterTrack';
+export const CPUSS_ESTIMATE_TRACK_KIND = 'CpuSubsystemEstimateTrack';
+export const CPU_PROFILE_TRACK_KIND = 'CpuProfileTrack';
+export const HEAP_PROFILE_TRACK_KIND = 'HeapProfileTrack';
+export const ANDROID_LOGS_TRACK_KIND = 'AndroidLogTrack';
diff --git a/ui/src/public/tracks.ts b/ui/src/public/tracks.ts
deleted file mode 100644
index 5bd3965..0000000
--- a/ui/src/public/tracks.ts
+++ /dev/null
@@ -1,239 +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 {duration, time} from '../base/time';
-import {Optional} from '../base/utils';
-import {UntypedEventSet} from '../core/event_set';
-import {LegacySelection, Selection} from '../core/selection_manager';
-import {Size} from '../base/geom';
-import {TimeScale} from '../frontend/time_scale';
-import {HighPrecisionTimeSpan} from '../common/high_precision_time_span';
-
-export interface TrackContext {
-  // This track's key, used for making selections et al.
-  readonly trackKey: string;
-}
-
-/**
- * Contextual information about the track passed to track lifecycle hooks &
- * render hooks with additional information about the timeline/canvas.
- */
-export interface TrackRenderContext extends TrackContext {
-  /**
-   * The time span of the visible window.
-   */
-  readonly visibleWindow: HighPrecisionTimeSpan;
-
-  /**
-   * The dimensions of the track on the canvas in pixels.
-   */
-  readonly size: Size;
-
-  /**
-   * Suggested data resolution.
-   *
-   * This number is the number of time units that corresponds to 1 pixel on the
-   * screen, rounded down to the nearest power of 2. The minimum value is 1.
-   *
-   * It's up to the track whether it would like to use this resolution or
-   * calculate their own based on the timespan and the track dimensions.
-   */
-  readonly resolution: duration;
-
-  /**
-   * Canvas context used for rendering.
-   */
-  readonly ctx: CanvasRenderingContext2D;
-
-  /**
-   * A time scale used for translating between pixels and time.
-   */
-  readonly timescale: TimeScale;
-}
-
-// A definition of a track, including a renderer implementation and metadata.
-export interface TrackDescriptor {
-  // A unique identifier for this track.
-  readonly uri: string;
-
-  // A factory function returning a new track instance.
-  readonly trackFactory: (ctx: TrackContext) => Track;
-
-  // Human readable title. Always displayed.
-  readonly title: string;
-
-  // Human readable subtitle. Sometimes displayed if there is room.
-  readonly subtitle?: string;
-
-  // Optional: A list of tags used for sorting, grouping and "chips".
-  readonly tags?: TrackTags;
-
-  readonly chips?: ReadonlyArray<string>;
-
-  readonly pluginId?: string;
-
-  // Optional: A details panel to use when this track is selected.
-  readonly detailsPanel?: TrackSelectionDetailsPanel;
-
-  // Optional: method to look up the start and duration of an event on this track
-  readonly getEventBounds?: (
-    id: number,
-  ) => Promise<Optional<{ts: time; dur: duration}>>;
-}
-
-export interface LegacyDetailsPanel {
-  render(selection: LegacySelection): m.Children;
-  isLoading?(): boolean;
-}
-
-export interface DetailsPanel {
-  render(selection: Selection): m.Children;
-  isLoading?(): boolean;
-}
-
-export interface TrackSelectionDetailsPanel {
-  render(id: number): m.Children;
-  isLoading?(): boolean;
-}
-
-export interface SliceRect {
-  left: number;
-  width: number;
-  top: number;
-  height: number;
-  visible: boolean;
-}
-
-/**
- * Contextual information passed to mouse events.
- */
-export interface TrackMouseEvent {
-  /**
-   * X coordinate of the mouse event w.r.t. the top-left of the track.
-   */
-  readonly x: number;
-
-  /**
-   * Y coordinate of the mouse event w.r.t the top-left of the track.
-   */
-  readonly y: number;
-
-  /**
-   * A time scale used for translating between pixels and time.
-   */
-  readonly timescale: TimeScale;
-}
-
-export interface Track {
-  /**
-   * Optional lifecycle hook called on the first render cycle. Should be used to
-   * create any required resources.
-   *
-   * These lifecycle hooks are asynchronous, but they are run synchronously,
-   * meaning that perfetto will wait for each one to complete before calling the
-   * next one, so the user doesn't have to serialize these calls manually.
-   *
-   * Exactly when this hook is called is left purposely undefined. The only
-   * guarantee is that it will be called exactly once before the first call to
-   * onUpdate().
-   *
-   * Note: On the first render cycle, both onCreate and onUpdate are called one
-   * after another.
-   */
-  onCreate?(ctx: TrackContext): Promise<void>;
-
-  /**
-   * Optional lifecycle hook called on every render cycle.
-   *
-   * The track should inspect things like the visible window, track size, and
-   * resolution to work out whether any data needs to be reloaded based on these
-   * properties and perform a reload.
-   */
-  onUpdate?(ctx: TrackRenderContext): Promise<void>;
-
-  /**
-   * Optional lifecycle hook called when the track is no longer visible. Should
-   * be used to clear up any resources.
-   */
-  onDestroy?(): Promise<void>;
-
-  /**
-   * Required method used to render the track's content to the canvas, called
-   * synchronously on every render cycle.
-   */
-  render(ctx: TrackRenderContext): void;
-  onFullRedraw?(): void;
-  getSliceRect?(
-    ctx: TrackRenderContext,
-    tStart: time,
-    tEnd: time,
-    depth: number,
-  ): Optional<SliceRect>;
-  getHeight(): number;
-  getTrackShellButtons?(): m.Children;
-  onMouseMove?(event: TrackMouseEvent): void;
-  onMouseClick?(event: TrackMouseEvent): boolean;
-  onMouseOut?(): void;
-
-  /**
-   * Optional: Get the event set that represents this track's data.
-   */
-  getEventSet?(): UntypedEventSet;
-}
-
-// An set of key/value pairs describing a given track. These are used for
-// selecting tracks to pin/unpin, diplsaying "chips" in the track shell, and
-// (in future) the sorting and grouping of tracks.
-// We define a handful of well known fields, and the rest are arbitrary key-
-// value pairs.
-export type TrackTags = Partial<WellKnownTrackTags> & {
-  // There may be arbitrary other key/value pairs.
-  [key: string]:
-    | undefined
-    | string
-    | number
-    | boolean
-    | ReadonlyArray<string>
-    | ReadonlyArray<number>;
-};
-
-interface WellKnownTrackTags {
-  // The track "kind", used by various subsystems e.g. aggregation controllers.
-  // This is where "XXX_TRACK_KIND" values should be placed.
-  // TODO(stevegolton): This will be deprecated once we handle group selections
-  // in a more generic way - i.e. EventSet.
-  kind: string;
-
-  // Optional: list of track IDs represented by this trace.
-  // This list is used for participation in track indexing by track ID.
-  // This index is used by various subsystems to find links between tracks based
-  // on the track IDs used by trace processor.
-  trackIds: ReadonlyArray<number>;
-
-  // Optional: The CPU number associated with this track.
-  cpu: number;
-
-  // Optional: The UTID associated with this track.
-  utid: number;
-
-  // Optional: The UPID associated with this track.
-  upid: number;
-
-  // Used for sorting and grouping
-  scope: string;
-
-  // Group name, used as a hint to ask track decider to put this in a group
-  groupName: string;
-}
diff --git a/ui/src/public/utils.ts b/ui/src/public/utils.ts
index 3b87315..657bed6 100644
--- a/ui/src/public/utils.ts
+++ b/ui/src/public/utils.ts
@@ -12,13 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import m from 'mithril';
-
-import {LegacySelection} from '../common/state';
-import {BottomTab} from '../frontend/bottom_tab';
-
-import {LegacyDetailsPanel, Tab} from '.';
 import {exists} from '../base/utils';
+import {Trace} from './trace';
+import {TimeSpan} from '../base/time';
 
 export function getTrackName(
   args: Partial<{
@@ -97,78 +93,6 @@
   return 'Unknown';
 }
 
-export interface BottomTabAdapterAttrs {
-  tabFactory: (sel: LegacySelection) => BottomTab | undefined;
-}
-
-/**
- * This adapter wraps a BottomTab, converting it into a the new "current
- * selection" API.
- * This adapter is required because most bottom tab implementations expect to
- * be created when the selection changes, however current selection sections
- * stick around in memory forever and produce a section only when they detect a
- * relevant selection.
- * This adapter, given a bottom tab factory function, will simply call the
- * factory function whenever the selection changes. It's up to the implementer
- * to work out whether the selection is relevant and to construct a bottom tab.
- *
- * @example
- * new BottomTabAdapter({
-      tabFactory: (sel) => {
-        if (sel.kind !== 'SLICE') {
-          return undefined;
-        }
-        return new ChromeSliceDetailsTab({
-          config: {
-            table: sel.table ?? 'slice',
-            id: sel.id,
-          },
-          engine: ctx.engine,
-          uuid: uuidv4(),
-        });
-      },
-    })
- */
-export class BottomTabToSCSAdapter implements LegacyDetailsPanel {
-  private oldSelection?: LegacySelection;
-  private bottomTab?: BottomTab;
-  private attrs: BottomTabAdapterAttrs;
-
-  constructor(attrs: BottomTabAdapterAttrs) {
-    this.attrs = attrs;
-  }
-
-  render(selection: LegacySelection): m.Children {
-    // Detect selection changes, assuming selection is immutable
-    if (selection !== this.oldSelection) {
-      this.oldSelection = selection;
-      this.bottomTab = this.attrs.tabFactory(selection);
-    }
-
-    return this.bottomTab?.renderPanel();
-  }
-
-  // Note: Must be called after render()
-  isLoading(): boolean {
-    return this.bottomTab?.isLoading() ?? false;
-  }
-}
-
-/**
- * This adapter wraps a BottomTab, converting it to work with the Tab API.
- */
-export class BottomTabToTabAdapter implements Tab {
-  constructor(private bottomTab: BottomTab) {}
-
-  getTitle(): string {
-    return this.bottomTab.getTitle();
-  }
-
-  render(): m.Children {
-    return this.bottomTab.viewTab();
-  }
-}
-
 export function getThreadOrProcUri(
   upid: number | null,
   utid: number | null,
@@ -189,3 +113,16 @@
     return `/thread_${utid}`;
   }
 }
+
+// Returns the time span of the current selection, or the visible window if
+// there is no current selection.
+export async function getTimeSpanOfSelectionOrVisibleWindow(
+  trace: Trace,
+): Promise<TimeSpan> {
+  const range = await trace.selection.findTimeRangeOfSelection();
+  if (exists(range)) {
+    return new TimeSpan(range.start, range.end);
+  } else {
+    return trace.timeline.visibleWindow.toTimeSpan();
+  }
+}
diff --git a/ui/src/public/workspace.ts b/ui/src/public/workspace.ts
new file mode 100644
index 0000000..a2bab1f
--- /dev/null
+++ b/ui/src/public/workspace.ts
@@ -0,0 +1,550 @@
+// 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';
+
+export interface WorkspaceManager {
+  // This is the same of ctx.workspace, exposed for consistency also here.
+  readonly currentWorkspace: Workspace;
+  readonly all: ReadonlyArray<Workspace>;
+  createEmptyWorkspace(displayName: string): Workspace;
+  switchWorkspace(workspace: Workspace): void;
+}
+
+let sessionUniqueIdCounter = 0;
+
+/**
+ * Creates a short ID which is unique to this instance of the UI.
+ *
+ * The advantage of using this over uuidv4() is that the ids produced are
+ * significantly shorter, saving memory and making them more human
+ * read/write-able which helps when debugging.
+ *
+ * Note: The ID range will reset every time the UI is restarted, so be careful
+ * not rely on these IDs in any medium that can survive between UI instances.
+ *
+ * TODO(stevegolton): We could possibly move this into its own module and use it
+ * everywhere where session-unique ids are required.
+ */
+function createSessionUniqueId(): string {
+  // Return the counter in base36 (0-z) to keep the string as short as possible
+  // but still human readable.
+  return (sessionUniqueIdCounter++).toString(36);
+}
+
+/**
+ * Describes generic parent track node functionality - i.e. any entity that can
+ * contain child TrackNodes, providing methods to add, remove, and access child
+ * nodes.
+ *
+ * This class is abstract because, while it can technically be instantiated on
+ * its own (no abstract methods/properties), it can't and shouldn't be
+ * instantiated anywhere in practice - all APIs require either a TrackNode or a
+ * Workspace.
+ *
+ * Thus, it serves two purposes:
+ * 1. Avoiding duplication between Workspace and TrackNode, which is an internal
+ *    implementation detail of this module.
+ * 2. Providing a typescript interface for a generic TrackNode container class,
+ *    which otherwise you might have to achieve using `Workspace | TrackNode`
+ *    which is uglier.
+ *
+ * If you find yourself using this as a Javascript class in external code, e.g.
+ * `instance of TrackNodeContainer`, you're probably doing something wrong.
+ */
+export abstract class TrackNodeContainer {
+  protected _children: Array<TrackNode> = [];
+  protected readonly tracksById = new Map<string, TrackNode>();
+  protected abstract fireOnChangeListener(): void;
+
+  /**
+   * True if this node has children, false otherwise.
+   */
+  get hasChildren(): boolean {
+    return this._children.length > 0;
+  }
+
+  /**
+   * The ordered list of children belonging to this node.
+   */
+  get children(): ReadonlyArray<TrackNode> {
+    return this._children;
+  }
+
+  /**
+   * Inserts a new child node considering it's sortOrder.
+   *
+   * The child will be added before the first child whose |sortOrder| is greater
+   * than the child node's sort order, or at the end if one does not exist. If
+   * |sortOrder| is omitted on either node in the comparison it is assumed to be
+   * 0.
+   *
+   * @param child - The child node to add.
+   */
+  addChildInOrder(child: TrackNode): void {
+    const insertPoint = this._children.find(
+      (n) => (n.sortOrder ?? 0) > (child.sortOrder ?? 0),
+    );
+    if (insertPoint) {
+      this.addChildBefore(child, insertPoint);
+    } else {
+      this.addChildLast(child);
+    }
+  }
+
+  /**
+   * Add a new child node at the start of the list of children.
+   *
+   * @param child The new child node to add.
+   */
+  addChildLast(child: TrackNode): void {
+    this.adopt(child);
+    this._children.push(child);
+    this.fireOnChangeListener();
+  }
+
+  /**
+   * Add a new child node at the end of the list of children.
+   *
+   * @param child The child node to add.
+   */
+  addChildFirst(child: TrackNode): void {
+    this.adopt(child);
+    this._children.unshift(child);
+    this.fireOnChangeListener();
+  }
+
+  /**
+   * Add a new child node before an existing child node.
+   *
+   * @param child The child node to add.
+   * @param referenceNode An existing child node. The new node will be added
+   * before this node.
+   */
+  addChildBefore(child: TrackNode, referenceNode: TrackNode): void {
+    if (child === referenceNode) return;
+
+    assertTrue(this.children.includes(referenceNode));
+
+    this.adopt(child);
+
+    const indexOfReference = this.children.indexOf(referenceNode);
+    this._children.splice(indexOfReference, 0, child);
+    this.fireOnChangeListener();
+  }
+
+  /**
+   * Add a new child node after an existing child node.
+   *
+   * @param child The child node to add.
+   * @param referenceNode An existing child node. The new node will be added
+   * after this node.
+   */
+  addChildAfter(child: TrackNode, referenceNode: TrackNode): void {
+    if (child === referenceNode) return;
+
+    assertTrue(this.children.includes(referenceNode));
+
+    this.adopt(child);
+
+    const indexOfReference = this.children.indexOf(referenceNode);
+    this._children.splice(indexOfReference + 1, 0, child);
+    this.fireOnChangeListener();
+  }
+
+  /**
+   * Remove a child node from this node.
+   *
+   * @param child The child node to remove.
+   */
+  removeChild(child: TrackNode): void {
+    this._children = this.children.filter((x) => child !== x);
+    child.parent = undefined;
+    child.id && this.tracksById.delete(child.id);
+    this.fireOnChangeListener();
+  }
+
+  /**
+   * The flattened list of all descendent nodes.
+   */
+  get flatTracks(): ReadonlyArray<TrackNode> {
+    return this.children.flatMap((node) => {
+      return [node, ...node.flatTracks];
+    });
+  }
+
+  /**
+   * Remove all children from this node.
+   */
+  clear(): void {
+    this._children = [];
+    this.tracksById.clear();
+    this.fireOnChangeListener();
+  }
+
+  /**
+   * Find a track node by its id.
+   *
+   * Node: This is an O(N) operation where N is the depth of the target node.
+   * I.e. this is more efficient than findTrackByURI().
+   *
+   * @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 {
+    const foundNode = this.tracksById.get(id);
+    if (foundNode) {
+      return foundNode;
+    } else {
+      // Recurse our children
+      for (const child of this._children) {
+        const foundNode = child.getTrackById(id);
+        if (foundNode) return foundNode;
+      }
+    }
+    return undefined;
+  }
+
+  private adopt(child: TrackNode): void {
+    if (child.parent) {
+      child.parent.removeChild(child);
+    }
+    child.parent = this;
+    child.id && this.tracksById.set(child.id, child);
+  }
+}
+
+export interface TrackNodeArgs {
+  title: string;
+  id: string;
+  uri: string;
+  headless: boolean;
+  sortOrder: number;
+  collapsed: boolean;
+  isSummary: boolean;
+  removable: boolean;
+}
+
+/**
+ * A base class for any node with children (i.e. a group or a workspace).
+ */
+export class TrackNode extends TrackNodeContainer {
+  // Immutable unique (within the workspace) ID of this track node. Used for
+  // efficiently retrieving this node object from a workspace. Note: This is
+  // different to |uri| which is used to reference a track to render on the
+  // track. If this means nothing to you, don't bother using it.
+  public readonly id: string;
+
+  // Parent node - could be the workspace or another node.
+  public parent?: TrackNodeContainer;
+
+  // A human readable string for this track - displayed in the track shell.
+  // TODO(stevegolton): Make this optional, so that if we implement a string for
+  // this track then we can implement it here as well.
+  public title: string;
+
+  // The URI of the track content to display here.
+  public uri?: string;
+
+  // Optional sort order, which workspaces may or may not take advantage of for
+  // sorting when displaying the workspace.
+  public sortOrder?: number;
+
+  // Don't show the header at all for this track, just show its un-nested
+  // children. This is helpful to group together tracks that logically belong to
+  // the same group (e.g. all ftrace cpu tracks) and ease the job of
+  // sorting/grouping plugins.
+  public headless: boolean;
+
+  // If true, this track is to be used as a summary for its children. When the
+  // group is expanded the track will become sticky to the top of the viewport
+  // to provide context for the tracks within, and the content of this track
+  // shall be omitted. It will also be squashed down to a smaller height to save
+  // vertical space.
+  public isSummary: boolean;
+
+  // If true, this node will be removable by the user. It will show a little
+  // close button in the track shell which the user can press to remove the
+  // track from the workspace.
+  public removable: boolean;
+
+  protected _collapsed = true;
+
+  constructor(args?: Partial<TrackNodeArgs>) {
+    super();
+
+    const {
+      title = '',
+      id = createSessionUniqueId(),
+      uri,
+      headless = false,
+      sortOrder,
+      collapsed = true,
+      isSummary = false,
+      removable = false,
+    } = args ?? {};
+
+    this.id = id;
+    this.uri = uri;
+    this.headless = headless;
+    this.title = title;
+    this.sortOrder = sortOrder;
+    this.isSummary = isSummary;
+    this._collapsed = collapsed;
+    this.removable = removable;
+  }
+
+  /**
+   * Remove this track from it's parent & unpin from the workspace if pinned.
+   */
+  remove(): void {
+    this.workspace?.unpinTrack(this);
+    this.parent?.removeChild(this);
+  }
+
+  /**
+   * Add this track to the list of pinned tracks in its parent workspace.
+   *
+   * Has no effect if this track is not added to a workspace.
+   */
+  pin(): void {
+    this.workspace?.pinTrack(this);
+  }
+
+  /**
+   * Remove this track from the list of pinned tracks in its parent workspace.
+   *
+   * Has no effect if this track is not added to a workspace.
+   */
+  unpin(): void {
+    this.workspace?.unpinTrack(this);
+  }
+
+  /**
+   * Returns true if this node is added to a workspace as is in the pinned track
+   * list of that workspace.
+   */
+  get isPinned(): boolean {
+    return Boolean(this.workspace?.hasPinnedTrack(this));
+  }
+
+  /**
+   * Find the closest visible ancestor TrackNode.
+   *
+   * Given the path from the root workspace to this node, find the fist one,
+   * starting from the root, which is collapsed. This will be, from the user's
+   * point of view, the closest ancestor of this node.
+   *
+   * Returns undefined if this node is actually visible.
+   *
+   * TODO(stevegolton): Should it return itself in this case?
+   */
+  findClosestVisibleAncestor(): TrackNode {
+    // Build a path from the root workspace to this node
+    const path: TrackNode[] = [];
+    let node = this.parent;
+    while (node && node instanceof TrackNode) {
+      path.unshift(node);
+      node = node.parent;
+    }
+
+    // Find the first collapsed track in the path starting from the root. This
+    // is effectively the closest we can get to this node without expanding any
+    // groups.
+    return path.find((node) => node.collapsed) ?? this;
+  }
+
+  /**
+   * Expand all ancestor nodes.
+   */
+  reveal(): void {
+    let parent = this.parent;
+    while (parent && parent instanceof TrackNode) {
+      parent.expand();
+      parent = parent.parent;
+    }
+  }
+
+  /**
+   * Find this node's root node - this may be a workspace or another node.
+   */
+  get rootNode(): TrackNodeContainer | undefined {
+    // Travel upwards through the tree to find the root node.
+    let parent: TrackNodeContainer | undefined = this;
+    while (parent && parent instanceof TrackNode) {
+      parent = parent.parent;
+    }
+    return parent;
+  }
+
+  /**
+   * Find this node's parent workspace if it is attached to one.
+   */
+  get workspace(): Workspace | undefined {
+    // Find the root node and return it if it's a Workspace instance
+    const rootNode = this.rootNode;
+    if (rootNode instanceof Workspace) {
+      return rootNode;
+    }
+    return undefined;
+  }
+
+  /**
+   * Mark this node as un-collapsed, indicating its children should be rendered.
+   */
+  expand(): void {
+    this._collapsed = false;
+    this.fireOnChangeListener();
+  }
+
+  /**
+   * Mark this node as collapsed, indicating its children should not be
+   * rendered.
+   */
+  collapse(): void {
+    this._collapsed = true;
+    this.fireOnChangeListener();
+  }
+
+  /**
+   * Toggle the collapsed state.
+   */
+  toggleCollapsed(): void {
+    this._collapsed = !this._collapsed;
+    this.fireOnChangeListener();
+  }
+
+  /**
+   * Whether this node is collapsed, indicating its children should be rendered.
+   */
+  get collapsed(): boolean {
+    return this._collapsed;
+  }
+
+  /**
+   * Whether this node is expanded - i.e. not collapsed, indicating its children
+   * should be rendered.
+   */
+  get expanded(): boolean {
+    return !this._collapsed;
+  }
+
+  /**
+   * Returns the list of titles representing the full path from the root node to
+   * the current node. This path consists only of node titles, workspaces are
+   * omitted.
+   */
+  get fullPath(): ReadonlyArray<string> {
+    let fullPath = [this.title];
+    let parent = this.parent;
+    while (parent && parent instanceof TrackNode) {
+      // Ignore headless containers as they don't appear in the tree...
+      if (!parent.headless) {
+        fullPath = [parent.title, ...fullPath];
+      }
+      parent = parent.parent;
+    }
+    return fullPath;
+  }
+
+  protected override fireOnChangeListener(): void {
+    this.workspace?.onchange(this.workspace);
+  }
+}
+
+/**
+ * Defines a workspace containing a track tree and a pinned area.
+ */
+export class Workspace extends TrackNodeContainer {
+  public title = '<untitled-workspace>';
+  public readonly id: string;
+  onchange: (w: Workspace) => void = () => {};
+
+  // Dummy node to contain the pinned tracks
+  private pinnedRoot = new TrackNode();
+
+  get pinnedTracks(): ReadonlyArray<TrackNode> {
+    return this.pinnedRoot.children;
+  }
+
+  constructor() {
+    super();
+    this.id = createSessionUniqueId();
+    this.pinnedRoot.parent = this;
+  }
+
+  /**
+   * Reset the entire workspace including the pinned tracks.
+   */
+  override clear(): void {
+    super.clear();
+    this.pinnedRoot.clear();
+  }
+
+  /**
+   * Adds a track node to this workspace's pinned area.
+   */
+  pinTrack(track: TrackNode): void {
+    // Make a lightweight clone of this track - just the uri and the title.
+    const cloned = new TrackNode({
+      uri: track.uri,
+      title: track.title,
+      removable: track.removable,
+    });
+    this.pinnedRoot.addChildLast(cloned);
+  }
+
+  /**
+   * Removes a track node from this workspace's pinned area.
+   */
+  unpinTrack(track: TrackNode): void {
+    const foundNode = this.pinnedRoot.children.find((t) => t.uri === track.uri);
+    if (foundNode) {
+      this.pinnedRoot.removeChild(foundNode);
+    }
+  }
+
+  /**
+   * Check if this workspace has a pinned track with the same URI as |track|.
+   */
+  hasPinnedTrack(track: TrackNode): boolean {
+    return this.pinnedTracks.some((p) => p.uri === track.uri);
+  }
+
+  /**
+   * Find a track node via its URI.
+   *
+   * Note: This in an O(N) operation where N is the number of nodes in the
+   * workspace.
+   *
+   * @param uri The uri of the track to find.
+   * @returns A reference to the track node if it exists in this workspace,
+   * otherwise undefined.
+   */
+  findTrackByUri(uri: string): TrackNode | undefined {
+    return this.flatTracks.find((t) => t.uri === uri);
+  }
+
+  /**
+   * Find a track by ID, also searching pinned tracks.
+   */
+  override getTrackById(id: string): TrackNode | undefined {
+    // Also search the pinned tracks
+    return this.pinnedRoot.getTrackById(id) || super.getTrackById(id);
+  }
+
+  protected override fireOnChangeListener(): void {
+    this.onchange(this);
+  }
+}
diff --git a/ui/src/public/workspace_unittest.ts b/ui/src/public/workspace_unittest.ts
new file mode 100644
index 0000000..1150550
--- /dev/null
+++ b/ui/src/public/workspace_unittest.ts
@@ -0,0 +1,183 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {TrackNode, Workspace} from './workspace';
+
+describe('workspace', () => {
+  test('getNodeByKey', () => {
+    const workspace = new Workspace();
+    const track = new TrackNode({id: 'foo'});
+    workspace.addChildLast(track);
+
+    expect(workspace.getTrackById('foo')).toEqual(track);
+  });
+
+  test('getNodeByKey', () => {
+    const track = new TrackNode({id: 'bar'});
+
+    const group = new TrackNode();
+    group.addChildLast(track);
+
+    // Add group to workspace AFTER adding the track to the group
+    const workspace = new Workspace();
+    workspace.addChildLast(group);
+
+    expect(workspace.getTrackById('bar')).toBe(track);
+  });
+
+  test('nested index lookup', () => {
+    const track = new TrackNode({id: 'bar'});
+
+    const group = new TrackNode();
+
+    // Add group to workspace before adding the track to the group
+    const workspace = new Workspace();
+    workspace.addChildLast(group);
+    group.addChildLast(track);
+
+    expect(workspace.getTrackById('bar')).toBe(track);
+  });
+
+  test('nested index lookup', () => {
+    const workspace = new Workspace();
+
+    const group = new TrackNode();
+
+    const track = new TrackNode({id: 'bar'});
+    group.addChildLast(track);
+
+    // Add group to workspace
+    workspace.addChildLast(group);
+    workspace.removeChild(group);
+
+    expect(workspace.getTrackById('bar')).toBe(undefined);
+  });
+
+  test('findTrackByUri()', () => {
+    const workspace = new Workspace();
+
+    const group = new TrackNode();
+
+    const track = new TrackNode({uri: 'foo'});
+    group.addChildLast(track);
+
+    // Add group to workspace
+    workspace.addChildLast(group);
+
+    expect(workspace.findTrackByUri('foo')).toBe(track);
+  });
+
+  test('findClosestVisibleAncestor()', () => {
+    const child = new TrackNode();
+    child.expand(); // Expanding the child node should have no effect
+
+    const parent = new TrackNode();
+    parent.expand();
+    parent.addChildLast(child);
+
+    // While everything is expanded and the child node is visible, the child
+    // should be returned.
+    expect(child.findClosestVisibleAncestor()).toBe(child);
+
+    // Collapse the parent node and this parent should be returned, as from the
+    // point of view of the root, this is the closest we can get to our target
+    // node without expanding any more nodes.
+    parent.collapse();
+    expect(child.findClosestVisibleAncestor()).toBe(parent);
+  });
+});
+
+describe('TrackNode.addChildInOrder', () => {
+  let container: TrackNode;
+
+  beforeEach(() => {
+    container = new TrackNode();
+  });
+
+  test('inserts a child into an empty container', () => {
+    const child = new TrackNode({id: 'track1'});
+
+    container.addChildInOrder(child);
+
+    expect(container.children).toHaveLength(1);
+    expect(container.children[0]).toBe(child);
+  });
+
+  test('inserts a child with a lower sortOrder before an existing child', () => {
+    const child1 = new TrackNode({sortOrder: 10});
+    const child2 = new TrackNode({sortOrder: 5});
+
+    container.addChildInOrder(child1);
+    container.addChildInOrder(child2);
+
+    expect(container.children).toHaveLength(2);
+    expect(container.children[0]).toBe(child2);
+    expect(container.children[1]).toBe(child1);
+  });
+
+  test('inserts a child with a higher sortOrder after an existing child', () => {
+    const child1 = new TrackNode({sortOrder: 5});
+    const child2 = new TrackNode({sortOrder: 10});
+
+    container.addChildInOrder(child1);
+    container.addChildInOrder(child2);
+
+    expect(container.children).toHaveLength(2);
+    expect(container.children[0]).toBe(child1);
+    expect(container.children[1]).toBe(child2);
+  });
+
+  test('inserts a child with the same sortOrder after an existing child', () => {
+    const child1 = new TrackNode({sortOrder: 5});
+    const child2 = new TrackNode({sortOrder: 5});
+
+    container.addChildInOrder(child1);
+    container.addChildInOrder(child2);
+
+    expect(container.children).toHaveLength(2);
+    expect(container.children[0]).toBe(child1);
+    expect(container.children[1]).toBe(child2);
+  });
+
+  test('inserts multiple children and maintains order', () => {
+    const child1 = new TrackNode({sortOrder: 15});
+    const child2 = new TrackNode({sortOrder: 10});
+    const child3 = new TrackNode({sortOrder: 20});
+
+    container.addChildInOrder(child1);
+    container.addChildInOrder(child2);
+    container.addChildInOrder(child3);
+
+    expect(container.children).toHaveLength(3);
+    expect(container.children[0]).toBe(child2);
+    expect(container.children[1]).toBe(child1);
+    expect(container.children[2]).toBe(child3);
+  });
+
+  test('inserts a child with undefined sortOrder as 0', () => {
+    const child1 = new TrackNode({sortOrder: 10});
+
+    // sortOrder is undefined, treated as 0
+    const child2 = new TrackNode();
+
+    container.addChildInOrder(child1);
+    container.addChildInOrder(child2);
+
+    expect(container.children).toHaveLength(2);
+
+    // child2 (sortOrder 0) should be first
+    expect(container.children[0]).toBe(child2);
+    expect(container.children[1]).toBe(child1);
+  });
+});
diff --git a/ui/src/test/aggregation.test.ts b/ui/src/test/aggregation.test.ts
new file mode 100644
index 0000000..449d23b
--- /dev/null
+++ b/ui/src/test/aggregation.test.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 {test, Page} from '@playwright/test';
+import {PerfettoTestHelper} from './perfetto_ui_test_helper';
+import {assertExists} from '../base/logging';
+
+test.describe.configure({mode: 'serial'});
+
+let pth: PerfettoTestHelper;
+let page: Page;
+
+test.beforeAll(async ({browser}, _testInfo) => {
+  page = await browser.newPage();
+  pth = new PerfettoTestHelper(page);
+  await pth.openTraceFile('api34_startup_cold.perfetto-trace');
+});
+
+test('sched', async () => {
+  await page.mouse.move(600, 250);
+  await page.mouse.down();
+  await page.mouse.move(800, 350);
+  await page.mouse.up();
+  await pth.waitForPerfettoIdle();
+  await pth.waitForIdleAndScreenshot('cpu-by-thread.png');
+
+  await page.click('button[label="CPU by process"]');
+  await pth.waitForIdleAndScreenshot('cpu-by-process.png');
+
+  // Now test sorting.
+
+  const hdr = page.getByRole('cell', {name: 'Avg Wall duration (ms)'});
+  await hdr.click();
+  await pth.waitForIdleAndScreenshot('sort-by-wall-duration.png');
+
+  await hdr.click();
+  await pth.waitForIdleAndScreenshot('sort-by-wall-duration-desc.png');
+
+  await page.getByRole('cell', {name: 'Occurrences'}).click();
+  await pth.waitForIdleAndScreenshot('sort-by-occurrences.png');
+});
+
+test('gpu counter', async () => {
+  await page.keyboard.press('Escape');
+  const gpuTrack = pth.locateTrack('Gpu 0 Frequency');
+  const coords = assertExists(await gpuTrack.boundingBox());
+  await page.mouse.move(600, coords.y + 10);
+  await page.mouse.down();
+  await page.mouse.move(800, coords.y + 60);
+  await page.mouse.up();
+  await pth.waitForIdleAndScreenshot('gpu-counter-aggregation.png');
+});
+
+test('frametimeline', async () => {
+  await page.keyboard.press('Escape');
+  const sysui = pth.locateTrackGroup('com.android.systemui 25348');
+  await sysui.scrollIntoViewIfNeeded();
+  await pth.toggleTrackGroup(sysui);
+  const actualTimeline = pth.locateTrack(
+    'com.android.systemui 25348/Actual Timeline',
+    sysui,
+  );
+  const coords = assertExists(await actualTimeline.boundingBox());
+  await page.mouse.move(600, coords.y + 10);
+  await page.mouse.down();
+  await page.mouse.move(1000, coords.y + 20);
+  await page.mouse.up();
+  await pth.waitForIdleAndScreenshot('frame-timeline-aggregation.png');
+});
+
+test('slices', async () => {
+  await page.keyboard.press('Escape');
+  const syssrv = pth.locateTrackGroup('system_server 1719');
+  await syssrv.scrollIntoViewIfNeeded();
+  await pth.toggleTrackGroup(syssrv);
+  const animThread = pth
+    .locateTrack('system_server 1719/android.anim 1754', syssrv)
+    .nth(1);
+  await animThread.scrollIntoViewIfNeeded();
+  await pth.waitForPerfettoIdle();
+  const coords = assertExists(await animThread.boundingBox());
+  await page.mouse.move(600, coords.y + 10);
+  await page.mouse.down();
+  await page.mouse.move(1000, coords.y + 20);
+  await page.mouse.up();
+  await pth.waitForIdleAndScreenshot('slice-aggregation.png');
+});
diff --git a/ui/src/test/chrome_missing_track_names.test.ts b/ui/src/test/chrome_missing_track_names.test.ts
new file mode 100644
index 0000000..3f20bc0
--- /dev/null
+++ b/ui/src/test/chrome_missing_track_names.test.ts
@@ -0,0 +1,36 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {test, Page} from '@playwright/test';
+import {PerfettoTestHelper} from './perfetto_ui_test_helper';
+
+test.describe.configure({mode: 'serial'});
+
+let pth: PerfettoTestHelper;
+let page: Page;
+
+test.beforeAll(async ({browser}, _testInfo) => {
+  page = await browser.newPage();
+  pth = new PerfettoTestHelper(page);
+  await pth.openTraceFile('chrome_missing_track_names.pb.gz');
+});
+
+test('trace loaded', async () => {
+  await pth.waitForIdleAndScreenshot('trace_loaded.png');
+});
+
+test('expand all tracks', async () => {
+  await page.click('.header-panel-container 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
new file mode 100644
index 0000000..8cc57ee
--- /dev/null
+++ b/ui/src/test/chrome_rendering_desktop.test.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 {test, Page} from '@playwright/test';
+import {PerfettoTestHelper} from './perfetto_ui_test_helper';
+
+test.describe.configure({mode: 'serial'});
+
+let pth: PerfettoTestHelper;
+let page: Page;
+
+test.beforeAll(async ({browser}, _testInfo) => {
+  page = await browser.newPage();
+  pth = new PerfettoTestHelper(page);
+  await pth.openTraceFile('chrome_rendering_desktop.pftrace');
+});
+
+test('load trace', async () => {
+  await pth.waitForIdleAndScreenshot('loaded.png');
+});
+
+test('expand browser', async () => {
+  const grp = pth.locateTrackGroup('Browser 12685');
+  grp.scrollIntoViewIfNeeded();
+  await pth.toggleTrackGroup(grp);
+  await pth.waitForIdleAndScreenshot('browser_expanded.png');
+  await pth.toggleTrackGroup(grp);
+});
+
+test('slice with flows', async () => {
+  await pth.searchSlice('GenerateRenderPass');
+  await pth.resetFocus();
+  await page.keyboard.press('f');
+  await pth.waitForPerfettoIdle();
+  await pth.waitForIdleAndScreenshot('slice_with_flows.png');
+});
diff --git a/ui/src/test/debug_tracks.test.ts b/ui/src/test/debug_tracks.test.ts
new file mode 100644
index 0000000..b24f515
--- /dev/null
+++ b/ui/src/test/debug_tracks.test.ts
@@ -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.
+
+import {test, Page} from '@playwright/test';
+import {PerfettoTestHelper} from './perfetto_ui_test_helper';
+
+test.describe.configure({mode: 'serial'});
+
+let pth: PerfettoTestHelper;
+let page: Page;
+
+const SQL_QUERY = `select id, ts, dur, name, category, track_id from slices
+where category is not null  limit 1000`;
+
+test.beforeAll(async ({browser}, _testInfo) => {
+  page = await browser.newPage();
+  pth = new PerfettoTestHelper(page);
+  await pth.openTraceFile('api34_startup_cold.perfetto-trace');
+});
+
+test('debug tracks', async () => {
+  const omnibox = page.locator('input[ref=omnibox]');
+  await omnibox.focus();
+  await omnibox.selectText();
+  await omnibox.press(':');
+  await pth.waitForPerfettoIdle();
+  await omnibox.fill(SQL_QUERY);
+  await pth.waitForPerfettoIdle();
+  await omnibox.press('Enter');
+  await pth.waitForPerfettoIdle();
+
+  await page.getByRole('button', {name: 'Show debug track'}).click();
+  await pth.waitForPerfettoIdle();
+  await page.keyboard.type('debug track'); // The track name
+  await page.keyboard.press('Enter');
+  await pth.waitForPerfettoIdle();
+  await pth.waitForIdleAndScreenshot('debug track added.png');
+
+  // Click on a slice on the debug track.
+  await page.mouse.click(590, 180);
+  await pth.waitForPerfettoIdle();
+  await pth.waitForIdleAndScreenshot('debug slice clicked.png');
+
+  // Close the debug track.
+  await pth.locateTrack('debug track').getByText('close').first().click();
+  await pth.waitForPerfettoIdle();
+  await pth.waitForIdleAndScreenshot('debug track removed.png');
+});
+
+test('debug tracks pivot', async () => {
+  const omnibox = page.locator('input[ref=omnibox]');
+  await omnibox.focus();
+  await omnibox.selectText();
+  await omnibox.press(':');
+  await pth.waitForPerfettoIdle();
+  await omnibox.fill(SQL_QUERY);
+  await pth.waitForPerfettoIdle();
+  await omnibox.press('Enter');
+
+  await page.getByRole('button', {name: 'Show debug track'}).click();
+  await pth.waitForPerfettoIdle();
+  await page.keyboard.type('pivot'); // The track name
+  await page.locator('.pf-popup-portal #pivot').selectOption('category');
+  await page.keyboard.press('Enter');
+  await pth.waitForPerfettoIdle();
+  await pth.waitForIdleAndScreenshot('debug track pivot.png', {
+    clip: {
+      x: (await pth.sidebarSize()).width,
+      y: 180,
+      width: 1920,
+      height: 600,
+    },
+  });
+});
diff --git a/ui/src/test/diff_viewer/README.md b/ui/src/test/diff_viewer/README.md
deleted file mode 100644
index 119dd75..0000000
--- a/ui/src/test/diff_viewer/README.md
+++ /dev/null
@@ -1,18 +0,0 @@
-# CI screenshot diff viewer
-
-This directory contains the source of screenshots diff viewer used on Perfetto
-CI. The way it works as follows:
-
-When a screenshot test is failing, the testing code will write a line of the
-form
-
-```
-failed-screenshot.png;failed-screenshot-diff.png
-```
-
-To a file called `report.txt`. Diff viewer is just a static page that uses Fetch
-API to download this file, parse it, and display images in a list of rows.
-
-The page assumes `report.txt` to be present in the same directory, same goes for
-screenshot files. To simplify deployment, the viewer is developed without a
-framework and constructs DOM using `document.createElement` API.
diff --git a/ui/src/test/diff_viewer/index.html b/ui/src/test/diff_viewer/index.html
deleted file mode 100644
index 9dffb79..0000000
--- a/ui/src/test/diff_viewer/index.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-	<meta charset="UTF-8">
-	<meta http-equiv="X-UA-Compatible" content="IE=edge">
-	<meta name="viewport" content="width=device-width, initial-scale=1.0">
-	<title>Diff screenshots report</title>
-	<style>
-		.row {
-			display: flex;
-			padding: 1rem;
-			border-radius: .5rem;
-			border: 1px solid black;
-			margin-bottom: 1rem;
-		}
-		.image-wrapper img {
-			max-width: 45vw;
-		}
-		.cmd {
-			font-family: monospace;
-			color: white;
-			background: black;
-		}
-	</style>
-</head>
-<body>
-	<div class="container">
-		Loading...
-	</div>
-	<script src="script.js"></script>
-</body>
-</html>
\ No newline at end of file
diff --git a/ui/src/test/diff_viewer/script.js b/ui/src/test/diff_viewer/script.js
deleted file mode 100644
index aee9e04..0000000
--- a/ui/src/test/diff_viewer/script.js
+++ /dev/null
@@ -1,143 +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.
-
-// Helper function to create DOM elements faster: takes a Mithril-style
-// "selector" of the form "tag.class1.class2" and a list of child objects that
-// can be either strings or DOM elements.
-function m(selector, ...children) {
-  const parts = selector.split('.');
-  if (parts.length === 0) {
-    throw new Error(
-      'Selector passed to element should be of a form tag.class1.class2',
-    );
-  }
-
-  const result = document.createElement(parts[0]);
-  for (let i = 1; i < parts.length; i++) {
-    result.classList.add(parts[i]);
-  }
-  for (const child of children) {
-    if (typeof child === 'string') {
-      const childNode = document.createTextNode(child);
-      result.appendChild(childNode);
-    } else {
-      result.appendChild(child);
-    }
-  }
-  return result;
-}
-
-function getCiRun() {
-  const url = new URL(window.location.href);
-  const parts = url.pathname.split('/');
-
-  // Example report URL:
-  // https://storage.googleapis.com/perfetto-ci-artifacts/20220711123401--cls-2149676-1--ui-clang-x86_64-release/ui-test-artifacts/index.html
-  // Parts would contain ['', 'perfetto-ci-artifacts',
-  // '20220711123401--cls-2149676-1--ui-clang-x86_64-release', ...] in this
-  // case, which means that we need to check length of the array and get third
-  // element out of it.
-  if (parts.length >= 3) {
-    return parts[2];
-  }
-  return null;
-}
-
-function imageLinkElement(path) {
-  const img = m('img');
-  img.src = path;
-  const link = m('a');
-  link.appendChild(img);
-  link.href = path;
-  link.target = '_blank';
-  return link;
-}
-
-function processLines(lines) {
-  const container = document.querySelector('.container');
-  container.innerHTML = '';
-  const children = [];
-
-  // report.txt is a text file with a pair of file names on each line, separated
-  // by semicolon. E.g. "screenshot.png;screenshot-diff.png"
-  for (const line of lines) {
-    // Skip empty lines (happens when the file is completely empty).
-    if (line.length === 0) {
-      continue;
-    }
-
-    const parts = line.split(';');
-    if (parts.length !== 2) {
-      console.warn(
-        `Malformed line (expected two files separated via semicolon) ${line}!`,
-      );
-      continue;
-    }
-
-    const [output, diff] = parts;
-    children.push(
-      m(
-        'div.row',
-        m('div.cell', output, m('div.image-wrapper', imageLinkElement(output))),
-        m('div.cell', diff, m('div.image-wrapper', imageLinkElement(diff))),
-      ),
-    );
-  }
-
-  if (children.length === 0) {
-    container.appendChild(m('div', 'All good!'));
-    return;
-  }
-
-  const run = getCiRun();
-
-  if (run !== null) {
-    const cmd = `tools/download_changed_screenshots.py ${run}`;
-    const button = m('button', 'Copy');
-    button.addEventListener('click', async () => {
-      await navigator.clipboard.writeText(cmd);
-      button.innerText = 'Copied!';
-    });
-
-    container.appendChild(
-      m(
-        'div.message',
-        'Use following command from Perfetto checkout directory to apply the ' +
-          'changes: ',
-        m('span.cmd', cmd),
-        button,
-      ),
-    );
-  }
-
-  for (const child of children) {
-    container.appendChild(child);
-  }
-}
-
-async function loadDiffs() {
-  try {
-    const report = await fetch('report.txt');
-    const response = await report.text();
-    processLines(response.split('\n'));
-  } catch (e) {
-    // report.txt is not available when all tests have succeeded, treat fetching
-    // error as absence of failures
-    processLines([]);
-  }
-}
-
-document.addEventListener('DOMContentLoaded', () => {
-  loadDiffs();
-});
diff --git a/ui/src/test/ftrace_tracks_and_tab.test.ts b/ui/src/test/ftrace_tracks_and_tab.test.ts
new file mode 100644
index 0000000..7bed34b
--- /dev/null
+++ b/ui/src/test/ftrace_tracks_and_tab.test.ts
@@ -0,0 +1,39 @@
+// 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 {test, Page} from '@playwright/test';
+import {PerfettoTestHelper} from './perfetto_ui_test_helper';
+
+test.describe.configure({mode: 'serial'});
+
+let pth: PerfettoTestHelper;
+let page: Page;
+
+test.beforeAll(async ({browser}, _testInfo) => {
+  page = await browser.newPage();
+  pth = new PerfettoTestHelper(page);
+  await pth.openTraceFile('api34_startup_cold.perfetto-trace');
+});
+
+test('ftrace tracks', async () => {
+  await page.click('h1[ref="Ftrace Events"]');
+  await pth.waitForIdleAndScreenshot('ftrace_events.png');
+});
+
+test('ftrace tab', async () => {
+  await page.mouse.move(0, 0);
+  await page.click('button[title="More Tabs"]');
+  await page.getByRole('button', {name: 'Ftrace Events'}).click();
+  await pth.waitForIdleAndScreenshot('ftrace_tab.png');
+});
diff --git a/ui/src/test/independent_features.test.ts b/ui/src/test/independent_features.test.ts
new file mode 100644
index 0000000..c761ded
--- /dev/null
+++ b/ui/src/test/independent_features.test.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 {test} from '@playwright/test';
+import {PerfettoTestHelper} from './perfetto_ui_test_helper';
+
+test.describe.configure({mode: 'parallel'});
+
+// Test that we show a (debuggable) chip next to tracks for debuggable apps.
+// Regression test for aosp/3106008 .
+test('debuggable chip', async ({browser}) => {
+  const page = await browser.newPage();
+  const pth = new PerfettoTestHelper(page);
+  await pth.openTraceFile('api32_startup_warm.perfetto-trace');
+  const trackGroup = pth.locateTrackGroup(
+    'androidx.benchmark.integration.macrobenchmark.test 7527',
+  );
+  await trackGroup.scrollIntoViewIfNeeded();
+  await pth.waitForIdleAndScreenshot('track_with_debuggable_chip.png');
+
+  await pth.toggleTrackGroup(trackGroup);
+  await pth.waitForIdleAndScreenshot('track_with_debuggable_chip_expanded.png');
+});
+
+test('trace error notification', async ({browser}) => {
+  const page = await browser.newPage();
+  const pth = new PerfettoTestHelper(page);
+  await pth.openTraceFile('clusterfuzz_14753');
+  await pth.waitForIdleAndScreenshot('error-icon.png', {
+    clip: {x: 1800, y: 0, width: 150, height: 150},
+  });
+});
diff --git a/ui/src/test/load_and_tracks.test.ts b/ui/src/test/load_and_tracks.test.ts
new file mode 100644
index 0000000..f02611c
--- /dev/null
+++ b/ui/src/test/load_and_tracks.test.ts
@@ -0,0 +1,99 @@
+// 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 {test, Page} from '@playwright/test';
+import {PerfettoTestHelper} from './perfetto_ui_test_helper';
+
+test.describe.configure({mode: 'serial'});
+
+let pth: PerfettoTestHelper;
+let page: Page;
+
+test.beforeAll(async ({browser}, _testInfo) => {
+  page = await browser.newPage();
+  pth = new PerfettoTestHelper(page);
+  await pth.openTraceFile('api34_startup_cold.perfetto-trace');
+});
+
+test('load trace', async () => {
+  await pth.waitForIdleAndScreenshot('loaded.png');
+});
+
+test('info and stats', async () => {
+  await pth.navigate('#!/info');
+  await pth.waitForIdleAndScreenshot('into_and_stats.png');
+  await pth.navigate('#!/viewer');
+  await pth.waitForIdleAndScreenshot('back_to_timeline.png');
+});
+
+test('omnibox search', async () => {
+  await pth.searchSlice('composite 572441');
+  await pth.resetFocus();
+  await page.keyboard.press('f');
+  await pth.waitForPerfettoIdle();
+  await pth.waitForIdleAndScreenshot('search_slice.png');
+
+  // Click on show process details in the details panel.
+  await page.getByText('/system/bin/surfaceflinger [598]').click();
+  await page.getByText('Show process details').click();
+  await pth.waitForIdleAndScreenshot('process_details.png');
+});
+
+test('mark', async () => {
+  await page.keyboard.press('/');
+  await pth.waitForPerfettoIdle();
+
+  await page.keyboard.type('doFrame');
+  await pth.waitForPerfettoIdle();
+
+  for (let i = 0; i < 4; i++) {
+    await page.keyboard.press('Enter');
+    await pth.waitForPerfettoIdle();
+
+    if (i == 2) {
+      await page.keyboard.press('Shift+M');
+    } else {
+      await page.keyboard.press('m');
+    }
+    await pth.waitForIdleAndScreenshot(`mark_${i}.png`);
+  }
+});
+
+test('track expand and collapse', async () => {
+  const trackGroup = pth.locateTrackGroup('traced_probes 1054');
+  await trackGroup.scrollIntoViewIfNeeded();
+  await trackGroup.click();
+  await pth.waitForIdleAndScreenshot('traced_probes_expanded.png');
+
+  // Click 5 times in rapid succession.
+  for (let i = 0; i < 5; i++) {
+    await trackGroup.click();
+    await pth.waitForPerfettoIdle(50);
+  }
+  await pth.waitForIdleAndScreenshot('traced_probes_compressed.png');
+});
+
+test('pin tracks', async () => {
+  const trackGroup = pth.locateTrackGroup('traced 1055');
+  await pth.toggleTrackGroup(trackGroup);
+  let track = pth.locateTrack('traced 1055/mem.rss', trackGroup);
+  await pth.pinTrackUsingShellBtn(track);
+  await pth.waitForPerfettoIdle();
+  await pth.waitForIdleAndScreenshot('one_track_pinned.png');
+
+  track = pth.locateTrack('traced 1055/traced 1055', trackGroup);
+  await pth.pinTrackUsingShellBtn(track);
+  await pth.waitForPerfettoIdle();
+  await pth.waitForIdleAndScreenshot('two_tracks_pinned.png');
+});
diff --git a/ui/src/test/local_cache_key.test.ts b/ui/src/test/local_cache_key.test.ts
new file mode 100644
index 0000000..3816ea1
--- /dev/null
+++ b/ui/src/test/local_cache_key.test.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.
+
+import {test, expect} from '@playwright/test';
+import {PerfettoTestHelper} from './perfetto_ui_test_helper';
+
+test('multiple traces via url and local_cache_key', async ({browser}) => {
+  const page = await browser.newPage();
+  const pth = new PerfettoTestHelper(page);
+
+  // Open first trace.
+  await pth.navigate(
+    '#!/?url=http://127.0.0.1:10000/test/data/perf_sample_annotations.pftrace',
+  );
+  const cacheKey1 = page.url().match(/local_cache_key=([a-z0-9-]+)/)![1];
+  await pth.waitForIdleAndScreenshot('trace_1.png');
+
+  // Open second trace.
+  await pth.navigate(
+    '#!/?url=http://127.0.0.1:10000/test/data/atrace_compressed.ctrace',
+  );
+  const cacheKey2 = page.url().match(/local_cache_key=([a-z0-9-]+)/)![1];
+  expect(cacheKey1).not.toEqual(cacheKey2);
+  await pth.waitForIdleAndScreenshot('trace_2.png');
+
+  // Navigate back to the first trace. A confirmation dialog will be shown
+  await pth.navigate('#!/viewer?local_cache_key=' + cacheKey1);
+  await pth.waitForIdleAndScreenshot('confirmation_dialog.png');
+
+  await page.locator('button.modal-btn-primary').click();
+  await pth.waitForPerfettoIdle();
+  await pth.waitForIdleAndScreenshot('back_to_trace_1.png');
+});
diff --git a/ui/src/test/perfetto_ui_test_helper.ts b/ui/src/test/perfetto_ui_test_helper.ts
index 57debf7..20e1f82 100644
--- a/ui/src/test/perfetto_ui_test_helper.ts
+++ b/ui/src/test/perfetto_ui_test_helper.ts
@@ -12,130 +12,123 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {
+  expect,
+  Locator,
+  Page,
+  PageAssertionsToHaveScreenshotOptions,
+} from '@playwright/test';
 import fs from 'fs';
-import net from 'net';
 import path from 'path';
-import pixelmatch from 'pixelmatch';
-import {PNG} from 'pngjs';
-import {Page} from 'puppeteer';
+import {IdleDetectorWindow} from '../frontend/idle_detector_interface';
+import {assertExists} from '../base/logging';
+import {Size2D} from '../base/geom';
 
-// These constants have been hand selected by comparing the diffs of screenshots
-// between Linux on Mac. Unfortunately font-rendering is platform-specific.
-// Even though we force the same antialiasing and hinting settings, some minimal
-// differences exist.
-const DIFF_PER_PIXEL_THRESHOLD = 0.35;
-const DIFF_MAX_PIXELS = 50;
+export class PerfettoTestHelper {
+  private cachedSidebarSize?: Size2D;
 
-// Waits for the Perfetto UI to be quiescent, using a union of heuristics:
-// - Check that the progress bar is not animating.
-// - Check that the omnibox is not showing a message.
-// - Check that no redraws are pending in our RAF scheduler.
-// - Check that all the above is satisfied for |minIdleMs| consecutive ms.
-export async function waitForPerfettoIdle(page: Page, minIdleMs?: number) {
-  // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-  minIdleMs = minIdleMs || 3000;
-  const tickMs = 250;
-  const timeoutMs = 60000;
-  const minIdleTicks = Math.ceil(minIdleMs / tickMs);
-  const timeoutTicks = Math.ceil(timeoutMs / tickMs);
-  let consecutiveIdleTicks = 0;
-  let reasons: string[] = [];
-  for (let ticks = 0; ticks < timeoutTicks; ticks++) {
-    await new Promise((r) => setTimeout(r, tickMs));
-    const isShowingMsg = !!(await page.$('.omnibox.message-mode'));
-    const isShowingAnim = !!(await page.$('.progress.progress-anim'));
-    const hasPendingRedraws = Boolean(
-      await (await page.evaluateHandle('raf.hasPendingRedraws')).jsonValue(),
-    );
+  constructor(readonly page: Page) {}
 
-    if (isShowingAnim || isShowingMsg || hasPendingRedraws) {
-      consecutiveIdleTicks = 0;
-      reasons = [];
-      if (isShowingAnim) {
-        reasons.push('showing progress animation');
-      }
-      if (isShowingMsg) {
-        reasons.push('showing omnibox message');
-      }
-      if (hasPendingRedraws) {
-        reasons.push('has pending redraws');
-      }
-      continue;
+  resetFocus(): Promise<void> {
+    return this.page.click('.sidebar img.brand');
+  }
+
+  async sidebarSize(): Promise<Size2D> {
+    if (this.cachedSidebarSize === undefined) {
+      const size = await this.page.locator('main > .sidebar').boundingBox();
+      this.cachedSidebarSize = assertExists(size);
     }
-    if (++consecutiveIdleTicks >= minIdleTicks) {
-      return;
+    return this.cachedSidebarSize;
+  }
+
+  async navigate(fragment: string): Promise<void> {
+    await this.page.goto('/?testing=1' + fragment);
+    await this.waitForPerfettoIdle();
+    await this.page.click('body');
+  }
+
+  async openTraceFile(traceName: string, args?: {}): Promise<void> {
+    args = {testing: '1', ...args};
+    const qs = Object.entries(args ?? {})
+      .map(([k, v]) => `${k}=${v}`)
+      .join('&');
+    await this.page.goto('/?' + qs);
+    const file = await this.page.waitForSelector('input.trace_file', {
+      state: 'attached',
+    });
+    await this.page.evaluate(() =>
+      localStorage.setItem('dismissedPanningHint', 'true'),
+    );
+    const tracePath = this.getTestTracePath(traceName);
+    assertExists(file).setInputFiles(tracePath);
+    await this.waitForPerfettoIdle();
+    await this.page.mouse.move(0, 0);
+  }
+
+  waitForPerfettoIdle(idleHysteresisMs?: number): Promise<void> {
+    return this.page.evaluate(
+      async (ms) =>
+        (window as {} as IdleDetectorWindow).waitForPerfettoIdle(ms),
+      idleHysteresisMs,
+    );
+  }
+
+  async waitForIdleAndScreenshot(
+    screenshotName: string,
+    opts?: PageAssertionsToHaveScreenshotOptions,
+  ) {
+    await this.page.mouse.move(0, 0); // Move mouse out of the way.
+    await this.waitForPerfettoIdle();
+    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 this.waitForPerfettoIdle();
+  }
+
+  locateTrack(name: string, trackGroup?: Locator): Locator {
+    return (trackGroup ?? this.page)
+      .locator('.pf-track')
+      .filter({has: this.page.locator(`h1[ref="${name}"]`)});
+  }
+
+  pinTrackUsingShellBtn(track: Locator) {
+    track.locator('button[title="Pin to top"]').click({force: true});
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  async runCommand(cmdId: string, ...args: any[]) {
+    await this.page.evaluate(
+      (arg) => self.app.commands.runCommand(arg.cmdId, ...arg.args),
+      {cmdId, args},
+    );
+  }
+
+  async searchSlice(name: string) {
+    const omnibox = this.page.locator('input[ref=omnibox]');
+    await omnibox.focus();
+    await omnibox.fill(name);
+    await this.waitForPerfettoIdle();
+    await omnibox.press('Enter');
+    await this.waitForPerfettoIdle();
+  }
+
+  getTestTracePath(fname: string): string {
+    const parts = ['test', 'data', fname];
+    if (process.cwd().endsWith('/ui')) {
+      parts.unshift('..');
     }
+    const fPath = path.join(...parts);
+    if (!fs.existsSync(fPath)) {
+      throw new Error(`Could not locate file ${fPath}, cwd=${process.cwd()}`);
+    }
+    return fPath;
   }
-  throw new Error(
-    `waitForPerfettoIdle() failed. Did not reach idle after ${timeoutMs} ms. ` +
-      `Reasons not considered idle: ${reasons.join(', ')}`,
-  );
-}
-
-export function getTestTracePath(fname: string): string {
-  const fPath = path.join('test', 'data', fname);
-  if (!fs.existsSync(fPath)) {
-    throw new Error('Could not locate trace file ' + fPath);
-  }
-  return fPath;
-}
-
-export async function compareScreenshots(
-  reportPath: string,
-  actualFilename: string,
-  expectedFilename: string,
-) {
-  if (!fs.existsSync(expectedFilename)) {
-    throw new Error(
-      `Could not find ${expectedFilename}. Run wih REBASELINE=1.`,
-    );
-  }
-  const actualImg = PNG.sync.read(fs.readFileSync(actualFilename));
-  const expectedImg = PNG.sync.read(fs.readFileSync(expectedFilename));
-  const {width, height} = actualImg;
-  expect(width).toEqual(expectedImg.width);
-  expect(height).toEqual(expectedImg.height);
-  const diffPng = new PNG({width, height});
-  const diff = await pixelmatch(
-    actualImg.data,
-    expectedImg.data,
-    diffPng.data,
-    width,
-    height,
-    {
-      threshold: DIFF_PER_PIXEL_THRESHOLD,
-    },
-  );
-  if (diff > DIFF_MAX_PIXELS) {
-    const diffFilename = actualFilename.replace('.png', '-diff.png');
-    fs.writeFileSync(diffFilename, PNG.sync.write(diffPng));
-    fs.appendFileSync(
-      reportPath,
-      `${path.basename(actualFilename)};${path.basename(diffFilename)}\n`,
-    );
-    throw new Error(`Diff test failed ${diffFilename}, delta: ${diff} pixels`);
-  }
-  return diff;
-}
-
-// If the user has a trace_processor_shell --httpd instance open, bail out,
-// as that will invalidate the test loading different data.
-export async function failIfTraceProcessorHttpdIsActive() {
-  return new Promise<void>((resolve, reject) => {
-    const client = new net.Socket();
-    client.connect(9001, '127.0.0.1', () => {
-      const err =
-        'trace_processor_shell --httpd detected on port 9001. ' +
-        'Bailing out as it interferes with the tests. ' +
-        'Please kill that and run the test again.';
-      console.error(err);
-      client.destroy();
-      reject(err);
-    });
-    client.on('error', (e: {code: string}) => {
-      expect(e.code).toBe('ECONNREFUSED');
-      resolve();
-    });
-    client.end();
-  });
 }
diff --git a/ui/src/test/queries.test.ts b/ui/src/test/queries.test.ts
new file mode 100644
index 0000000..3882063
--- /dev/null
+++ b/ui/src/test/queries.test.ts
@@ -0,0 +1,91 @@
+// 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 {test, Page, expect} from '@playwright/test';
+import {PerfettoTestHelper} from './perfetto_ui_test_helper';
+
+test.describe.configure({mode: 'serial'});
+
+let pth: PerfettoTestHelper;
+let page: Page;
+
+test.beforeAll(async ({browser}, _testInfo) => {
+  page = await browser.newPage();
+  pth = new PerfettoTestHelper(page);
+  await pth.openTraceFile('api34_startup_cold.perfetto-trace');
+});
+
+test('omnibox query', async () => {
+  const omnibox = page.locator('input[ref=omnibox]');
+  await omnibox.focus();
+  await omnibox.fill('foo');
+  await omnibox.selectText();
+  await omnibox.press(':');
+  await pth.waitForPerfettoIdle();
+  await omnibox.fill(
+    'select id, ts, dur, name, track_id from slices limit 100',
+  );
+  await pth.waitForPerfettoIdle();
+  await omnibox.press('Enter');
+
+  await pth.waitForIdleAndScreenshot('query mode.png');
+
+  page.locator('.pf-query-table').getByText('17806091326279').click();
+  await pth.waitForIdleAndScreenshot('row 1 clicked.png');
+
+  page.locator('.pf-query-table').getByText('17806092405136').click();
+  await pth.waitForIdleAndScreenshot('row 2 clicked.png');
+
+  // Clear the omnibox
+  await omnibox.selectText();
+  for (let i = 0; i < 2; i++) {
+    await omnibox.press('Backspace');
+    await pth.waitForPerfettoIdle();
+  }
+  await pth.waitForIdleAndScreenshot('omnibox cleared.png', {
+    clip: {x: 0, y: 0, width: 1920, height: 100},
+  });
+});
+
+test('query page', async () => {
+  await pth.navigate('#!/query');
+  await pth.waitForPerfettoIdle();
+  const textbox = page.locator('.pf-editor div[role=textbox]');
+  for (let i = 1; i <= 3; i++) {
+    await textbox.focus();
+    await textbox.clear();
+    await textbox.fill(`select id, ts, dur, name from slices limit ${i}`);
+    await textbox.press('ControlOrMeta+Enter');
+    await textbox.blur();
+    await pth.waitForIdleAndScreenshot(`query limit ${i}.png`);
+  }
+
+  // Now test the query history.
+  page.locator('.query-history .history-item').nth(0).click();
+  await pth.waitForPerfettoIdle();
+  expect(await textbox.textContent()).toEqual(
+    'select id, ts, dur, name from slices limit 3',
+  );
+
+  page.locator('.query-history .history-item').nth(2).click();
+  await pth.waitForPerfettoIdle();
+  expect(await textbox.textContent()).toEqual(
+    'select id, ts, dur, name from slices limit 1',
+  );
+
+  // Double click on the 2nd one and expect the query is re-ran.
+  page.locator('.query-history .history-item').nth(1).dblclick();
+  await pth.waitForPerfettoIdle();
+  expect(await page.locator('.pf-query-table tbody tr').count()).toEqual(2);
+});
diff --git a/ui/src/test/sql_table_tab.test.ts b/ui/src/test/sql_table_tab.test.ts
new file mode 100644
index 0000000..99466e0
--- /dev/null
+++ b/ui/src/test/sql_table_tab.test.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.
+
+import {test, Page} from '@playwright/test';
+import {PerfettoTestHelper} from './perfetto_ui_test_helper';
+
+test.describe.configure({mode: 'serial'});
+
+let pth: PerfettoTestHelper;
+let page: Page;
+
+test.beforeAll(async ({browser}, _testInfo) => {
+  page = await browser.newPage();
+  pth = new PerfettoTestHelper(page);
+  await pth.openTraceFile('api34_startup_cold.perfetto-trace');
+});
+
+test('slices with same name', async () => {
+  const sliceName = 'animation';
+  await pth.searchSlice(sliceName);
+  await page
+    .locator('.details-panel-container a.pf-anchor', {hasText: sliceName})
+    .click();
+  await page
+    .locator('.pf-popup-portal button', {hasText: 'Slices with the same name'})
+    .click();
+  await pth.waitForIdleAndScreenshot(`slices-with-same-name.png`);
+});
+
+test('ShowTable command', async () => {
+  await pth.runCommand('perfetto.ShowTable.slice');
+  await pth.waitForIdleAndScreenshot(`slices-table.png`);
+});
diff --git a/ui/src/test/track_event_ordered_tracks.test.ts b/ui/src/test/track_event_ordered_tracks.test.ts
new file mode 100644
index 0000000..8f39a3a
--- /dev/null
+++ b/ui/src/test/track_event_ordered_tracks.test.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 {test, Page} from '@playwright/test';
+import {PerfettoTestHelper} from './perfetto_ui_test_helper';
+
+test.describe.configure({mode: 'serial'});
+
+let pth: PerfettoTestHelper;
+let page: Page;
+
+test.beforeAll(async ({browser}, _testInfo) => {
+  page = await browser.newPage();
+  pth = new PerfettoTestHelper(page);
+  await pth.openTraceFile('track_event_ordered.pb');
+});
+
+test('load trace', async () => {
+  await pth.waitForIdleAndScreenshot('loaded.png');
+});
+
+test('chronological order', async () => {
+  const chronologicalGrp = pth.locateTrackGroup('Root Chronological');
+  await chronologicalGrp.scrollIntoViewIfNeeded();
+  await pth.toggleTrackGroup(chronologicalGrp);
+
+  await pth.waitForIdleAndScreenshot('chronological.png');
+});
+
+test('explicit order', async () => {
+  const explicitGrp = pth.locateTrackGroup('Root Explicit');
+  await explicitGrp.scrollIntoViewIfNeeded();
+  await pth.toggleTrackGroup(explicitGrp);
+
+  await pth.waitForIdleAndScreenshot('explicit.png');
+});
+
+test('lexicographic tracks', async () => {
+  const lexicographicGrp = pth.locateTrackGroup('Root Lexicographic');
+  await lexicographicGrp.scrollIntoViewIfNeeded();
+  await pth.toggleTrackGroup(lexicographicGrp);
+
+  await pth.waitForIdleAndScreenshot('lexicographic.png');
+});
diff --git a/ui/src/test/ui_integrationtest.ts b/ui/src/test/ui_integrationtest.ts
deleted file mode 100644
index a8c622e..0000000
--- a/ui/src/test/ui_integrationtest.ts
+++ /dev/null
@@ -1,356 +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 fs from 'fs';
-import path from 'path';
-import {Browser, Page} from 'puppeteer';
-
-import {assertExists} from '../base/logging';
-
-import {
-  compareScreenshots,
-  failIfTraceProcessorHttpdIsActive,
-  getTestTracePath,
-  waitForPerfettoIdle,
-} from './perfetto_ui_test_helper';
-
-declare let global: {__BROWSER__: Browser};
-const browser = assertExists(global.__BROWSER__);
-const expectedScreenshotPath = path.join('test', 'data', 'ui-screenshots');
-const tmpDir = path.resolve('./ui-test-artifacts');
-const reportPath = path.join(tmpDir, 'report.txt');
-
-async function getPage(): Promise<Page> {
-  const pages = await browser.pages();
-  expect(pages.length).toBe(1);
-  return pages[pages.length - 1];
-}
-
-jest.setTimeout(60000);
-
-// Executed once at the beginning of the test. Navigates to the UI.
-beforeAll(async () => {
-  await failIfTraceProcessorHttpdIsActive();
-  const page = await getPage();
-  await page.setViewport({width: 1920, height: 1080});
-
-  // Empty the file with collected screenshot diffs
-  fs.writeFileSync(reportPath, '');
-});
-
-// After each test (regardless of nesting) capture a screenshot named after the
-// test('') name and compare the screenshot with the expected one in
-// /test/data/ui-screenshots.
-afterEach(async () => {
-  let testName = assertExists(expect.getState().currentTestName);
-  testName = testName.replace(/[^a-z0-9-]/gim, '_').toLowerCase();
-  const page = await getPage();
-
-  const screenshotName = `ui-${testName}.png`;
-  const actualFilename = path.join(tmpDir, screenshotName);
-  const expectedFilename = path.join(expectedScreenshotPath, screenshotName);
-  await page.screenshot({path: actualFilename});
-  const rebaseline = process.env['PERFETTO_UI_TESTS_REBASELINE'] === '1';
-  if (rebaseline) {
-    console.log('Saving reference screenshot into', expectedFilename);
-    fs.copyFileSync(actualFilename, expectedFilename);
-  } else {
-    await compareScreenshots(reportPath, actualFilename, expectedFilename);
-  }
-});
-
-describe('android_trace_30s', () => {
-  let page: Page;
-
-  beforeAll(async () => {
-    page = await getPage();
-    await page.goto('http://localhost:10000/?testing=1');
-    await waitForPerfettoIdle(page);
-  });
-
-  test('load', async () => {
-    const file = await page.waitForSelector('input.trace_file');
-    const tracePath = getTestTracePath('example_android_trace_30s.pb');
-    assertExists(file).uploadFile(tracePath);
-    await waitForPerfettoIdle(page);
-  });
-
-  test('expand_camera', async () => {
-    await page.click('.pf-overlay');
-    await page.click('h1[title="com.google.android.GoogleCamera 5506"]');
-    await page.evaluate(() => {
-      document.querySelector('.scrolling-panel-container')!.scrollTo(0, 400);
-    });
-    await waitForPerfettoIdle(page);
-  });
-});
-
-describe('chrome_rendering_desktop', () => {
-  let page: Page;
-
-  beforeAll(async () => {
-    page = await getPage();
-    await page.goto('http://localhost:10000/?testing=1');
-    await waitForPerfettoIdle(page);
-  });
-
-  test('load', async () => {
-    const page = await getPage();
-    const file = await page.waitForSelector('input.trace_file');
-    const tracePath = getTestTracePath('chrome_rendering_desktop.pftrace');
-    assertExists(file).uploadFile(tracePath);
-    await waitForPerfettoIdle(page);
-  });
-
-  test('expand_browser_proc', async () => {
-    const page = await getPage();
-    await page.click('.pf-overlay');
-    await page.click('h1[title="Browser 12685"]');
-    await waitForPerfettoIdle(page);
-  });
-
-  test('select_slice_with_flows', async () => {
-    const page = await getPage();
-    const searchInput = '.omnibox input';
-    await page.focus(searchInput);
-    await page.keyboard.type('GenerateRenderPass');
-    await waitForPerfettoIdle(page);
-    for (let i = 0; i < 3; i++) {
-      await page.keyboard.type('\n');
-    }
-    await waitForPerfettoIdle(page);
-    await page.focus('canvas');
-    await page.keyboard.type('f'); // Zoom to selection
-    await waitForPerfettoIdle(page);
-  });
-});
-
-// Tests that chrome traces with missing process/thread names still open
-// correctly in the UI.
-describe('chrome_missing_track_names', () => {
-  let page: Page;
-
-  beforeAll(async () => {
-    page = await getPage();
-    await page.goto('http://localhost:10000/?testing=1');
-    await waitForPerfettoIdle(page);
-  });
-
-  test('load', async () => {
-    const page = await getPage();
-    const file = await page.waitForSelector('input.trace_file');
-    const tracePath = getTestTracePath('chrome_missing_track_names.pb.gz');
-    assertExists(file).uploadFile(tracePath);
-    await waitForPerfettoIdle(page);
-  });
-});
-
-describe('routing', () => {
-  describe('open_two_traces_then_go_back', () => {
-    let page: Page;
-
-    beforeAll(async () => {
-      page = await getPage();
-      await page.goto('http://localhost:10000/?testing=1');
-      await waitForPerfettoIdle(page);
-    });
-
-    test('open_first_trace_from_url', async () => {
-      await page.goto(
-        'http://localhost:10000/?testing=1/#!/?url=http://localhost:10000/test/data/chrome_memory_snapshot.pftrace',
-      );
-      await waitForPerfettoIdle(page);
-    });
-
-    test('open_second_trace_from_url', async () => {
-      await page.goto(
-        'http://localhost:10000/?testing=1#!/?url=http://localhost:10000/test/data/chrome_scroll_without_vsync.pftrace',
-      );
-      await waitForPerfettoIdle(page);
-    });
-
-    test('access_subpage_then_go_back', async () => {
-      await waitForPerfettoIdle(page);
-      await page.goto(
-        'http://localhost:10000/?testing=1/#!/metrics?local_cache_key=76c25a80-25dd-1eb7-2246-d7b3c7a10f91',
-      );
-      await page.goBack();
-      await waitForPerfettoIdle(page);
-    });
-  });
-
-  describe('start_from_no_trace', () => {
-    let page: Page;
-
-    beforeAll(async () => {
-      page = await getPage();
-      await page.goto('about:blank');
-    });
-
-    test('go_to_page_with_no_trace', async () => {
-      await page.goto('http://localhost:10000/?testing=1#!/info');
-      await waitForPerfettoIdle(page);
-    });
-
-    test('open_trace ', async () => {
-      await page.goto(
-        'http://localhost:10000/?testing=1#!/viewer?local_cache_key=76c25a80-25dd-1eb7-2246-d7b3c7a10f91',
-      );
-      await waitForPerfettoIdle(page);
-    });
-
-    test('refresh', async () => {
-      await page.reload();
-      await waitForPerfettoIdle(page);
-    });
-
-    test('open_second_trace', async () => {
-      await page.goto(
-        'http://localhost:10000/?testing=1#!/viewer?local_cache_key=00000000-0000-0000-e13c-bd7db4ff646f',
-      );
-      await waitForPerfettoIdle(page);
-
-      // click on the 'Continue' button in the interstitial
-      await page.click('[id="trace_id_open"]');
-      await waitForPerfettoIdle(page);
-    });
-
-    test('go_back_to_first_trace', async () => {
-      await page.goBack();
-      await waitForPerfettoIdle(page);
-      // click on the 'Continue' button in the interstitial
-      await page.click('[id="trace_id_open"]');
-      await waitForPerfettoIdle(page);
-    });
-
-    test('open_invalid_trace', async () => {
-      await page.goto(
-        'http://localhost:10000/?testing=1#!/viewer?local_cache_key=invalid',
-      );
-      await waitForPerfettoIdle(page);
-    });
-  });
-
-  describe('navigate', () => {
-    let page: Page;
-
-    beforeAll(async () => {
-      page = await getPage();
-      await page.goto('http://localhost:10000/?testing=1');
-      await waitForPerfettoIdle(page);
-    });
-
-    test('open_trace_from_url', async () => {
-      await page.goto(
-        'http://localhost:10000/?testing=1/#!/?url=http://localhost:10000/test/data/chrome_memory_snapshot.pftrace',
-      );
-      await waitForPerfettoIdle(page);
-    });
-
-    test('navigate_back_and_forward', async () => {
-      await page.click('[id="info_and_stats"]');
-      await waitForPerfettoIdle(page);
-      await page.click('[id="metrics"]');
-      await waitForPerfettoIdle(page);
-      await page.goBack();
-      await waitForPerfettoIdle(page);
-      await page.goBack();
-      await waitForPerfettoIdle(page);
-      await page.goForward();
-      await waitForPerfettoIdle(page);
-      await page.goForward();
-      await waitForPerfettoIdle(page);
-    });
-  });
-
-  test('open_trace_and_go_back_to_landing_page', async () => {
-    const page = await getPage();
-    await page.goto('http://localhost:10000/?testing=1');
-    await page.goto(
-      'http://localhost:10000/?testing=1#!/viewer?local_cache_key=76c25a80-25dd-1eb7-2246-d7b3c7a10f91',
-    );
-    await waitForPerfettoIdle(page);
-    await page.goBack();
-    await waitForPerfettoIdle(page);
-  });
-
-  test('open_invalid_trace_from_blank_page', async () => {
-    const page = await getPage();
-    await page.goto('about:blank');
-    await page.goto(
-      'http://localhost:10000/?testing=1#!/viewer?local_cache_key=invalid',
-    );
-    await waitForPerfettoIdle(page);
-  });
-});
-
-// Regression test for b/235335853.
-describe('modal_dialog', () => {
-  let page: Page;
-
-  beforeAll(async () => {
-    page = await getPage();
-    await page.goto('http://localhost:10000/?testing=1');
-    await waitForPerfettoIdle(page);
-  });
-
-  test('show_dialog_1', async () => {
-    await page.click('#keyboard_shortcuts');
-    await waitForPerfettoIdle(page);
-  });
-
-  test('dismiss_1', async () => {
-    await page.keyboard.press('Escape');
-    await waitForPerfettoIdle(page);
-  });
-
-  test('switch_page_no_dialog', async () => {
-    await page.click('#record_new_trace');
-    await waitForPerfettoIdle(page);
-  });
-
-  test('show_dialog_2', async () => {
-    await page.click('#keyboard_shortcuts');
-    await waitForPerfettoIdle(page);
-  });
-
-  test('dismiss_2', async () => {
-    await page.keyboard.press('Escape');
-    await waitForPerfettoIdle(page);
-  });
-});
-
-describe('features', () => {
-  let page: Page;
-
-  beforeAll(async () => {
-    page = await getPage();
-    await page.goto('http://localhost:10000/?testing=1');
-    await waitForPerfettoIdle(page);
-  });
-
-  // Test that we show a (debuggable) chip next to tracks for debuggable apps.
-  // Regression test for aosp/3106008 .
-  test('track_debuggable_chip', async () => {
-    const page = await getPage();
-    await page.goto(
-      'http://localhost:10000/?testing=1/#!/?url=http://localhost:10000/test/data/api32_startup_warm.perfetto-trace',
-    );
-    await waitForPerfettoIdle(page);
-    await page.hover(
-      'h1[title="androidx.benchmark.integration.macrobenchmark.test 7527"]',
-    );
-    await waitForPerfettoIdle(page);
-  });
-});
diff --git a/ui/src/test/wattson.test.ts b/ui/src/test/wattson.test.ts
new file mode 100644
index 0000000..7f660d2
--- /dev/null
+++ b/ui/src/test/wattson.test.ts
@@ -0,0 +1,74 @@
+// 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 {test, Page} from '@playwright/test';
+import {PerfettoTestHelper} from './perfetto_ui_test_helper';
+import {assertExists} from '../base/logging';
+
+test.describe.configure({mode: 'serial'});
+
+let pth: PerfettoTestHelper;
+let page: Page;
+
+// Clip only the bottom half of the UI. When dealing with area selection, the
+// time-width of the mouse-based region (which then is showed up in the upper
+// ruler) is not 100% reproducible.
+const SCREEN_CLIP = {
+  clip: {
+    x: 230,
+    y: 500,
+    width: 1920,
+    height: 1080,
+  },
+};
+
+test.beforeAll(async ({browser}, _testInfo) => {
+  page = await browser.newPage();
+  pth = new PerfettoTestHelper(page);
+  await pth.openTraceFile('wattson_dsu_pmu.pb', {
+    enablePlugins: 'org.kernel.Wattson',
+  });
+});
+
+test('wattson aggregations', async () => {
+  const wattsonGrp = pth.locateTrackGroup('Wattson');
+  await wattsonGrp.scrollIntoViewIfNeeded();
+  await pth.toggleTrackGroup(wattsonGrp);
+  const cpuEstimate = pth.locateTrack('Wattson/Cpu0 Estimate', wattsonGrp);
+  const coords = assertExists(await cpuEstimate.boundingBox());
+  await page.keyboard.press('Escape');
+  await page.mouse.move(600, coords.y + 10);
+  await page.mouse.down();
+  await page.mouse.move(1000, coords.y + 80);
+  await page.mouse.up();
+  await pth.waitForIdleAndScreenshot('wattson-estimate-aggr.png', SCREEN_CLIP);
+  await page.keyboard.press('Escape');
+});
+
+test('sched aggregations', async () => {
+  await page.keyboard.press('Escape');
+  await page.mouse.move(600, 250);
+  await page.mouse.down();
+  await page.mouse.move(800, 350);
+  await page.mouse.up();
+  await pth.waitForPerfettoIdle();
+
+  await page.click('button[label="Wattson by thread"]');
+  await pth.waitForIdleAndScreenshot('sched-aggr-thread.png', SCREEN_CLIP);
+
+  await page.click('button[label="Wattson by process"]');
+  await pth.waitForIdleAndScreenshot('sched-aggr-process.png', SCREEN_CLIP);
+
+  await page.keyboard.press('Escape');
+});
diff --git a/ui/src/trace_processor/engine.ts b/ui/src/trace_processor/engine.ts
index ad92ca6..ccb8a03 100644
--- a/ui/src/trace_processor/engine.ts
+++ b/ui/src/trace_processor/engine.ts
@@ -22,11 +22,11 @@
   MetatraceCategories,
   QueryArgs,
   QueryResult as ProtoQueryResult,
+  RegisterSqlPackageArgs,
   ResetTraceProcessorArgs,
   TraceProcessorRpc,
   TraceProcessorRpcStream,
 } from '../protos';
-
 import {ProtoRingBuffer} from './proto_ring_buffer';
 import {
   createQueryResult,
@@ -34,20 +34,11 @@
   QueryResult,
   WritableQueryResult,
 } from './query_result';
-
 import TPM = TraceProcessorRpc.TraceProcessorMethod;
+import {exists, Result} from '../base/utils';
 
-import {Result} from '../base/utils';
-
-export interface LoadingTracker {
-  beginLoading(): void;
-  endLoading(): void;
-}
-
-export class NullLoadingTracker implements LoadingTracker {
-  beginLoading(): void {}
-  endLoading(): void {}
-}
+export type EngineMode = 'WASM' | 'HTTP_RPC';
+export type NewEngineMode = 'USE_HTTP_RPC_IF_AVAILABLE' | 'FORCE_BUILTIN_WASM';
 
 // This is used to skip the decoding of queryResult from protobufjs and deal
 // with it ourselves. See the comment below around `QueryResult.decode = ...`.
@@ -63,6 +54,9 @@
 }
 
 export interface Engine {
+  readonly mode: EngineMode;
+  readonly engineId: string;
+
   /**
    * Execute a query against the database, returning a promise that resolves
    * when the query has completed but rejected when the query fails for whatever
@@ -100,6 +94,13 @@
     metrics: string[],
     format: 'json' | 'prototext' | 'proto',
   ): Promise<string | Uint8Array>;
+
+  enableMetatrace(categories?: MetatraceCategories): void;
+  stopAndGetMetatrace(): Promise<DisableAndReadMetatraceResult>;
+
+  getProxy(tag: string): EngineProxy;
+  readonly numRequestsPending: number;
+  readonly failed: string | undefined;
 }
 
 // Abstract interface of a trace proccessor.
@@ -113,9 +114,9 @@
 // 1. Implement the abstract rpcSendRequestBytes() function, sending the
 //    proto-encoded TraceProcessorRpc requests to the TraceProcessor instance.
 // 2. Call onRpcResponseBytes() when response data is received.
-export abstract class EngineBase implements Engine {
+export abstract class EngineBase implements Engine, Disposable {
   abstract readonly id: string;
-  private loadingTracker: LoadingTracker;
+  abstract readonly mode: EngineMode;
   private txSeqId = 0;
   private rxSeqId = 0;
   private rxBuf = new ProtoRingBuffer();
@@ -126,11 +127,13 @@
   private pendingRestoreTables = new Array<Deferred<void>>();
   private pendingComputeMetrics = new Array<Deferred<string | Uint8Array>>();
   private pendingReadMetatrace?: Deferred<DisableAndReadMetatraceResult>;
+  private pendingRegisterSqlPackage?: Deferred<void>;
   private _isMetatracingEnabled = false;
+  private _numRequestsPending = 0;
+  private _failed: string | undefined = undefined;
 
-  constructor(tracker?: LoadingTracker) {
-    this.loadingTracker = tracker ? tracker : new NullLoadingTracker();
-  }
+  // TraceController sets this to raf.scheduleFullRedraw().
+  onResponseReceived?: () => void;
 
   // Called to send data to the TraceProcessor instance. This turns into a
   // postMessage() or a HTTP request, depending on the Engine implementation.
@@ -187,15 +190,16 @@
     const rpc = TraceProcessorRpc.decode(rpcMsgEncoded);
 
     if (rpc.fatalError !== undefined && rpc.fatalError.length > 0) {
-      throw new Error(`${rpc.fatalError}`);
+      this.fail(`${rpc.fatalError}`);
     }
 
     // Allow restarting sequences from zero (when reloading the browser).
     if (rpc.seq !== this.rxSeqId + 1 && this.rxSeqId !== 0 && rpc.seq !== 0) {
       // "(ERR:rpc_seq)" is intercepted by error_dialog.ts to show a more
       // graceful and actionable error.
-      throw new Error(
-        `RPC sequence id mismatch cur=${rpc.seq} last=${this.rxSeqId} (ERR:rpc_seq)`,
+      this.fail(
+        `RPC sequence id mismatch ` +
+          `cur=${rpc.seq} last=${this.rxSeqId} (ERR:rpc_seq)`,
       );
     }
 
@@ -207,7 +211,7 @@
       case TPM.TPM_APPEND_TRACE_DATA:
         const appendResult = assertExists(rpc.appendResult);
         const pendingPromise = assertExists(this.pendingParses.shift());
-        if (appendResult.error && appendResult.error.length > 0) {
+        if (exists(appendResult.error) && appendResult.error.length > 0) {
           pendingPromise.reject(appendResult.error);
         } else {
           pendingPromise.resolve();
@@ -237,7 +241,7 @@
         const pendingComputeMetric = assertExists(
           this.pendingComputeMetrics.shift(),
         );
-        if (metricRes.error && metricRes.error.length > 0) {
+        if (exists(metricRes.error) && metricRes.error.length > 0) {
           const error = new QueryError(
             `ComputeMetric() error: ${metricRes.error}`,
             {
@@ -261,6 +265,15 @@
         assertExists(this.pendingReadMetatrace).resolve(metatraceRes);
         this.pendingReadMetatrace = undefined;
         break;
+      case TPM.TPM_REGISTER_SQL_PACKAGE:
+        const registerResult = assertExists(rpc.registerSqlPackageResult);
+        const res = assertExists(this.pendingRegisterSqlPackage);
+        if (exists(registerResult.error) && registerResult.error.length > 0) {
+          res.reject(registerResult.error);
+        } else {
+          res.resolve();
+        }
+        break;
       default:
         console.log(
           'Unexpected TraceProcessor response received: ',
@@ -270,8 +283,10 @@
     } // switch(rpc.response);
 
     if (isFinalResponse) {
-      this.loadingTracker.endLoading();
+      --this._numRequestsPending;
     }
+
+    this.onResponseReceived?.();
   }
 
   // TraceProcessor methods below this point.
@@ -461,6 +476,27 @@
     return result;
   }
 
+  registerSqlPackages(p: {
+    name: string;
+    modules: {name: string; sql: string}[];
+  }): Promise<void> {
+    if (this.pendingRegisterSqlPackage) {
+      return Promise.reject(new Error('Already finalising a metatrace'));
+    }
+
+    const result = defer<void>();
+
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_REGISTER_SQL_PACKAGE;
+    const args = (rpc.registerSqlPackageArgs = new RegisterSqlPackageArgs());
+    args.packageName = p.name;
+    args.modules = p.modules;
+    args.allowOverride = true;
+    this.pendingRegisterSqlPackage = result;
+    this.rpcSendRequest(rpc);
+    return result;
+  }
+
   // Marshals the TraceProcessorRpc request arguments and sends the request
   // to the concrete Engine (Wasm or HTTP).
   private rpcSendRequest(rpc: TraceProcessorRpc) {
@@ -470,13 +506,32 @@
     const outerProto = TraceProcessorRpcStream.create();
     outerProto.msg.push(rpc);
     const buf = TraceProcessorRpcStream.encode(outerProto).finish();
-    this.loadingTracker.beginLoading();
+    ++this._numRequestsPending;
     this.rpcSendRequestBytes(buf);
   }
 
+  get engineId(): string {
+    return this.id;
+  }
+
+  get numRequestsPending(): number {
+    return this._numRequestsPending;
+  }
+
   getProxy(tag: string): EngineProxy {
     return new EngineProxy(this, tag);
   }
+
+  protected fail(reason: string) {
+    this._failed = reason;
+    throw new Error(reason);
+  }
+
+  get failed(): string | undefined {
+    return this._failed;
+  }
+
+  abstract [Symbol.dispose](): void;
 }
 
 // Lightweight engine proxy which annotates all queries with a tag
@@ -521,10 +576,34 @@
     return this.engine.computeMetric(metrics, format);
   }
 
+  enableMetatrace(categories?: MetatraceCategories): void {
+    this.engine.enableMetatrace(categories);
+  }
+
+  stopAndGetMetatrace(): Promise<DisableAndReadMetatraceResult> {
+    return this.engine.stopAndGetMetatrace();
+  }
+
   get engineId(): string {
     return this.engine.id;
   }
 
+  getProxy(tag: string): EngineProxy {
+    return this.engine.getProxy(`${this.tag}/${tag}`);
+  }
+
+  get numRequestsPending() {
+    return this.engine.numRequestsPending;
+  }
+
+  get mode() {
+    return this.engine.mode;
+  }
+
+  get failed() {
+    return this.engine.failed;
+  }
+
   [Symbol.dispose]() {
     this._isAlive = false;
   }
@@ -545,3 +624,8 @@
     });
   }
 }
+
+// A convenience interface to inject the App in Mithril components.
+export interface EngineAttrs {
+  engine: Engine;
+}
diff --git a/ui/src/trace_processor/http_rpc_engine.ts b/ui/src/trace_processor/http_rpc_engine.ts
index af573d0..c849299 100644
--- a/ui/src/trace_processor/http_rpc_engine.ts
+++ b/ui/src/trace_processor/http_rpc_engine.ts
@@ -15,7 +15,7 @@
 import {fetchWithTimeout} from '../base/http_utils';
 import {assertExists} from '../base/logging';
 import {StatusResult} from '../protos';
-import {EngineBase, LoadingTracker} from '../trace_processor/engine';
+import {EngineBase} from '../trace_processor/engine';
 
 const RPC_CONNECT_TIMEOUT_MS = 2000;
 
@@ -26,30 +26,32 @@
 }
 
 export class HttpRpcEngine extends EngineBase {
+  readonly mode = 'HTTP_RPC';
   readonly id: string;
-  errorHandler: (err: string) => void = () => {};
   private requestQueue = new Array<Uint8Array>();
   private websocket?: WebSocket;
   private connected = false;
+  private disposed = false;
 
   // Can be changed by frontend/index.ts when passing ?rpc_port=1234 .
   static rpcPort = '9001';
 
-  constructor(id: string, loadingTracker?: LoadingTracker) {
-    super(loadingTracker);
+  constructor(id: string) {
+    super();
     this.id = id;
   }
 
   rpcSendRequestBytes(data: Uint8Array): void {
     if (this.websocket === undefined) {
+      if (this.disposed) return;
       const wsUrl = `ws://${HttpRpcEngine.hostAndPort}/websocket`;
       this.websocket = new WebSocket(wsUrl);
       this.websocket.onopen = () => this.onWebsocketConnected();
       this.websocket.onmessage = (e) => this.onWebsocketMessage(e);
       this.websocket.onclose = (e) => this.onWebsocketClosed(e);
       this.websocket.onerror = (e) =>
-        this.errorHandler(
-          `WebSocket error (state=${(e.target as WebSocket)?.readyState})`,
+        super.fail(
+          `WebSocket error rs=${(e.target as WebSocket)?.readyState} (ERR:ws)`,
         );
     }
 
@@ -70,6 +72,7 @@
   }
 
   private onWebsocketClosed(e: CloseEvent) {
+    if (this.disposed) return;
     if (e.code === 1006 && this.connected) {
       // On macbooks the act of closing the lid / suspending often causes socket
       // disconnections. Try to gracefully re-connect.
@@ -78,7 +81,7 @@
       this.connected = false;
       this.rpcSendRequestBytes(new Uint8Array()); // Triggers a reconnection.
     } else {
-      this.errorHandler(`Websocket closed (${e.code}: ${e.reason})`);
+      super.fail(`Websocket closed (${e.code}: ${e.reason}) (ERR:ws)`);
     }
   }
 
@@ -120,4 +123,11 @@
   static get hostAndPort() {
     return `127.0.0.1:${HttpRpcEngine.rpcPort}`;
   }
+
+  [Symbol.dispose]() {
+    this.disposed = true;
+    const websocket = this.websocket;
+    this.websocket = undefined;
+    websocket?.close();
+  }
 }
diff --git a/ui/src/trace_processor/proto_ring_buffer_unittest.ts b/ui/src/trace_processor/proto_ring_buffer_unittest.ts
index 2491a94..9485720 100644
--- a/ui/src/trace_processor/proto_ring_buffer_unittest.ts
+++ b/ui/src/trace_processor/proto_ring_buffer_unittest.ts
@@ -13,9 +13,7 @@
 // limitations under the License.
 
 import protobuf from 'protobufjs/minimal';
-
 import {assertTrue} from '../base/logging';
-
 import {ProtoRingBuffer} from './proto_ring_buffer';
 
 let seed = 1;
diff --git a/ui/src/trace_processor/query_result.ts b/ui/src/trace_processor/query_result.ts
index d65b986..b12e06c 100644
--- a/ui/src/trace_processor/query_result.ts
+++ b/ui/src/trace_processor/query_result.ts
@@ -49,9 +49,7 @@
 
 // Ensure protobuf is initialized.
 import '../base/static_initializers';
-
 import protobuf from 'protobufjs/minimal';
-
 import {defer, Deferred} from '../base/deferred';
 import {assertExists, assertFalse, assertTrue} from '../base/logging';
 import {utf8Decode} from '../base/string_utils';
@@ -283,6 +281,9 @@
   // first result.
   firstRow<T extends Row>(spec: T): T;
 
+  // Like firstRow() but returns undefined if no rows are available.
+  maybeFirstRow<T extends Row>(spec: T): T | undefined;
+
   // If != undefined the query errored out and error() contains the message.
   error(): string | undefined;
 
@@ -408,6 +409,14 @@
     return impl as {} as RowIterator<T> as T;
   }
 
+  maybeFirstRow<T extends Row>(spec: T): T | undefined {
+    const impl = new RowIteratorImplWithRowData(spec, this);
+    if (!impl.valid()) {
+      return undefined;
+    }
+    return impl as {} as RowIterator<T> as T;
+  }
+
   // Can be called only once.
   waitAllRows(): Promise<QueryResult> {
     assertTrue(this.allRowsPromise === undefined);
@@ -939,6 +948,9 @@
   firstRow<T extends Row>(spec: T) {
     return this.impl.firstRow(spec);
   }
+  maybeFirstRow<T extends Row>(spec: T) {
+    return this.impl.maybeFirstRow(spec);
+  }
   waitAllRows() {
     return this.impl.waitAllRows();
   }
diff --git a/ui/src/trace_processor/query_result_unittest.ts b/ui/src/trace_processor/query_result_unittest.ts
index 110f89e..2cebde2 100644
--- a/ui/src/trace_processor/query_result_unittest.ts
+++ b/ui/src/trace_processor/query_result_unittest.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import {QueryResult as QueryResultProto} from '../protos';
-
 import {
   createQueryResult,
   decodeInt64Varint,
diff --git a/ui/src/trace_processor/query_utils.ts b/ui/src/trace_processor/query_utils.ts
index e4c2fba..0e01da0 100644
--- a/ui/src/trace_processor/query_utils.ts
+++ b/ui/src/trace_processor/query_utils.ts
@@ -1,18 +1,16 @@
-/*
- * 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.
- */
+// 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.
 
 enum EscapeFlag {
   CaseInsensitive = 1,
diff --git a/ui/src/trace_processor/sql_utils.ts b/ui/src/trace_processor/sql_utils.ts
index 97b5f84..b84b258 100644
--- a/ui/src/trace_processor/sql_utils.ts
+++ b/ui/src/trace_processor/sql_utils.ts
@@ -13,10 +13,8 @@
 // limitations under the License.
 
 import {SortDirection} from '../base/comparison_utils';
-
 import {isString} from '../base/object_utils';
 import {sqliteString} from '../base/string_utils';
-
 import {Engine} from './engine';
 import {NUM, SqlValue} from './query_result';
 
@@ -196,7 +194,7 @@
  *
  * @param engine - The database engine to execute the query.
  * @param viewName - The name of the view to be created.
- * @param expression - The SQL expression to define the table.
+ * @param as - The SQL expression to define the table.
  * @returns An AsyncDisposable which drops the created table when disposed.
  *
  * @example
@@ -214,9 +212,9 @@
 export async function createView(
   engine: Engine,
   viewName: string,
-  expression: string,
+  as: string,
 ): Promise<AsyncDisposable> {
-  await engine.query(`CREATE VIEW ${viewName} AS ${expression}`);
+  await engine.query(`CREATE VIEW ${viewName} AS ${as}`);
   return {
     [Symbol.asyncDispose]: async () => {
       await engine.tryQuery(`DROP VIEW IF EXISTS ${viewName}`);
diff --git a/ui/src/common/internal_layout_utils.ts b/ui/src/trace_processor/sql_utils/layout.ts
similarity index 100%
rename from ui/src/common/internal_layout_utils.ts
rename to ui/src/trace_processor/sql_utils/layout.ts
diff --git a/ui/src/trace_processor/sql_utils/process.ts b/ui/src/trace_processor/sql_utils/process.ts
index e1a7f38..30c9799 100644
--- a/ui/src/trace_processor/sql_utils/process.ts
+++ b/ui/src/trace_processor/sql_utils/process.ts
@@ -15,7 +15,6 @@
 import {Engine} from '../engine';
 import {NUM, NUM_NULL, STR_NULL} from '../query_result';
 import {fromNumNull} from '../sql_utils';
-
 import {Upid} from './core_types';
 
 // TODO(altimin): We should consider implementing some form of cache rather than querying
@@ -75,6 +74,9 @@
   return id === undefined ? name : `${name} [${id}]`;
 }
 
-export function getProcessName(info?: ProcessInfo): string | undefined {
+export function getProcessName(info?: {
+  name?: string;
+  pid?: number;
+}): string | undefined {
   return getDisplayName(info?.name, info?.pid);
 }
diff --git a/ui/src/trace_processor/sql_utils/sched.ts b/ui/src/trace_processor/sql_utils/sched.ts
new file mode 100644
index 0000000..24c6c45
--- /dev/null
+++ b/ui/src/trace_processor/sql_utils/sched.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 size 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 {duration, Time, time} from '../../base/time';
+import {Engine} from '../engine';
+import {LONG, NUM, NUM_NULL, STR_NULL} from '../query_result';
+import {constraintsToQuerySuffix, SQLConstraints} from '../sql_utils';
+import {
+  asSchedSqlId,
+  asThreadStateSqlId,
+  asUtid,
+  SchedSqlId,
+  ThreadStateSqlId,
+  Utid,
+} from './core_types';
+import {getThreadInfo, ThreadInfo} from './thread';
+import {getThreadState, getThreadStateFromConstraints} from './thread_state';
+
+// Representation of a single thread state object, corresponding to
+// a row for the |thread_slice| table.
+export interface Sched {
+  // Id into |sched| table.
+  id: SchedSqlId;
+  // Id of the corresponding entry in the |sched| table.
+  threadStateId?: ThreadStateSqlId;
+  // Timestamp of the beginning of this thread state in nanoseconds.
+  ts: time;
+  // Duration of this thread state in nanoseconds.
+  dur: duration;
+  cpu: number;
+  priority: number;
+  endState?: string;
+  thread: ThreadInfo;
+}
+
+export interface SchedWakeupInfo {
+  wakeupTs?: time;
+  wakerUtid?: Utid;
+  wakerCpu?: number;
+}
+
+// Gets a list of sched objects from Trace Processor with given
+// constraints.
+export async function getSchedFromConstraints(
+  engine: Engine,
+  constraints: SQLConstraints,
+): Promise<Sched[]> {
+  const query = await engine.query(`
+    SELECT
+      sched.id as schedSqlId,
+      (
+        SELECT id
+        FROM thread_state
+        WHERE
+          thread_state.ts = sched.ts
+          AND thread_state.utid = sched.utid
+      ) as threadStateSqlId,
+      sched.ts,
+      sched.dur,
+      sched.cpu,
+      sched.priority as priority,
+      sched.end_state as endState,
+      sched.utid
+    FROM sched
+    ${constraintsToQuerySuffix(constraints)}`);
+  const it = query.iter({
+    schedSqlId: NUM,
+    threadStateSqlId: NUM_NULL,
+    ts: LONG,
+    dur: LONG,
+    cpu: NUM,
+    priority: NUM,
+    endState: STR_NULL,
+    utid: NUM,
+  });
+
+  const result: Sched[] = [];
+
+  for (; it.valid(); it.next()) {
+    result.push({
+      id: asSchedSqlId(it.schedSqlId),
+      threadStateId: asThreadStateSqlId(it.threadStateSqlId ?? undefined),
+      ts: Time.fromRaw(it.ts),
+      dur: it.dur,
+      priority: it.priority,
+      endState: it.endState ?? undefined,
+      cpu: it.cpu ?? undefined,
+      thread: await getThreadInfo(engine, asUtid(it.utid)),
+    });
+  }
+  return result;
+}
+
+export async function getSched(
+  engine: Engine,
+  id: SchedSqlId,
+): Promise<Sched | undefined> {
+  const result = await getSchedFromConstraints(engine, {
+    filters: [`sched.id=${id}`],
+  });
+  assertTrue(result.length <= 1);
+  if (result.length === 0) {
+    return undefined;
+  }
+  return result[0];
+}
+
+// Returns the thread and time of the wakeup that resulted in this running
+// sched slice. Omits wakeups that are known to be from interrupt context,
+// since we cannot always recover the correct waker cpu with the current
+// table layout.
+export async function getSchedWakeupInfo(
+  engine: Engine,
+  sched: Sched,
+): Promise<SchedWakeupInfo | undefined> {
+  const prevRunnable = await getThreadStateFromConstraints(engine, {
+    filters: [
+      'state = "R"',
+      `ts + dur = ${sched.ts}`,
+      `utid = ${sched.thread.utid}`,
+      `(irq_context is null or irq_context = 0)`,
+    ],
+  });
+  if (prevRunnable.length === 0 || prevRunnable[0].wakerId === undefined) {
+    return undefined;
+  }
+  const waker = await getThreadState(engine, prevRunnable[0].wakerId);
+  if (waker === undefined) {
+    return undefined;
+  }
+  return {
+    wakerCpu: waker?.cpu,
+    wakerUtid: prevRunnable[0].wakerUtid,
+    wakeupTs: prevRunnable[0].ts,
+  };
+}
diff --git a/ui/src/trace_processor/sql_utils/slice.ts b/ui/src/trace_processor/sql_utils/slice.ts
index c53cb73..17efb3d 100644
--- a/ui/src/trace_processor/sql_utils/slice.ts
+++ b/ui/src/trace_processor/sql_utils/slice.ts
@@ -26,7 +26,6 @@
   Upid,
   Utid,
 } from './core_types';
-
 import {Arg, getArgs} from './args';
 import {getThreadInfo, ThreadInfo} from './thread';
 import {getProcessInfo, ProcessInfo} from './process';
diff --git a/ui/src/trace_processor/sql_utils/thread.ts b/ui/src/trace_processor/sql_utils/thread.ts
index df70bf5..76d9073 100644
--- a/ui/src/trace_processor/sql_utils/thread.ts
+++ b/ui/src/trace_processor/sql_utils/thread.ts
@@ -16,7 +16,6 @@
 import {NUM, NUM_NULL, STR_NULL} from '../query_result';
 import {fromNumNull} from '../sql_utils';
 import {ProcessInfo, getProcessInfo, getProcessName} from './process';
-
 import {Upid, Utid} from './core_types';
 
 // TODO(altimin): We should consider implementing some form of cache rather than querying
@@ -64,7 +63,10 @@
   return id === undefined ? name : `${name} [${id}]`;
 }
 
-export function getThreadName(info?: ThreadInfo): string | undefined {
+export function getThreadName(info?: {
+  name?: string;
+  tid?: number;
+}): string | undefined {
   return getDisplayName(info?.name, info?.tid);
 }
 
diff --git a/ui/src/trace_processor/sql_utils/thread_state.ts b/ui/src/trace_processor/sql_utils/thread_state.ts
index 4836ddf..cc10a5f 100644
--- a/ui/src/trace_processor/sql_utils/thread_state.ts
+++ b/ui/src/trace_processor/sql_utils/thread_state.ts
@@ -13,8 +13,6 @@
 // limitations under the License.
 
 import {duration, Time, time} from '../../base/time';
-import {exists} from '../../base/utils';
-import {translateState} from '../../common/thread_state';
 import {Engine} from '../engine';
 import {LONG, NUM, NUM_NULL, STR_NULL} from '../query_result';
 import {
@@ -22,18 +20,72 @@
   fromNumNull,
   SQLConstraints,
 } from '../sql_utils';
-
-import {globals} from '../../frontend/globals';
-import {scrollToTrackAndTs} from '../../frontend/scroll_helper';
-import {asUtid, SchedSqlId, ThreadStateSqlId} from './core_types';
-import {CPU_SLICE_TRACK_KIND} from '../../core/track_kinds';
+import {
+  asThreadStateSqlId,
+  asUtid,
+  SchedSqlId,
+  ThreadStateSqlId,
+  Utid,
+} from './core_types';
 import {getThreadInfo, ThreadInfo} from './thread';
 
-// Representation of a single thread state object, corresponding to
-// a row for the |thread_slice| table.
+const states: {[key: string]: string} = {
+  'R': 'Runnable',
+  'S': 'Sleeping',
+  'D': 'Uninterruptible Sleep',
+  'T': 'Stopped',
+  't': 'Traced',
+  'X': 'Exit (Dead)',
+  'Z': 'Exit (Zombie)',
+  'x': 'Task Dead',
+  'I': 'Idle',
+  'K': 'Wake Kill',
+  'W': 'Waking',
+  'P': 'Parked',
+  'N': 'No Load',
+  '+': '(Preempted)',
+};
+
+export function translateState(
+  state: string | undefined | null,
+  ioWait: boolean | undefined = undefined,
+) {
+  if (state === undefined) return '';
+
+  // Self describing states
+  switch (state) {
+    case 'Running':
+    case 'Initialized':
+    case 'Deferred Ready':
+    case 'Transition':
+    case 'Stand By':
+    case 'Waiting':
+      return state;
+  }
+
+  if (state === null) {
+    return 'Unknown';
+  }
+  let result = states[state[0]];
+  if (ioWait === true) {
+    result += ' (IO)';
+  } else if (ioWait === false) {
+    result += ' (non-IO)';
+  }
+  for (let i = 1; i < state.length; i++) {
+    result += state[i] === '+' ? ' ' : ' + ';
+    result += states[state[i]];
+  }
+  // state is some string we don't know how to translate.
+  if (result === undefined) return state;
+
+  return result;
+}
+
+// Single thread state slice, corresponding to a row of |thread_slice| table.
 export interface ThreadState {
   // Id into |thread_state| table.
-  threadStateSqlId: ThreadStateSqlId;
+  id: ThreadStateSqlId;
   // Id of the corresponding entry in the |sched| table.
   schedSqlId?: SchedSqlId;
   // Timestamp of the beginning of this thread state in nanoseconds.
@@ -44,10 +96,20 @@
   cpu?: number;
   // Human-readable name of this thread state.
   state: string;
+  // Kernel function where the thread has suspended.
   blockedFunction?: string;
-
+  // Description of the thread itself.
   thread?: ThreadInfo;
-  wakerThread?: ThreadInfo;
+  // Thread that was running when this thread was woken up.
+  wakerUtid?: Utid;
+  // Active thread state at the time of the wakeup.
+  wakerId?: ThreadStateSqlId;
+  // Was the wakeup from an interrupt context? It is possible for this to be
+  // unset even for runnable states, if the trace was recorded without
+  // interrupt information.
+  wakerInterruptCtx?: boolean;
+  // Kernel priority of this thread state.
+  priority?: number;
 }
 
 // Gets a list of thread state objects from Trace Processor with given
@@ -57,56 +119,63 @@
   constraints: SQLConstraints,
 ): Promise<ThreadState[]> {
   const query = await engine.query(`
-    SELECT
-      thread_state.id as threadStateSqlId,
-      (select sched.id
-        from sched
-        where sched.ts=thread_state.ts and sched.utid=thread_state.utid
-        limit 1
-       ) as schedSqlId,
-      ts,
-      thread_state.dur as dur,
-      thread_state.cpu as cpu,
-      state,
-      thread_state.blocked_function as blockedFunction,
-      io_wait as ioWait,
-      thread_state.utid as utid,
-      waker_utid as wakerUtid
-    FROM thread_state
+    WITH raw AS (
+      SELECT
+      ts.id,
+      sched.id AS sched_id,
+      ts.ts,
+      ts.dur,
+      ts.cpu,
+      ts.state,
+      ts.blocked_function,
+      ts.io_wait,
+      ts.utid,
+      ts.waker_utid,
+      ts.waker_id,
+      ts.irq_context,
+      sched.priority
+    FROM thread_state ts
+    LEFT JOIN sched USING (utid, ts)
+    )
+    SELECT * FROM raw
+
     ${constraintsToQuerySuffix(constraints)}`);
   const it = query.iter({
-    threadStateSqlId: NUM,
-    schedSqlId: NUM_NULL,
+    id: NUM,
+    sched_id: NUM_NULL,
     ts: LONG,
     dur: LONG,
     cpu: NUM_NULL,
     state: STR_NULL,
-    blockedFunction: STR_NULL,
-    ioWait: NUM_NULL,
+    blocked_function: STR_NULL,
+    io_wait: NUM_NULL,
     utid: NUM,
-    wakerUtid: NUM_NULL,
+    waker_utid: NUM_NULL,
+    waker_id: NUM_NULL,
+    irq_context: NUM_NULL,
+    priority: NUM_NULL,
   });
 
   const result: ThreadState[] = [];
 
   for (; it.valid(); it.next()) {
-    const ioWait = it.ioWait === null ? undefined : it.ioWait > 0;
-    const wakerUtid = asUtid(it.wakerUtid ?? undefined);
+    const ioWait = it.io_wait === null ? undefined : it.io_wait > 0;
 
-    // TODO(altimin): Consider fetcing thread / process info using a single
+    // TODO(altimin): Consider fetching thread / process info using a single
     // query instead of one per row.
     result.push({
-      threadStateSqlId: it.threadStateSqlId as ThreadStateSqlId,
-      schedSqlId: fromNumNull(it.schedSqlId) as SchedSqlId | undefined,
+      id: it.id as ThreadStateSqlId,
+      schedSqlId: fromNumNull(it.sched_id) as SchedSqlId | undefined,
       ts: Time.fromRaw(it.ts),
       dur: it.dur,
       cpu: fromNumNull(it.cpu),
       state: translateState(it.state ?? undefined, ioWait),
-      blockedFunction: it.blockedFunction ?? undefined,
+      blockedFunction: it.blocked_function ?? undefined,
       thread: await getThreadInfo(engine, asUtid(it.utid)),
-      wakerThread: wakerUtid
-        ? await getThreadInfo(engine, wakerUtid)
-        : undefined,
+      wakerUtid: asUtid(it.waker_id ?? undefined),
+      wakerId: asThreadStateSqlId(it.waker_id ?? undefined),
+      wakerInterruptCtx: fromNumNull(it.irq_context) as boolean | undefined,
+      priority: fromNumNull(it.priority),
     });
   }
   return result;
@@ -127,35 +196,3 @@
   }
   return result[0];
 }
-
-export function goToSchedSlice(cpu: number, id: SchedSqlId, ts: time) {
-  let trackId: string | undefined;
-  for (const track of Object.values(globals.state.tracks)) {
-    if (exists(track?.uri)) {
-      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-      if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-        if (trackInfo?.tags?.cpu === cpu) {
-          trackId = track.key;
-          break;
-        }
-      }
-    }
-  }
-  if (trackId === undefined) {
-    return;
-  }
-  globals.setLegacySelection(
-    {
-      kind: 'SCHED_SLICE',
-      id,
-      trackKey: trackId,
-    },
-    {
-      clearSearch: true,
-      pendingScrollId: undefined,
-      switchToCurrentSelectionTab: true,
-    },
-  );
-
-  scrollToTrackAndTs(trackId, ts);
-}
diff --git a/ui/src/trace_processor/wasm_engine_proxy.ts b/ui/src/trace_processor/wasm_engine_proxy.ts
index 163fac5..123cb86 100644
--- a/ui/src/trace_processor/wasm_engine_proxy.ts
+++ b/ui/src/trace_processor/wasm_engine_proxy.ts
@@ -12,49 +12,44 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {assetSrc} from '../base/assets';
 import {assertExists, assertTrue} from '../base/logging';
-import {EngineBase, LoadingTracker} from '../trace_processor/engine';
+import {EngineBase} from '../trace_processor/engine';
 
-let bundlePath: string;
 let idleWasmWorker: Worker;
-let activeWasmWorker: Worker;
 
-export function initWasm(root: string) {
-  bundlePath = root + 'engine_bundle.js';
-  idleWasmWorker = new Worker(bundlePath);
-}
-
-// This method is called trace_controller whenever a new trace is loaded.
-export function resetEngineWorker(): MessagePort {
-  const channel = new MessageChannel();
-  const port = channel.port1;
-
-  // We keep always an idle worker around, the first one is created by the
-  // main() below, so we can hide the latency of the Wasm initialization.
-  if (activeWasmWorker !== undefined) {
-    activeWasmWorker.terminate();
-  }
-
-  // Swap the active worker with the idle one and create a new idle worker
-  // for the next trace.
-  activeWasmWorker = assertExists(idleWasmWorker);
-  activeWasmWorker.postMessage(port, [port]);
-  idleWasmWorker = new Worker(bundlePath);
-  return channel.port2;
+export function initWasm() {
+  idleWasmWorker = new Worker(assetSrc('engine_bundle.js'));
 }
 
 /**
  * This implementation of Engine uses a WASM backend hosted in a separate
- * worker thread.
+ * worker thread. The entrypoint of the worker thread is engine/index.ts.
  */
-export class WasmEngineProxy extends EngineBase {
+export class WasmEngineProxy extends EngineBase implements Disposable {
+  readonly mode = 'WASM';
   readonly id: string;
   private port: MessagePort;
+  private worker: Worker;
 
-  constructor(id: string, port: MessagePort, loadingTracker?: LoadingTracker) {
-    super(loadingTracker);
+  constructor(id: string) {
+    super();
     this.id = id;
-    this.port = port;
+
+    const channel = new MessageChannel();
+    const port1 = channel.port1;
+    this.port = channel.port2;
+
+    // We keep an idle instance around to hide the latency of initializing the
+    // instance. Creating the worker (new Worker()) is ~instantaneous, but then
+    // the initialization in the worker thread (i.e. the call to
+    // `new WasmBridge()` that engine/index.ts makes) takes several seconds.
+    // Here we hide that initialization latency by always keeping an idle worker
+    // around. The latency is hidden by the fact that the user usually takes few
+    // seconds until they click on "open trace file" and pick a file.
+    this.worker = assertExists(idleWasmWorker);
+    idleWasmWorker = new Worker(assetSrc('engine_bundle.js'));
+    this.worker.postMessage(port1, [port1]);
     this.port.onmessage = this.onMessage.bind(this);
   }
 
@@ -69,4 +64,8 @@
     // TypedArray for each decode operation would be too expensive).
     this.port.postMessage(data);
   }
+
+  [Symbol.dispose]() {
+    this.worker.terminate();
+  }
 }
diff --git a/ui/src/traceconv/index.ts b/ui/src/traceconv/index.ts
index 9181920..cc721ba 100644
--- a/ui/src/traceconv/index.ts
+++ b/ui/src/traceconv/index.ts
@@ -20,10 +20,6 @@
   reportError,
 } from '../base/logging';
 import {time} from '../base/time';
-import {
-  ConversionJobName,
-  ConversionJobStatus,
-} from '../common/conversion_jobs';
 import traceconv from '../gen/traceconv';
 
 const selfWorker = self as {} as Worker;
@@ -44,12 +40,8 @@
   });
 }
 
-function updateJobStatus(name: ConversionJobName, status: ConversionJobStatus) {
-  selfWorker.postMessage({
-    kind: 'updateJobStatus',
-    name,
-    status,
-  });
+function notifyJobCompleted() {
+  selfWorker.postMessage({kind: 'jobCompleted'});
 }
 
 function downloadFile(buffer: Uint8Array, name: string) {
@@ -131,8 +123,6 @@
   format: Format,
   truncate?: 'start' | 'end',
 ): Promise<void> {
-  const jobName = format === 'json' ? 'convert_json' : 'convert_systrace';
-  updateJobStatus(jobName, ConversionJobStatus.InProgress);
   const outPath = '/trace.json';
   const args: string[] = [format];
   if (truncate !== undefined) {
@@ -145,7 +135,7 @@
     downloadFile(fsNodeToBuffer(fsNode), `trace.${format}`);
     module.FS.unlink(outPath);
   } finally {
-    updateJobStatus(jobName, ConversionJobStatus.NotRunning);
+    notifyJobCompleted();
   }
 }
 
@@ -168,8 +158,6 @@
   trace: Blob,
   truncate?: 'start' | 'end',
 ) {
-  const jobName = 'open_in_legacy';
-  updateJobStatus(jobName, ConversionJobStatus.InProgress);
   const outPath = '/trace.json';
   const args: string[] = ['json'];
   if (truncate !== undefined) {
@@ -185,7 +173,7 @@
     openTraceInLegacy(buffer);
     module.FS.unlink(outPath);
   } finally {
-    updateJobStatus(jobName, ConversionJobStatus.NotRunning);
+    notifyJobCompleted();
   }
 }
 
@@ -204,8 +192,6 @@
 }
 
 async function ConvertTraceToPprof(trace: Blob, pid: number, ts: time) {
-  const jobName = 'convert_pprof';
-  updateJobStatus(jobName, ConversionJobStatus.InProgress);
   const args = [
     'profile',
     `--pid`,
@@ -232,7 +218,7 @@
       downloadFile(fsNodeToBuffer(fileNode), fileName);
     }
   } finally {
-    updateJobStatus(jobName, ConversionJobStatus.NotRunning);
+    notifyJobCompleted();
   }
 }
 
diff --git a/ui/src/widgets/anchor.ts b/ui/src/widgets/anchor.ts
index 419601f..b1b0eff 100644
--- a/ui/src/widgets/anchor.ts
+++ b/ui/src/widgets/anchor.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {HTMLAnchorAttrs} from './common';
 
 interface AnchorAttrs extends HTMLAnchorAttrs {
@@ -28,8 +27,8 @@
     return m(
       'a.pf-anchor',
       htmlAttrs,
-      icon && m('i.material-icons', icon),
       children,
+      icon && m('i.material-icons', icon),
     );
   }
 }
diff --git a/ui/src/widgets/basic_table.ts b/ui/src/widgets/basic_table.ts
index 5231a9f..64490e5 100644
--- a/ui/src/widgets/basic_table.ts
+++ b/ui/src/widgets/basic_table.ts
@@ -13,27 +13,56 @@
 // limitations under the License.
 
 import m from 'mithril';
+import {scheduleFullRedraw} from './raf';
 
 export interface ColumnDescriptor<T> {
   readonly title: m.Children;
   render: (row: T) => m.Children;
 }
 
+// This is a class to be able to perform runtime checks on `columns` below.
+export class ReorderableColumns<T> {
+  constructor(
+    public columns: ColumnDescriptor<T>[],
+    public reorder?: (from: number, to: number) => void,
+  ) {}
+}
+
 export interface TableAttrs<T> {
   readonly data: ReadonlyArray<T>;
-  readonly columns: ReadonlyArray<ColumnDescriptor<T>>;
+  readonly columns: ReadonlyArray<ColumnDescriptor<T> | ReorderableColumns<T>>;
 }
 
 export class BasicTable<T> implements m.ClassComponent<TableAttrs<T>> {
-  private renderColumnHeader(
-    _vnode: m.Vnode<TableAttrs<T>>,
-    column: ColumnDescriptor<T>,
-  ): m.Children {
-    return m('td', column.title);
-  }
-
   view(vnode: m.Vnode<TableAttrs<T>>): m.Children {
     const attrs = vnode.attrs;
+    const columnBlocks: ColumnBlock<T>[] = getColumns(attrs);
+
+    const columns: {column: ColumnDescriptor<T>; extraClasses: string}[] = [];
+    const headers: m.Children[] = [];
+    for (const [blockIndex, block] of columnBlocks.entries()) {
+      const currentColumns = block.columns.map((column, columnIndex) => ({
+        column,
+        extraClasses:
+          columnIndex === 0 && blockIndex !== 0 ? '.has-left-border' : '',
+      }));
+      if (block.reorder === undefined) {
+        for (const {column, extraClasses} of currentColumns) {
+          headers.push(m(`td${extraClasses}`, column.title));
+        }
+      } else {
+        headers.push(
+          m(ReorderableCellGroup, {
+            cells: currentColumns.map(({column, extraClasses}) => ({
+              content: column.title,
+              extraClasses,
+            })),
+            onReorder: block.reorder,
+          }),
+        );
+      }
+      columns.push(...currentColumns);
+    }
 
     return m(
       'table.generic-table',
@@ -45,19 +74,152 @@
           'table-layout': 'auto',
         },
       },
-      m(
-        'thead',
-        m(
-          'tr.header',
-          attrs.columns.map((column) => this.renderColumnHeader(vnode, column)),
-        ),
-      ),
+      m('thead', m('tr.header', headers)),
       attrs.data.map((row) =>
         m(
           'tr',
-          attrs.columns.map((column) => m('td', column.render(row))),
+          columns.map(({column, extraClasses}) =>
+            m(`td${extraClasses}`, column.render(row)),
+          ),
         ),
       ),
     );
   }
 }
+
+type ColumnBlock<T> = {
+  columns: ColumnDescriptor<T>[];
+  reorder?: (from: number, to: number) => void;
+};
+
+function getColumns<T>(attrs: TableAttrs<T>): ColumnBlock<T>[] {
+  const result: ColumnBlock<T>[] = [];
+  let current: ColumnBlock<T> = {columns: []};
+  for (const col of attrs.columns) {
+    if (col instanceof ReorderableColumns) {
+      if (current.columns.length > 0) {
+        result.push(current);
+        current = {columns: []};
+      }
+      result.push(col);
+    } else {
+      current.columns.push(col);
+    }
+  }
+  if (current.columns.length > 0) {
+    result.push(current);
+  }
+  return result;
+}
+
+export interface ReorderableCellGroupAttrs {
+  cells: {
+    content: m.Children;
+    extraClasses: string;
+  }[];
+  onReorder: (from: number, to: number) => void;
+}
+
+const placeholderElement = document.createElement('span');
+
+// A component that renders a group of cells on the same row that can be
+// reordered between each other by using drag'n'drop.
+//
+// On completed reorder, a callback is fired.
+class ReorderableCellGroup
+  implements m.ClassComponent<ReorderableCellGroupAttrs>
+{
+  private drag?: {
+    from: number;
+    to?: number;
+  };
+
+  private getClassForIndex(index: number): string {
+    if (this.drag?.from === index) {
+      return 'dragged';
+    }
+    if (this.drag?.to === index) {
+      return 'highlight-left';
+    }
+    if (this.drag?.to === index + 1) {
+      return 'highlight-right';
+    }
+    return '';
+  }
+
+  view(vnode: m.Vnode<ReorderableCellGroupAttrs>): m.Children {
+    return vnode.attrs.cells.map((cell, index) =>
+      m(
+        `td.reorderable-cell${cell.extraClasses}`,
+        {
+          draggable: 'draggable',
+          class: this.getClassForIndex(index),
+          ondragstart: (e: DragEvent) => {
+            this.drag = {
+              from: index,
+            };
+            if (e.dataTransfer !== null) {
+              e.dataTransfer.setDragImage(placeholderElement, 0, 0);
+            }
+
+            scheduleFullRedraw();
+          },
+          ondragover: (e: DragEvent) => {
+            let target = e.target as HTMLElement;
+            if (this.drag === undefined || this.drag?.from === index) {
+              // Don't do anything when hovering on the same cell that's
+              // been dragged, or when dragging something other than the
+              // cell from the same group.
+              return;
+            }
+
+            while (
+              target.tagName.toLowerCase() !== 'td' &&
+              target.parentElement !== null
+            ) {
+              target = target.parentElement;
+            }
+
+            // When hovering over cell on the right half, the cell will be
+            // moved to the right of it, vice versa for the left side. This
+            // is done such that it's possible to put dragged cell to every
+            // possible position.
+            const offset = e.clientX - target.getBoundingClientRect().x;
+            const direction =
+              offset > target.clientWidth / 2 ? 'right' : 'left';
+            const dest = direction === 'left' ? index : index + 1;
+            const adjustedDest =
+              dest === this.drag.from || dest === this.drag.from + 1
+                ? undefined
+                : 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';
+            }
+          },
+          ondragend: () => {
+            if (
+              this.drag !== undefined &&
+              this.drag.to !== undefined &&
+              this.drag.from !== this.drag.to
+            ) {
+              vnode.attrs.onReorder(this.drag.from, this.drag.to);
+            }
+
+            this.drag = undefined;
+            scheduleFullRedraw();
+          },
+        },
+        cell.content,
+      ),
+    );
+  }
+}
diff --git a/ui/src/widgets/button.ts b/ui/src/widgets/button.ts
index 6f6a8c0..368a9f3 100644
--- a/ui/src/widgets/button.ts
+++ b/ui/src/widgets/button.ts
@@ -13,9 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {classNames} from '../base/classnames';
-
 import {HTMLAttrs, HTMLButtonAttrs, Intent, classForIntent} from './common';
 import {Icon} from './icon';
 import {Popup} from './popup';
diff --git a/ui/src/widgets/callout.ts b/ui/src/widgets/callout.ts
index 7b2ae97..a217b3d 100644
--- a/ui/src/widgets/callout.ts
+++ b/ui/src/widgets/callout.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {HTMLAttrs} from './common';
 import {Icon} from './icon';
 
diff --git a/ui/src/widgets/checkbox.ts b/ui/src/widgets/checkbox.ts
index f203622..9762ea8 100644
--- a/ui/src/widgets/checkbox.ts
+++ b/ui/src/widgets/checkbox.ts
@@ -13,9 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {classNames} from '../base/classnames';
-
 import {HTMLCheckboxAttrs} from './common';
 
 export interface CheckboxAttrs extends HTMLCheckboxAttrs {
diff --git a/ui/src/widgets/chip.ts b/ui/src/widgets/chip.ts
new file mode 100644
index 0000000..cd71d3f
--- /dev/null
+++ b/ui/src/widgets/chip.ts
@@ -0,0 +1,116 @@
+// 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 {HTMLAttrs, Intent, classForIntent} from './common';
+import {Icon} from './icon';
+import {Spinner} from './spinner';
+
+interface CommonAttrs extends HTMLAttrs {
+  // Use minimal padding, reducing the overall size of the chip by a few px.
+  // Defaults to false.
+  compact?: boolean;
+  // Optional right icon.
+  rightIcon?: string;
+  // List of space separated class names forwarded to the icon.
+  className?: string;
+  // Show loading spinner instead of icon.
+  // Defaults to false.
+  loading?: boolean;
+  // Whether to use a filled icon
+  // Defaults to false;
+  iconFilled?: boolean;
+  // Indicate chip colouring by intent.
+  // Defaults to undefined aka "None"
+  intent?: Intent;
+  // Turns the chip into a pill shape.
+  rounded?: boolean;
+}
+
+interface IconChipAttrs extends CommonAttrs {
+  // Icon chips require an icon.
+  icon: string;
+}
+
+interface LabelChipAttrs extends CommonAttrs {
+  // Label chips require a label.
+  label: string;
+  // Label chips can have an optional icon.
+  icon?: string;
+}
+
+export type ChipAttrs = LabelChipAttrs | IconChipAttrs;
+
+export class Chip implements m.ClassComponent<ChipAttrs> {
+  view({attrs}: m.CVnode<ChipAttrs>) {
+    const {
+      icon,
+      compact,
+      rightIcon,
+      className,
+      iconFilled,
+      intent = Intent.None,
+      rounded,
+      ...htmlAttrs
+    } = attrs;
+
+    const label = 'label' in attrs ? attrs.label : undefined;
+
+    const classes = classNames(
+      compact && 'pf-compact',
+      classForIntent(intent),
+      icon && !label && 'pf-icon-only',
+      className,
+      rounded && 'pf-rounded',
+    );
+
+    return m(
+      '.pf-chip',
+      {
+        ...htmlAttrs,
+        className: classes,
+      },
+      this.renderIcon(attrs),
+      rightIcon &&
+        m(Icon, {
+          className: 'pf-right-icon',
+          icon: rightIcon,
+          filled: iconFilled,
+        }),
+      label || '\u200B', // Zero width space keeps chip in-flow
+    );
+  }
+
+  private renderIcon(attrs: ChipAttrs): m.Children {
+    const {icon, iconFilled} = attrs;
+    const className = 'pf-left-icon';
+    if (attrs.loading) {
+      return m(Spinner, {className});
+    } else if (icon) {
+      return m(Icon, {className, icon, filled: iconFilled});
+    } else {
+      return undefined;
+    }
+  }
+}
+
+/**
+ * Space chips out with a little gap between each one.
+ */
+export class ChipBar implements m.ClassComponent<HTMLAttrs> {
+  view({attrs, children}: m.CVnode<HTMLAttrs>): m.Children {
+    return m('.pf-chip-bar', attrs, children);
+  }
+}
diff --git a/ui/src/widgets/copyable_link.ts b/ui/src/widgets/copyable_link.ts
new file mode 100644
index 0000000..9244107
--- /dev/null
+++ b/ui/src/widgets/copyable_link.ts
@@ -0,0 +1,46 @@
+// 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 {copyToClipboard} from '../base/clipboard';
+import {Anchor} from './anchor';
+
+interface CopyableLinkAttrs {
+  url: string;
+  text?: string; // Will use url if omitted.
+  noicon?: boolean;
+}
+
+export class CopyableLink implements m.ClassComponent<CopyableLinkAttrs> {
+  view({attrs}: m.CVnode<CopyableLinkAttrs>) {
+    const url = attrs.url;
+    return m(
+      'div',
+      m(
+        Anchor,
+        {
+          href: url,
+          title: 'Click to copy the URL into the clipboard',
+          target: '_blank',
+          icon: attrs.noicon ? undefined : 'content_copy',
+          onclick: (e: Event) => {
+            e.preventDefault();
+            copyToClipboard(url);
+          },
+        },
+        attrs.text ?? url,
+      ),
+    );
+  }
+}
diff --git a/ui/src/widgets/details_shell.ts b/ui/src/widgets/details_shell.ts
index e323d37..2ec56da 100644
--- a/ui/src/widgets/details_shell.ts
+++ b/ui/src/widgets/details_shell.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {classNames} from '../base/classnames';
 
 interface DetailsShellAttrs {
diff --git a/ui/src/widgets/editor.ts b/ui/src/widgets/editor.ts
index 576fbcb..58ea153 100644
--- a/ui/src/widgets/editor.ts
+++ b/ui/src/widgets/editor.ts
@@ -18,6 +18,9 @@
 import {keymap} from '@codemirror/view';
 import {basicSetup, EditorView} from 'codemirror';
 import m from 'mithril';
+import {assertExists} from '../base/logging';
+import {DragGestureHandler} from '../base/drag_gesture_handler';
+import {DisposableStack} from '../base/disposable_stack';
 
 export interface EditorAttrs {
   // Initial state for the editor.
@@ -37,6 +40,7 @@
 export class Editor implements m.ClassComponent<EditorAttrs> {
   private editorView?: EditorView;
   private generation?: number;
+  private trash = new DisposableStack();
 
   oncreate({dom, attrs}: m.CVnodeDOM<EditorAttrs>) {
     const keymaps = [indentWithTab];
@@ -82,6 +86,20 @@
       parent: dom,
       dispatch,
     });
+
+    // Install the drag handler for the resize bar.
+    let initialH = 0;
+    this.trash.use(
+      new DragGestureHandler(
+        assertExists(dom.querySelector('.resize-handler')) as HTMLElement,
+        /* onDrag */
+        (_x, y) => ((dom as HTMLElement).style.height = `${initialH + y}px`),
+        /* onDragStarted */
+        () => (initialH = dom.clientHeight),
+        /* onDragFinished */
+        () => {},
+      ),
+    );
   }
 
   onupdate({attrs}: m.CVnodeDOM<EditorAttrs>): void {
@@ -103,9 +121,10 @@
       this.editorView.destroy();
       this.editorView = undefined;
     }
+    this.trash.dispose();
   }
 
   view({}: m.Vnode<EditorAttrs, this>): void | m.Children {
-    return m('.pf-editor');
+    return m('.pf-editor', m('.resize-handler'));
   }
 }
diff --git a/ui/src/widgets/flamegraph.ts b/ui/src/widgets/flamegraph.ts
index 22722f6..3c831dd 100644
--- a/ui/src/widgets/flamegraph.ts
+++ b/ui/src/widgets/flamegraph.ts
@@ -13,11 +13,9 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {findRef} from '../base/dom_utils';
 import {assertExists, assertTrue} from '../base/logging';
 import {Monitor} from '../base/monitor';
-
 import {Button, ButtonBar} from './button';
 import {EmptyState} from './empty_state';
 import {Popup, PopupPosition} from './popup';
@@ -26,6 +24,7 @@
 import {Spinner} from './spinner';
 import {TagInput} from './tag_input';
 import {SegmentedButtons} from './segmented_buttons';
+import {z} from 'zod';
 
 const LABEL_FONT_STYLE = '12px Roboto';
 const NODE_HEIGHT = 20;
@@ -88,6 +87,7 @@
     readonly name: string;
     readonly selfValue: number;
     readonly cumulativeValue: number;
+    readonly parentCumulativeValue?: number;
     readonly properties: ReadonlyMap<string, string>;
     readonly xStart: number;
     readonly xEnd: number;
@@ -98,42 +98,56 @@
   readonly maxDepth: number;
 }
 
-export interface FlamegraphTopDown {
-  readonly kind: 'TOP_DOWN';
-}
+const FLAMEGRAPH_FILTER_SCHEMA = z
+  .object({
+    kind: z
+      .union([
+        z.literal('SHOW_STACK').readonly(),
+        z.literal('HIDE_STACK').readonly(),
+        z.literal('SHOW_FROM_FRAME').readonly(),
+        z.literal('HIDE_FRAME').readonly(),
+      ])
+      .readonly(),
+    filter: z.string().readonly(),
+  })
+  .readonly();
 
-export interface FlamegraphBottomUp {
-  readonly kind: 'BOTTOM_UP';
-}
+type FlamegraphFilter = z.infer<typeof FLAMEGRAPH_FILTER_SCHEMA>;
 
-export interface FlamegraphPivot {
-  readonly kind: 'PIVOT';
-  readonly pivot: string;
-}
+const FLAMEGRAPH_VIEW_SCHEMA = z
+  .discriminatedUnion('kind', [
+    z.object({kind: z.literal('TOP_DOWN').readonly()}),
+    z.object({kind: z.literal('BOTTOM_UP').readonly()}),
+    z.object({
+      kind: z.literal('PIVOT').readonly(),
+      pivot: z.string().readonly(),
+    }),
+  ])
+  .readonly();
 
-export type FlamegraphView =
-  | FlamegraphTopDown
-  | FlamegraphBottomUp
-  | FlamegraphPivot;
+export type FlamegraphView = z.infer<typeof FLAMEGRAPH_VIEW_SCHEMA>;
 
-export interface FlamegraphFilters {
-  readonly showStack: ReadonlyArray<string>;
-  readonly hideStack: ReadonlyArray<string>;
-  readonly showFromFrame: ReadonlyArray<string>;
-  readonly hideFrame: ReadonlyArray<string>;
-  readonly view: FlamegraphView;
+export const FLAMEGRAPH_STATE_SCHEMA = z
+  .object({
+    selectedMetricName: z.string().readonly(),
+    filters: z.array(FLAMEGRAPH_FILTER_SCHEMA).readonly(),
+    view: FLAMEGRAPH_VIEW_SCHEMA,
+  })
+  .readonly();
+
+export type FlamegraphState = z.infer<typeof FLAMEGRAPH_STATE_SCHEMA>;
+
+interface FlamegraphMetric {
+  readonly name: string;
+  readonly unit: string;
 }
 
 export interface FlamegraphAttrs {
-  readonly metrics: ReadonlyArray<{
-    readonly name: string;
-    readonly unit: string;
-  }>;
-  readonly selectedMetricName: string;
+  readonly metrics: ReadonlyArray<FlamegraphMetric>;
+  readonly state: FlamegraphState;
   readonly data: FlamegraphQueryData | undefined;
 
-  readonly onMetricChange: (metricName: string) => void;
-  readonly onFiltersChanged: (filters: FlamegraphFilters) => void;
+  readonly onStateChange: (filters: FlamegraphState) => void;
 }
 
 /*
@@ -151,21 +165,15 @@
  *
  * ```
  * const metrics = [...];
- * const selectedMetricName = ...;
- * const filters = ...;
- * const data = ...;
+ * let state = ...;
+ * let data = ...;
  *
  * m(Flamegraph, {
  *   metrics,
- *   selectedMetricName,
- *   onMetricChange: (metricName) => {
- *     selectedMetricName = metricName;
- *     data = undefined;
- *     fetchData();
- *   },
+ *   state,
  *   data,
- *   onFiltersChanged: (showStack, hideStack, hideFrame) => {
- *     updateFilters(showStack, hideStack, hideFrame);
+ *   onStateChange: (newState) => {
+ *     state = newState,
  *     data = undefined;
  *     fetchData();
  *   },
@@ -176,9 +184,7 @@
   private attrs: FlamegraphAttrs;
 
   private rawFilterText: string = '';
-  private rawFilters: ReadonlyArray<string> = [];
   private filterFocus: boolean = false;
-  private switchState: 'TOP_DOWN' | 'BOTTOM_UP' = 'TOP_DOWN';
 
   private dataChangeMonitor = new Monitor([() => this.attrs.data]);
   private zoomRegion?: ZoomRegion;
@@ -355,6 +361,16 @@
     this.drawCanvas(dom);
   }
 
+  static createDefaultState(
+    metrics: ReadonlyArray<FlamegraphMetric>,
+  ): FlamegraphState {
+    return {
+      selectedMetricName: metrics[0].name,
+      filters: [],
+      view: {kind: 'TOP_DOWN'},
+    };
+  }
+
   private drawCanvas(dom: Element) {
     // TODO(lalitm): consider migrating to VirtualCanvas to improve performance here.
     const canvasContainer = findRef(dom, 'canvas-container');
@@ -490,10 +506,13 @@
       m(
         Select,
         {
-          value: attrs.selectedMetricName,
+          value: attrs.state.selectedMetricName,
           onchange: (e: Event) => {
             const el = e.target as HTMLSelectElement;
-            attrs.onMetricChange(el.value);
+            attrs.onStateChange({
+              ...self.attrs.state,
+              selectedMetricName: el.value,
+            });
             scheduleFullRedraw();
           },
         },
@@ -505,30 +524,31 @@
         Popup,
         {
           trigger: m(TagInput, {
-            tags: this.rawFilters,
+            tags: toTags(self.attrs.state),
             value: this.rawFilterText,
             onChange: (value: string) => {
               self.rawFilterText = value;
               scheduleFullRedraw();
             },
             onTagAdd: (tag: string) => {
-              self.rawFilters = addFilter(
-                self.rawFilters,
-                normalizeFilter(tag),
-              );
               self.rawFilterText = '';
-              self.attrs.onFiltersChanged(
-                computeFilters(self.switchState, self.rawFilters),
-              );
+              self.attrs.onStateChange(updateState(self.attrs.state, tag));
               scheduleFullRedraw();
             },
             onTagRemove(index: number) {
-              const filters = Array.from(self.rawFilters);
-              filters.splice(index, 1);
-              self.rawFilters = filters;
-              self.attrs.onFiltersChanged(
-                computeFilters(self.switchState, self.rawFilters),
-              );
+              if (index === self.attrs.state.filters.length) {
+                self.attrs.onStateChange({
+                  ...self.attrs.state,
+                  view: {kind: 'TOP_DOWN'},
+                });
+              } else {
+                const filters = Array.from(self.attrs.state.filters);
+                filters.splice(index, 1);
+                self.attrs.onStateChange({
+                  ...self.attrs.state,
+                  filters,
+                });
+              }
               scheduleFullRedraw();
             },
             onfocus() {
@@ -546,15 +566,15 @@
       ),
       m(SegmentedButtons, {
         options: [{label: 'Top Down'}, {label: 'Bottom Up'}],
-        selectedOption: this.switchState === 'TOP_DOWN' ? 0 : 1,
+        selectedOption: this.attrs.state.view.kind === 'TOP_DOWN' ? 0 : 1,
         onOptionSelected: (num) => {
-          this.switchState = num === 0 ? 'TOP_DOWN' : 'BOTTOM_UP';
-          self.attrs.onFiltersChanged(
-            computeFilters(self.switchState, self.rawFilters),
-          );
+          self.attrs.onStateChange({
+            ...this.attrs.state,
+            view: {kind: num === 0 ? 'TOP_DOWN' : 'BOTTOM_UP'},
+          });
           scheduleFullRedraw();
         },
-        disabled: hasPivot(this.rawFilters),
+        disabled: this.attrs.state.view.kind === 'PIVOT',
       }),
     );
   }
@@ -591,32 +611,57 @@
       );
     }
     const {queryIdx} = node.source;
-    const {name, cumulativeValue, selfValue, properties} = nodes[queryIdx];
-    const filterButtonClick = (filter: string) => {
-      this.rawFilters = addFilter(this.rawFilters, filter);
-      this.attrs.onFiltersChanged(
-        computeFilters(this.switchState, this.rawFilters),
-      );
+    const {
+      name,
+      cumulativeValue,
+      selfValue,
+      parentCumulativeValue,
+      properties,
+    } = nodes[queryIdx];
+    const filterButtonClick = (state: FlamegraphState) => {
+      this.attrs.onStateChange(state);
       this.tooltipPos = undefined;
       scheduleFullRedraw();
     };
+
     const percent = displayPercentage(
       cumulativeValue,
       unfilteredCumulativeValue,
     );
     const selfPercent = displayPercentage(selfValue, unfilteredCumulativeValue);
+
+    let percentText = `all: ${percent}`;
+    let selfPercentText = `all: ${selfPercent}`;
+    if (parentCumulativeValue !== undefined) {
+      const parentPercent = displayPercentage(
+        cumulativeValue,
+        parentCumulativeValue,
+      );
+      percentText += `, parent: ${parentPercent}`;
+      const parentSelfPercent = displayPercentage(
+        selfValue,
+        parentCumulativeValue,
+      );
+      selfPercentText += `, parent: ${parentSelfPercent}`;
+    }
     return m(
       'div',
       m('.tooltip-bold-text', name),
       m(
         '.tooltip-text-line',
         m('.tooltip-bold-text', 'Cumulative:'),
-        m('.tooltip-text', `${displaySize(cumulativeValue, unit)}, ${percent}`),
+        m(
+          '.tooltip-text',
+          `${displaySize(cumulativeValue, unit)} (${percentText})`,
+        ),
       ),
       m(
         '.tooltip-text-line',
         m('.tooltip-bold-text', 'Self:'),
-        m('.tooltip-text', `${displaySize(selfValue, unit)}, ${selfPercent}`),
+        m(
+          '.tooltip-text',
+          `${displaySize(selfValue, unit)} (${selfPercentText})`,
+        ),
       ),
       Array.from(properties, ([key, value]) => {
         return m(
@@ -638,31 +683,54 @@
         m(Button, {
           label: 'Show Stack',
           onclick: () => {
-            filterButtonClick(`Show Stack: ^${name}$`);
+            filterButtonClick(
+              addFilter(this.attrs.state, {
+                kind: 'SHOW_STACK',
+                filter: `^${name}$`,
+              }),
+            );
           },
         }),
         m(Button, {
           label: 'Hide Stack',
           onclick: () => {
-            filterButtonClick(`Hide Stack: ^${name}$`);
+            filterButtonClick(
+              addFilter(this.attrs.state, {
+                kind: 'HIDE_STACK',
+                filter: `^${name}$`,
+              }),
+            );
           },
         }),
         m(Button, {
           label: 'Hide Frame',
           onclick: () => {
-            filterButtonClick(`Hide Frame: ^${name}$`);
+            filterButtonClick(
+              addFilter(this.attrs.state, {
+                kind: 'HIDE_FRAME',
+                filter: `^${name}$`,
+              }),
+            );
           },
         }),
         m(Button, {
           label: 'Show From Frame',
           onclick: () => {
-            filterButtonClick(`Show From Frame: ^${name}$`);
+            filterButtonClick(
+              addFilter(this.attrs.state, {
+                kind: 'SHOW_FROM_FRAME',
+                filter: `^${name}$`,
+              }),
+            );
           },
         }),
         m(Button, {
           label: 'Pivot',
           onclick: () => {
-            filterButtonClick(`Pivot: ^${name}$`);
+            filterButtonClick({
+              ...this.attrs.state,
+              view: {kind: 'PIVOT', pivot: name},
+            });
           },
         }),
       ),
@@ -671,7 +739,7 @@
 
   private get selectedMetric() {
     return this.attrs.metrics.find(
-      (x) => x.name === this.attrs.selectedMetricName,
+      (x) => x.name === this.attrs.state.selectedMetricName,
     );
   }
 }
@@ -868,73 +936,67 @@
   return `${((size / totalSize) * 100.0).toFixed(2)}%`;
 }
 
-function normalizeFilter(filter: string): string {
+function updateState(state: FlamegraphState, filter: string): FlamegraphState {
   const lwr = filter.toLowerCase();
   if (lwr.startsWith('ss: ') || lwr.startsWith('show stack: ')) {
-    return 'Show Stack: ' + filter.split(': ', 2)[1];
+    return addFilter(state, {
+      kind: 'SHOW_STACK',
+      filter: filter.split(': ', 2)[1],
+    });
   } else if (lwr.startsWith('hs: ') || lwr.startsWith('hide stack: ')) {
-    return 'Hide Stack: ' + filter.split(': ', 2)[1];
+    return addFilter(state, {
+      kind: 'HIDE_STACK',
+      filter: filter.split(': ', 2)[1],
+    });
   } else if (lwr.startsWith('sff: ') || lwr.startsWith('show from frame: ')) {
-    return 'Show From Frame: ' + filter.split(': ', 2)[1];
+    return addFilter(state, {
+      kind: 'SHOW_FROM_FRAME',
+      filter: filter.split(': ', 2)[1],
+    });
   } else if (lwr.startsWith('hf: ') || lwr.startsWith('hide frame: ')) {
-    return 'Hide Frame: ' + filter.split(': ', 2)[1];
+    return addFilter(state, {
+      kind: 'HIDE_FRAME',
+      filter: filter.split(': ', 2)[1],
+    });
   } else if (lwr.startsWith('p:') || lwr.startsWith('pivot: ')) {
-    return 'Pivot: ' + filter.split(': ', 2)[1];
+    return {
+      ...state,
+      view: {kind: 'PIVOT', pivot: filter.split(': ', 2)[1]},
+    };
   }
-  return 'Show Stack: ' + filter;
+  return addFilter(state, {
+    kind: 'SHOW_STACK',
+    filter: filter,
+  });
 }
 
-function addFilter(filters: ReadonlyArray<string>, filter: string): string[] {
-  if (filter.startsWith('Pivot: ')) {
-    return [...filters.filter((x) => !x.startsWith('Pivot: ')), filter];
-  }
-  return [...filters, filter];
-}
-
-function computeFilters(
-  switchState: 'TOP_DOWN' | 'BOTTOM_UP',
-  rawFilters: readonly string[],
-): FlamegraphFilters {
-  const showStack = rawFilters
-    .filter((x) => x.startsWith('Show Stack: '))
-    .map((x) => x.split(': ', 2)[1]);
-  assertTrue(
-    showStack.length < 32,
-    'More than 32 show stack filters is not supported',
-  );
-
-  const showFromFrame = rawFilters
-    .filter((x) => x.startsWith('Show From Frame: '))
-    .map((x) => x.split(': ', 2)[1]);
-  assertTrue(
-    showFromFrame.length < 32,
-    'More than 32 show from frame filters is not supported',
-  );
-
-  const pivot = rawFilters.filter((x) => x.startsWith('Pivot: '));
-  assertTrue(pivot.length <= 1, 'Only one pivot can be active');
-
-  const view: FlamegraphView =
-    pivot.length === 0
-      ? {kind: switchState}
-      : {kind: 'PIVOT', pivot: pivot[0].split(': ', 2)[1]};
-  return {
-    showStack,
-    hideStack: rawFilters
-      .filter((x) => x.startsWith('Hide Stack: '))
-      .map((x) => x.split(': ', 2)[1]),
-    showFromFrame,
-    hideFrame: rawFilters
-      .filter((x) => x.startsWith('Hide Frame: '))
-      .map((x) => x.split(': ', 2)[1]),
-    view,
+function toTags(state: FlamegraphState): ReadonlyArray<string> {
+  const toString = (x: FlamegraphFilter) => {
+    switch (x.kind) {
+      case 'HIDE_FRAME':
+        return 'Hide Frame: ' + x.filter;
+      case 'HIDE_STACK':
+        return 'Hide Stack: ' + x.filter;
+      case 'SHOW_FROM_FRAME':
+        return 'Show From Frame: ' + x.filter;
+      case 'SHOW_STACK':
+        return 'Show Stack: ' + x.filter;
+    }
   };
+  const filters = state.filters.map((x) => toString(x));
+  return filters.concat(
+    state.view.kind === 'PIVOT' ? ['Pivot: ' + state.view.pivot] : [],
+  );
 }
 
-function hasPivot(rawFilters: readonly string[]) {
-  const pivot = rawFilters.filter((x) => x.startsWith('Pivot: '));
-  assertTrue(pivot.length <= 1, 'Only one pivot can be active');
-  return pivot.length === 1;
+function addFilter(
+  state: FlamegraphState,
+  filter: FlamegraphFilter,
+): FlamegraphState {
+  return {
+    ...state,
+    filters: state.filters.concat([filter]),
+  };
 }
 
 function generateColor(name: string, greyed: boolean, hovered: boolean) {
diff --git a/ui/src/widgets/form.ts b/ui/src/widgets/form.ts
index 8105f6d..8ac927a 100644
--- a/ui/src/widgets/form.ts
+++ b/ui/src/widgets/form.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {Button} from './button';
 import {HTMLAttrs, HTMLLabelAttrs} from './common';
 import {Popup} from './popup';
diff --git a/ui/src/widgets/hotkey_context.ts b/ui/src/widgets/hotkey_context.ts
index 8b21211..f4d702a 100644
--- a/ui/src/widgets/hotkey_context.ts
+++ b/ui/src/widgets/hotkey_context.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {checkHotkey, Hotkey} from '../base/hotkeys';
 
 export interface HotkeyConfig {
diff --git a/ui/src/widgets/hotkey_glyphs.ts b/ui/src/widgets/hotkey_glyphs.ts
index 90ac6d4..9d7f14b8 100644
--- a/ui/src/widgets/hotkey_glyphs.ts
+++ b/ui/src/widgets/hotkey_glyphs.ts
@@ -13,9 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {getPlatform, Hotkey, Key, parseHotkey, Platform} from '../base/hotkeys';
-
 import {Icon} from './icon';
 
 export interface HotkeyGlyphsAttrs {
diff --git a/ui/src/widgets/icon.ts b/ui/src/widgets/icon.ts
index 4f166bb..201ffe4 100644
--- a/ui/src/widgets/icon.ts
+++ b/ui/src/widgets/icon.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {HTMLAttrs} from './common';
+import {classNames} from '../base/classnames';
 
 export interface IconAttrs extends HTMLAttrs {
   // The material icon name.
@@ -26,10 +26,13 @@
 
 export class Icon implements m.ClassComponent<IconAttrs> {
   view({attrs}: m.Vnode<IconAttrs>): m.Child {
-    const {icon, filled, ...htmlAttrs} = attrs;
+    const {icon, filled, className, ...htmlAttrs} = attrs;
     return m(
-      filled ? 'i.material-icons-filled' : 'i.material-icons',
-      htmlAttrs,
+      'i.pf-icon',
+      {
+        ...htmlAttrs,
+        className: classNames(className, filled && 'pf-filled'),
+      },
       icon,
     );
   }
diff --git a/ui/src/widgets/menu.ts b/ui/src/widgets/menu.ts
index 9c53c49..20c91ff 100644
--- a/ui/src/widgets/menu.ts
+++ b/ui/src/widgets/menu.ts
@@ -13,10 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {classNames} from '../base/classnames';
 import {hasChildren} from '../base/mithril_utils';
-
 import {HTMLAttrs} from './common';
 import {Icon} from './icon';
 import {Popup, PopupAttrs, PopupPosition} from './popup';
@@ -120,9 +118,9 @@
 // A siple container for a menu.
 // The menu contents are passed in as children, and are typically MenuItems or
 // MenuDividers, but really they can be any Mithril component.
-export class Menu implements m.ClassComponent {
-  view({children}: m.CVnode) {
-    return m('.pf-menu', children);
+export class Menu implements m.ClassComponent<HTMLAttrs> {
+  view({attrs, children}: m.CVnode<HTMLAttrs>) {
+    return m('.pf-menu', attrs, children);
   }
 }
 
diff --git a/ui/src/widgets/middle_ellipsis.ts b/ui/src/widgets/middle_ellipsis.ts
new file mode 100644
index 0000000..8c9a423
--- /dev/null
+++ b/ui/src/widgets/middle_ellipsis.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 m from 'mithril';
+
+export interface MiddleEllipsisAttrs {
+  text: string;
+  endChars?: number;
+}
+
+function replaceLeadingTrailingSpacesWithNbsp(text: string) {
+  return text.replace(/^\s+|\s+$/g, function (match) {
+    return '\u00A0'.repeat(match.length);
+  });
+}
+
+/**
+ * Puts ellipsis in the middle of a long string, rather than putting them at
+ * either end, for occasions where the start and end of the text are more
+ * important than the middle.
+ */
+export class MiddleEllipsis implements m.ClassComponent<MiddleEllipsisAttrs> {
+  view({attrs, children}: m.Vnode<MiddleEllipsisAttrs>): m.Children {
+    const {text, endChars = text.length > 16 ? 10 : 0} = attrs;
+    const trimmed = text.trim();
+    const index = trimmed.length - endChars;
+    const left = trimmed.substring(0, index);
+    const right = trimmed.substring(index);
+    return m(
+      '.pf-middle-ellipsis',
+      m(
+        'span.pf-middle-ellipsis-left',
+        replaceLeadingTrailingSpacesWithNbsp(left),
+      ),
+      m(
+        'span.pf-middle-ellipsis-right',
+        replaceLeadingTrailingSpacesWithNbsp(right),
+      ),
+      children,
+    );
+  }
+}
diff --git a/ui/src/widgets/modal.ts b/ui/src/widgets/modal.ts
index 40e6c3b..e32f329 100644
--- a/ui/src/widgets/modal.ts
+++ b/ui/src/widgets/modal.ts
@@ -13,10 +13,9 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {defer} from '../base/deferred';
-
 import {scheduleFullRedraw} from './raf';
+import {Icon} from './icon';
 
 // 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
@@ -156,7 +155,7 @@
           m(
             'button[aria-label=Close Modal]',
             {onclick: () => closeModal(attrs.key)},
-            m.trust('&#x2715'),
+            m(Icon, {icon: 'close'}),
           ),
         ),
         m('main', vnode.children),
diff --git a/ui/src/widgets/multiselect.ts b/ui/src/widgets/multiselect.ts
index 9756dc7..a91c8ee 100644
--- a/ui/src/widgets/multiselect.ts
+++ b/ui/src/widgets/multiselect.ts
@@ -13,9 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {Icons} from '../base/semantic_icons';
-
 import {Button} from './button';
 import {Checkbox} from './checkbox';
 import {EmptyState} from './empty_state';
diff --git a/ui/src/widgets/popup_menu.ts b/ui/src/widgets/popup_menu.ts
new file mode 100644
index 0000000..737815c
--- /dev/null
+++ b/ui/src/widgets/popup_menu.ts
@@ -0,0 +1,198 @@
+// 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 {SortDirection} from '../base/comparison_utils';
+import {scheduleFullRedraw} from './raf';
+
+export interface RegularPopupMenuItem {
+  itemType: 'regular';
+  // Display text
+  text: string;
+  // Action on menu item click
+  callback: () => void;
+}
+
+// Helper function for simplifying defining menus.
+export function menuItem(
+  text: string,
+  action: () => void,
+): RegularPopupMenuItem {
+  return {
+    itemType: 'regular',
+    text,
+    callback: action,
+  };
+}
+
+export interface GroupPopupMenuItem {
+  itemType: 'group';
+  text: string;
+  itemId: string;
+  children: PopupMenuItem[];
+}
+
+export type PopupMenuItem = RegularPopupMenuItem | GroupPopupMenuItem;
+
+export interface PopupMenuButtonAttrs {
+  // Icon for button opening a menu
+  icon: string;
+  // List of popup menu items
+  items: PopupMenuItem[];
+}
+
+// To ensure having at most one popup menu on the screen at a time, we need to
+// listen to click events on the whole page and close currently opened popup, if
+// there's any. This class, used as a singleton, does exactly that.
+class PopupHolder {
+  // Invariant: global listener should be register if and only if this.popup is
+  // not undefined.
+  popup: PopupMenuButton | undefined = undefined;
+  initialized = false;
+  listener: (e: MouseEvent) => void;
+
+  constructor() {
+    this.listener = (e: MouseEvent) => {
+      // Only handle those events that are not part of dropdown menu themselves.
+      const hasDropdown =
+        e.composedPath().find(PopupHolder.isDropdownElement) !== undefined;
+      if (!hasDropdown) {
+        this.ensureHidden();
+      }
+    };
+  }
+
+  static isDropdownElement(target: EventTarget) {
+    if (target instanceof HTMLElement) {
+      return target.tagName === 'DIV' && target.classList.contains('dropdown');
+    }
+    return false;
+  }
+
+  ensureHidden() {
+    if (this.popup !== undefined) {
+      this.popup.setVisible(false);
+    }
+  }
+
+  clear() {
+    if (this.popup !== undefined) {
+      this.popup = undefined;
+      window.removeEventListener('click', this.listener);
+    }
+  }
+
+  showPopup(popup: PopupMenuButton) {
+    this.ensureHidden();
+    this.popup = popup;
+    window.addEventListener('click', this.listener);
+  }
+}
+
+// Singleton instance of PopupHolder
+const popupHolder = new PopupHolder();
+
+// For a table column that can be sorted; the standard popup icon should
+// reflect the current sorting direction. This function returns an icon
+// corresponding to optional SortDirection according to which the column is
+// sorted. (Optional because column might be unsorted)
+export function popupMenuIcon(sortDirection?: SortDirection) {
+  switch (sortDirection) {
+    case undefined:
+      return 'more_horiz';
+    case 'DESC':
+      return 'arrow_drop_down';
+    case 'ASC':
+      return 'arrow_drop_up';
+  }
+}
+
+// Component that displays a button that shows a popup menu on click.
+export class PopupMenuButton implements m.ClassComponent<PopupMenuButtonAttrs> {
+  popupShown = false;
+  expandedGroups: Set<string> = new Set();
+
+  setVisible(visible: boolean) {
+    this.popupShown = visible;
+    if (this.popupShown) {
+      popupHolder.showPopup(this);
+    } else {
+      popupHolder.clear();
+    }
+    scheduleFullRedraw();
+  }
+
+  renderItem(item: PopupMenuItem): m.Child {
+    switch (item.itemType) {
+      case 'regular':
+        return m(
+          'button.open-menu',
+          {
+            onclick: () => {
+              item.callback();
+              // Hide the menu item after the action has been invoked
+              this.setVisible(false);
+            },
+          },
+          item.text,
+        );
+      case 'group':
+        const isExpanded = this.expandedGroups.has(item.itemId);
+        return m(
+          'div',
+          m(
+            'button.open-menu.disallow-selection',
+            {
+              onclick: () => {
+                if (this.expandedGroups.has(item.itemId)) {
+                  this.expandedGroups.delete(item.itemId);
+                } else {
+                  this.expandedGroups.add(item.itemId);
+                }
+                scheduleFullRedraw();
+              },
+            },
+            // Show text with up/down arrow, depending on expanded state.
+            item.text + (isExpanded ? ' \u25B2' : ' \u25BC'),
+          ),
+          isExpanded
+            ? m(
+                'div.nested-menu',
+                item.children.map((item) => this.renderItem(item)),
+              )
+            : null,
+        );
+    }
+  }
+
+  view(vnode: m.Vnode<PopupMenuButtonAttrs, this>) {
+    return m(
+      '.dropdown',
+      m(
+        '.dropdown-button',
+        {
+          onclick: () => {
+            this.setVisible(!this.popupShown);
+          },
+        },
+        vnode.children,
+        m('i.material-icons', vnode.attrs.icon),
+      ),
+      m(
+        this.popupShown ? '.popup-menu.opened' : '.popup-menu.closed',
+        vnode.attrs.items.map((item) => this.renderItem(item)),
+      ),
+    );
+  }
+}
diff --git a/ui/src/widgets/section.ts b/ui/src/widgets/section.ts
index 2cd1d81..06d4f48 100644
--- a/ui/src/widgets/section.ts
+++ b/ui/src/widgets/section.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {HTMLAttrs} from './common';
 
 export interface SectionAttrs extends HTMLAttrs {
diff --git a/ui/src/widgets/segmented_buttons.ts b/ui/src/widgets/segmented_buttons.ts
index 9e8c533..27292ad 100644
--- a/ui/src/widgets/segmented_buttons.ts
+++ b/ui/src/widgets/segmented_buttons.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {Button} from './button';
 import {HTMLAttrs} from './common';
 
diff --git a/ui/src/widgets/select.ts b/ui/src/widgets/select.ts
index 7d59a79..c2cd3b7 100644
--- a/ui/src/widgets/select.ts
+++ b/ui/src/widgets/select.ts
@@ -13,87 +13,10 @@
 // limitations under the License.
 
 import m from 'mithril';
-
-import {exists} from '../base/utils';
-
 import {HTMLInputAttrs} from './common';
-import {Menu, MenuItem} from './menu';
-import {scheduleFullRedraw} from './raf';
-import {TextInput} from './text_input';
 
 export class Select implements m.ClassComponent<HTMLInputAttrs> {
   view({attrs, children}: m.CVnode<HTMLInputAttrs>) {
     return m('select.pf-select', attrs, children);
   }
 }
-
-export interface FilterableSelectAttrs {
-  // Whether to show a search box. Defaults to false.
-  filterable?: boolean;
-  // The values to show in the select.
-  values: string[];
-  // Called when the user selects an option.
-  onSelected: (value: string) => void;
-  // If set, only the first maxDisplayedItems will be shown.
-  maxDisplayedItems?: number;
-  // Whether the input field should be focused when the widget is created.
-  autofocusInput?: boolean;
-}
-
-// A select widget with a search box, allowing the user to filter the options.
-export class FilterableSelect
-  implements m.ClassComponent<FilterableSelectAttrs>
-{
-  searchText = '';
-
-  view({attrs}: m.CVnode<FilterableSelectAttrs>) {
-    const filteredValues = attrs.values.filter((name) => {
-      return name.toLowerCase().includes(this.searchText.toLowerCase());
-    });
-
-    const displayedValues =
-      attrs.maxDisplayedItems === undefined
-        ? filteredValues
-        : filteredValues.slice(0, attrs.maxDisplayedItems);
-
-    const extraItems =
-      exists(attrs.maxDisplayedItems) &&
-      Math.max(0, filteredValues.length - attrs.maxDisplayedItems);
-
-    // TODO(altimin): when the user presses enter and there is only one item,
-    // select the first one.
-    // MAYBE(altimin): when the user presses enter and there are multiple items,
-    // select the first one.
-    return m(
-      'div',
-      m(
-        '.pf-search-bar',
-        m(TextInput, {
-          oninput: (event: Event) => {
-            const eventTarget = event.target as HTMLTextAreaElement;
-            this.searchText = eventTarget.value;
-            scheduleFullRedraw();
-          },
-          onload: (event: Event) => {
-            if (!attrs.autofocusInput) return;
-            const eventTarget = event.target as HTMLTextAreaElement;
-            eventTarget.focus();
-          },
-          value: this.searchText,
-          placeholder: 'Filter...',
-          className: 'pf-search-box',
-        }),
-        m(
-          Menu,
-          ...displayedValues.map((value) =>
-            m(MenuItem, {
-              label: value,
-              onclick: () => attrs.onSelected(value),
-            }),
-          ),
-          Boolean(extraItems) && m('i', `+${extraItems} more`),
-        ),
-      ),
-    );
-  }
-}
diff --git a/ui/src/widgets/spinner.ts b/ui/src/widgets/spinner.ts
index 4c5e6d2..766ad32 100644
--- a/ui/src/widgets/spinner.ts
+++ b/ui/src/widgets/spinner.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {classNames} from '../base/classnames';
 
 interface SpinnerAttrs {
diff --git a/ui/src/widgets/sql_ref.ts b/ui/src/widgets/sql_ref.ts
index 1d00789..9da1353 100644
--- a/ui/src/widgets/sql_ref.ts
+++ b/ui/src/widgets/sql_ref.ts
@@ -13,10 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {copyToClipboard} from '../base/clipboard';
 import {Icons} from '../base/semantic_icons';
-
 import {Anchor} from './anchor';
 import {MenuItem, PopupMenu2} from './menu';
 
diff --git a/ui/src/widgets/switch.ts b/ui/src/widgets/switch.ts
index 62f735f..a40ac9e 100644
--- a/ui/src/widgets/switch.ts
+++ b/ui/src/widgets/switch.ts
@@ -13,9 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {classNames} from '../base/classnames';
-
 import {HTMLCheckboxAttrs} from './common';
 
 export interface SwitchAttrs extends HTMLCheckboxAttrs {
diff --git a/ui/src/widgets/table.ts b/ui/src/widgets/table.ts
new file mode 100644
index 0000000..ee195d0
--- /dev/null
+++ b/ui/src/widgets/table.ts
@@ -0,0 +1,277 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use size 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 {allUnique, range} from '../base/array_utils';
+import {
+  compareUniversal,
+  comparingBy,
+  ComparisonFn,
+  SortableValue,
+  SortDirection,
+  withDirection,
+} from '../base/comparison_utils';
+import {
+  menuItem,
+  PopupMenuButton,
+  popupMenuIcon,
+  PopupMenuItem,
+} from './popup_menu';
+import {scheduleFullRedraw} from './raf';
+
+export interface ColumnDescriptorAttrs<T> {
+  // Context menu items displayed on the column header.
+  contextMenu?: PopupMenuItem[];
+
+  // Unique column ID, used to identify which column is currently sorted.
+  columnId?: string;
+
+  // Sorting predicate: if provided, column would be sortable.
+  ordering?: ComparisonFn<T>;
+
+  // Simpler way to provide a sorting: instead of full predicate, the function
+  // can map the row for "sorting key" associated with the column.
+  sortKey?: (value: T) => SortableValue;
+}
+
+export class ColumnDescriptor<T> {
+  name: string;
+  render: (row: T) => m.Child;
+  id: string;
+  contextMenu?: PopupMenuItem[];
+  ordering?: ComparisonFn<T>;
+
+  constructor(
+    name: string,
+    render: (row: T) => m.Child,
+    attrs?: ColumnDescriptorAttrs<T>,
+  ) {
+    this.name = name;
+    this.render = render;
+    this.id = attrs?.columnId === undefined ? name : attrs.columnId;
+
+    if (attrs === undefined) {
+      return;
+    }
+
+    if (attrs.sortKey !== undefined && attrs.ordering !== undefined) {
+      throw new Error('only one way to order a column should be specified');
+    }
+
+    if (attrs.sortKey !== undefined) {
+      this.ordering = comparingBy(attrs.sortKey, compareUniversal);
+    }
+    if (attrs.ordering !== undefined) {
+      this.ordering = attrs.ordering;
+    }
+  }
+}
+
+export function numberColumn<T>(
+  name: string,
+  getter: (t: T) => number,
+  contextMenu?: PopupMenuItem[],
+): ColumnDescriptor<T> {
+  return new ColumnDescriptor<T>(name, getter, {contextMenu, sortKey: getter});
+}
+
+export function stringColumn<T>(
+  name: string,
+  getter: (t: T) => string,
+  contextMenu?: PopupMenuItem[],
+): ColumnDescriptor<T> {
+  return new ColumnDescriptor<T>(name, getter, {contextMenu, sortKey: getter});
+}
+
+export function widgetColumn<T>(
+  name: string,
+  getter: (t: T) => m.Child,
+): ColumnDescriptor<T> {
+  return new ColumnDescriptor<T>(name, getter);
+}
+
+interface SortingInfo<T> {
+  columnId: string;
+  direction: SortDirection;
+  // TODO(ddrone): figure out if storing this can be avoided.
+  ordering: ComparisonFn<T>;
+}
+
+// Encapsulated table data, that contains the input to be displayed, as well as
+// some helper information to allow sorting.
+export class TableData<T> {
+  data: T[];
+  private _sortingInfo?: SortingInfo<T>;
+  private permutation: number[];
+
+  constructor(data: T[]) {
+    this.data = data;
+    this.permutation = range(data.length);
+  }
+
+  *iterateItems(): Generator<T> {
+    for (const index of this.permutation) {
+      yield this.data[index];
+    }
+  }
+
+  items(): T[] {
+    return Array.from(this.iterateItems());
+  }
+
+  setItems(newItems: T[]) {
+    this.data = newItems;
+    this.permutation = range(newItems.length);
+    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 {
+    return this._sortingInfo;
+  }
+
+  reorder(info: SortingInfo<T>) {
+    this._sortingInfo = info;
+    this.permutation.sort(
+      withDirection(
+        comparingBy((index: number) => this.data[index], info.ordering),
+        info.direction,
+      ),
+    );
+    scheduleFullRedraw();
+  }
+}
+
+export interface TableAttrs<T> {
+  data: TableData<T>;
+  columns: ColumnDescriptor<T>[];
+}
+
+function directionOnIndex(
+  columnId: string,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  info?: SortingInfo<any>,
+): SortDirection | undefined {
+  if (info === undefined) {
+    return undefined;
+  }
+  return info.columnId === columnId ? info.direction : undefined;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export class Table implements m.ClassComponent<TableAttrs<any>> {
+  renderColumnHeader(
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    vnode: m.Vnode<TableAttrs<any>>,
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    column: ColumnDescriptor<any>,
+  ): m.Child {
+    let currDirection: SortDirection | undefined = undefined;
+
+    let items = column.contextMenu;
+    if (column.ordering !== undefined) {
+      const ordering = column.ordering;
+      currDirection = directionOnIndex(column.id, vnode.attrs.data.sortingInfo);
+      const newItems: PopupMenuItem[] = [];
+      if (currDirection !== 'ASC') {
+        newItems.push(
+          menuItem('Sort ascending', () => {
+            vnode.attrs.data.reorder({
+              columnId: column.id,
+              direction: 'ASC',
+              ordering,
+            });
+          }),
+        );
+      }
+      if (currDirection !== 'DESC') {
+        newItems.push(
+          menuItem('Sort descending', () => {
+            vnode.attrs.data.reorder({
+              columnId: column.id,
+              direction: 'DESC',
+              ordering,
+            });
+          }),
+        );
+      }
+      if (currDirection !== undefined) {
+        newItems.push(
+          menuItem('Restore original order', () => {
+            vnode.attrs.data.resetOrder();
+          }),
+        );
+      }
+      items = [...newItems, ...(items ?? [])];
+    }
+
+    return m(
+      'td',
+      column.name,
+      items === undefined
+        ? null
+        : m(PopupMenuButton, {
+            icon: popupMenuIcon(currDirection),
+            items,
+          }),
+    );
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  checkValid(attrs: TableAttrs<any>) {
+    if (!allUnique(attrs.columns.map((c) => c.id))) {
+      throw new Error('column IDs should be unique');
+    }
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  oncreate(vnode: m.VnodeDOM<TableAttrs<any>, this>) {
+    this.checkValid(vnode.attrs);
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  onupdate(vnode: m.VnodeDOM<TableAttrs<any>, this>) {
+    this.checkValid(vnode.attrs);
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  view(vnode: m.Vnode<TableAttrs<any>>): m.Child {
+    const attrs = vnode.attrs;
+
+    return m(
+      'table.generic-table',
+      m(
+        'thead',
+        m(
+          'tr.header',
+          attrs.columns.map((column) => this.renderColumnHeader(vnode, column)),
+        ),
+      ),
+      attrs.data.items().map((row) =>
+        m(
+          'tr',
+          attrs.columns.map((column) => m('td', column.render(row))),
+        ),
+      ),
+    );
+  }
+}
diff --git a/ui/src/widgets/tag_input.ts b/ui/src/widgets/tag_input.ts
index bd2f6e3..ac4e469 100644
--- a/ui/src/widgets/tag_input.ts
+++ b/ui/src/widgets/tag_input.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {HTMLFocusableAttrs} from './common';
 import {Icon} from './icon';
 import {findRef} from '../base/dom_utils';
diff --git a/ui/src/widgets/text_input.ts b/ui/src/widgets/text_input.ts
index fe4487c..307201f 100644
--- a/ui/src/widgets/text_input.ts
+++ b/ui/src/widgets/text_input.ts
@@ -13,16 +13,26 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {HTMLInputAttrs} from './common';
 
+type TextInputAttrs = HTMLInputAttrs & {
+  // Whether the input should autofocus when it is created.
+  autofocus?: boolean;
+};
+
 // For now, this component is just a simple wrapper around a plain old input
 // element, which does no more than specify a class. However, in the future we
 // might want to add more features such as an optional icon or button (e.g. a
 // clear button), at which point the benefit of having this as a component would
 // become more apparent.
-export class TextInput implements m.ClassComponent<HTMLInputAttrs> {
-  view({attrs}: m.CVnode<HTMLInputAttrs>) {
+export class TextInput implements m.ClassComponent<TextInputAttrs> {
+  oncreate(vnode: m.CVnodeDOM<TextInputAttrs>) {
+    if (vnode.attrs.autofocus) {
+      (vnode.dom as HTMLElement).focus();
+    }
+  }
+
+  view({attrs}: m.CVnode<TextInputAttrs>) {
     return m('input.pf-text-input', attrs);
   }
 }
diff --git a/ui/src/widgets/track_widget.ts b/ui/src/widgets/track_widget.ts
new file mode 100644
index 0000000..2369e0d
--- /dev/null
+++ b/ui/src/widgets/track_widget.ts
@@ -0,0 +1,356 @@
+// 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 {currentTargetOffset} from '../base/dom_utils';
+import {Bounds2D, Point2D, Vector2D} from '../base/geom';
+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 {clamp} from '../base/math_utils';
+
+/**
+ * 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 TrackComponentAttrs {
+  // The title of this track.
+  readonly title: 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<TrackComponentAttrs> {
+  view({attrs}: m.CVnode<TrackComponentAttrs>) {
+    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(vnode: m.VnodeDOM<TrackComponentAttrs>) {
+    this.onupdate(vnode);
+
+    if (vnode.attrs.revealOnCreate) {
+      vnode.dom.scrollIntoView({behavior: 'smooth', block: 'nearest'});
+    }
+  }
+
+  onupdate(vnode: m.VnodeDOM<TrackComponentAttrs>) {
+    this.decidePopupRequired(vnode.dom);
+  }
+
+  // Works out whether to display a title popup on hover, based on whether the
+  // current title is truncated.
+  private decidePopupRequired(dom: Element) {
+    const popupTitleElement = dom.querySelector(
+      '.pf-track-title-popup',
+    ) as HTMLElement;
+    const truncatedTitleElement = dom.querySelector(
+      '.pf-middle-ellipsis',
+    ) as HTMLElement;
+
+    if (popupTitleElement.clientWidth > truncatedTitleElement.clientWidth) {
+      popupTitleElement.classList.add('pf-visible');
+    } else {
+      popupTitleElement.classList.remove('pf-visible');
+    }
+  }
+
+  private renderShell(attrs: TrackComponentAttrs): 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`,
+          },
+        },
+        m(
+          'h1.pf-track-title',
+          {
+            ref: attrs.path, // TODO(stevegolton): Replace with aria tags?
+          },
+          collapsible &&
+            m(Icon, {icon: collapsed ? Icons.ExpandDown : Icons.ExpandUp}),
+          m(
+            MiddleEllipsis,
+            {text: attrs.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,
+        ),
+      ),
+    );
+  }
+
+  private mouseDownPos?: Vector2D;
+  private selectionOccurred = false;
+
+  private renderContent(attrs: TrackComponentAttrs): 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();
+        }
+      },
+    });
+  }
+}
+
+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 0df2f58..d38bc65 100644
--- a/ui/src/widgets/tree.ts
+++ b/ui/src/widgets/tree.ts
@@ -13,10 +13,8 @@
 // limitations under the License.
 
 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.
diff --git a/ui/src/widgets/vega_view.ts b/ui/src/widgets/vega_view.ts
index 2873946..7cbf533 100644
--- a/ui/src/widgets/vega_view.ts
+++ b/ui/src/widgets/vega_view.ts
@@ -15,7 +15,6 @@
 import m from 'mithril';
 import * as vega from 'vega';
 import * as vegaLite from 'vega-lite';
-
 import {getErrorMessage} from '../base/errors';
 import {isString, shallowEquals} from '../base/object_utils';
 import {SimpleResizeObserver} from '../base/resize_observer';
diff --git a/ui/src/widgets/virtual_scroll_helper.ts b/ui/src/widgets/virtual_scroll_helper.ts
index 0fd5137..475c360 100644
--- a/ui/src/widgets/virtual_scroll_helper.ts
+++ b/ui/src/widgets/virtual_scroll_helper.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import {DisposableStack} from '../base/disposable_stack';
-import * as Geometry from '../base/geom';
+import {Bounds2D, Rect2D} from '../base/geom';
 
 export interface VirtualScrollHelperOpts {
   overdrawPx: number;
@@ -21,12 +21,12 @@
   // How close we can get to undrawn regions before updating
   tolerancePx: number;
 
-  callback: (r: Geometry.Rect) => void;
+  callback: (r: Rect2D) => void;
 }
 
 export interface Data {
   opts: VirtualScrollHelperOpts;
-  rect?: Geometry.Rect;
+  rect?: Bounds2D;
 }
 
 export class VirtualScrollHelper {
@@ -87,26 +87,23 @@
     callback(targetPuckRect);
     data.rect = targetPuckRect;
   } else {
-    const viewportRect = containerElement.getBoundingClientRect();
+    const viewportRect = new Rect2D(containerElement.getBoundingClientRect());
 
     // Expand the viewportRect by the tolerance
-    const viewportExpandedRect = Geometry.expandRect(viewportRect, tolerancePx);
+    const viewportExpandedRect = viewportRect.expand(tolerancePx);
 
     const sliderClientRect = sliderElement.getBoundingClientRect();
-    const viewportClamped = Geometry.intersectRects(
-      viewportExpandedRect,
-      sliderClientRect,
-    );
+    const viewportClamped = viewportExpandedRect.intersect(sliderClientRect);
 
     // Translate the puck rect into client space (currently in slider space)
-    const puckClientRect = Geometry.translateRect(data.rect, {
+    const puckClientRect = viewportClamped.translate({
       x: sliderClientRect.x,
       y: sliderClientRect.y,
     });
 
     // Check if the tolerance rect entirely contains the expanded viewport rect
     // If not, request an update
-    if (!Geometry.containsRect(puckClientRect, viewportClamped)) {
+    if (!puckClientRect.contains(viewportClamped)) {
       const targetPuckRect = getTargetPuckRect(
         sliderElement,
         containerElement,
@@ -125,26 +122,16 @@
   overdrawPx: number,
 ) {
   const sliderElementRect = sliderElement.getBoundingClientRect();
-  const containerRect = containerElement.getBoundingClientRect();
+  const containerRect = new Rect2D(containerElement.getBoundingClientRect());
 
   // Calculate the intersection of the container's viewport and the target
-  const intersection = Geometry.intersectRects(
-    containerRect,
-    sliderElementRect,
-  );
+  const intersection = containerRect.intersect(sliderElementRect);
 
   // Pad the intersection by the overdraw amount
-  const intersectionExpanded = Geometry.expandRect(intersection, overdrawPx);
+  const intersectionExpanded = intersection.expand(overdrawPx);
 
   // Intersect with the original target rect unless we want to avoid resizes
-  const targetRect = Geometry.intersectRects(
-    intersectionExpanded,
-    sliderElementRect,
-  );
+  const targetRect = intersectionExpanded.intersect(sliderElementRect);
 
-  return Geometry.rebaseRect(
-    targetRect,
-    sliderElementRect.x,
-    sliderElementRect.y,
-  );
+  return targetRect.reframe(sliderElementRect);
 }
diff --git a/ui/src/widgets/virtual_table.ts b/ui/src/widgets/virtual_table.ts
index c513da9..b5d8643 100644
--- a/ui/src/widgets/virtual_table.ts
+++ b/ui/src/widgets/virtual_table.ts
@@ -13,9 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-
 import {findRef, toHTMLElement} from '../base/dom_utils';
-import {Rect} from '../base/geom';
 import {assertExists} from '../base/logging';
 import {Style} from './common';
 import {scheduleFullRedraw} from './raf';
@@ -236,10 +234,9 @@
       {
         overdrawPx: renderOverdrawPx,
         tolerancePx: renderTolerancePx,
-        callback: ({top, bottom}: Rect) => {
-          const height = bottom - top;
-          const rowStart = Math.floor(top / attrs.rowHeight / 2) * 2;
-          const rowCount = Math.ceil(height / attrs.rowHeight / 2) * 2;
+        callback: (rect) => {
+          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();
         },
@@ -247,9 +244,9 @@
       {
         overdrawPx: queryOverdrawPx,
         tolerancePx: queryTolerancePx,
-        callback: ({top, bottom}: Rect) => {
-          const rowStart = Math.floor(top / attrs.rowHeight / 2) * 2;
-          const rowEnd = Math.ceil(bottom / attrs.rowHeight);
+        callback: (rect) => {
+          const rowStart = Math.floor(rect.top / attrs.rowHeight / 2) * 2;
+          const rowEnd = Math.ceil(rect.bottom / attrs.rowHeight);
           attrs.onReload?.(rowStart, rowEnd - rowStart);
         },
       },